diff --git a/.gitignore b/.gitignore index af745d27..8a421353 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ _RocqProject # Only at the root, since it comes from enabling `dune.disabled` locally /dune +*.bbl diff --git a/papers/cpp-mutex/specs.tex b/papers/cpp-mutex/specs.tex index 6a540fde..6102bc6c 100644 --- a/papers/cpp-mutex/specs.tex +++ b/papers/cpp-mutex/specs.tex @@ -18,8 +18,8 @@ \section{Three Flavors of Mutex} \NewDocumentCommand{\allusers}{g}{% \ensuremath{\mathsf{all\_users}\IfValueT{#1}{(#1)}}} \newcommand{\usedthreads}[2]{\ensuremath{\allusers_{#1}(#2)}} -\newcommand{\notlocked}[2]{\ensuremath{\mathsf{not\_locked}_{#1}(#2)}} -\newcommand{\locked}[2]{\ensuremath{\mathsf{m\_locked}_{#1}(#2)}} +\newcommand{\notlocked}[3]{\ensuremath{\mathsf{not\_locked}_{#1}(#2, #3)}} +\newcommand{\locked}[3]{\ensuremath{\mathsf{m\_locked}_{#1}(#2, #3)}} \newcommand{\smlocked}[2]{\ensuremath{\mathsf{sm\_locked}_{#1}(#2)}} \newcommand{\rmlocked}[2]{\ensuremath{\mathsf{rm\_locked}_{#1}(#2)}} \newcommand{\mutex}[2]{\ensuremath{\mathsf{mutex}_{#1}(#2)}} @@ -27,8 +27,12 @@ \section{Three Flavors of Mutex} \newcommand{\rmutex}[2]{\ensuremath{\mathsf{rmutex}_{#1}(#2)}} \newcommand{\smreaderlocked}[3]{\ensuremath{\mathsf{sm\_reader\_locked}_{#1}(#2, #3)}} \newcommand{\smutex}[2]{\ensuremath{\mathsf{smutex}_{#1}(#2)}} +\newcommand{\token}[2]{\ensuremath{\mathsf{token}_{#1}(#2)}} +\newcommand{\mutexperm}[2]{\ensuremath{\mathsf{mutex\_perm}_{#1}(#2)}} +\newcommand{\mutexname}[1]{\ensuremath{\mathsf{name}(#1)}} \subsection{\texorpdfstring{\lstinline{std::mutex}}{std::mutex}} +\label{mutex-spec} \begin{figure}[htb] \begin{lstlisting} @@ -47,43 +51,39 @@ \subsection{\texorpdfstring{\lstinline{std::mutex}}{std::mutex}} \end{figure} %Both this implementation and a more sophisticated one that checks thread IDs meet our specifications. Show the code for both, and note that the latter doesn't meet the standard HeapLang lock specs. We begin by examining \lstinline{std::mutex}, the basic lock class. The class contains methods \lstinline{lock}, \lstinline{unlock}, and \lstinline{try_lock}, as well as a constructor and destructor. The intended behavior is simply to provide mutual exclusion; a simple implementation could be a spinlock as shown in Figure~\ref{mutex-simple}, although more realistic implementations use OS primitives for efficiency. While \lstinline{std::mutex} is the library class corresponding to a standard lock, its informal specification has three restrictions that do not appear in the standard separation logic treatment: -\begin{itemize} +\begin{enumerate} \item When a thread calls \lstinline{unlock}, it must own the mutex (i.e., it must have been the thread that most recently called \lstinline{lock}). \item When a thread calls \lstinline{lock}, it must \emph{not} own the mutex. \item When a mutex is destroyed, it must not be owned by any threads. -\end{itemize} +\end{enumerate} A program that violates any of these conditions has \emph{undefined behavior} according to the library specification, so our program logic must ensure that they are respected. %include checks in toy implementation, or just mention that they could be added? -The representation for a mutex has the same shape as a standard lock: we write $m \mapsto \mutex{\gamma}{P}$ for a mutex at $m$ associated with ghost identifier $\gamma$ and protected resources $P$. However, we must extend the specifications of the mutex operations to enforce the requirements above. One of the original selling points of CSL was that it could naturally verify \emph{daring} concurrency patterns~\cite{csl} in which a lock acquired by one thread is released by another; in fact, in ordinary separation logics every predicate is freely transferable between threads. Fortunately, as mentioned in Section~\ref{brick}, the design of the BRiCk logic supports a non-transferable predicate $\currentthread{t}$ asserting that the current thread is $t$, which we can use to tie resources to a specific thread. -\ignore{When we spawn a thread $t$ that will using the mutex, it will receive a predicate $\notlocked{\gamma}{t}$, which it exchanges for another predicate $\locked{\gamma}{t}$ when it locks the mutex. A matching predicate $\usedthreads{\gamma}{T}$ will hold the set of all threads that are currently using the mutex, and the precondition of the destructor will require $\usedthreads{\gamma}{\emptyset}$, ensuring that no thread currently holds the lock. This leads to the following specifications for thread creation and joining: -\[\begin{array}{c} -\{ \usedthreads{\gamma}{T} \ast P \ast \{\currentthread{t} \ast \notlocked{\gamma}{t} \ast P \}\ f\ \{ \notlocked{\gamma}{t} \ast Q \} \}\\ \texttt{std::thread(}f\texttt{)}\\ \{ \usedthreads{\gamma}{T \cup \{t\}} \ast t \mapsto \mathsf{thread}(Q) \} \vspace{1em} \\ %use wp instead? constructor notation? -\{ \usedthreads{\gamma}{T} \ast t \mapsto \mathsf{thread}(Q) \}\\ t\texttt{.join()}\\ \{ \usedthreads{\gamma}{T \setminus \{t\}} \ast Q \} -\end{array} -\] -When a thread is spawned running a function $f$, it receives in addition to its precondition $P$ the assertions $\currentthread{t}$ and $\notlocked{\gamma}{t}$, and $\notlocked{\gamma}{t}$ must again hold when it returns. Meanwhile, the parent thread adds $t$ to the set of users of the mutex, and receives a thread handle $\mathsf{thread}(Q)$ that can be used to join with the child thread. When \lstinline{join} is called, $t$ is removed from $\mathsf{all\_users}$ and the caller receives the postcondition $Q$ of the terminated thread. - -The $\mathsf{locked}$ and $\mathsf{not\_locked}$ predicates must also satisfy the following properties, guaranteeing that every user of the mutex has been registered, $\mathsf{locked}$ implies mutual exclusion, and each thread holds at most one of the two predicates:} -Formally, these predicates satisfy the following properties: +The representation predicate for a mutex has the same shape as a standard lock: we write $m \mapsto \mutex{\gamma}{P}$ for a mutex at $m$ associated with ghost identifier $\gamma$ and protected resources $P$. However, we must extend the specifications of the mutex operations to enforce the requirements above. The third property is the simplest from a separation logic perspective: we can define a fractional resource $\token{}{q}$ that a thread must give up in order to acquire the mutex, and require full ownership $\token{}{1}$ in the precondition of the destructor. The first two properties are more complicated, since they check whether a thread performing an operation is \emph{the same thread} as one that performed a previous operation. Our basic approach is to define predicates $\notlocked{\gamma}{t}{q}$ and $\locked{\gamma}{t}{q}$ such that a thread $t$ exchanges $\notlocked{\gamma}{t}{q}$ for $\locked{\gamma}{t}{q}$ when acquiring the mutex, and vice versa when releasing it. However, these predicates, like any other CSL predicate, can be transferred between threads when they synchronize: how can we ensure that the thread that owns $\notlocked{\gamma}{t}{q}$ is actually thread $t$? For this, we use BRiCk's non-transferable predicate $\currentthread{t}$ described in Section~\ref{brick}: a thread must own both $\currentthread{t}$ and $\notlocked{\gamma}{t}{q}$ (resp. $\locked{\gamma}{t}{q}$) to acquire (resp. release) the mutex, proving that it is in fact the thread identified by the predicate. These predicates satisfy the following properties: %make this a figure? \begin{mathpar} -\inferrule{t \not\in T}{\usedthreads{\gamma}{T} \vs \usedthreads{\gamma}{T \cup \{t\}} \ast \notlocked{\gamma}{t}} \and -\usedthreads{\gamma}{T} \ast \notlocked{\gamma}{t} \vdash t \in T \and -\usedthreads{\gamma}{T} \ast \notlocked{\gamma}{t} \vs \usedthreads{\gamma}{T \setminus \{t\}} \and -\notlocked{\gamma}{t} \ast \notlocked{\gamma}{t} \vdash \mathsf{False} \and \notlocked{\gamma}{t} \ast \locked{\gamma}{t} \vdash \mathsf{False} \and \locked{\gamma}{t_1} \ast \locked{\gamma}{t_2} \vdash \mathsf{False} %anything else we need? +\notlocked{\gamma}{t}{q_1} \ast \notlocked{\gamma}{t}{q_2} \vdash \mathsf{False} \and %\notlocked{\gamma}{t} \ast \locked{\gamma}{t} \vdash \mathsf{False} \and +\locked{\gamma}{t_1}{q_1} \ast \locked{\gamma}{t_2}{q_2} \vdash \mathsf{False} +\and \token{\gamma}{1} \ast \locked{\gamma}{t}{q} \vdash \mathsf{False} %anything else we need? \end{mathpar} %explain a little more, and explain view shifts -This user-tracking pattern is very useful for describing thread-specific properties of mutexes; we will extend it for the following mutex types, and explain its implementation in Section~\ref{login}. -We can then specify the mutex as follows: +How does a thread obtain $\notlocked{\gamma}{t}{q}$ to begin with? We could explicitly specify a set of threads that are allowed to use a mutex when it is created, but we may not know this set of threads in advance and it may even be unbounded (e.g., if we spawn threads in a loop). Instead, we make it possible for a thread $t$ to \emph{register} itself with an existing mutex $\gamma$ and obtain $\notlocked{\gamma}{t}{q}$, as long as $t$ can guarantee that it is not already registered with $\gamma$. Each thread holds a set of as-yet-unregistered mutex names $\mutexperm{t}{A}$, where $A$ is initially the universe of possible mutex names $\top$. When $t$ wishes to register with a mutex $\gamma$, it removes the name of $\gamma$ from $A$ and exchanges it (along with a share of $\mathsf{token}$) for $\mathsf{not\_locked}$: +\[ \mutexperm{t}{\mutexname{\gamma}} \ast \token{\gamma}{q} \dashv \vdash \notlocked{\gamma}{t}{q} \] +When a thread is finished using the mutex (and does not currently hold it), it can perform the same operation in reverse to recover $\mutexperm{t}{\mutexname{\gamma}}$ and $\token{\gamma}{q}$. +This name-tracking pattern is very useful for describing thread-specific properties of mutexes; we will extend it for the following mutex types, and explain its implementation in Section~\ref{login}. + +Using the $\mathsf{m\_locked}$ and $\mathsf{not\_locked}$ predicates, we can specify the mutex as follows: \[\begin{array}{c} -\{\later P\}\\ \texttt{mutex()}\\ \{\exists \gamma.\ \mutex{\gamma}{P} \ast \usedthreads{\gamma}{\emptyset}\} \vspace{1em}\\%constructor notation? -\{\currentthread{t} \ast m \mapsto_q \mutex{\gamma}{P}\ast\notlocked{\gamma}{t}\}\\ {m\texttt{.lock()}} \\ \{m \mapsto_q \mutex{\gamma}{P}\ast P\ast\locked{\gamma}{t}\} \vspace{1em}\\ - \{\currentthread{t} \ast m \mapsto_q \mutex{\gamma}{P}\ast\locked{\gamma}{t} - \ast\triangleright P\}\\ {m\texttt{.unlock}()}\\ - \{m \mapsto_q \mutex{\gamma}{P}\ast\notlocked{\gamma}{t}\} \vspace{1em} \\ -\{m \mapsto \mutex{\gamma}{P} \ast \usedthreads{\gamma}{\emptyset}\}\\ \sim m()\\ \{P\} %destructor notation? +% TODO: update constructor with my_mutexes +\{\later P\}\\ \texttt{mutex()}\\ \{m.\ \exists \gamma.\ m \mapsto \mutex{\gamma}{P} \ast \token{\gamma}{1}\} \vspace{1em}\\%constructor notation? +%% TODO: add token(q) or add a fraction to not_locked/locked +\{\currentthread{t} \ast m \mapsto_\pi \mutex{\gamma}{P}\ast\notlocked{\gamma}{t}{q}\}\\ {\texttt{m.lock()}} \\ \{m \mapsto_\pi \mutex{\gamma}{P}\ast P\ast\locked{\gamma}{t}{q}\} \vspace{1em}\\ + \{\currentthread{t} \ast m \mapsto_\pi \mutex{\gamma}{P}\ast\locked{\gamma}{t}{q} + \ast\triangleright P\}\\ {\texttt{m.unlock}()}\\ + \{m \mapsto_\pi \mutex{\gamma}{P}\ast\notlocked{\gamma}{t}{q}\} \vspace{1em} \\ +\{m \mapsto \mutex{\gamma}{P} \ast \token{\gamma}{1}\}\\ \texttt{m.}\!\!\sim\!\texttt{mutex()}\\ \{P\} %destructor notation? \end{array}\] %note that current_thread is persistent -A thread $t$ exchanges $\notlocked{\gamma}{t}$ for $\locked{\gamma}{t}$ when acquiring the mutex and vice versa when releasing; these predicates are freely transferable, but $\currentthread{t}$ ensures that each thread holds its own predicate when calling \lstinline{lock} or \lstinline{unlock}, and the laws of $\mathsf{m\_locked}$ and $\mathsf{not\_locked}$ ensure that no two threads hold the mutex at once and that a thread cannot simultaneously be $\mathsf{m\_locked}$ and $\mathsf{not\_locked}$. Finally, the $\usedthreads{\gamma}{\emptyset}$ in the precondition of the destructor ensures that all threads have been removed from the set of users (and thus cannot hold the mutex) before deallocation. When these specifications are used in proofs, the thread that allocates the mutex will acquire and hold $\usedthreads{\gamma}{T}$, and generate a $\notlocked{\gamma}{t}$ predicate for each thread $t$ participating in the mutex; when those threads terminate, the main thread will recollect all the $\notlocked{}{}$ predicates and remove them from $\usedthreads{}{}$ before the mutex is destroyed. +A thread $t$ exchanges $\notlocked{\gamma}{t}{q}$ for $\locked{\gamma}{t}{q}$ when acquiring the mutex and vice versa when releasing; $\currentthread{t}$ ensures that each thread holds its own predicate when calling \lstinline{lock} or \lstinline{unlock}%, and the laws of $\mathsf{m\_locked}$ and $\mathsf{not\_locked}$ ensure that no two threads hold the mutex at once and that a thread cannot simultaneously be $\mathsf{m\_locked}$ and $\mathsf{not\_locked}$ +. Finally, $\token{\gamma}{1}$ in the precondition of the destructor ensures that no threads hold the mutex before deallocation. %When these specifications are used in proofs, the thread that allocates the mutex will acquire and hold $\usedthreads{\gamma}{T}$, and generate a $\notlocked{\gamma}{t}$ predicate for each thread $t$ participating in the mutex; when those threads terminate, the main thread will recollect all the $\notlocked{}{}$ predicates and remove them from $\usedthreads{}{}$ before the mutex is destroyed. \ignore{ \begin{mathpar} @@ -145,52 +145,53 @@ \subsection{\texorpdfstring{\lstinline{std::mutex}}{std::mutex}} \caption{An example using \lstinline{std::mutex}} \label{mutex-ex} \end{figure} -Figure~\ref{mutex-ex} shows a simple application of \lstinline{mutex}. The mutex \lstinline{m} protects the variable \lstinline{x}, which is incremented by two separate threads. We sketch a proof with the lock invariant $P \triangleq \exists n.\ \texttt{x} \mapsto n \ast \mathsf{even}(n)$; we could use ghost state to show that \lstinline{x} is exactly 4 at the end of the program, but this is orthogonal to the novelties of our mutex specification. The proof is outlined in Figure~\ref{mutex-ex-proof}. The main thread registers itself and the thread to be spawned with $\mathsf{all\_users}$, obtaining $\mathsf{not\_locked}$ predicates for each of them. When a thread locks \lstinline{m} it exchanges its $\mathsf{not\_locked}$ for $\mathsf{locked}$ and ownership of $P$; unlocking \lstinline{m} performs the same operation in reverse. Finally, the main thread joins with the spawned thread, recollects its $\mathsf{not\_locked}$ predicate, and unregisters both threads from $\mathsf{all\_users}$, so that at the end of \lstinline{main} the precondition of \lstinline{m}'s destructor (namely, $\usedthreads{\gamma}{\emptyset}$) is satisfied, guaranteeing that no thread holds the lock. +Figure~\ref{mutex-ex} shows a simple application of \lstinline{mutex}. The mutex \lstinline{m} protects the variable \lstinline{x}, which is incremented by two separate threads. We sketch a proof with the lock invariant $P \triangleq \exists n.\ \texttt{x} \mapsto n \ast \mathsf{even}(n)$; we could use ghost state to show that \lstinline{x} is exactly 4 at the end of the program, but this is orthogonal to the novelties of our mutex specification. The proof is outlined in Figure~\ref{mutex-ex-proof}. When the main thread spawns a new thread executing \lstinline{double_incr}, it gives the new thread partial ownership of the mutex and its token; it does not need to give up $\currentthread{t_1}$ or $\mutexperm{t}{\top}$\kedu{this sentence is a bit strange to me, but I don't have a better alternative right now}, because the new thread automatically receives its own thread-specific resources at spawn time. Before calling \lstinline{m.lock}, each thread registers with the mutex, exchanging $\mutexperm{}{\mutexname{\gamma}}$ and a piece of the token for a $\mathsf{not\_locked}$ predicate. When a thread locks \lstinline{m} it exchanges its $\mathsf{not\_locked}$ for $\mathsf{m\_locked}$ and ownership of $P$; unlocking \lstinline{m} performs the same operation in reverse. After each thread finishes using \lstinline{m}, it unregisters and recovers its token and mutex permission. +Finally, the main thread joins with the spawned thread and recollects its shares of the mutex and token; the full $\token{\gamma}{1}$ guarantees that no thread holds the lock, satisfying the precondition of \lstinline{m}'s destructor. \begin{figure}[htb] \begin{lstlisting}[escapeinside = {(*@}{@*)}] unsigned int x = 0; std::mutex m; - (*@ $\{ \texttt{m} \mapsto_q \mutex{\gamma}{P} \ast \notlocked{\gamma}{\{t_2\}}$ @*) + (*@ $\{ \currentthread{t} \ast \texttt{m} \mapsto_\pi \mutex{\gamma}{P} \ast \token{\gamma}{q} \ast \mutexperm{t}{\mutexname{\gamma}} \}$ @*) void double_incr() { - (*@ $\{ \currentthread{t_2} \ast \texttt{m} \mapsto_q \mutex{\gamma}{P} \ast \notlocked{\gamma}{t_2} \}$ @*) + (*@ $\Rightarrow \{ \currentthread{t} \ast \texttt{m} \mapsto_\pi \mutex{\gamma}{P} \ast \notlocked{\gamma}{t}{q} \}$ @*) m.lock(); - (*@ $\{ ... \ast P \ast \locked{\gamma}{t_2} \}$ @*) + (*@ $\{ ... \ast P \ast \locked{\gamma}{t}{q} \}$ @*) x++; x++; - (*@ $\{ ... \ast P \ast \locked{\gamma}{t_2} \}$ @*) + (*@ $\{ ... \ast P \ast \locked{\gamma}{t}{q} \}$ @*) m.unlock(); - (*@ $\{ \texttt{m} \mapsto_q \mutex{\gamma}{P} \ast \notlocked{\gamma}{t_2} \}$ @*) + (*@ $\{ \texttt{m} \mapsto_\pi \mutex{\gamma}{P} \ast \notlocked{\gamma}{t}{q} \} \Rightarrow$ @*) } - (*@ $\{ \texttt{m} \mapsto_q \mutex{\gamma}{P} \ast \notlocked{\gamma}{t_2} \}$ @*) + (*@ $\{ \texttt{m} \mapsto_\pi \mutex{\gamma}{P} \ast \token{\gamma}{q} \ast \mutexperm{t}{\mutexname{\gamma}} \}$ @*) void main() { - (*@ $\{ \currentthread{t_1} \ast \texttt{m} \mapsto \mutex{\gamma}{P} \ast \usedthreads{\gamma}{\emptyset} \} \vs$ @*) - (*@ $\{ \currentthread{t_1} \ast \texttt{m} \mapsto \mutex{\gamma}{P} \ast \usedthreads{\gamma}{\{t_1, t_2\}} \ast \notlocked{\gamma}{t_1} \ast \notlocked{\gamma}{t_2} \}$ @*) + (*@ $\{ \currentthread{t_1} \ast \texttt{m} \mapsto \mutex{\gamma}{P} \ast \token{\gamma}{1} \ast \mutexperm{t}{\top} \}$ @*) std::thread t(double_incr); - (*@ $\{ \currentthread{t_1} \ast \texttt{m} \mapsto_{.5} \mutex{\gamma}{P} \ast \usedthreads{\gamma}{\{t_1, t_2\}} \ast \notlocked{\gamma}{t_1} \}$ @*) + (*@ $\{ \currentthread{t_1} \ast \texttt{m} \mapsto_{.5} \mutex{\gamma}{P} \ast \token{\gamma}{.5} \ast \mutexperm{t}{\top} \} \Rightarrow$ @*) + (*@ $\{ \currentthread{t_1} \ast \texttt{m} \mapsto_{.5} \mutex{\gamma}{P} \ast \mutexperm{t}{\top \setminus \mutexname{\gamma}} \ast \notlocked{\gamma}{t_1}{.5} \}$ @*) m.lock(); - (*@ $\{ ... \ast P \ast \locked{\gamma}{t_1} \}$ @*) + (*@ $\{ ... \ast P \ast \locked{\gamma}{t_1}{.5} \}$ @*) x += 2; - (*@ $\{ ... \ast P \ast \locked{\gamma}{t_1} \}$ @*) + (*@ $\{ ... \ast P \ast \locked{\gamma}{t_1}{.5} \}$ @*) m.unlock(); - (*@ $\{ ... \ast \notlocked{\gamma}{t_1} \}$ @*) + (*@ $\{ ... \ast \notlocked{\gamma}{t_1}{.5} \} \Rightarrow$ @*) + (*@ $\{ ... \ast \token{\gamma}{.5} \ast \mutexperm{t}{\top} \}$ @*) t.join(); - (*@ $\{ \texttt{m} \mapsto \mutex{\gamma}{P} \ast \usedthreads{\gamma}{\{t_1, t_2\}} \ast \notlocked{\gamma}{t_1} \ast \notlocked{\gamma}{t_2} \} \vs$ @*) - (*@ $\{ \texttt{m} \mapsto \mutex{\gamma}{P} \ast \usedthreads{\gamma}{\emptyset} \}$ @*) + (*@ $\{ \texttt{m} \mapsto \mutex{\gamma}{P} \ast \token{\gamma}{1} \ast \mutexperm{t}{\top} \}$ @*) } -\end{lstlisting}%say something about knowing t2 before creating the thread? -\caption{The proof outline for the example} +\end{lstlisting} +\caption{The proof outline for the example\kedu{can we use t0 for the main thread, t1 for the spawned thread, and t for a general thread (used in double\_incr)?}} \label{mutex-ex-proof} \end{figure} \subsection{\texorpdfstring{\lstinline{std::recursive_mutex}}{std::recursive\_mutex}} -\emph{Recursive} or \emph{reentrant} mutexes can be acquired multiple times by the same thread, and only become available to another thread once every acquire has been matched with a release. Recursive mutexes are popular in object-oriented programming, allowing a method to acquire a lock every time it is called even if it is called from another locked method. A canonical recursive mutex implementation is shown in Figure~\ref{rmutex-simple}. When no thread holds the lock, \lstinline{count} is 0 and \lstinline{owner} is \lstinline{null}. When a thread that does not already own the lock tries to acquire it, it calls the \lstinline{lock} method of an underlying conventional mutex; likewise, when the owner releases the lock for the last time (\lstinline{count} is 1), it \lstinline{unlock}s the underlying mutex. However, when the owner re-acquires a lock it already owns, it performs no synchronization operations, instead simply incrementing the count---and likewise, a non-final release simply decrements the count. This avoids redundant synchronization and deadlock while still providing mutual exclusion. %note that an implementation requires a get-thread-id function +\emph{Recursive} or \emph{reentrant} mutexes can be acquired multiple times by the same thread, and only become available to another thread once every acquire has been matched with a release. Recursive mutexes are popular in object-oriented programming, allowing a method to acquire a lock every time it is called even if it is called from another locked method. A canonical recursive mutex implementation is shown in Figure~\ref{rmutex-simple}. When no thread holds the lock, \lstinline{count} is 0 and \lstinline{owner} is \lstinline{null}. When a thread that does not already own the lock tries to acquire it, it calls the \lstinline{lock} method of an underlying conventional mutex; likewise, when the owner releases the lock for the last time (\lstinline{count} is 1), it \lstinline{unlock}s the underlying mutex. However, when the owner re-acquires a lock it already owns, it performs no synchronization operations, instead simply incrementing the count---and likewise, a non-final release simply decrements the count. This avoids redundant synchronization and deadlock while still providing mutual exclusion. %note that an implementation requires a get-thread-id function %note that there's no natural daring version of the recursive mutex---only the thread ID distinguishes re-acquisition from mutex-breaking \begin{figure}[htb] \begin{lstlisting} -std::atomic owner; // default val neq any thread ID +std::atomic owner; int count; std::mutex m; @@ -222,8 +223,8 @@ \subsection{\texorpdfstring{\lstinline{std::recursive_mutex}}{std::recursive\_mu Using $\mathsf{holds}$, we can specify \lstinline{lock} and \lstinline{unlock} as simply increasing and decreasing $n$, respectively: \[\begin{array}{c} -\{\currentthread{t} \ast m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n}{P}{a}\}\\ {m\texttt{.lock()}} \\ \{m \mapsto_q \rmutex{\gamma}{P}\ast \exists a'.\ \mathsf{acquire}(n, a, a') \ast \acquirable{\gamma}{t}{n+1}{P}{a'}\} \vspace{1em}\\ - \{\currentthread{t} \ast m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n+1}{P}{a}\}\\ {m\texttt{.unlock}()}\\ +\{\currentthread{t} \ast m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n}{P}{a}\}\\ {\texttt{m.lock()}} \\ \{m \mapsto_q \rmutex{\gamma}{P}\ast \exists a'.\ \mathsf{acquire}(n, a, a') \ast \acquirable{\gamma}{t}{n+1}{P}{a'}\} \vspace{1em}\\ + \{\currentthread{t} \ast m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n+1}{P}{a}\}\\ {\texttt{m.unlock}()}\\ \{m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n}{P}{a}\} \end{array}\] where $\mathsf{acquire}(n, a, a') \triangleq (n > 0 \Rightarrow a' = a)$ allows the argument $a$ to change arbitrarily if the caller of \lstinline{lock} did not already own the lock, but enforces that $a$ is unchanged if the caller did already own the lock. Other than that, these specifications are entirely agnostic to whether any given call is the first \lstinline{lock} or last \lstinline{unlock} on the mutex: we can thus reason about a method that acquires a recursive mutex, manipulates the associated resources, and releases the mutex without ever having to do case analysis on whether the caller already owned the mutex. @@ -294,10 +295,10 @@ \subsection{\texorpdfstring{\lstinline{std::recursive_mutex}}{std::recursive\_mu (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast \acquirable{\gamma}{t}{n}{P}{(a,b)} \}$ @*) void update_a(long x) { m.lock(); - (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast \acquirable{\gamma}{t}{n+1}{P}{(a,b)} \} \Rightarrow$ @*) - (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast P(a, b) \ast (\forall a', b'.\ P(a', b') \wand \acquirable{\gamma}{t}{n+1}{P}{(a',b')}) \}$ @*) + (*@ $\{ ... \ast \acquirable{\gamma}{t}{n+1}{P}{(a,b)} \} \Rightarrow$ @*) + (*@ $\{ ... \ast P(a, b) \ast (\forall a', b'.\ P(a', b') \wand \acquirable{\gamma}{t}{n+1}{P}{(a',b')}) \}$ @*) balance_a += x; - (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast \acquirable{\gamma}{t}{n+1}{P}{(a+x,b)}) \}$ @*) + (*@ $\{ ... \ast \acquirable{\gamma}{t}{n+1}{P}{(a+x,b)}) \}$ @*) m.unlock(); } (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast \acquirable{\gamma}{t}{n}{P}{(a + \texttt{x},b)} \}$ @*) @@ -313,15 +314,16 @@ \subsection{\texorpdfstring{\lstinline{std::recursive_mutex}}{std::recursive\_mu (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast \acquirable{\gamma}{t}{n}{P}{(a,b)} \}$ @*) void transfer(int x) { m.lock(); - (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast P(a, b) \ast (\forall a', b'.\ P(a', b') \wand \acquirable{\gamma}{t}{n+1}{P}{(a,b)}) \}$ @*) + (*@ $\{ ... \ast \acquirable{\gamma}{t}{n+1}{P}{(a,b)} \} \provesIff$ @*) + (*@ $\{ ... \ast P(a, b) \ast (\forall a', b'.\ P(a', b') \wand \acquirable{\gamma}{t}{n+1}{P}{(a',b')}) \}$ @*) update_a(x); update_b(-(long)x); - (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast \acquirable{\gamma}{t}{n+1}{P}{(a+x,b-x)} \}$ @*) + (*@ $\{ ... \ast \acquirable{\gamma}{t}{n+1}{P}{(a+x,b-x)} \}$ @*) m.unlock(); } (*@ $\{ \texttt{m} \mapsto_q \rmutex{\gamma}{P} \ast \acquirable{\gamma}{t}{n}{P}{(a+x,b-x)} \}$ @*) \end{lstlisting} -\caption{The proof outline for the \lstinline{recursive_mutex} example} +\caption{The proof outline for the \lstinline{recursive_mutex} example. The mutex points-to assertion is unchanged in these functions and abbreviated with ellipses.} \label{rmutex-ex-proof} \end{figure} @@ -331,6 +333,7 @@ \subsection{\texorpdfstring{\lstinline{std::shared_mutex}}{std::shared\_mutex}} C++'s \lstinline{shared_mutex} class is more commonly known as a \emph{reader-writer lock}. It provides two separate pairs of acquire and release operations, \emph{exclusive} \lstinline{lock}/\lstinline{unlock} and \emph{shared} \lstinline{lock_shared}/\lstinline{unlock_shared}; while \lstinline{lock} and \lstinline{unlock} ensure mutual exclusion, any number of threads can successfully perform \lstinline{lock_shared} at once as long as no thread has acquired the lock exclusively. Reader-writer locks have been modeled in separation logic by \citet{heaplang-rw-lock}, and we build on their work, but must also account for C++'s additional restrictions: no thread may acquire shared ownership of the mutex more than once, and only threads that have acquired it may release it. +\ignore{ \begin{figure}[htb] \begin{lstlisting} %not sure whether this is the right canonical implementation @@ -358,6 +361,7 @@ \subsection{\texorpdfstring{\lstinline{std::shared_mutex}}{std::shared\_mutex}} \caption{A simple implementation of \lstinline{std::shared_mutex}} \label{smutex-simple} \end{figure} +} Following \citet{heaplang-rw-lock}, we take the resource $P$ protected by a shared mutex to be a fractional predicate; calling \lstinline{lock} yields full ownership of the resource $P(1)$, @@ -367,50 +371,61 @@ \subsection{\texorpdfstring{\lstinline{std::shared_mutex}}{std::shared\_mutex}} \[ \begin{array}{@{}c@{}} \{\currentthread{t} \ast m \mapsto_q \smutex{\gamma}{P}\ast\notlocked{\gamma}{t}\} \\ - m\texttt{.lock()} \\[-1pt] + \texttt{m.lock()} \\[-1pt] \{m \mapsto_q \smutex{\gamma}{P}\ast P(1)\ast\smlocked{\gamma}{t}\} \\[1.8em] \{\currentthread{t} \ast m \mapsto_q \smutex{\gamma}{P}\ast\smlocked{\gamma}{t}\ast\triangleright P(1)\}\\ - m\texttt{.unlock()} \\[-1pt] + \texttt{m.unlock()} \\[-1pt] \{m \mapsto_q \smutex{\gamma}{P}\ast\notlocked{\gamma}{t}\} \\[1.8em] \{\currentthread{t} \ast m \mapsto_q \smutex{\gamma}{P}\ast\notlocked{\gamma}{t}\}\\ - m\texttt{.lock\_shared()} \\[-1pt] + \texttt{m.lock\_shared()} \\[-1pt] \{m \mapsto_q \smutex{\gamma}{P}\ast\exists q_P.\;P(q_P)\ast\smreaderlocked{\gamma}{t}{q_P}\} \\[1.8em] \{\currentthread{t} \ast m \mapsto_q \smutex{\gamma}{P}\ast\smreaderlocked{\gamma}{t}{q_P} \ast\triangleright P(q_P)\} \\[-1pt] - m\texttt{.unlock\_shared()} \\ + \texttt{m.unlock\_shared()} \\ \{m \mapsto_q \smutex{\gamma}{P}\ast\notlocked{\gamma}{t}\} \end{array} \] \begin{figure}[htb] -\begin{lstlisting} +\begin{lstlisting}[escapeinside = {(*@}{@*)}] bool resource = false; std::shared_mutex m; +(*@ $\{ \texttt{m} \mapsto_q \smutex{\gamma}{P} \ast \notlocked{\gamma}{t} \}$ @*) void set_resource() { + (*@ $\{ \currentthread{t} \ast \texttt{m} \mapsto_q \smutex{\gamma}{P} \ast \notlocked{\gamma}{t} \}$ @*) m.lock(); + (*@ $\{ ... \ast \texttt{resource} \mapsto b \ast \smlocked{\gamma}{t} \}$ @*) resource = true; + (*@ $\{ ... \ast \texttt{resource} \mapsto \mathsf{true} \ast \smlocked{\gamma}{t} \}$ @*) m.unlock(); + (*@ $\{ ... \ast \notlocked{\gamma}{t} \}$ @*) } +(*@ $\{ \texttt{m} \mapsto_q \smutex{\gamma}{P} \ast \notlocked{\gamma}{t} \}$ @*) +(*@ $\{ \texttt{m} \mapsto_q \smutex{\gamma}{P} \ast \notlocked{\gamma}{t} \}$ @*) void await_resource() { + (*@ $\{ \currentthread{t} \ast \texttt{m} \mapsto_q \smutex{\gamma}{P} \ast \notlocked{\gamma}{t} \}$ @*) for (;;) { m.lock_shared(); + (*@ $\{ ... \ast \texttt{resource} \mapsto_{q'} b \ast \smreaderlocked{\gamma}{t}{q'} \}$ @*) bool ret = resource; m.unlock_shared(); + (*@ $\{ ... \ast \notlocked{\gamma}{t} \}$ @*) if (ret) break; } } +(*@ $\{ \texttt{m} \mapsto_q \smutex{\gamma}{P} \ast \notlocked{\gamma}{t} \}$ @*) \end{lstlisting} -\caption{A program using \lstinline{shared_mutex}} -\label{shared-mutex-ex} +\caption{A program using \lstinline{shared_mutex} and its proof sketch} +\label{shared-mutex-ex-proof} \end{figure} -Figure~\ref{shared-mutex-ex} shows an example using \lstinline{shared_mutex}. Exclusive ownership of \lstinline{m} gives write access to \lstinline{resource}, while shared ownership gives read access to \lstinline{resource}. %proof sketch +Figure~\ref{shared-mutex-ex-proof} shows an example using \lstinline{shared_mutex}. Exclusive ownership of \lstinline{m} gives write access to \lstinline{resource}, while shared ownership gives read access to \lstinline{resource}. Its proof uses the resource predicate $P(q) \triangleq \exists b.\ \texttt{resource} \mapsto_q b$, so that \lstinline{lock} gives full ownership of \lstinline{resource} while \lstinline{lock_shared} gives partial (read-only) ownership. \ignore{ \begin{mathpar} @@ -482,44 +497,179 @@ \subsection{\texorpdfstring{\lstinline{std::shared_mutex}}{std::shared\_mutex}} } \section{Implementing the Mutex Specifications} -Our specifications of C++ mutexes capture the informal specifications of the mutexes and can be used to verify client programs. Ideally, we would next show that standard library implementations actually meet our specifications. However, the widely used implementations of the C++ standard library would be difficult to verify---they use Pthread implementations of the corresponding mutex types, which have nearly the same specifications as the standard library mutexes, and the Pthread implementations in turn make heavy use of operating system primitives with complicated low-level specifications (e.g., Linux's \lstinline{futex}). In this section, we instead verify simple and portable implementations of the mutexes that synchronize using atomic operations instead of OS primitives. We first describe the ghost state construction that underlies the $\mathsf{locked}$ and $\mathsf{not\_locked}$ predicates for each mutex, and then show that our simple implementation of each mutex meets the specifications of the previous section. +Our specifications of C++ mutexes capture the informal specifications of the mutexes and can be used to verify client programs. Ideally, we would next show that standard library implementations actually meet our specifications. However, the widely used implementations of the C++ standard library would be difficult to verify---they use Pthread implementations of the corresponding mutex types, which have nearly the same specifications as the standard library mutexes, and the Pthread implementations in turn make heavy use of operating system primitives with complicated low-level specifications (e.g., Linux's \lstinline{futex}). In this section, we instead verify simple and portable implementations of the mutexes that synchronize using atomic operations instead of OS primitives.\kedu{Do we want to explain why we think they are functionally equivalent?} We first describe the ghost state construction that underlies the $\mathsf{locked}$ and $\mathsf{not\_locked}$ predicates for each mutex, and then show that our simple implementation of each mutex meets the specifications of the previous section. -\subsection{The User Library} +\subsection{The Mutex Name Library} \label{login} -TODO cite the Iris reader/writer lock library -TODO mutex is not implemented with the user library yet +All three of our mutex specifications use thread-specific predicates ($\mathsf{not\_locked}$, $\mathsf{m\_locked}$, $\mathsf{holds}$, etc.) to enforce the mutexes' requirements on thread behavior. Here we describe a general ghost construction that we use to implement all of our thread-specific predicates. + +In order to acquire a non-recursive mutex, a thread needs to prove that it does not own that mutex: in separation-logic terms, it needs to own a resource demonstrating that it is not the current holder of the mutex. This means that each thread must own, or be able to create, a not-the-owner resource (call it a ``mutex permission'' \kedu{"mutex deposit"?}) %name? +for every mutex it does not currently hold. We model this using disjoint infinite sets as ghost state: each thread $t$ is created with a resource $\mutexperm{t}{\top}$, where $\top$ is the universe of possible mutex names, and $\ownGhost{}{\mutexperm{t}{A \uplus B}} \dashv\vdash \ownGhost{}{\mutexperm{t}{A}} \ast \ownGhost{}{\mutexperm{t}{B}}$ (where $A \uplus B$ is only defined if $A$ and $B$ are disjoint). Then we can implement $\notlocked{\gamma}{t}$ as precisely $\ownGhost{}{\mutexperm{t}{\{\gamma\}}}$. %update with token ownership +When a thread wants to interact with a mutex $\gamma$, it detaches $\mutexperm{t}{\{\gamma\}}$ from its set and uses it as permission to acquire $\gamma$; disjointness guarantees that each thread can only have one such permission. +%contrast with the less-good user library approach? + +%The mutex invariants are slightly +%different for different kinds of mutexes, but they all ensure that a thread +%must trade in its unique \notlocked{\gamma}{t} handle to acquire protected resources. +%This ensures that a thread can't acquire protected resources if it already has +%some, and unsafe programs that calls \texttt{mutex.lock()} or +%\texttt{shared\_mutex.lock\_shared()} twice consecutively in a single thread +%would get stuck. Notably, for recursive mutexes, if a thread already has the +%resource, it is safe to \texttt{lock()} again, and the thread simply does not +%exchange anything with the invariant. + +%A thread can also "logout" and turn in its \notlocked{\gamma}{t} handle. Deallocation +%of any kind of mutex requires no "logged-in" users +%($\allusers^{\gamma}(\varnothing)$), enforcing that any allocated +%\notlocked{\gamma}{t} handle is deallocated with \texttt{logout} +%(\texttt{all-not\_locked-logged-out}), and so the mutex is not held +%by any thread, thus safe to deallocate. + +%specific locked instances here or below? + +\subsection{Mutex Implementations} +\subsubsection{\lstinline{std::mutex}} + +\renewcommand{\token}[1]{\ensuremath{\ownGhost{#1}{()}}} + +\begin{figure}[htb] +\begin{lstlisting} +class Mutex1 { + std::atomic lock; + + void lock(){ + while(lock.exchange(true)) { yield(); } + } -The $\mathsf{not\_locked}$ ghost state provides an interface that allows a thread to acquire -locked resources at most once regardless of the mutex kinds. Each flavor of mutex -is associated with a disjoint set of thread IDs, managed by one -authoritative piece \allusers\ and fragment pieces \notlocked{\gamma}{t}: + void unlock(){ + lock = false; + } +} +\end{lstlisting} +\caption{A simple spinlock implementation of \lstinline{std::mutex}} +\label{mutex-impl1} +\end{figure} + +\begin{figure}[htb] +\begin{lstlisting} +class Mutex2 { + std::atomic lock; + std::thread::id owner; + + void lock(){ + while(lock.exchange(true)) { yield(); } + owner = get_id(); + } + + void unlock(){ + assert(owner == get_id()); + owner = std::thread::id(); + lock = false; + } + + ~Mutex2() { + assert(!lock); + assert(owner == std::thread::id()); //unowned + } +} +\end{lstlisting} +\caption{An implementation of \lstinline{std::mutex} that checks some thread requirements} +\label{mutex-impl2} +\end{figure} + +We consider two spinlock implementations of \lstinline{std::mutex}. The first, in Figure~\ref{mutex-impl1}, is a straightforward implementation similar to that used in HeapLang: \lstinline{lock} atomically exchanges \lstinline{true} with the current value of \lstinline{lock}, which is \lstinline{true} if the mutex is currently held (in which case the exchange does nothing and the loop continues) and \lstinline{false} if the mutex is not held (in which case the exchange sets \lstinline{lock} to \lstinline{true} and the loop ends). Unlike in HeapLang, a location that will be accessed concurrently by multiple threads must explicitly be declared to be of an \lstinline{atomic} type: data races on non-atomic locations are undefined behavior in C++. The second implementation, in Figure~\ref{mutex-impl2}, additionally records the thread ID of the current owner, and uses it to assert some of the mutex requirements: a thread calling \lstinline{unlock} must be the current owner, and when the mutex is destroyed it must not be held by any thread. (Because \lstinline{owner} is only accessed by the thread that holds the mutex and in the destructor, it does not need to be atomic.) Both of these implementations meet the informal specification of \lstinline{mutex}: since violating the mutex requirements is undefined behavior, assert failure and abnormal termination is a permissible result. However, the second implementation does \emph{not} meet the usual separation logic specification of locks, in which any thread that holds a transferable $\mathsf{locked}$ predicate can release the lock. We prove that both of these implementations satisfy the mutex specifications of Section~\ref{mutex-spec}. We outline the proof for \lstinline{Mutex2}; the proof for \lstinline{Mutex1} is strictly simpler. + +The standard Iris approach to verifying a spinlock is to define an invariant of the form +\[ + \knowInv{}{\begin{aligned} + &(\texttt{lock} \mapsto \mathsf{true}) \\ + {}\vee{} &(\texttt{lock} \mapsto \mathsf{false} \ast P \ast \token{\gamma}) + \end{aligned}}, +\] +where whenever the lock is not held the invariant contains the protected resources $P$ and some exclusive token $\token{\gamma}$, and when the lock is held, these are transferred to the holder. Having $\token{\gamma}$ allows the thread to unlock the mutex: when it opens the invariant, it must be the unlocked branch because there cannot be two copies of $\token{\gamma}$. The mutex invariant is slightly more complex:\footnote{We use a cancelable invariant instead so that the invariant can be "deallocated" and the protected resources unboxed in the mutex destructor.\kedu{The \lstinline|cinv_own| detail should probably be omitted?} } +\[ + I(P) \triangleq \knowInv{}{\begin{aligned} + &(\exists t.\ \ownGhost{}{\authfull t}) \\ + &{}\ast \left(\begin{aligned} + &(\texttt{lock} \mapsto \mathsf{true} + \ast \notlocked{\gamma}{t}) \\ + {}\vee{} &(\texttt{lock} \mapsto \mathsf{false} \ast P + \ast \ownGhost{}{\authfrag \bot} + \ast \texttt{owner} \mapsto \bot) + \end{aligned}\right) + \end{aligned}} +\] %don't reuse \gamma, or explain that it's a record + +First, the locking thread $t$ hands over its $\notlocked{\gamma}{t}$ in the invariant. As mentioned before, this ensures the mutex is only destructed when no thread currently locks it. But when $\notlocked{\gamma}{t}$ is stored in the invariant, its argument $t$ is forgotten, so we use a pair of \textit{authoritative} ghost states $\ownGhost{}{\authfull t}$ and $\ownGhost{}{\authfrag t}$ to remedy this: when a thread $t$ locks the mutex, it also receives $\ownGhost{}{\authfrag t}$ from the mutex \kedu{Do we want to explain ghost update?} with the same $t$; the other piece $\ownGhost{}{\authfull t}$ always store the same value as $\ownGhost{}{\authfrag t}$ and is stored in the invariant, so when thread $t$ unlocks the mutex, it can use $\ownGhost{}{\authfrag t}$ to recover $\notlocked{\gamma}{t}$. The $\ownGhost{}{\authfrag t}$ ghost state is also exclusive and does the job of $\token{\gamma}$. Lastly, the \lstinline{owner} field is also a protected resource, and it is required to have the default value $\bot$ when the mutex is unowned. + +\kedu{Now \locked{}{} can't have \notlocked{}{} in it, $\notlocked{\gamma}{t} \ast \locked{\gamma}{t} \vdash \mathsf{False}$ is probably only provable with the invariant. } +Now we can define $\locked{\gamma}{t}$ as $\notlocked{\gamma}{t} \ast \texttt{owner} \mapsto t$. We include $\mathsf{not\_locked}$ in $\mathsf{m\_locked}$ because this immediately gives us $\notlocked{\gamma}{t} \ast \locked{\gamma}{t} \vdash \mathsf{False}$, and the exclusivity of $\mapsto$ guarantees that $\notlocked{\gamma}{t_1} \ast \locked{\gamma}{t_2} \vdash \mathsf{False}$. Furthermore, $\currentthread{t} \ast \locked{\gamma}{t}$ guarantees that \lstinline{owner} is precisely the current thread $t$, so the precondition of \lstinline{unlock} is sufficient to ensure that the \lstinline{assert} will succeed. %But this doesn't seem to be enough to pass !lock in the destructor -- we need something in the true case that's inconsistent with used_threads(\empty). Maybe use some extra ghost state to split not_locked and put half into the invariant. +The representation predicate is defined as $\mutex{\gamma}{P}_q \triangleq I(P) \ast \mathsf{tok}_q$. With these definitions, we can prove that each method meets its specification: ... + +\subsubsection{\lstinline{std::recursive_mutex}} +\begin{figure}[htb] +\begin{lstlisting} +class RecursiveMutex { + std::atomic owner; + int count; + std::mutex m; + + void lock(){ + if(owner != get_id()){ + m.lock(); + owner = get_id(); + } + count++; + } + + void unlock(){ + count--; + if(count == 0){ + m.unlock(); + owner = std::thread::id{}; // reset to default + } + } +} +\end{lstlisting} %destructor? +\caption{A simple implementation of \lstinline{std::recursive_mutex}} +\label{rmutex-simple} +\end{figure} + +The simplest way to implement a recursive mutex is by combining an underlying non-recursive mutex \lstinline{m} with an integer \lstinline{count}: each thread's first acquire and last release of the recursive mutex (per critical section) are performed on \lstinline{m}, while all intervening operations by the same thread only increment and decrement \lstinline{count}. Threads must check whether they already hold the recursive mutex before locking \lstinline{m}, so the \lstinline{owner} field may be accessed concurrently and must be atomic. The abstract state of the lock is a pair $(t, n)$ of optional owning thread and acquisition count, where $t = \bot \leftrightarrow n = 0$. %skipping atomic specs for now +We store this abstract state in a piece of authoritative ghost state, where the invariant $I$ always owns the authoritative copy and the owning thread (if any) owns the fragment. This lets us define the central $\mathsf{holds}$ predicate as: \begin{align*} - \allusers^{\gamma}(T) - &\eqdef \mathsf{own}^{\gamma}(\mathord{\bullet}T), \\ - % \mathsf{users}^{\gamma}(T) - % &\eqdef \mathsf{own}^{\gamma}(\mathord{\circ}T), \\ - \mathsf{not\_locked}^{\gamma}(t) - &\eqdef \mathsf{own}^{\gamma}(\mathord{\circ}\{t\}). +\acquirable{\gamma}{t}{n}{P}{a} \triangleq \currentthread{t}\ \ast\ &\text{if } n = 0 \text{ then } \notlocked{\gamma}{t} \\&\text{ else } \ownGhost{}{\authfrag (t, n)}%acquirable includes current_thread, unlike not_locked and locked: is this what we want? \end{align*} -The \allusers{T} ghost state tracks all "logged-in" threads $T$. Each thread with thread ID $t$ can get -a \notlocked{\gamma}{t} handle for that mutex with the \texttt{login} rule, and the handle is -unique for each thread (not-locked-unique). The mutex invariants are slightly -different for different kinds of mutexes, but they all ensure that a thread -must trade in its unique \notlocked{\gamma}{t} handle to acquire protected resources. -This ensures that a thread can't acquire protected resources if it already has -some, and unsafe programs that calls \texttt{mutex.lock()} or -\texttt{shared\_mutex.lock\_shared()} twice consecutively in a single thread -would get stuck. Notably, for recursive mutexes, if a thread already has the -resource, it is safe to \texttt{lock()} again, and the thread simply does not -exchange anything with the invariant. - -A thread can also "logout" and turn in its \notlocked{\gamma}{t} handle. Deallocation -of any kind of mutex requires no "logged-in" users -($\allusers^{\gamma}(\varnothing)$), enforcing that any allocated -\notlocked{\gamma}{t} handle is deallocated with \texttt{logout} -(\texttt{all-not\_locked-logged-out}), and so the mutex is not held -by any thread, thus safe to deallocate. +%invariant -\subsection{Mutex Implementations} -%move sample implementations here? +%proof sketch + +\subsubsection{\lstinline{std::shared_mutex}} +\begin{figure}[htb] +\begin{lstlisting} + +std::atomic sharers{0}; + +void lock(){ + int idle = 0; + while(!sharers.compare_exchange_strong(idle, -1)) { idle = 0; } +} + +void unlock(){ + sharers = 0; +} + +void lock_shared(){ + int val; + do { val = sharers; } + while(val < 0 || !sharers.compare_exchange_strong(val, val + 1)) {} +} + +void unlock_shared(){ + sharers--; +} +\end{lstlisting} +\caption{A simple implementation of \lstinline{std::shared_mutex}} +\label{smutex-simple} +\end{figure} diff --git a/papers/cpp-mutex/stdcpp.tex b/papers/cpp-mutex/stdcpp.tex index 49d9f262..28b2cec1 100644 --- a/papers/cpp-mutex/stdcpp.tex +++ b/papers/cpp-mutex/stdcpp.tex @@ -419,11 +419,11 @@ \subsection{\lstinline{unique_lock}} %how much of this detail is useful? There are a lot of variations. \[\begin{array}{c} -\{p \mapsto R(q, m) \ast \dolock{p}{m}{K}\}\\ \texttt{unique\_lock(p)}\\ \{\ulock(\mathsf{Some}\ (\mathsf{false}, p, q, m))\} \vspace{1em}\\%constructor notation? -\{u \mapsto \ulock(m)\ast \neg \mathsf{held}(m) \ast \dolock{\mathsf{ptr}(m)}{\mathsf{m}(m)}{K}\}\\ {u\texttt{.lock()}} \\ \{m \mapsto \ulock(m[\mathsf{held} := \mathsf{true}]) \ast K\} \vspace{1em}\\ - \{u \mapsto \ulock(m)\ast \mathsf{held}(m) \ast \dounlock{\mathsf{ptr}(m)}{\mathsf{m}(m)}{K}\}\\ {u\texttt{.unlock}()}\\ +\{p \mapsto R(q, m) \ast \dolock{p}{m}{K}\}\\ \texttt{unique\_lock(p)}\\ \{u.\ u \mapsto \ulock(\mathsf{Some}\ (\mathsf{false}, p, q, m))\} \vspace{1em}\\%constructor notation? +\{u \mapsto \ulock(m)\ast \neg \mathsf{held}(m) \ast \dolock{\mathsf{ptr}(m)}{\mathsf{m}(m)}{K}\}\\ {\texttt{u.lock()}} \\ \{m \mapsto \ulock(m[\mathsf{held} := \mathsf{true}]) \ast K\} \vspace{1em}\\ + \{u \mapsto \ulock(m)\ast \mathsf{held}(m) \ast \dounlock{\mathsf{ptr}(m)}{\mathsf{m}(m)}{K}\}\\ {\texttt{u.unlock}()}\\ \{m \mapsto \ulock(m[\mathsf{held} := \mathsf{false}]) \ast K\} \vspace{1em} \\ -\{u \mapsto \ulock() \ast (\text{if } \mathsf{held}(m) \text{ then } \dounlock{\mathsf{ptr}(m)}{\mathsf{m}(m)}{K}\} \text{ else } K)\}\\ \sim u()\\ \{K \ast \text{if } m \text{ then } \mathsf{ptr}(m) \mapsto R(\mathsf{q}(m), \mathsf{m}(m)) \text { else } \mathsf{emp}\} %destructor notation, Some/None notation +\{u \mapsto \ulock() \ast (\text{if } \mathsf{held}(m) \text{ then } \dounlock{\mathsf{ptr}(m)}{\mathsf{m}(m)}{K}\} \text{ else } K)\}\\ \texttt{u.}\!\!\sim\!\texttt{unique\_lock}()\\ \{K \ast \text{if } m \text{ then } \mathsf{ptr}(m) \mapsto R(\mathsf{q}(m), \mathsf{m}(m)) \text { else } \mathsf{emp}\} %destructor notation, Some/None notation \end{array}\] %verify the code from cppreference or a similar example? @@ -449,15 +449,82 @@ \subsection{\lstinline{unique_lock}} As discussed in Section~\ref{basic-lockable}, the \lstinline{BasicLockable} requirement does not explicitly require that \lstinline{lock} lead to mutual exclusion, and indeed recursive mutexes can be \lstinline{lock}ed multiple times without being \lstinline{unlock}ed. This interacts in an interesting and useful way with \lstinline{unique_lock}'s acquisition tracking (the $\mathsf{held}$ predicate in our specifications): each \lstinline{unique_lock} can only acquire its mutex at most once, but we can construct multiple \lstinline{unique_lock}s around a single recursive mutex, each of which manages a single acquisition. %Is this equally true of lock_guard? We could call unlock once manually to illustrate the point. The code in Figure~\ref{unique-recursive} shows how this can be used. When \lstinline{answer} is called, it acquires the recursive mutex \lstinline{m} by constructing a \lstinline{unique_lock} around it; it then calls \lstinline{inc}, which constructs another \lstinline{unique_lock} that acquires \lstinline{m} a second time. When each method returns, its \lstinline{unique_lock} is destroyed and \lstinline{m} is released once, so that \lstinline{m} is no longer locked after \lstinline{answer} returns. Our specifications can be used to verify this program. %proof outline? -\section{Case Studies?} -\input{ring_buffer.tex} +\section{Case Study: Mutex Encapsulation} +%\input{ring_buffer.tex} %TODO: find or make some interesting examples +We have seen that enforcing the requirements of C++ mutexes requires the management of thread-specific permission predicates that must be transferred between threads and locks. We have demonstrated that this is possible in simple examples of each mutex type, but we also want to know that our approach scales to programs with multiple mutex-carrying objects. %rephrase +There are at least two possible pitfalls. First, it might be difficult for a thread to acquire two different mutexes, because we need to manually prove that the second mutex is not the same as the first. Second and more subtly, thread-specific predicates might leak through layers of abstraction. Our mutex specifications must include predicates like $\notlocked{\gamma}{t}$ that explicitly mention thread IDs, but a data structure that uses a mutex internally should not need to expose thread IDs in its interface; it should be possible to share the data structure among threads exactly as one would a non-mutex-based implementation, without extra registration steps. In this section, we verify a case study showing that we avoid both these pitfalls, suggesting that our mutex specifications can indeed be used for modular verification. + +\begin{figure}[htb] +\begin{lstlisting} +class Account { +private: + unsigned long balance; + std::recursive_mutex mut; // protects balance + +public: + explicit Account(unsigned long initial_balance) : balance(initial_balance) {} + + void incr_balance(unsigned long i) { + std::lock_guard lg(mut); + balance += i; + } + + void transfer_to(Account target, unsigned long i) { + std::lock_guard lg(mut); + balance -= i; + target.incr_balance(i); + } +} + +int main() { + Account from{4}; + Account to{2}; + from.transfer_to(to, 3); +} +\end{lstlisting} +\caption{The code of our case study} +\label{transfer} +\end{figure} + + +The code of our case study is shown in Figure~\ref{transfer}. Each object of class \lstinline{Account} has a \lstinline{balance} protected by a mutex (in this code we use a \lstinline{recursive_mutex}, but any form of mutex would work). %make generic? +The \lstinline{transfer_to} method decrements the receiver's %terminology? +balance and increments the target object's balance, acquiring both mutexes in the process. Our questions are then: +\begin{enumerate} +\item How do we prove that we can acquire \lstinline{target}'s mutex, given that we already hold the receiver's mutex? +\item Can we use a representation predicate for \lstinline{Account} that is freely transferable between threads and does not take a thread ID as a parameter? +\end{enumerate} + +%how much do we need to explain namespaces? + +For question 2, recall that our specifications for \lstinline{recursive_mutex} are: +\[\begin{array}{c} +\{\currentthread{t} \ast m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n}{P}{a}\}\\ {\texttt{m.lock()}} \\ \{m \mapsto_q \rmutex{\gamma}{P}\ast \exists a'.\ \mathsf{acquire}(n, a, a') \ast \acquirable{\gamma}{t}{n+1}{P}{a'}\} \vspace{1em}\\ + \{\currentthread{t} \ast m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n+1}{P}{a}\}\\ {\texttt{m.unlock}()}\\ + \{m \mapsto_q \rmutex{\gamma}{P}\ast\acquirable{\gamma}{t}{n}{P}{a}\} +\end{array}\] +The $\mathsf{holds}$ predicate takes a thread $t$ that must match the current thread, and we must have it at line 15 %confirm +in order to acquire \lstinline{mut}. On the other hand, \lstinline{transfer_to} is a function that can be called by any thread that has access to the two accounts involved, and should have a specification like +\[ \{ a \mapsto_q \mathsf{account}{\gamma_a} \ast b \mapsto_q \mathsf{account}{\gamma_b} \}\ \texttt{a.transfer\_to(b, n)}\ \{ a \mapsto_q \mathsf{account}{\gamma_a} \ast b \mapsto_q \mathsf{account}{\gamma_b} \}\] +In particular, if $\mathsf{account}$ takes a thread $t$ as an argument, then we will not be able to freely transfer ownership of it between threads (since it would only be usable by that thread $t$). The mutex permission library allows us to isolate this thread-local information. + +If the precondition of \lstinline{transfer_to} includes $\mathsf{holds}$, then it will also need to mention the thread $t$, and so will not be freely + +The representation predicate for \lstinline{Account} is defined as +\[p \mapsto_q \mathsf{account}(\gamma) \triangleq p.\texttt{mut} \mapsto \rmutex{\gamma}{\lambda n.\ p.\texttt{balance} \mapsto n} \ast \mathsf{token}_\gamma(q) +\] +Ownership of an \lstinline{Account} is simply ownership of the mutex protecting the \lstinline{balance} field, plus ownership of the $\mathsf{token}$ that tracks whether the lock is held. In particular, it makes no mention of the $\mathsf{holds}$ predicate that appears in the precondition of the \lstinline{recursive_mutex} specifications (and explicitly takes a thread $t$ as an argument). Then, how does + \section{BRiCk Formalization} +note that BRiCk itself is an axiomatic semantics \section{Conclusion and Future Work} ... +%the client problem: all preconditions need not_locked, can't be transferred between threads, so if we include it in an abstract predicate that predicate can't be transferred either (even if it uses locks in a way that would never allow double acquire) + condition variables, futures, shared pointers, ... verified Pthread? diff --git a/rocq-brick-libstdcpp/proof/atomic/pred.v b/rocq-brick-libstdcpp/proof/atomic/pred.v index 52e2a14a..d525684b 100644 --- a/rocq-brick-libstdcpp/proof/atomic/pred.v +++ b/rocq-brick-libstdcpp/proof/atomic/pred.v @@ -5,6 +5,7 @@ * SPDX-License-Identifier: LGPL-2.1 WITH BlueRock Exception for use over network, see repository root for details. *) Require Import skylabs.auto.cpp.spec. +Require Import skylabs.bi.weakly_objective. Require Import skylabs.cpp.spec.concepts. Require skylabs.brick.libstdcpp.atomic.inc_int_cpp. @@ -48,6 +49,8 @@ Module Type ATOMIC_PREDS. #[global] Declare Instance R_frac_valid : CFracValid1 R. #[global] Declare Instance R_agree : Cbn (Learn (any ==> learn_eq ==> learn_hints.fin) R). #[global] Declare Instance R_type : Typed2 (class_name ty) R. + #[global] Declare Instance R_weakly_objective q x p : + WeaklyObjective (R q x p). End R_props. End ATOMIC_PREDS. diff --git a/rocq-brick-libstdcpp/proof/dune.inc b/rocq-brick-libstdcpp/proof/dune.inc index b173654d..2dcf6668 100644 --- a/rocq-brick-libstdcpp/proof/dune.inc +++ b/rocq-brick-libstdcpp/proof/dune.inc @@ -227,6 +227,18 @@ (with-stderr-to inc_string_cpp.v.stderr (run cpp2v -v %{input} -o inc_string_cpp.v --no-elaborate --templates=inc_string_cpp_templates.v -- -std=c++20 -stdlib=libstdc++ )))) (alias (name srcs) (deps inc_string.cpp)) ) +(subdir thread + (rule + (targets thread_hpp.v.stderr thread_hpp.v thread_hpp_templates.v) + (alias test_ast) + (deps + (:input thread.hpp) + (env_var CPP2V_DOCKER_ENABLED) + (glob_files_rec ../*.hpp)) + (action + (with-stderr-to thread_hpp.v.stderr (run cpp2v -v %{input} -o thread_hpp.v --no-elaborate --templates=thread_hpp_templates.v -- -std=c++20 -stdlib=libstdc++ )))) + (alias (name srcs) (deps thread.hpp)) +) (subdir vector (rule (targets inc_vector_cpp.v.stderr inc_vector_cpp.v inc_vector_cpp_templates.v) diff --git a/rocq-brick-libstdcpp/proof/lib/lock_ghost2.v b/rocq-brick-libstdcpp/proof/lib/lock_ghost2.v new file mode 100644 index 00000000..4c6de200 --- /dev/null +++ b/rocq-brick-libstdcpp/proof/lib/lock_ghost2.v @@ -0,0 +1,362 @@ +Require Import iris.algebra.agree. +Require Import iris.algebra.frac. +Require Import iris.algebra.functions. +Require Import iris.algebra.gmap. +Require Import iris.algebra.gset. +Require Import iris.algebra.lib.excl_auth. +Require Import iris.algebra.lib.gmap_view. +Require Import iris.algebra.coPset. + +Require Import skylabs.auto.cpp.proof. +Require Export skylabs.brick.libstdcpp.runtime.pred. + +Import linearity. + +(** Various ghost state constructions and laws for concurrency library specs and proofs. *) + +Module mutex_set_G. + Canonical Structure cmraR : cmra := + thread_idT -d> coPset_disjR. + + Class mutex_set_preG `{Σ : cpp_logic} := { + #[local] has_own :: HasOwn (iPropI _Σ) cmraR; + #[local] has_upd :: HasOwnUpd (iPropI _Σ) cmraR; + #[local] has_valid :: HasOwnValid (iPropI _Σ) cmraR; + }. + #[global] Arguments mutex_set_preG {_ _} Σ : assert. + + (* Users of a mutex_set_map ghost state should assume a single instance for + the entire C++ program. This can be done by using a single instance of + [mutex_set_G] to fix [mutex_sets_name]. *) + Class mutex_set_G `{Σ : cpp_logic} := { + #[global] pre_G :: mutex_set_preG Σ; + mutex_sets_name : iprop.gname; + }. + #[global] Arguments mutex_set_G {_ _} Σ : assert. +End mutex_set_G. + +Module MutexSets. + Import mutex_set_G. + #[local] Existing Instances has_own has_upd has_valid. + + Definition my_mutexes `{Σ : cpp_logic, !mutex_set_G Σ} + (th : thread_idT) (E : coPset_disj) : mpred := + own mutex_sets_name (discrete_fun_singleton th E : cmraR). + + (** The pool reserves the full set for each thread not yet registered. *) + Definition reserve (M : gset thread_idT) : cmraR := + fun th => if decide (th ∈ M) then ε else CoPset ⊤. + + Definition mutex_set_map `{Σ : cpp_logic, !mutex_set_G Σ} + (M : gset thread_idT) : mpred := + own mutex_sets_name (reserve M). + + Lemma alloc_mutex_set_map `{Σ : cpp_logic, !mutex_set_preG Σ} : + ⊢ |==> ∃ _ : mutex_set_G Σ, mutex_set_map ∅. + Proof. + iMod (own_alloc (reserve ∅)) as (γ) "Hmap". + { intros th. rewrite /reserve. case_decide; done. } + iModIntro. iExists {| pre_G := _; mutex_sets_name := γ |}. + iExact "Hmap". + Qed. + + Section theory. + Context `{Σ : cpp_logic, !mutex_set_G Σ}. + + #[global] Instance mutex_set_map_timeless M : + Timeless (mutex_set_map M). + Proof. rewrite /mutex_set_map. apply _. Qed. + #[global] Instance my_mutexes_timeless th E : + Timeless (my_mutexes th E). + Proof. rewrite /my_mutexes. apply _. Qed. + + #[global] Instance mutex_set_map_WeaklyObjective M : + WeaklyObjective (mutex_set_map M). + Proof. rewrite /mutex_set_map. apply _. Qed. + #[global] Instance my_mutexes_WeaklyObjective th E : + WeaklyObjective (my_mutexes th E). + Proof. rewrite /my_mutexes. apply _. Qed. + #[global] Instance my_mutexes_objective th E : + Objective (my_mutexes th E). + Proof. rewrite /my_mutexes. apply _. Qed. + + Lemma my_mutexes_exclusive th (E1 E2 : coPset) : + ~(E1 ## E2) -> + my_mutexes th (CoPset E1) ** + my_mutexes th (CoPset E2) |-- False. + Proof. + rewrite /my_mutexes. + iIntros (Hoverlap) "[H1 H2]". + iDestruct (own_valid_2 with "H1 H2") as %Hvalid. + iPureIntro. + specialize (Hvalid th). + rewrite discrete_fun_lookup_op !discrete_fun_lookup_singleton in Hvalid. + rewrite coPset_disj_valid_op in Hvalid. + set_solver. + Qed. + + (* does not need the update, but is implementation detail. *) + Lemma mutex_sets_alloc_thread' T th : + th ∉ T -> + mutex_set_map T |-- + mutex_set_map (T ∪ {[th]}) ** my_mutexes th (CoPset ⊤). + Proof. + intros Hfresh. + rewrite /mutex_set_map /my_mutexes -own_op. + have Heq : reserve T ≡ + reserve (T ∪ {[th]}) ⋅ discrete_fun_singleton th (CoPset ⊤). + { intros th'. + rewrite discrete_fun_lookup_op. + destruct (decide (th = th')) as [<-|Hne]. + - rewrite discrete_fun_lookup_singleton /reserve. + rewrite decide_False; last done. + rewrite decide_True; last set_solver. + by rewrite left_id. + - rewrite discrete_fun_lookup_singleton_ne; last done. + rewrite right_id /reserve. + destruct (decide (th' ∈ T)). + + rewrite !decide_True; try set_solver. + + rewrite !decide_False; try set_solver. + } + by rewrite Heq. + Qed. + + Lemma mutex_sets_alloc_thread T th : + th ∉ T -> + mutex_set_map T |-- + (|==> mutex_set_map (T ∪ {[th]}) ** + my_mutexes th (CoPset ⊤)). + Proof. + iIntros (Hfresh) "Hmap". iModIntro. + iApply (mutex_sets_alloc_thread' T th Hfresh with "Hmap"). + Qed. + + Lemma mutex_sets_free_thread T th : + th ∈ T -> + mutex_set_map T ** my_mutexes th (CoPset ⊤) |-- + mutex_set_map (T \ {[th]}). + Proof. + intros Hmember. + rewrite /mutex_set_map /my_mutexes -own_op. + have Heq : reserve T ⋅ discrete_fun_singleton th (CoPset ⊤) ≡ + reserve (T \ {[th]}). + { intros th'. + rewrite discrete_fun_lookup_op. + destruct (decide (th = th')) as [<-|Hne]. + - rewrite discrete_fun_lookup_singleton /reserve. + rewrite decide_True; last done. + rewrite decide_False; last set_solver. + by rewrite left_id. + - rewrite discrete_fun_lookup_singleton_ne; last done. + rewrite right_id /reserve. + destruct (decide (th' ∈ T)). + + rewrite !decide_True; try set_solver. + + rewrite !decide_False; try set_solver. + } + by rewrite Heq. + Qed. + + Lemma mutex_set_map_empty_no_my_mutexes th (N : namespace) : + mutex_set_map ∅ ** my_mutexes th (CoPset (↑N)) |-- False. + Proof. + rewrite /mutex_set_map /my_mutexes. + iIntros "[Hmap Hmy]". + iDestruct (own_valid_2 with "Hmap Hmy") as %Hvalid. + iPureIntro. + specialize (Hvalid th). + rewrite discrete_fun_lookup_op discrete_fun_lookup_singleton /reserve in Hvalid. + rewrite coPset_disj_valid_op in Hvalid. + have Hnonempty := nclose_non_empty N. + set_solver. + Qed. + + Lemma my_mutexes_alloc_mutex_name th (E N : coPset) : + N ⊆ E -> + my_mutexes th (CoPset E) |-- + my_mutexes th (CoPset (E \ N)) ** my_mutexes th (CoPset N). + Proof. + intros Hsub. + rewrite /my_mutexes -own_op discrete_fun_singleton_op. + rewrite coPset_disj_union; last set_solver. + rewrite difference_union_L. + have -> : E ∪ N = E by set_solver. + done. + Qed. + + Lemma my_mutexes_split th (E1 E2 : coPset) : + E1 ## E2 -> + my_mutexes th (CoPset (E1 ∪ E2)) ⊣⊢ + my_mutexes th (CoPset E1) ** my_mutexes th (CoPset E2). + Proof. + intros Hdisjoint. + rewrite /my_mutexes -own_op discrete_fun_singleton_op. + by rewrite coPset_disj_union. + Qed. + + Lemma my_mutexes_join_mutex_name th (E N : coPset) : + N ⊆ E -> + my_mutexes th (CoPset (E \ N)) ** my_mutexes th (CoPset N) |-- + my_mutexes th (CoPset E). + Proof. + intros Hsub. + rewrite -my_mutexes_split; last set_solver. + rewrite difference_union_L. + have -> : E ∪ N = E by set_solver. + done. + Qed. + + End theory. + + (* an example that two threads can allocate the same namespace token. *) + Lemma my_mutexes_alloc_eg : forall `{Σ : cpp_logic, !mutex_set_preG Σ} + (th1 th2 : thread_idT) (N : coPset), + th1 ≠ th2 -> + ⊢ |==> ∃ _ : mutex_set_G Σ, + my_mutexes th1 (CoPset N) ** + my_mutexes th2 (CoPset N) ** + (mutex_set_map {[th1; th2]} ** + my_mutexes th1 (CoPset (⊤ \ N)) ** + my_mutexes th2 (CoPset (⊤ \ N))). + Proof. + intros until N. intros Hneq. + iMod alloc_mutex_set_map as (HG) "Hmap". + iMod (mutex_sets_alloc_thread ∅ th1 ltac:(set_solver) + with "Hmap") as "[Hmap Ht1]". + iEval (rewrite left_id_L) in "Hmap". + iMod (mutex_sets_alloc_thread {[th1]} th2 ltac:(set_solver) + with "Hmap") as "[Hmap Ht2]". + iDestruct (my_mutexes_alloc_mutex_name th1 ⊤ N ltac:(set_solver) + with "Ht1") as "[Hr1 Hn1]". + iDestruct (my_mutexes_alloc_mutex_name th2 ⊤ N ltac:(set_solver) + with "Ht2") as "[Hr2 Hn2]". + iModIntro. iExists HG. iFrame. + Qed. + + #[global] Hint Opaque mutex_set_map my_mutexes : sl_opacity typeclass_instances. +End MutexSets. + +(** Tracks a fraction. *) + +Module MutexTokens. + Canonical Structure cmraR : cmra := fracR. + + Class G `{Σ : cpp_logic} := { + #[local] has_own :: HasOwn (iPropI _Σ) cmraR; + #[local] has_upd :: HasOwnUpd (iPropI _Σ) cmraR; + #[local] has_valid :: HasOwnValid (iPropI _Σ) cmraR; + }. + #[global] Arguments G {_ _} Σ : assert. + + Definition token `{Σ : cpp_logic, !G Σ} + (γ : iprop.gname) (q : Qp) : mpred := + own γ (q : cmraR). + + #[global] Instance token_fractional + `{Σ : cpp_logic, !G Σ} γ : Fractional (token γ). + Proof. + intros q1 q2. rewrite /token -own_op /=. done. + Qed. + + #[global] Instance token_timeless + `{Σ : cpp_logic, !G Σ} γ q : Timeless (token γ q). + Proof. rewrite /token. apply _. Qed. + + #[global] Hint Opaque token : sl_opacity typeclass_instances. + + Section theory. + Context `{Σ : cpp_logic, !G Σ}. + + #[local] Existing Instance mpred_BiAffine. + + Lemma token_valid γ q : token γ q |-- [| (q ≤ 1)%Qp |]. + Proof. + rewrite /token. iIntros "H". + iDestruct (own_valid with "H") as %Hvalid. + iPureIntro. exact Hvalid. + Qed. + + Lemma token_valid_2 γ q1 q2 : + token γ q1 ** token γ q2 |-- [| (q1 + q2 ≤ 1)%Qp |]. + Proof. rewrite -fractional. apply token_valid. Qed. + + Lemma alloc : ⊢ |==> ∃ γ, token γ 1. + Proof. + iMod (own_alloc (1%Qp : cmraR)) as (γ) "H"; first done. + iModIntro. iExists γ. iExact "H". + Qed. + End theory. +End MutexTokens. + +(** Authoritative and exclusive fragment ownership of an optional thread ID + and the exact token share deposited by that thread. *) +Module OwnerTid. + Canonical Structure cmraR : cmra := + excl_authR (optionO (prodO thread_idTO (leibnizO cQp.t))). + + Class G `{Σ : cpp_logic} := { + #[local] has_own :: HasOwn (iPropI _Σ) cmraR; + #[local] has_upd :: HasOwnUpd (iPropI _Σ) cmraR; + #[local] has_valid :: HasOwnValid (iPropI _Σ) cmraR; + }. + #[global] Arguments G {_ _} Σ : assert. + + Definition owner_tid_auth `{Σ : cpp_logic, !G Σ} + (γ : iprop.gname) (o : option (thread_idT * cQp.t)) : mpred := + own γ ((●E o) : cmraR). + Definition owner_tid_frag `{Σ : cpp_logic, !G Σ} + (γ : iprop.gname) (o : option (thread_idT * cQp.t)) : mpred := + own γ ((◯E o) : cmraR). + + #[global] Instance owner_tid_auth_timeless + `{Σ : cpp_logic, !G Σ} γ o : Timeless (owner_tid_auth γ o). + Proof. rewrite /owner_tid_auth. apply _. Qed. + #[global] Instance owner_tid_frag_timeless + `{Σ : cpp_logic, !G Σ} γ o : Timeless (owner_tid_frag γ o). + Proof. rewrite /owner_tid_frag. apply _. Qed. + + #[global] Instance owner_tid_frag_exclusive + `{Σ : cpp_logic, !G Σ} γ : Exclusive1 (owner_tid_frag γ). + Proof. + intros o1 o2. rewrite /owner_tid_frag. + iIntros "H1 H2". + iDestruct (own_valid_2 with "H1 H2") as %Hvalid. + move: Hvalid. rewrite excl_auth_frag_op_valid. done. + Qed. + + #[global] Instance owner_tid_auth_WeaklyObjective + `{Σ : cpp_logic, !G Σ} γ o : WeaklyObjective (owner_tid_auth γ o). + Proof. rewrite /owner_tid_auth. apply _. Qed. + #[global] Instance owner_tid_frag_WeaklyObjective + `{Σ : cpp_logic, !G Σ} γ o : WeaklyObjective (owner_tid_frag γ o). + Proof. rewrite /owner_tid_frag. apply _. Qed. + + #[global] Instance owner_agree `{Σ : cpp_logic, !G Σ} γ o1 o2 : + Observe2 [| o1 = o2 |] (owner_tid_auth γ o1) (owner_tid_frag γ o2). + Proof. + apply observe_2_intro_only_provable. + rewrite /owner_tid_auth /owner_tid_frag. iIntros "A F". + iDestruct (own_valid_2 with "A F") as %HV. + iPureIntro. apply leibniz_equiv, excl_auth_agree, HV. + Qed. + + Lemma alloc `{Σ : cpp_logic, !G Σ} o : + ⊢ |==> ∃ γ, owner_tid_auth γ o ** owner_tid_frag γ o. + Proof. + iMod (own_alloc ((●E o ⋅ ◯E o) : cmraR)) as (γ) "H". + { apply excl_auth_valid. } + iModIntro. iExists γ. + rewrite /owner_tid_auth /owner_tid_frag -own_op. iExact "H". + Qed. + + Lemma owner_update `{Σ : cpp_logic, !G Σ} γ oa ofrag o' : + owner_tid_auth γ oa ** owner_tid_frag γ ofrag |-- + (|==> owner_tid_auth γ o' ** owner_tid_frag γ o'). + Proof. + rewrite /owner_tid_auth /owner_tid_frag. iIntros "[A F]". + iMod (own_update_2 with "A F") as "[$ $]"; + first apply (excl_auth_update _ _ o'). + done. + Qed. + + #[global] Hint Opaque owner_tid_auth owner_tid_frag : sl_opacity typeclass_instances. +End OwnerTid. diff --git a/rocq-brick-libstdcpp/proof/mutex/demo_cpp_proof.v b/rocq-brick-libstdcpp/proof/mutex/demo_cpp_proof.v index c7d36864..a5099c77 100644 --- a/rocq-brick-libstdcpp/proof/mutex/demo_cpp_proof.v +++ b/rocq-brick-libstdcpp/proof/mutex/demo_cpp_proof.v @@ -22,7 +22,7 @@ Definition TT : tele := [tele (_ : Z) (_ : Z)]. (** Canonical "constructor" for our telescope. *) Polymorphic Definition mk (a b : Z) : TT := {| tele_arg_head := a; tele_arg_tail := {| tele_arg_head := b; tele_arg_tail := () |} |}. -Succeed Definition b := recursive_mutex.Held 0 (mk 0 0). +Succeed Definition b := std_recursive_mutex.Held 0 (mk 0 0). (** Proof that [mk] is injective. Doesn't appear to be necessary here. *) #[global] Instance mk_inj: Inj2 eq eq eq mk. @@ -34,32 +34,32 @@ Definition P `{Σ : cpp_logic, σ : genv} (this : ptr) : TT -t> mpred := sl.lock Definition CR - `{Σ : cpp_logic, σ : genv, HasOwn (iPropI _) recursive_mutex.cmraR, !recursive_mutex.lockedG Σ} - (γ : recursive_mutex.rmutex_gname) (q : cQp.t) : Rep := + `{Σ : cpp_logic, σ : genv, MUTEX_SETS : !mutex_set_G Σ, + !std_recursive_mutex.G Σ, !HasStdThreads Σ} + (γ : std_recursive_mutex.gname) (q : cQp.t) : Rep := structR "C" q ** - _field "C::mut" |-> recursive_mutex.derivedR γ q ** as_Rep (fun this : ptr => - recursive_mutex.inv_rmutex γ (∃ a_b : tele_arg TT, - @tele_app TT (fun _ => mpred) (P this) a_b)). + this ,, _field "C::mut" |-> std_recursive_mutex.R γ q + (∃ a_b : tele_arg _, @tele_app TT (fun _ => mpred) (P this) a_b)). #[only(cfractional,ascfractional,type_ptr)] derive CR. #[only(lazy_unfold)] derive CR. -Section recursive_mutex. - Import recursive_mutex. +Section std_recursive_mutex. + Import std_recursive_mutex. Context `{Σ : cpp_logic, σ : genv}. Context {HAS_THREADS : HasStdThreads Σ}. - Context {has_rmutex : HasOwn (iPropI _) recursive_mutex.cmraR}. - Context `{!recursive_mutex.lockedG Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. - Lemma acquireable_update_equiv {TT : tele} γ th f t1 t2 P : + Lemma acquireable_update_equiv {TT : tele} this γ qt th f t1 t2 P : acquire t1 t2 -> - acquireable γ th (update f t1) P ⊣⊢ acquireable γ th (release (TT := TT) (update f t2)) P. + acquireable this γ qt th (update f t1) P ⊣⊢ acquireable this γ qt th (release (TT := TT) (update f t2)) P. Proof. intros. - by erewrite recursive_mutex.update_eq. + by erewrite std_recursive_mutex.update_eq. Qed. -End recursive_mutex. +End std_recursive_mutex. #[only(fwd(l2r))] derive acquireable_update_equiv. #[only(bwd(l2r))] derive acquireable_update_equiv. @@ -67,8 +67,8 @@ End recursive_mutex. Section with_cpp. Context `{Σ : cpp_logic, σ : genv}. Context {HAS_THREADS : HasStdThreads Σ}. - Context {has_rmutex : HasOwn (iPropI _) recursive_mutex.cmraR}. - Context `{!recursive_mutex.lockedG Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. #[global] Instance: LearnEq2 CR'. Proof. solve_learnable. Qed. @@ -77,15 +77,17 @@ Section with_cpp. (\this this \arg{x} "x" (Vint x) \prepost{γ q} this |-> CR γ q - \pre{args th} recursive_mutex.acquireable γ th args (TT:=TT) (P this) - \post recursive_mutex.acquireable γ th (TT:=TT) (recursive_mutex.update (TT:=TT) (fun (a b : Z) => mk (trim 64 (a+x)) b) args) (P this)). + \persist{th} current_thread th + \pre{args qt} std_recursive_mutex.acquireable (this ,, _field "C::mut") γ qt th args (TT:=TT) (P this) + \post std_recursive_mutex.acquireable (this ,, _field "C::mut") γ qt th (TT:=TT) (std_recursive_mutex.update (TT:=TT) (fun (a b : Z) => mk (trim 64 (a+x)) b) args) (P this)). cpp.spec "C::update_b(long)" as C_update_b from demo_cpp.source with (\this this \arg{x} "x" (Vint x) \prepost{γ q} this |-> CR γ q - \pre{args th} recursive_mutex.acquireable γ th args (TT:=TT) (P this) - \post recursive_mutex.acquireable γ th (TT:=TT) (recursive_mutex.update (TT:=TT) (fun (a b : Z) => mk a (trim 64 (b + x))) args) (P this)). + \persist{th} current_thread th + \pre{args qt} std_recursive_mutex.acquireable (this ,, _field "C::mut") γ qt th args (TT:=TT) (P this) + \post std_recursive_mutex.acquireable (this ,, _field "C::mut") γ qt th (TT:=TT) (std_recursive_mutex.update (TT:=TT) (fun (a b : Z) => mk a (trim 64 (b + x))) args) (P this)). #[global] Instance CR_learn : Cbn (Learn (learn_eq ==> any ==> learn_hints.fin) CR). Proof. solve_learnable. Qed. @@ -145,18 +147,23 @@ Section with_cpp. (\this this \arg{x} "x" (Vint x) \prepost{γ q} this |-> CR γ q - \pre{args th} recursive_mutex.acquireable γ th args (TT:=TT) (P this) - \post recursive_mutex.acquireable γ th (TT:=TT) (recursive_mutex.update (TT:=TT) (fun (a b : Z) => mk (trim 64 (a+x)) (trim 64 (b-x))) args) (P this)). + \persist{th} current_thread th + \pre{args qt} std_recursive_mutex.acquireable (this ,, _field "C::mut") γ qt th args (TT:=TT) (P this) + \post std_recursive_mutex.acquireable (this ,, _field "C::mut") γ qt th (TT:=TT) (std_recursive_mutex.update (TT:=TT) (fun (a b : Z) => mk (trim 64 (a+x)) (trim 64 (b-x))) args) (P this)). Lemma transfer_ok : verify[source] "C::transfer(int)". Proof. verify_spec; go. destruct args as [a [b []]]; work. + pose (updated := mk (trim 64 (a + x)) (trim 64 (b + (0 - x)))). + iExists (std_recursive_mutex.acquireable (this ,, _field "C::mut") γ qt th + (std_recursive_mutex.release (std_recursive_mutex.Held n updated)) (P this)), updated, n. + go. Qed. Lemma partial_transfer_link : denoteModule source ∗ - recursive_mutex.lock_spec_alt' ∗ recursive_mutex.unlock_spec_alt' + std_recursive_mutex.std_lock_spec_alt ∗ std_recursive_mutex.std_unlock_spec_alt ⊢ C_transfer_int. Proof. work. diff --git a/rocq-brick-libstdcpp/proof/mutex/inc.hpp b/rocq-brick-libstdcpp/proof/mutex/inc.hpp index de27243d..0df2d109 100644 --- a/rocq-brick-libstdcpp/proof/mutex/inc.hpp +++ b/rocq-brick-libstdcpp/proof/mutex/inc.hpp @@ -1,6 +1,10 @@ #include +#include template class std::lock_guard; +template class std::lock_guard; +template class std::lock_guard; +template class std::lock_guard>; template class std::unique_lock; template class std::unique_lock; template class std::scoped_lock; diff --git a/rocq-brick-libstdcpp/proof/mutex/proof/lock_guard.v b/rocq-brick-libstdcpp/proof/mutex/proof/lock_guard.v index 74de4d0d..59d7de28 100644 --- a/rocq-brick-libstdcpp/proof/mutex/proof/lock_guard.v +++ b/rocq-brick-libstdcpp/proof/mutex/proof/lock_guard.v @@ -1,40 +1,518 @@ Require Import skylabs.auto.cpp.proof. Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. +Require Import skylabs.brick.libstdcpp.mutex.spec.recursive_mutex. +Require Import skylabs.brick.libstdcpp.mutex.spec.unique_lock. +Require Import skylabs.brick.libstdcpp.shared_mutex.shared_mutex. Require Import skylabs.brick.libstdcpp.mutex.spec.lock_guard. +Require Import skylabs.brick.libstdcpp.mutex.requirements. Require Import skylabs.brick.libstdcpp.mutex.inc_hpp. Import linearity. +Import lock_guard R_unfold. + +(* Verification of the lock_guard template code. *) + +#[local] Hint Resolve fractional.UNSAFE_read_prim_learn : sl_opacity. +#[local] Hint Opaque do_lock : sl_opacity. + +Definition lock_guard_name (mutex : name) : name := + "std::lock_guard" .<< Atype (Tnamed mutex) >>. + +(* Pull the C++ lock_guard template AST. *) +Definition lock_guard_ctor_template : Ctor. +Proof. + let c := eval vm_compute in + (meta__source.(skylabs.lang.cpp.syntax.templates.msymbols) !! + (("std::lock_guard" .<< Atype (Tparam "_Mutex") >>) .:: + Nctor [Tref (Tparam "_Mutex")])) in + lazymatch c with + | Some (mcore.Template _ (Oconstructor ?ctor)) => exact ctor + end. +Defined. + +Definition lock_guard_ctor_subst (mutex : name) : subst.subst.t := + (∅ &: subst.subst._type_param .= {["_Mutex"%pstring := Tnamed mutex]} + &: subst.subst._resolved_parenlist .= + {[(Tref (Tnamed mutex), [Tref (Tnamed mutex)]) := + fun es => default (Eunsupported "lock_guard reference initializer" Tvoid) + (head es)]} + &: subst.subst._resolved_member_type .= + {[(Tnamed mutex, Nlocal (Nid "lock")) := + Tnamed (mutex .:: Nfunction function_qualifiers.N "lock" [])]} + &: subst.subst._resolved_call .= + {[(mutex .:: Nfunction function_qualifiers.N "lock" [], []) := + fun es => Emember_call false + (inl (mutex .:: Nfunction function_qualifiers.N "lock" [], + Direct, "void()"%cpp_type)) + (Emember true (Ethis (Tptr (Tnamed (lock_guard_name mutex)))) + (Nid "_M_device") false (Tref (Tnamed mutex))) es]})%lens. + +(* Template lock_guard constructor. *) +Definition lock_guard_ctor (mutex : name) : Ctor. +Proof. + let rec extract result := + let result := eval cbv -[compare.name_eq_dec compare.type_eq_dec] in result in + lazymatch result with + | inr ?ctor => exact ctor + | context C [compare.name_eq_dec ?n ?n] => + let result := context C [@left (n = n) (n ≠ n) eq_refl] in extract result + | context C [compare.type_eq_dec ?ty ?ty] => + let result := context C [@left (ty = ty) (ty ≠ ty) eq_refl] in extract result + end in + extract (subst_ap.subst.run2 + (subst_ap.subst.M2.subst_ctor meta__source lock_guard_ctor_template) + (lock_guard_ctor_subst mutex)). +Defined. + +(* Verify that [lock_guard_ctor] specialized to "std::mutex" is equal to the AST + of "std::lock_guard::lock_guard(std::mutex&)". + can take anything BasicLockable, and we test specializations with + , , , and >. *) +Lemma lock_guard_ctor_std_mutex_ast : + source.(symbols) !! "std::lock_guard::lock_guard(std::mutex&)"%cpp_name = + Some (Oconstructor (lock_guard_ctor "std::mutex")). +Proof. vm_compute. reflexivity. Qed. + +Lemma lock_guard_ctor_std_recursive_mutex_ast : + source.(symbols) !! "std::lock_guard::lock_guard(std::recursive_mutex&)"%cpp_name = + Some (Oconstructor (lock_guard_ctor "std::recursive_mutex")). +Proof. vm_compute. reflexivity. Qed. + +Lemma lock_guard_ctor_std_shared_mutex_ast : + source.(symbols) !! "std::lock_guard::lock_guard(std::shared_mutex&)"%cpp_name = + Some (Oconstructor (lock_guard_ctor "std::shared_mutex")). +Proof. vm_compute. reflexivity. Qed. + +Lemma lock_guard_ctor_std_unique_lock_mutex_ast : + source.(symbols) !! "std::lock_guard>::lock_guard(std::unique_lock&)"%cpp_name = + Some (Oconstructor (lock_guard_ctor "std::unique_lock"%cpp_name)). +Proof. vm_compute. reflexivity. Qed. + +Definition lock_guard_struct (mutex : name) (sz al : N) : Struct := + Build_Struct [] + [mkMember (Nid "_M_device") (Tref (Tnamed mutex)) false None (Build_LayoutInfo 0)] + [] [] (lock_guard_name mutex .:: Ndtor) false None Unspecified sz al. + Section with_cpp. + Context `{Σ : cpp_logic, σ : genv}. + Context (tu : translation_unit) (mutex : name) (sz al : N). + Context {mutexT : Type} (mutexR : cQp.t -> mutexT -> Rep). + Context `{!BasicLockable (Tnamed mutex) mutexR}. + + Definition inner_lock_spec : mpred := + specify.exact.method (mutex .:: Nfunction function_qualifiers.N "lock" []) + function_qualifiers.N Tvoid [] (lock_basic_lockable (Tnamed mutex) mutexR). + #[local] Hint Opaque inner_lock_spec : sl_opacity. + + Hypothesis Hstruct : tu.(types) !! lock_guard_name mutex = + Some (Gstruct (lock_guard_struct mutex sz al)). + (* Assume the specific instantiation of lock_guard for some given mutex code + exists. *) + Hypothesis Hcode : + genv_tu.find_code (lock_guard_name mutex .:: Nctor [Tref (Tnamed mutex)]) tu = + Some (genv_tu.CTctor (inl (lock_guard_struct mutex sz al)) (lock_guard_ctor mutex)). + + (* Verification of lock_guard.ctor_spec against the template code (Hcode). *) + Lemma ctor_ok : + denoteModule tu ** inner_lock_spec |-- + lock_guard.ctor_spec (mutexR:=mutexR) (Tnamed mutex) tu. + Proof using Hcode Hstruct. + have Hfuel : exists n, NM.cardinal (tu.(types)) = S n. + { rewrite NM.cardinal_1. + have Hin := NM.elements_1 (NM.find_2 Hstruct). + destruct (NM.elements tu.(types)) eqn:Helems. + - inversion Hin. + - eexists. reflexivity. } + destruct Hfuel as [fuel Hfuel]. + match goal with |- _ |-- ?SP => + unshelve epose proof (tactics.VerifySpec.verify_spec_hint_obligation_1 + false false tu σ _ _ Σ _ _ + (genv_tu.CTctor (inl (lock_guard_struct mutex sz al)) (lock_guard_ctor mutex)) + SP ltac:(unfold lock_guard.ctor_spec, specify.template.ctor, + specify.exact.ctor; apply find_spec.unmaterialized_isspec) ltac:( + unfold mvcg.find_code_of_type; rewrite Hcode; + rewrite bool_decide_true; [constructor | reflexivity])) as Hverify + end. + iIntros "[#Htu #Hlock]". + iDestruct (Hverify with "Htu") as "(%Hmodels & Hverify)". + iApply "Hverify". + iClear "Hverify". + clear Hverify. + unfold lock_guard_ctor, lock_guard_struct, lock_guard_name. + repeat lazymatch goal with + | |- environments.envs_entails _ (wp_init_identity _ _ _ _) => fail + | _ => step + end. + PostCondition.hide_post. + iApply (identity.init_identity_struct' tu (lock_guard_name mutex) _ this Hmodels + (identity.Rec []) (identity.Rec []) emp emp + ltac:(cbn zeta; rewrite Hfuel; cbn [identity.derivations]; + rewrite Hstruct; constructor) + (identity.red_result _) (identity.red_result _)). + iSplit; first done. + iIntros "_". + unfold inner_lock_spec, specify.exact.method, lock_basic_lockable. + go. + rewrite wp_minvoke_O.unlock /wp_minvoke /wp_mfptr. + cbn. + go. + iApply (invoke.use_cptrR_method tu _ (Tnamed mutex) CC_C mutex QM + Tvoid Ar_Definite [] _ _ "void()"%cpp_type mp [] _ eq_refl with "Hlock"). + go. + iExists (q$m)%cQp, m, K. + iFrame. + go. + by rewrite cQp.scale_mut (right_id_L 1%Qp Qp.mul). + Qed. +End with_cpp. + + +Definition lock_guard_dtor_template : Dtor. +Proof. + let c := eval vm_compute in + (meta__source.(skylabs.lang.cpp.syntax.templates.msymbols) !! + (("std::lock_guard" .<< Atype (Tparam "_Mutex") >>) .:: Ndtor)) in + lazymatch c with + | Some (mcore.Template _ (Odestructor ?dtor)) => exact dtor + end. +Defined. + +Definition lock_guard_dtor_subst (mutex : name) : subst.subst.t := + (∅ &: subst.subst._type_param .= {["_Mutex"%pstring := Tnamed mutex]} + &: subst.subst._resolved_member_type .= + {[(Tnamed mutex, Nlocal (Nid "unlock")) := + Tnamed (mutex .:: Nfunction function_qualifiers.N "unlock" [])]} + &: subst.subst._resolved_call .= + {[(mutex .:: Nfunction function_qualifiers.N "unlock" [], []) := + fun es => Emember_call false + (inl (mutex .:: Nfunction function_qualifiers.N "unlock" [], + Direct, "void()"%cpp_type)) + (Emember true (Ethis (Tptr (Tnamed (lock_guard_name mutex)))) + (Nid "_M_device") false (Tref (Tnamed mutex))) es]})%lens. + +(* Resolve the destructor's implicit noexcept after instantiation. *) +Definition lock_guard_dtor (mutex : name) : Dtor. +Proof. + let rec extract result := + let result := eval cbv -[compare.name_eq_dec compare.type_eq_dec] in result in + lazymatch result with + | inr ?dtor => + exact (Build_Dtor dtor.(d_class) dtor.(d_cc) exception_spec.NoThrow dtor.(d_body)) + | context C [compare.name_eq_dec ?n ?n] => + let result := context C [@left (n = n) (n ≠ n) eq_refl] in extract result + | context C [compare.type_eq_dec ?ty ?ty] => + let result := context C [@left (ty = ty) (ty ≠ ty) eq_refl] in extract result + end in + extract (subst_ap.subst.run2 + (subst_ap.subst.M2.subst_dtor lock_guard_dtor_template) + (lock_guard_dtor_subst mutex)). +Defined. + +Lemma lock_guard_dtor_std_mutex_ast : + source.(symbols) !! "std::lock_guard::~lock_guard()"%cpp_name = + Some (Odestructor (lock_guard_dtor "std::mutex")). +Proof. vm_compute. reflexivity. Qed. + +Lemma lock_guard_dtor_std_recursive_mutex_ast : + source.(symbols) !! "std::lock_guard::~lock_guard()"%cpp_name = + Some (Odestructor (lock_guard_dtor "std::recursive_mutex"%cpp_name)). +Proof. vm_compute. reflexivity. Qed. + +Lemma lock_guard_dtor_std_shared_mutex_ast : + source.(symbols) !! "std::lock_guard::~lock_guard()"%cpp_name = + Some (Odestructor (lock_guard_dtor "std::shared_mutex"%cpp_name)). +Proof. vm_compute. reflexivity. Qed. + +Lemma lock_guard_dtor_std_unique_lock_mutex_ast : + source.(symbols) !! "std::lock_guard>::~lock_guard()"%cpp_name = + Some (Odestructor (lock_guard_dtor "std::unique_lock"%cpp_name)). +Proof. vm_compute. reflexivity. Qed. + +Section with_generic_dtor. + Context `{Σ : cpp_logic, σ : genv}. + Context (tu : translation_unit) (mutex : name) (sz al : N). + Context {mutexT : Type} (mutexR : cQp.t -> mutexT -> Rep). + Context `{!BasicLockable (Tnamed mutex) mutexR}. + Definition inner_unlock_spec : mpred := + specify.exact.method (mutex .:: Nfunction function_qualifiers.N "unlock" []) + function_qualifiers.N Tvoid [] (unlock_basic_lockable (Tnamed mutex) mutexR). + #[local] Hint Opaque do_unlock : sl_opacity. + #[local] Hint Opaque inner_unlock_spec : sl_opacity. + Hypothesis Hstruct : tu.(types) !! lock_guard_name mutex = + Some (Gstruct (lock_guard_struct mutex sz al)). + Hypothesis Hcode : + genv_tu.find_code (lock_guard_name mutex .:: Ndtor) tu = + Some (genv_tu.CTdtor (inl (lock_guard_struct mutex sz al)) (lock_guard_dtor mutex)). + (* Verification of lock_guard.dtor_spec against the template code (Hcode). *) + Lemma dtor_template_ok : + denoteModule tu ** inner_unlock_spec |-- + lock_guard.dtor_spec (mutexR:=mutexR) (Tnamed mutex) tu. + Proof using Hcode Hstruct. + have Hfuel : exists n, NM.cardinal (tu.(types)) = S n. + { rewrite NM.cardinal_1. + have Hin := NM.elements_1 (NM.find_2 Hstruct). + destruct (NM.elements tu.(types)) eqn:Helems. + - inversion Hin. + - eexists. reflexivity. } + destruct Hfuel as [fuel Hfuel]. + match goal with |- _ |-- ?SP => + unshelve epose proof (tactics.VerifySpec.verify_spec_hint_obligation_1 + false false tu σ _ _ Σ _ _ + (genv_tu.CTdtor (inl (lock_guard_struct mutex sz al)) (lock_guard_dtor mutex)) + SP ltac:(unfold lock_guard.dtor_spec, specify.template.dtor, + specify.exact.dtor; apply find_spec.unmaterialized_isspec) ltac:( + unfold mvcg.find_code_of_type; rewrite Hcode; + rewrite bool_decide_true; [constructor | reflexivity])) as Hverify + end. + iIntros "[#Htu #Hunlock]". + iDestruct (Hverify with "Htu") as "(%Hmodels & Hverify)". + iApply "Hverify". + iClear "Hverify". + clear Hverify. + unfold lock_guard_dtor, lock_guard_struct, lock_guard_name. + unfold inner_unlock_spec, specify.exact.method, unlock_basic_lockable. + go. + rewrite wp_minvoke_O.unlock /wp_minvoke /wp_mfptr. + cbn. + go. + iApply (invoke.use_cptrR_method tu _ (Tnamed mutex) CC_C mutex QM + Tvoid Ar_Definite [] _ _ "void()"%cpp_type (mutex_ptr m) [] _ eq_refl with "Hunlock"). + go. + iExists ((mutex_q m)$m)%cQp, (mutex_m m), K. + rewrite cQp.scale_mut (right_id_L 1%Qp Qp.mul). + repeat lazymatch goal with + | |- environments.envs_entails _ (wp_revert_identity _ _ _ _) => fail + | _ => step + end. + PostCondition.hide_post. + iApply (identity.revert_identity_struct' tu (lock_guard_name mutex) _ this Hmodels + (identity.Rec []) (identity.Rec []) emp emp + ltac:(cbn zeta; rewrite Hfuel; cbn [identity.derivations]; + rewrite Hstruct; constructor) + (identity.red_result _) (identity.red_result _)). + iSplit; first done. + iIntros "_". + go. + Qed. +End with_generic_dtor. + +(* Specialization of [ctor_ok] and [dtor_template_ok] for , + , , and >, whatever that means. *) + +Section with_std_mutex. Context `{Σ : cpp_logic, σ : genv}. Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!mutex.G Σ}. Import lock_guard. + Import R_unfold. + + Lemma ctor_std_mutex_ok : + denoteModule source ** mutex.lock_spec |-- + lock_guard.ctor_spec "std::mutex" source. + Proof. + apply (ctor_ok source "std::mutex" 8 8 + (fun q gP => mutex.R gP.1 q gP.2)). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_ctor_std_mutex_ast. + vm_compute. reflexivity. + Qed. + + Lemma dtor_std_mutex_ok : + denoteModule source ** mutex.unlock_spec |-- + lock_guard.dtor_spec "std::mutex" source. + Proof. + apply (dtor_template_ok source "std::mutex" 8 8 + (fun q gP => mutex.R gP.1 q gP.2)). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_dtor_std_mutex_ast. + vm_compute. reflexivity. + Qed. - (* TODO: not ideal *) #[global] Instance UNSAFE_R_learnable : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv}, Cbn (Learn (learn_eq ==> learn_eq ==> learn_eq ==> fin_at) mutex.R). Proof. solve_learnable. Qed. - #[global] Instance UNSAFE_token_learn : Cbn (Learn (req_eq ==> learn_eq ==> learn_hints.fin) mutex.token). - Proof. solve_learnable. Qed. + #[local] Instance: `{SplitRecord (M T)} := {}. - #[local] Hint Resolve fractional.UNSAFE_read_prim_learn : sl_opacity. +End with_std_mutex. - Lemma ctor_ok : verify[source] ctor_spec. +Section with_std_recursive_mutex. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. + + Lemma ctor_std_recursive_mutex_ok : + denoteModule source ** std_recursive_mutex.std_lock_spec_alt |-- + lock_guard.ctor_spec "std::recursive_mutex" source. Proof. - verify_spec. - go. - by rewrite left_id_L. + apply (ctor_ok source "std::recursive_mutex" 8 8 + (fun q gP => std_recursive_mutex.R gP.1 q gP.2)). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_ctor_std_recursive_mutex_ast. + vm_compute. reflexivity. Qed. - Lemma dtor_ok : verify[source] dtor_spec. + Lemma dtor_std_recursive_mutex_ok : + denoteModule source ** std_recursive_mutex.std_unlock_spec_alt |-- + lock_guard.dtor_spec "std::recursive_mutex" source. Proof. - verify_spec. - rewrite !R.unlock. - go. - by rewrite !left_id_L. + apply (dtor_template_ok source "std::recursive_mutex" 8 8 + (fun q gP => std_recursive_mutex.R gP.1 q gP.2)). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_dtor_std_recursive_mutex_ast. + vm_compute. reflexivity. Qed. +End with_std_recursive_mutex. -End with_cpp. +Section with_std_shared_mutex. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!shared_mutex.G Σ}. + + Lemma ctor_std_shared_mutex_ok : + denoteModule source ** shared_mutex.lock_spec |-- + lock_guard.ctor_spec "std::shared_mutex" source. + Proof. + apply (ctor_ok source "std::shared_mutex" 8 8 + (fun q gP => shared_mutex.R gP.1 q gP.2)). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_ctor_std_shared_mutex_ast. + vm_compute. reflexivity. + Qed. + + Lemma dtor_std_shared_mutex_ok : + denoteModule source ** shared_mutex.unlock_spec |-- + lock_guard.dtor_spec "std::shared_mutex" source. + Proof. + apply (dtor_template_ok source "std::shared_mutex" 8 8 + (fun q gP => shared_mutex.R gP.1 q gP.2)). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_dtor_std_shared_mutex_ast. + vm_compute. reflexivity. + Qed. +End with_std_shared_mutex. + +Section with_std_unique_lock_mutex. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!mutex.G Σ}. + + Definition unique_lock_mutexR (_ : cQp.t) (_ : unit) : Rep := + type_ptrR "std::unique_lock". + #[local] Hint Opaque unique_lock_mutexR : sl_opacity. + + #[local] Abbreviation unique_lockR := + (unique_lock.R "std::mutex" (fun q gP => mutex.R gP.1 q gP.2)). + + Definition unique_lock_mutex_do_lock (this : ptr) (_ : unit) (K : mpred) : mpred := + ∃ mm, + this |-> unique_lockR 1$m (Some mm) ** + [| ~~ mm.(unique_lock.is_held) |] ** + do_lock "std::mutex" mm.(unique_lock.mutex_ptr) mm.(unique_lock.mutex_m) + (this |-> unique_lockR 1$m + (Some (mm &: unique_lock._is_held .= true)%lens) -* K). + + Definition unique_lock_mutex_do_unlock (this : ptr) (_ : unit) (K : mpred) : mpred := + ∃ mm, + this |-> unique_lockR 1$m (Some mm) ** + [| mm.(unique_lock.is_held) |] ** + do_unlock "std::mutex" mm.(unique_lock.mutex_ptr) mm.(unique_lock.mutex_m) + (this |-> unique_lockR 1$m + (Some (mm &: unique_lock._is_held .= false)%lens) -* K). + + #[local] Instance unique_lock_mutex_basic_lockable : + BasicLockable "std::unique_lock" unique_lock_mutexR := + {| do_lock := unique_lock_mutex_do_lock; + do_unlock := unique_lock_mutex_do_unlock |}. + + Lemma unique_lock_mutex_lock_spec : + unique_lock.lock_spec "std::mutex" source |-- + inner_lock_spec "std::unique_lock"%cpp_name unique_lock_mutexR. + Proof. + unfold unique_lock.lock_spec, inner_lock_spec. + rewrite specify_notation.template_specify.unlock. + cbn. + have -> : resolve_value source "std::unique_lock"%cpp_name = None + by vm_compute. + change (specify.exact.method "std::unique_lock::lock()" + function_qualifiers.N Tvoid [] + (\this this + \pre{mm} this |-> unique_lockR 1$m (Some mm) + \require ~~ mm.(unique_lock.is_held) + \pre{K} do_lock "std::mutex" mm.(unique_lock.mutex_ptr) + mm.(unique_lock.mutex_m) K + \post this |-> unique_lockR 1$m + (Some (mm &: unique_lock._is_held .= true)%lens) ** K) |-- + inner_lock_spec "std::unique_lock"%cpp_name unique_lock_mutexR). + iApply specify_mono; intros this xs Q. + unfold lock_basic_lockable, unique_lock_mutex_do_lock. + cbn. + ework with br_erefl. + Qed. + + Lemma unique_lock_mutex_unlock_spec : + unique_lock.unlock_spec "std::mutex" source |-- + inner_unlock_spec "std::unique_lock"%cpp_name unique_lock_mutexR. + Proof. + unfold unique_lock.unlock_spec, inner_unlock_spec. + rewrite specify_notation.template_specify.unlock. + cbn. + have -> : resolve_value source "std::unique_lock"%cpp_name = None + by vm_compute. + change (specify.exact.method "std::unique_lock::unlock()" + function_qualifiers.N Tvoid [] + (\this this + \pre{mm} this |-> unique_lockR 1$m (Some mm) + \require mm.(unique_lock.is_held) + \pre{K} do_unlock "std::mutex" mm.(unique_lock.mutex_ptr) + mm.(unique_lock.mutex_m) K + \post this |-> unique_lockR 1$m + (Some (mm &: unique_lock._is_held .= false)%lens) ** K) |-- + inner_unlock_spec "std::unique_lock"%cpp_name unique_lock_mutexR). + iApply specify_mono; intros this xs Q. + unfold unlock_basic_lockable, unique_lock_mutex_do_unlock. + cbn. + ework with br_erefl. + Qed. + + Lemma ctor_std_unique_lock_mutex_ok : + denoteModule source ** unique_lock.lock_spec "std::mutex" source |-- + lock_guard.ctor_spec (mutexR := unique_lock_mutexR) + "std::unique_lock" source. + Proof. + rewrite unique_lock_mutex_lock_spec. + apply (ctor_ok source "std::unique_lock"%cpp_name 8 8 + unique_lock_mutexR). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_ctor_std_unique_lock_mutex_ast. + vm_compute. reflexivity. + Qed. + + Lemma dtor_std_unique_lock_mutex_ok : + denoteModule source ** unique_lock.unlock_spec "std::mutex" source |-- + lock_guard.dtor_spec (mutexR := unique_lock_mutexR) + "std::unique_lock" source. + Proof. + rewrite unique_lock_mutex_unlock_spec. + apply (dtor_template_ok source "std::unique_lock"%cpp_name 8 8 + unique_lock_mutexR). + - vm_compute. reflexivity. + - unfold genv_tu.find_code. + rewrite lock_guard_dtor_std_unique_lock_mutex_ast. + vm_compute. reflexivity. + Qed. +End with_std_unique_lock_mutex. diff --git a/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock.v b/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock.v index 45ff3244..6f71d058 100644 --- a/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock.v +++ b/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock.v @@ -11,6 +11,8 @@ NES.Begin unique_lock. Section with_cpp. Context `{Σ : cpp_logic} {σ : genv}. Context `{HAS_THREADS : !HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!mutex.G Σ}. Import R_unfold. @@ -43,10 +45,9 @@ NES.Begin unique_lock. "std::unique_lock::unique_lock(std::mutex&)". Proof. verify_spec; go. - iExists K. + iExists K, t, q. (* Time Succeed solve [setoid_rewrite cQp.scale_mut; setoid_rewrite (right_id_L 1%Qp Qp.mul); ego with br_erefl]. *) - iExists _, q. rewrite cQp.scale_mut (right_id_L 1%Qp Qp.mul). go with br_erefl. Qed. diff --git a/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock_recursive_mutex.v b/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock_recursive_mutex.v index dbb6e62e..36f540a2 100644 --- a/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock_recursive_mutex.v +++ b/rocq-brick-libstdcpp/proof/mutex/proof/unique_lock_recursive_mutex.v @@ -12,8 +12,8 @@ NES.Begin unique_lock. Context `{Σ : cpp_logic} {σ : genv}. Context `{HAS_THREADS : !HasStdThreads Σ}. - Context `{!recursive_mutex.lockedG Σ}. - Context `{!HasOwn (iPropI _) recursive_mutex.cmraR}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. Import R_unfold. diff --git a/rocq-brick-libstdcpp/proof/mutex/spec/lock_guard.v b/rocq-brick-libstdcpp/proof/mutex/spec/lock_guard.v index 392b5445..5586ec66 100644 --- a/rocq-brick-libstdcpp/proof/mutex/spec/lock_guard.v +++ b/rocq-brick-libstdcpp/proof/mutex/spec/lock_guard.v @@ -1,127 +1,103 @@ Require Import skylabs.auto.cpp.proof. -Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. -Require Export skylabs.brick.libstdcpp.runtime.pred. - Require Import skylabs.brick.libstdcpp.mutex.inc_hpp. +Require Import skylabs.brick.libstdcpp.mutex.inc_hpp_templates. - -Import linearity. - -Section TO_UPSTREAM. - Lemma cQp_mut_add q1 q2 : - (q1 + q2)$m%cQp = (q1$m + q2$m)%cQp. - Proof. done. Qed. -End TO_UPSTREAM. - -Module lock_guard. - +Require Export skylabs.brick.libstdcpp.runtime.pred. +Require Import skylabs.brick.libstdcpp.mutex.spec.prelude. +Require Import skylabs.brick.libstdcpp.mutex.requirements. + +(* Generic specs for "lock_guard". *) +NES.Begin lock_guard. + Record M {T : Type} : Type := Mk + { mutex_ptr : ptr + ; mutex_q : Qp + ; mutex_m : T }. + #[global] Arguments M _ : clear implicits. + #[global] Arguments Mk {_} _ _ _. + #[only(lens)] derive M. + + #[global] Hint Opaque mutex_ptr : sl_opacity. + + (* Wrapper of a basic lockable object with resource predicate [mutexR]. *) sl.lock - Definition R `{Σ : cpp_logic, !HasStdThreads Σ} {σ : genv} (mp : ptr * gname * Qp) (q : cQp.t) (P : mpred) : Rep := - structR "std::lock_guard" q ** - let '(mp, g, q') := mp in - _field "std::lock_guard::_M_device" |-> refR<"std::mutex"> q mp ** - pureR ( - mp |-> mutex.R g (q * q')$m P). - - #[only(type_ptr)] derive R. - #[only(lazy_unfold)] derive R. + Definition R + `{Σ : cpp_logic} {σ : genv} ty {T} mutexR `{!BasicLockable ty (T:=T) mutexR} + (q : cQp.t) (m : M T) : Rep := + let lgty := "std::lock_guard" .<< Atype ty >> in + structR lgty q ** + _field (lgty .:: Nid "_M_device") |-> refR q m.(mutex_ptr) ** + pureR (m.(mutex_ptr) |-> mutexR (cQp.scale m.(mutex_q) q) m.(mutex_m)). Section with_RepFor. Import rep.RepFor. Import RepScheme. - #[global] Instance repfor `{Σ : cpp_logic, !HasStdThreads Σ} {σ : genv} : - rep.RepFor.C "std::lock_guard" - [ArgType.Constant _; ArgType.CFrac; ArgType.Constant _] - R := {}. + #[global] Instance repfor `{Σ : cpp_logic} {σ : genv} ty {T} mutexR `{!BasicLockable ty (T:=T) mutexR} : + rep.RepFor.C (Tnamed $ "std::lock_guard" .<< Atype ty >>) + [ArgType.CFrac; ArgType.Constant _] (R ty mutexR) := {}. End with_RepFor. - (** - These automated proofs fail, so we prove it by hand. - [R_cfrac] does not seem too useful (why ever split a lock guard?), but let's - prove it anyway to test our infrastructure. *) - Fail #[only(cfractional,cfracvalid,ascfractional)] derive R. - - #[only(cfracvalid)] derive R. - -Section with_cpp. - Context `{Σ : cpp_logic, σ : genv}. - Context {HAS_THREADS : HasStdThreads Σ}. - - #[global] Instance R_learn : - Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) lock_guard.R) := - ltac:(solve_learnable). - - Set Printing Coercions. - - Section with_R_cfrac'. - #[local] Instance R_cfrac' g q' P : - CFractional (λ q, mutex.R g (cQp.frac q * q')$m P). - (* Proof. - intros q1 q2. - rewrite -(cfractional (P := λ q, mutex.R _ q _)). - rewrite -cQp_mut_add. - rewrite -Qp.mul_add_distr_r. - Succeed done. - by rewrite -cQp.frac_add. - Restart. - *) - Proof. - intros q1 q2. - rewrite cQp.frac_add. - rewrite Qp.mul_add_distr_r. - rewrite cQp_mut_add. - by rewrite (cfractional (P := λ q, mutex.R g q P)). - Qed. - - #[global] Instance R_cfrac mp : CFractional1 (R mp). - Proof. rewrite R.unlock. apply _. Qed. - End with_R_cfrac'. - - Fail #[only(ascfractional)] derive R. - #[global] Instance R_as_cfrac mp : AsCFractional1 (R mp). - Proof. solve_as_cfrac. Qed. - - cpp.spec "std::lock_guard::lock_guard(std::mutex &)" as ctor_spec from source with ( - \this this - \arg{mp} "m" (Vptr mp) - \persist{thr} current_thread thr - \pre{g q P} mp |-> mutex.R g q$m P - \pre mutex.token g q - \post - this |-> R (mp, g, q) 1$m P ** - P ** mutex.locked g thr q - ). - - cpp.spec "std::lock_guard::~lock_guard()" as dtor_spec from source with ( - \this this - \pre{mp g q P} this |-> R (mp, g, q) 1$m P - \persist{thr} current_thread thr - \pre mutex.locked g thr q - \pre ▷P - \post - mutex.token g q ** - mp |-> mutex.R g q$m P - ). - - Section with_prelude. - - Import skylabs.auto.cpp.prelude.proof. - - Lemma mutex_borrow mp g P (this : ptr) (q1 q2 : Qp) : - this |-> R (mp, g, (q1 + q2)%Qp) 1$m P |-- - mp |-> mutex.R g q1$m P ** - this |-> R (mp, g, q2) 1$m P. - Proof. - rewrite R.unlock. - work. - iDestruct select (mp |-> mutex.R g _ P) as "[??]". - (* Unnecessary with our prelude. *) - (* rewrite !left_id_L. *) - work. - Qed. - End with_prelude. - -End with_cpp. -End lock_guard. - + Module R_unfold. + #[only(lazy_unfold(export))] derive R. + End R_unfold. + + Section with_cpp. + Context `{Σ : cpp_logic} {σ : genv}. + Context ty {mutexT mutexR} `{!BasicLockable ty (T:=mutexT) mutexR}. + + #[only(type_ptr)] derive R. + #[only(cfracvalid)] derive R. + + Section with_cfrac. + Context `{CFrac : !CFractional1 mutexR}. + #[local] Set Default Proof Using "CFrac". + + #[global] Instance R_cfrac : CFractional1 (R ty mutexR). + Proof. rewrite R.unlock. apply _. Qed. + + #[global] Instance R_as_cfrac : AsCFractional1 (R ty mutexR). + Proof. solve_as_cfrac. Qed. + + Lemma mutex_borrow mp m (this : ptr) (q1 q2 : Qp) : + this |-> R ty mutexR 1$m (Mk mp (q1 + q2) m) |-- + mp |-> mutexR q1$m m ** + this |-> R ty mutexR 1$m (Mk mp q2 m). + Proof. + rewrite R.unlock /= !cQp.scale_mut !(right_id_L 1%Qp Qp.mul). + change ((q1 + q2)$m)%cQp with (q1$m + q2$m)%cQp. + rewrite (cfractional (P := λ q, mutexR q m)). + iIntros "($ & $ & $ & $)". + Qed. + End with_cfrac. + + #[local] Abbreviation R := (R ty (T:=mutexT) mutexR). + + #[global] Instance R_learn : LearnEqF1 R := ltac:(solve_learnable). + + (* Register the instantiated signature directly so it can also be verified + with a symbolic mutex type. Keep [tu] for the existing spec interface. *) + Definition ctor_spec (tu : translation_unit) : mpred := + specify.template.ctor ("std::lock_guard" .<< Atype ty >>) [Tref ty] $ + \this this + \arg{mp} "" (Vptr mp) + \pre{q m} mp |-> mutexR q$m m + \pre{K} do_lock ty mp m K + \post + this |-> R 1$m (Mk mp q m) ** K. + #[global] Hint Opaque ctor_spec : sl_opacity. + #[global] Arguments ctor_spec : simpl never. + Definition SpecFor_ctor_spec := RegisterSpec (ctor_spec source). + #[global] Existing Instance SpecFor_ctor_spec. + + Definition dtor_spec (tu : translation_unit) : mpred := + specify.template.dtor ("std::lock_guard" .<< Atype ty >>) $ + \this this + \pre{m} this |-> R 1$m m + \pre{K} do_unlock ty m.(mutex_ptr) m.(mutex_m) K + \post K ** m.(mutex_ptr) |-> mutexR m.(mutex_q)$m m.(mutex_m). + #[global] Hint Opaque dtor_spec : sl_opacity. + #[global] Arguments dtor_spec : simpl never. + Definition SpecFor_dtor_spec := RegisterSpec (dtor_spec source). + #[global] Existing Instance SpecFor_dtor_spec. + End with_cpp. +NES.End lock_guard. diff --git a/rocq-brick-libstdcpp/proof/mutex/spec/mutex.v b/rocq-brick-libstdcpp/proof/mutex/spec/mutex.v index f013828f..544c6eb6 100644 --- a/rocq-brick-libstdcpp/proof/mutex/spec/mutex.v +++ b/rocq-brick-libstdcpp/proof/mutex/spec/mutex.v @@ -12,165 +12,363 @@ Require Export skylabs.brick.libstdcpp.runtime.pred. Require Import skylabs.brick.libstdcpp.mutex.inc_hpp. Require Import skylabs.brick.libstdcpp.mutex.requirements. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Export lock_ghost2.mutex_set_G (mutex_set_G). Import linearity. -(* TODO UPSTREAM. *) +(** A MUTEX_PREDS module says a mutex spec is parametrized by some `token` and + `locked`. The exact model depends on the implementation. *) + +Module Type MUTEX_PREDS. + Parameter gname : Set. + Parameter cpp_ty : type. + Parameter mutex_inv_namespace : gname -> namespace. + (* The invariant namespace cannot be [nroot] for practical purposes. + TODO maybe other mutex preds should also have this? *) + Axiom mutex_inv_namespace_not_root : forall g, mutex_inv_namespace g <> nroot. + + Parameter G : forall `{Σ : cpp_logic}, Type. + Existing Class G. + #[global] Arguments G {_ _} Σ : assert. + + (** [cQp.t] describes the permissions transferred by lock and unlock. + The permission might involve points-to, so use cQp.t instead of QP. *) + Parameter token : forall `{Σ : cpp_logic, !G Σ}, + gname -> cQp.t -> mpred. + Definition not_locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (g : gname) (th : thread_idT) (qt : cQp.t) : mpred := + token g qt ** MutexSets.my_mutexes th + (coPset.CoPset $ ↑mutex_inv_namespace g). + #[global] Hint Opaque not_locked : sl_opacity typeclass_instances. + + Parameter locked : forall `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv}, + ptr -> gname -> thread_idT -> cQp.t -> mpred. + + #[global] Declare Instance token_fractional + `{Σ : cpp_logic, !G Σ} γ : CFractional (token γ). + #[global] Declare Instance token_timeless + `{Σ : cpp_logic, !G Σ} γ q : Timeless (token γ q). + #[global] Instance not_locked_timeless + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this γ th q : + Timeless (not_locked this γ th q). + Proof. rewrite /not_locked. apply _. Qed. + (** Each thread has at most one handle to attempt locking this mutex. *) + #[global] Instance not_locked_exclusive + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this γ th : + Exclusive1 (not_locked this γ th). + Proof. + intros q1 q2. rewrite /not_locked. + iIntros "[_ H1] [_ H2]". + have Hoverlap : ~ ((↑mutex_inv_namespace γ : coPset) ## ↑mutex_inv_namespace γ). + { have Hnonempty := nclose_non_empty (mutex_inv_namespace γ). set_solver. } + iDestruct (MutexSets.my_mutexes_exclusive th + (↑mutex_inv_namespace γ) (↑mutex_inv_namespace γ) Hoverlap + with "[$H1 $H2]") as %[]. + Qed. + #[global] Declare Instance locked_timeless + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this γ th q : + Timeless (locked this γ th q). + #[global] Declare Instance locked_exclusive + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this γ : + Exclusive2 (locked this γ). + + #[global] Declare Instance locked_WeaklyObjective + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this γ th q : + WeaklyObjective (locked this γ th q). + + Section with_cpp. + Context `{Σ : cpp_logic}. + + (** Fractional ownership of a mutex guarding the predicate <

>. *) + Parameter R : forall `{MUTEX_SETS : !mutex_set_G Σ, !G Σ} + {HAS_THREADS : HasStdThreads Σ} {σ : genv}, gname -> cQp.t -> mpred -> Rep. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + #[global] Hint Opaque R : sl_opacity typeclass_instances. + #[only(cfractional,cfracvalid,ascfractional)] derive R. + #[global] Declare Instance R_type_ptr : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv} g q P, + Observe (type_ptrR cpp_ty) (R g q P). + #[global] Declare Instance R_learnable : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv}, + Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). + + Context {σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + + #[global] Instance locked_learn : + Cbn (Learn (req_eq ==> learn_eq ==> req_eq ==> req_eq ==> learn_hints.fin) locked). + Proof. solve_learnable. Qed. + + (* TODO readd the later on the lock/unlock continuations. *) + Definition do_lock (this : ptr) (lk : gname * mpred) (K : mpred) : mpred := + ∃ thr qt, current_thread thr ** not_locked this lk.1 thr qt ** + (locked this lk.1 thr qt ** lk.2 -* K). + #[global] Arguments do_lock /. + + Definition do_unlock (this : ptr) (lk : gname * mpred) (K : mpred) : mpred := + ∃ thr qt, current_thread thr ** locked this lk.1 thr qt ** ▷lk.2 ** + (not_locked this lk.1 thr qt -* K). + #[global] Arguments do_unlock /. + + #[global] Instance mutex_basic_lockable : + BasicLockable (T := gname * mpred) cpp_ty + (fun q gP => R gP.1 q gP.2) := + { do_lock := do_lock + ; do_unlock := do_unlock }. + + Definition do_try_lock (this : ptr) (lk : gname * mpred) + (K : bool -> mpred) : mpred := + ∃ thr qt, current_thread thr ** not_locked this lk.1 thr qt ** + ∀ b : bool, + (if b then lk.2 ** locked this lk.1 thr qt + else not_locked this lk.1 thr qt) -* K b. + #[global] Arguments do_try_lock /. + + #[global] Instance mutex_lockable : + Lockable (T:=gname * mpred) cpp_ty (λ q γP, R γP.1 q γP.2) := + { do_try_lock := do_try_lock }. + + (** Can switch the protected resource from [emp] to [P]. + FIXME maybe it should be any [P'] to [P]? + + Initializing [this |-> R old 1$m emp] probably depends on the mutex + implementation. *) + Axiom init_R : forall (this : ptr) (old : gname) + (N : namespace) (P : mpred), + WeaklyObjective P -> + N <> nroot -> + this |-> R old 1$m emp ** token old 1$m ** ▷P |-- + (|={⊤}=> ∃ g, + [| mutex_inv_namespace g = N |] ** + this |-> R g 1$m P ** token g 1$m). + + (** For any [h1],[h2] ∈ {[locked _ _ th _], [not_locked _ _ th _]}, we + would like [h1 ** h2 ⊢ False] because there should be at most one such + handle for thread [th]. We already have [locked_exclusive] and + [not_locked_exclusive], so we also want [locked ** not_locked ⊢ False]. + But here we require something more specific because it is easier for the + implementation proof: we don't need the whole [not_locked] but just the + [my_mutex] part, and we also need to prove this by opening the mutex + invariant in [R], thus the [this |-> R ...] in the LHS and the update on + the RHS. *) + Axiom locked_my_mutexes_exclusive : forall (this : ptr) g th q qt P E, + ↑mutex_inv_namespace g ⊆ E -> + this |-> R g q P ** locked this g th qt ** + MutexSets.my_mutexes th + (coPset.CoPset $ ↑mutex_inv_namespace g) |-- (|={E}=> False). + + End with_cpp. +End MUTEX_PREDS. + +(* TODO UPSTREAM. FIXME what does this do? *) #[global] Instance SplitRecord_prod A B : SplitRecord (@prod A B) := {}. -Module mutex. +Module mutex_spec (Preds : MUTEX_PREDS). Section with_cpp. - Context `{Σ : cpp_logic}. - - (** Fractional ownership of a <> guarding the predicate <

>. *) - Parameter R : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv}, gname -> cQp.t -> mpred -> Rep. - #[only(cfractional,cfracvalid,ascfractional,type_ptr="std::mutex")] derive R. - #[global] Declare Instance R_learnable : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv}, - Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). - - (** Owning [mutex_token γ 1] proves that the mutex is not locked, and - therefore can be safely destroyed: the standard specifies that calling - [std::mutex::~mutex()] while holding the lock results in undefined behavior. - *) - Parameter token : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv}, gname -> Qp -> mpred. - #[only(fractional,fracvalid,asfractional,timeless)] derive token. - - Section with_RepFor. - Import rep.RepFor. - Import RepScheme. - - #[global] Instance repfor `{!HasStdThreads Σ} {σ : genv} : - rep.RepFor.C "std::mutex" [ArgType.Constant _; ArgType.CFrac; ArgType.Constant _] - (funI γ q P => R γ q P ∗ pureR (token γ q)) := {}. - End with_RepFor. - - - (** A resource enforcing that the thread calling unlock must be the same thread - that owns the lock - - << - \persist{th} >={ L_TI } th - \pre{j} mutex_locked g j - test_unlock(std::mutex & m) { - m.unlock(); - } - >> - - this succeeds: - - << - \persist{th} >={ L_TI } th - \pre mutex_locked g th - same test_unlock - >> - *) - Parameter locked : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv}, - gname -> thread_idT -> Qp -> mpred. - #[only(timeless)] derive locked. - - (** locked takes a [Qp] but _cannot_ be split. *) - #[only(exclusive)] derive locked. - - Context `{MOD : source ⊧ σ}. + Import Preds. + Context `{Σ : cpp_logic} {σ : genv}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. Context {HAS_THREADS : HasStdThreads Σ}. - #[global] Instance locked_learn : Cbn (Learn (req_eq ==> learn_eq ==> learn_eq ==> learn_hints.fin) locked). - Proof. solve_learnable. Qed. - - - cpp.spec "std::mutex::mutex()" as ctor_spec with ( - \this this - \pre{P} ▷P - \post Exists g, this |-> R g 1$m P ** token g 1). - - cpp.spec "std::mutex::~mutex()" as dtor_spec with ( - \this this - \pre{g P} this |-> R g 1$m P ** token g 1 - \post P). - - (* "Inline" version of these specs. *) - cpp.spec "std::mutex::lock()" as lock_spec_alt with ( - \this this - \prepost{q P g} this |-> R g q P - \persist{thr} current_thread thr - \pre{q'} token g q' - \post P ** locked g thr q'). - - Definition do_lock (lk : gname * mpred) (K: mpred) : mpred := - let g := lk.1 in - let P := lk.2 in - ∃ q thr, current_thread thr ∗ token g q ∗ - (* TODO readd *) - (* ▷ *) - (locked g thr q ** P -* K). - #[global] Arguments do_lock /. - - cpp.spec "std::mutex::unlock()" as unlock_spec_alt with ( - \this this - \prepost{q P g} this |-> R g q P - \persist{thr} current_thread thr - \pre{q'} locked g thr q' - \pre ▷P - \post token g q'). - - Definition do_unlock (lk : gname * mpred) (Q : mpred) : mpred := - let g := lk.1 in - let P := lk.2 in - Exists q thr, current_thread thr ** locked g thr q ** ▷P ** - (* TODO readd *) - (* ▷ *) - (token g q -* Q). - #[global] Arguments do_unlock /. - - cpp.spec "std::mutex::try_lock()" as try_lock_spec_alt with ( - \this this - \prepost{q P g} this |-> R g q P - \persist{th} current_thread th - \pre{q'} token g q' - \post{b}[Vbool b] if b then P ** locked g th q' else token g q'). - - (* Obtain same specs from (Basic)Lockable. *) - (** <> implements [BasicLockable] *) - Definition T : Type := gname * mpred. - - #[global] Instance mutex_basic_lockable : BasicLockable (T:=T) "std::mutex" (λ q γP, R γP.1 q γP.2) := - { do_lock := fun this => do_lock - ; do_unlock := fun this => do_unlock }. - - cpp.spec "std::mutex::lock()" as lock_spec with - (\exact Reduce (lock_basic_lockable "std::mutex" (λ q γP, R γP.1 q γP.2))). - - cpp.spec "std::mutex::unlock()" as unlock_spec with - (\exact Reduce (unlock_basic_lockable "std::mutex" (λ q γP, R γP.1 q γP.2))). - - Definition do_try_lock (lk : gname * mpred) (Q : bool -> mpred) : mpred := - let g := lk.1 in - let P := lk.2 in - ∃ q thr, current_thread thr ∗ token g q ∗ - ∀ b : bool, - (if b then P ** locked g thr q else token g q) -∗ Q b. - #[global] Arguments do_try_lock /. - - #[global,program] Instance mutex_lockable : Lockable (T:=T) "std::mutex" (λ q γP, R γP.1 q γP.2) := - { do_try_lock := fun this => do_try_lock }. - - cpp.spec "std::mutex::try_lock()" as try_lock_spec with - (\exact Reduce (try_lock_lockable "std::mutex" (λ q γP, R γP.1 q γP.2))). - - Lemma lock_spec_entails_lock_spec_alt : lock_spec -|- lock_spec_alt. - Proof. - iSplit; iApply specify_mono; ework with br_erefl. - Qed. - Lemma unlock_spec_entails_unlock_spec_alt : unlock_spec -|- unlock_spec_alt. + (** Choose a sibling namespace for an auxiliary invariant. *) + Lemma mutex_inv_namespace_disjoint g : + ∃ No : namespace, mutex_inv_namespace g ## No. Proof. - iSplit; iApply specify_mono; ework with br_erefl. + have HN := mutex_inv_namespace_not_root g. + destruct (mutex_inv_namespace g) as [[|p ns]]; first done. + exists (Namespace ns .@ Pos.succ p). + have -> : Namespace (p :: ns) = Namespace ns .@ p. + { by rewrite namespaces.ndot_unseal. } + apply ndot_ne_disjoint. lia. Qed. - Lemma try_lock_spec_entails_try_lock_spec_alt : try_lock_spec -|- try_lock_spec_alt. + Lemma locked_not_locked_exclusive (this : ptr) g th q qt qt' P E : + ↑mutex_inv_namespace g ⊆ E -> + this |-> R g q P ** locked this g th qt ** not_locked this g th qt' |-- + (|={E}=> False). Proof. - iSplit; iApply specify_mono; ework with br_erefl. + rewrite /not_locked. iIntros (Hmask) "(HR & HL & _ & Hnames)". + iApply (locked_my_mutexes_exclusive with "[$HR $HL $Hnames]"). done. Qed. + + (** The guarded predicate must be weakly objective for invariant allocation, + which R likely has. *) + Definition ctor_spec (this : ptr) : WpSpec mpred val val := + (\pre{N P} ▷P + \require N <> nroot + \require WeaklyObjective P + \post |={⊤}=> Exists g, + [| mutex_inv_namespace g = N |] ** + this |-> R g 1$m P ** token g 1$m). + + Definition dtor_spec : ptr -> WpSpec mpred val val := + (\this this + \pre{g P} this |-> R g 1$m P ** token g 1$m + \post P). + + Definition lock_spec_alt : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} not_locked this g thr qt + \post P ** locked this g thr qt). + + Definition unlock_spec_alt : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} locked this g thr qt + \pre ▷P + \post not_locked this g thr qt). + + Definition try_lock_spec_alt : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} not_locked this g thr qt + \post{b}[Vbool b] + if b then P ** locked this g thr qt + else not_locked this g thr qt). + + Definition lock_spec : ptr -> WpSpec mpred val val := + lock_basic_lockable cpp_ty (fun q gP => R gP.1 q gP.2). + + Definition unlock_spec : ptr -> WpSpec mpred val val := + unlock_basic_lockable cpp_ty (fun q gP => R gP.1 q gP.2). + + Section equivalences. + Lemma lock_spec_equiv_lock_spec_alt this xs K : + lock_spec this xs K ⊣⊢ lock_spec_alt this xs K. + Proof. + unfold lock_spec, lock_basic_lockable. + unfold lock_spec_alt, do_lock. + cbn. iSplit. + - ework with br_erefl. + - iIntros "H". iDestruct "H" as (q P g thr qt) + "(%Hxs & HR & #HT & HNL & HK)". + iExists q, (g, P), (P ** Preds.locked this g thr qt)%I. + iFrame "HR HK". iSplit; first done. + iExists thr, qt. iFrame "HT HNL". + iIntros "[HL HP]". iFrame. + Qed. + + Lemma unlock_spec_equiv_unlock_spec_alt this xs K : + unlock_spec this xs K ⊣⊢ unlock_spec_alt this xs K. + Proof. + unfold unlock_spec, unlock_basic_lockable. + unfold unlock_spec_alt, do_unlock. + cbn. iSplit. + - ework with br_erefl. + - iIntros "H". iDestruct "H" as (q P g thr qt) + "(%Hxs & HR & #HT & HL & HP & HK)". + iExists q, (g, P), (Preds.not_locked this g thr qt). + iFrame "HR HK". iSplit; first done. + iExists thr, qt. iFrame "HT HL HP". + iIntros "$". + Qed. + + Definition try_lock_spec : ptr -> WpSpec mpred val val := + try_lock_lockable cpp_ty (fun q gP => R gP.1 q gP.2). + + Lemma try_lock_spec_equiv_try_lock_spec_alt + (Htry_lock : requirements.do_try_lock cpp_ty = do_try_lock) this xs K : + try_lock_spec this xs K ⊣⊢ try_lock_spec_alt this xs K. + Proof. + unfold try_lock_spec, try_lock_lockable. rewrite Htry_lock. + unfold try_lock_spec_alt, do_try_lock. + cbn. iSplit. + - ework with br_erefl. + - iIntros "H". iDestruct "H" as (q P g thr qt) + "(%Hxs & HR & #HT & HNL & HK)". + iExists q, (g, P), (fun b : bool => + if b then (P ** Preds.locked this g thr qt)%I + else Preds.not_locked this g thr qt). + iFrame "HR HK". iSplit; first done. + iExists thr, qt. iFrame "HT HNL". + iIntros (b) "$". + Qed. + End equivalences. End with_cpp. -End mutex. +End mutex_spec. + + +(** Specialize the reusable specs to the standard mutex representation and + bind them to their C++ names. *) +Module StdMutex (Preds : MUTEX_PREDS with Definition cpp_ty := "std::mutex"%cpp_type). + Include Preds. + #[global] Hint Opaque cpp_ty token not_locked locked R : sl_opacity. + + Module Spec := mutex_spec Preds. + + Section with_cpp. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + + (* FIXME can we delete this? *) + Section with_RepFor. + Import rep.RepFor. + Import RepScheme. + + #[global] Instance repfor `{!HasStdThreads Σ} {σ : genv} : + rep.RepFor.C "std::mutex" [ArgType.Constant _; ArgType.CFrac; ArgType.Constant _] + (funI γ q P => R γ q P) := {}. + End with_RepFor. + + Context `{MOD : source ⊧ σ}. + Context {HAS_THREADS : HasStdThreads Σ}. + + #[global] Instance R_std_type_ptr g q P : + Observe (type_ptrR "std::mutex") (R g q P). + Proof. apply R_type_ptr. Qed. + + cpp.spec "std::mutex::mutex()" as ctor_spec with + (\exact Reduce (Spec.ctor_spec)). + + cpp.spec "std::mutex::~mutex()" as dtor_spec with + (\exact Reduce (Spec.dtor_spec)). + + cpp.spec "std::mutex::lock()" as lock_spec_alt with + (\exact Reduce (Spec.lock_spec_alt)). + + cpp.spec "std::mutex::unlock()" as unlock_spec_alt with + (\exact Reduce (Spec.unlock_spec_alt)). + + cpp.spec "std::mutex::try_lock()" as try_lock_spec_alt with + (\exact Reduce (Spec.try_lock_spec_alt)). + + cpp.spec "std::mutex::lock()" as lock_spec with + (\exact Reduce (Spec.lock_spec)). + + cpp.spec "std::mutex::unlock()" as unlock_spec with + (\exact Reduce (Spec.unlock_spec)). + + cpp.spec "std::mutex::try_lock()" as try_lock_spec with + (\exact Reduce (Spec.try_lock_spec)). + + Lemma lock_spec_equiv_lock_spec_alt : lock_spec -|- lock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.lock_spec_equiv_lock_spec_alt; done. + Qed. + + Lemma unlock_spec_equiv_unlock_spec_alt : unlock_spec -|- unlock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.unlock_spec_equiv_unlock_spec_alt; done. + Qed. + + Lemma try_lock_spec_equiv_try_lock_spec_alt : try_lock_spec -|- try_lock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.try_lock_spec_equiv_try_lock_spec_alt; done. + Qed. + End with_cpp. +End StdMutex. +(** The standard-library implementation remains abstract; concrete mutex + implementations supply their own [MUTEX_PREDS]. *) +Declare Module StdMutexPreds : MUTEX_PREDS with Definition cpp_ty := "std::mutex"%cpp_type. +Module mutex := StdMutex StdMutexPreds. diff --git a/rocq-brick-libstdcpp/proof/mutex/spec/recursive_mutex.v b/rocq-brick-libstdcpp/proof/mutex/spec/recursive_mutex.v index 666a7a05..291148a5 100644 --- a/rocq-brick-libstdcpp/proof/mutex/spec/recursive_mutex.v +++ b/rocq-brick-libstdcpp/proof/mutex/spec/recursive_mutex.v @@ -1,312 +1,38 @@ Require Import iris.algebra.gset. +Require Import iris.algebra.coPset. Require Import iris.algebra.lib.excl_auth. Require Import skylabs.bi.tls_modalities. Require Import skylabs.bi.tls_modalities_rep. -Require Import skylabs.bi.weakly_objective. -Require Import skylabs.auto.cpp.weakly_local_with. Require Import skylabs.auto.cpp.proof. Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. Require Export skylabs.brick.libstdcpp.runtime.pred. Require Import skylabs.brick.libstdcpp.mutex.requirements. -Require Import skylabs.brick.libstdcpp.lib.lock_ghost. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Export lock_ghost2.mutex_set_G (mutex_set_G). Require Import skylabs.brick.libstdcpp.mutex.inc_hpp. Import linearity. -Module recursive_mutex. - - (* Not prodO thread_idTO natO. *) - (* A thread that has zero, locked γ th 0 does not even know which thread has non-0. *) - Canonical Structure phys_stateUR := authR (optionR (exclR (prodO thread_idTO natO))). - - (** <> <> owns the mutex <<γ>> <> times. *) - Class lockedG `{Σ : cpp_logic} := { - #[local] has_lock_ghost :: lock_ghost.lockG Σ; - - #[local] has_phys_state :: HasOwn (iPropI _Σ) phys_stateUR; - #[local] has_phys_state_upd :: HasOwnUpd (iPropI _Σ) phys_stateUR; - #[local] has_phys_state_valid :: HasOwnValid (iPropI _Σ) phys_stateUR; - #[local] has_phys_state_unit :: HasOwnUnit mpredI phys_stateUR; - }. - #[global] Arguments lockedG {_ _} Σ : assert. - - Record gname : Set := MkGname - { owned_count_id : iprop.gname; - locked_gname : iprop.gname; - inv_gname : iprop.gname; - }. - - (** [owned_count_id_auth γ Some (th, n)] implies that the lock's count is [n + 1]. *) - sl.lock - Definition owned_count_id_auth `{Σ : cpp_logic, !lockedG Σ} - (γ : gname) (om : option (thread_idT * natO)) : mpred := - own γ.(owned_count_id) (● (option_map Excl om)). - #[only(timeless)] derive owned_count_id_auth. - - (** [owned_count_id_frag γ Some (th, n)] implies that the lock's count is [n + 1]. *) - sl.lock - Definition owned_count_id_frag `{Σ : cpp_logic, !lockedG Σ} - (γ : gname) (om : option (thread_idT * natO)) : mpred := - own γ.(owned_count_id) (◯ (option_map Excl om)). - #[only(timeless)] derive owned_count_id_frag. - - #[local] Open Scope nat_scope. - - (** [locked γ th n] implies that the lock's count is [n]: see [used_threads]'s - definition and [owned_count_id_auth]'s informal contract. *) - sl.lock - Definition locked `{Σ : cpp_logic, !lockedG Σ} - (γ : gname) (th : thread_idT) (n : nat) : mpred := - user γ.(locked_gname) th ** - match n with - | 0 => owned_count_id_frag γ None - | S n => owned_count_id_frag γ (Some (th, n)) - end. - #[only(timeless)] derive locked. - - (* TODO: we should abstract this over the ownership that is produced and - then it can be used more generally. *) - sl.lock - Definition used_threads - `{Σ : cpp_logic, !lockedG Σ, !HasStdThreads Σ} - (γ : gname) (s : gset thread_idT) : mpred := - lock_ghost.used_threads γ.(locked_gname) s. - #[only(timeless)] derive used_threads. - - Section locked_with_cpp. - Context `{Σ : cpp_logic}. - Context `{!lockedG Σ}. - Context `{!HasStdThreads Σ}. - - Lemma use_thread th g s : - th ∉ s -> - used_threads g s |-- - (|==> used_threads g (s ∪ {[ th ]}) ** locked g th 0). - Proof. - rewrite used_threads.unlock locked.unlock owned_count_id_frag.unlock /=. - iIntros (Hni) "A". - iMod (lock_ghost.login with "A") as "[$ $]"; first done. - iApply own_unit. - Qed. - - Lemma logout th g s : - th ∉ s -> - used_threads g (s ∪ {[ th ]}) ** locked g th 0 |-- - (|==> used_threads g s ** owned_count_id_frag g None). - Proof. - rewrite used_threads.unlock locked.unlock. - iIntros (Hni) "(? & ? & $)". - iApply lock_ghost.logout; first done. - work. - Qed. - - #[global] Instance owned_count_id_frag_WeaklyObjective γ om : - WeaklyObjective (PROP := iPropI _) (owned_count_id_frag γ om). - Proof. rewrite owned_count_id_frag.unlock. apply _. Qed. - - #[global] Instance - locked_WeaklyObjective γ thr n : - WeaklyObjective (PROP := iPropI _) (locked γ thr n). - Proof. rewrite locked.unlock. apply _. Qed. - - Lemma locked_excl_same_thread g th n m : - locked g th n ** locked g th m |-- False. - Proof. - rewrite locked.unlock. - work. - iDestruct (user_unique with "[$]") as "[]". - Qed. - - Lemma locked_excl_different_thread g th th' n m : - locked g th n ** locked g th' m |-- [| n = 0 \/ m = 0 |] ** True. - Proof. - destruct (decide (th = th')) as [->|Hne]. { - rewrite locked_excl_same_thread. work. - } - rewrite locked.unlock. - iIntros "[[_ A] [_ B]]". - destruct n, m; try auto. iExFalso. - rewrite owned_count_id_frag.unlock. - iDestruct (own_valid_2 with "A B") as %HV; exfalso. - rewrite -auth_frag_op auth_frag_valid in HV. - done. - Qed. - - End locked_with_cpp. - -(** -Underlying pthread implementation for [PTHREAD_MUTEX_RECURSIVE_NP] case: - - << - /* Check whether we already hold the mutex. */ - if (mutex->__data.__owner == id) - { - /* Just bump the counter. */ - if (__glibc_unlikely (mutex->__data.__count + 1 == 0)) - /* Overflow of the counter. */ - return EAGAIN; - - ++mutex->__data.__count; - - return 0; - } - (* LLL_MUTEX_LOCK_OPTIMIZED (mutex); *) - LLL_MUTEX_LOCK (mutex); - >> - -Informally, we can read mutex->__data.__owner atomically, and we know that -mutex->__data.__owner == id if and only if our thread has completed locking the -recursive mutex; hence, mutex->__data.__owner != id means that nobody is -touching the mutex or other threads are operating on it, but at no point will they set __owner to our ID. - -Hence: -1. [if (mutex->__data.__owner == id)], we can get obtain sequential ownership of -mutex->__data.__count, and of the underlying resources, and complete the lock operation. -2. else, we can attempt to grab the underlying non-recursive lock, and be sure we - won't deadlock against ourselves. - -Formalizing step 1 seems nontrivial, but relatively routine. -But the full pthread implementation would add annoying details. - -The right invariant might resemble the following, but significant details are TBD. -[[ -cinv ( - \exists x, - mutex->__data.__owner |-> x ** - if bool_decide (x <> 0) then - (sequential ownership of count ** ownership of data protected by the lock) \/ - some exclusive token (* needed to take the sequential out *) - else - emp - ) -]] -*) - (* the mask of recursive_mutex *) - Definition mask := nroot .@@ "std" .@@ "recursive_mutex" .@@ "mask". - - (** We base the implementation protocol on - https://github.com/bminor/glibc/blob/04e750e75b73957cf1c791535a3f4319534a52fc/nptl/pthread_mutex_lock.c#L90-L112. - - official mirror: - https://sourceware.org/git/?p=glibc.git;a=blob;f=nptl/pthread_mutex_lock.c;h=a697f2b6ca8dfa9e4557ab3f44b87bc5ceeec014;hb=HEAD#l90 - TODO: revise. - *) - - (* NOTE: Invariant used to protect resource [r] - - [[ - inv (r \\// exists th n, locked th (S n)) - ]] - *) - - - (** Intended meaning: ownership of physical C++ state for an instance of "std::recursive_mutex". *) - Parameter rawR : ∀ `{Σ : cpp_logic, σ : genv}, option thread_idT -> nat -> Rep. - (* The thread_idT is None (0) if there is no owner. *) - #[only(type_ptr="std::recursive_mutex")] derive rawR. - - Definition rmutex_N : namespace := - nroot .@@ "std" .@@ "recursive_mutex" .@ "raw_inv". - - (* recursive mutex -- ownership of the class. *) - sl.lock - Definition I `{Σ : cpp_logic, σ : genv, !lockedG Σ} (γ : gname) : Rep := - type_ptrR "std::recursive_mutex" ** - cinv rmutex_N γ.(inv_gname) (∃ owner count, rawR owner count ** - (* We use [Nat.pred] because [owned_count_id_auth] stores [counter - 1]. *) - pureR (owned_count_id_auth γ ((λ t, (t, Nat.pred count)) <$> owner))). - (* TODO: readd [|owner = None <-> count = O|] elsewhere, as sequential invariant in [R]. *) - #[only(knowledge,type_ptr="std::recursive_mutex")] derive I. - - sl.lock - Definition R `{Σ : cpp_logic, σ : genv, !lockedG Σ} (γ : gname) (q : cQp.t) : Rep := - type_ptrR "std::recursive_mutex" ** - (* TODO: add here sequential ownership of the lock, and maybe replace I by the lock invariant. - Something like *) - (* _mutex_field |-> mutex.R q ... ** *) - pureR (cinv_own γ.(inv_gname) q). - #[only(cfracsplittable,type_ptr="std::recursive_mutex")] derive R. - - - Section base_construction. - Context `{Σ : cpp_logic} `{MOD : source ⊧ σ}. - Context {HAS_THREADS : HasStdThreads Σ}. - Context `{!lockedG Σ}. - - #[global] Instance I_learn : Cbn (Learn (learn_eq ==> learn_hints.fin) I). - Proof. solve_learnable. Qed. - #[global] Instance R_learn : Cbn (Learn (learn_eq ==> any ==> learn_hints.fin) R). - Proof. solve_learnable. Qed. - - cpp.spec "std::recursive_mutex::recursive_mutex()" as ctor_spec with - (\this this - \post Exists g, this |-> R g 1$m ** used_threads g empty). - - cpp.spec "std::recursive_mutex::~recursive_mutex()" as dtor_spec with - (\this this - \pre{g} this |-> R g 1$m - \pre used_threads g empty - \post emp). - - cpp.spec "std::recursive_mutex::lock()" as lock_spec with - (\this this - \prepost{q g} this |-> R g q (* part of both pre and post *) - \persist{th} current_thread th - \pre{Q} AC << ∀ n , locked g th n >> @ top \ ↑ mask , empty - << locked g th (S n) , COMM Q >> - \post Q). - - cpp.spec "std::recursive_mutex::unlock()" as unlock_spec with - (\this this - \prepost{q g} this |-> R g q (* part of both pre and post *) - \persist{th} current_thread th - \pre{Q} AC << ∀ n , locked g th (S n) >> @ top \ ↑ mask , empty - << locked g th n , COMM Q >> - \post Q). - - End base_construction. - - - (** * Derived construction *) - Record rmutex_gname := - { lock_gname : gname - ; level_gname : iprop.gname - ; cinv_gname : iprop.gname - }. - Definition rmutex_namespace := nroot .@@ "std" .@@ "recursive_mutex" .@@ "derived". - - Canonical Structure cmraR := (excl_authR (prodO natO thread_idTO)). - - sl.lock - Definition inv_rmutex - `{Σ : cpp_logic} `{!lockedG Σ} `{!HasOwn (iPropI _) cmraR} - (g : rmutex_gname) (P : mpred) : mpred := - cinv rmutex_namespace g.(cinv_gname) - (Exists n th, own g.(level_gname) (●E (n, th)) ** - match n with - | 0 => P ** own g.(level_gname) (◯E (n, th)) - | S n => locked g.(lock_gname) th (S n) - end). - #[only(knowledge)] derive inv_rmutex. - - (** Fractional ownership of the physical recursive mutex and of the - cancellable invariant that protects the custom resource P. - The two fractions do not have to be the same, we just choose to make them - equal for convenience. - *) - sl.lock - Definition derivedR - `{Σ : cpp_logic, σ : genv, !lockedG Σ} - (g : rmutex_gname) (q : cQp.t) : Rep := - R g.(lock_gname) q ** - pureR (cinv_own g.(cinv_gname) q). - #[only(cfracsplittable,type_ptr="std::recursive_mutex")] derive derivedR. - - (** [acquire_state] tracks the acquisition state of a recursive_mutex. - *) +(** * This file contains: + - The logical states [RecursiveMutexState] + - The predicate interface [RECURSIVE_MUTEX_PREDS] + - Recursive mutex specs that are parametric to their C++ names + [recursive_mutex_spec] + - And specs that are bound to stdlib names [StdRecursiveMutex] *) + +(** The C++ class a recursive-mutex specification is stated at. *) +Module Type MutexCPPName. + Parameter cpp_ty : type. +End MutexCPPName. + +(** Logical state of the recursive mutex. + A recursive mutex starts in NotHeld, lock increment the held count, + and unlock decrements it: + NotHeld <-> Held 0 _ <-> Held 1 _ <-> ... *) +Module RecursiveMutexState. Inductive acquire_state {TT : tele} : Type := | NotHeld (* not held *) | Held (n : nat) (xs : TT) (* acquired [n + 1] times with quantifiers [xs] *). @@ -341,352 +67,382 @@ cinv ( end end. - sl.lock - Definition acquireable - `{Σ : cpp_logic, !lockedG Σ, !HasStdThreads Σ, !HasOwn (iPropI _) cmraR} - (g : rmutex_gname) (th : thread_idT) {TT: tele} (t : acquire_state TT) - (P : TT -t> mpred) : mpred := - current_thread th ** - match t with - | NotHeld => locked g.(lock_gname) th 0 - | Held n args => own g.(level_gname) (◯E (S n, th)) ** - @tele_app TT (fun _ => mpred) P args + Lemma is_held {TT : tele} {t1 t2 : acquire_state TT} : + acquire t1 t2 -> + ∃ n xs, t2 = Held n xs /\ t1 = release t2. + Proof. + rewrite acquire.unlock release.unlock. + intros. destruct t1; simpl in H; eauto. + - exists 0. naive_solver. + - exists (S n). naive_solver. + Qed. + + Definition update {TT : tele} (f : TT -t> TT) + (x : acquire_state TT) : acquire_state TT := + match x with + | NotHeld => NotHeld + | Held n xs => Held n (tele_app f xs) end. + Lemma update_eq {TT : tele} f t1 t2 : acquire t1 t2 -> + update f t1 = release (TT := TT) (update f t2). + Proof. + by intros ([|] & ? & -> & ->)%is_held; rewrite !release.unlock. + Qed. +End RecursiveMutexState. + +(** Predicates of a recursive_mutex. *) +Module Type RECURSIVE_MUTEX_PREDS (T : MutexCPPName). + Include RecursiveMutexState. + Definition cpp_ty : type := T.cpp_ty. + Parameter gname : Set. + Parameter G : forall `{Σ : cpp_logic}, Type. + Existing Class G. + #[global] Arguments G {_ _} Σ : assert. + + Parameter rmutex_inv_namespace : gname -> namespace. + + Parameter token : forall `{Σ : cpp_logic, !G Σ}, gname -> cQp.t -> mpred. + #[global] Declare Instance token_fractional + `{Σ : cpp_logic, !G Σ} g : CFractional (token g). + #[global] Declare Instance token_timeless + `{Σ : cpp_logic, !G Σ} g qt : Timeless (token g qt). + + Definition not_locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} + (this : ptr) (g : gname) (qt : cQp.t) (th : thread_idT) : mpred := + token g qt ** MutexSets.my_mutexes th + (CoPset $ ↑rmutex_inv_namespace g). + Parameter locked : forall `{Σ : cpp_logic, !G Σ, σ : genv}, + ptr -> gname -> cQp.t -> thread_idT -> nat -> mpred. + + (* abstract predicate of owership of a recursive mutex that protects some + resource [P], e.g. in the form of [inv _ P]. Note that it only represents + the mutex data structure, not the ownership of [P]. *) + Parameter R : forall `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} + {HAS_THREADS : HasStdThreads Σ} {σ : genv}, + gname -> cQp.t -> mpred -> Rep. + #[global] Hint Opaque token not_locked locked R : sl_opacity typeclass_instances. + Section with_cpp. Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. - Context `{!HasOwn (iPropI _) cmraR, !HasStdThreads Σ}. - Context `{!lockedG Σ}. + #[global] Instance not_locked_timeless {σ : genv} this g qt th : + Timeless (not_locked this g qt th). + Proof. rewrite /not_locked. apply _. Qed. - #[global] Instance acquireable_learn γ th TT : LearnEq2 (acquireable γ th (TT := TT)). - Proof. solve_learnable. Qed. + Context {HAS_THREADS : HasStdThreads Σ} {σ : genv}. - #[global] Instance acquireable_current_thread : - `{Observe (current_thread th) (acquireable g th (TT := TT) t P)}. - Proof. rewrite acquireable.unlock; apply _. Qed. + #[global] Declare Instance locked_timeless this g qt th n : + Timeless (locked this g qt th n). - Lemma use_thread_acquirable {TT} th g m P : - th ∉ m -> - current_thread th ** used_threads g.(lock_gname) m |-- (|==> - used_threads g.(lock_gname) (m ∪ {[ th ]}) ** acquireable (TT := TT) g th NotHeld P). - Proof. - rewrite acquireable.unlock /=. - work. - wapply use_thread; first done. - work with br_erefl. - iModIntro; work. - Qed. - End with_cpp. + #[only(cfractional,cfracvalid,ascfractional)] derive R. + #[global] Declare Instance R_type_ptr g q P : Observe (type_ptrR cpp_ty) (R g q P). - Section with_cpp. - Context `{Σ : cpp_logic} `{MOD : source ⊧ σ}. - Context {HAS_THREADS : HasStdThreads Σ}. - Context `{!lockedG Σ}. - Context `{!HasOwn (iPropI _) cmraR}. - - (* Alternative style: - << - R γ q r ** locked γ th (S n) |--| R γ q r ** r ** was_locked γ th (S n) - >> - - possible solution: two specs/choice in the spec for unlock: either - <<{locked γ th (n+1)} unlock() {locked γ th n}>> - or - <<{was_locked γ th (n+2)} unlock() {locked γ th (n+1)}>> - *) - - (* TODO make this into a hint *) - Lemma is_held {TT : tele} {t1 t2 : acquire_state TT} : - acquire t1 t2 -> - ∃ n xs, t2 = Held n xs /\ - t1 = release t2. - Proof. - rewrite acquire.unlock release.unlock. - intros. - destruct t1; simpl in H; eauto. - - exists 0. naive_solver. - - exists (S n). naive_solver. - Qed. + End with_cpp. +End RECURSIVE_MUTEX_PREDS. - #[program] - Definition acquireable_is_acquired_C {TT} g th t t' P - (_ : acquire (TT := TT) t t') := - \cancelx - \consuming acquireable g th t' P - \deduce{args} @tele_app TT (fun _ => mpred) P args - \deduce{n} [| t' = Held n args /\ t = release t' |] - \deduce own g.(level_gname) (◯E (S n, th)) - \end. - Next Obligation. - intros * (? & ? & -> & ->)%is_held. - rewrite acquireable.unlock. - ego. - Qed. +(** Shared contracts, [BasicLockable] presentation, and proof automation. *) +Module recursive_mutex_spec (T : MutexCPPName) + (Preds : RECURSIVE_MUTEX_PREDS T). + Import Preds RecursiveMutexState. - #[program] - Definition acquireable_acquireable_C γ := - \cancelx - \consuming{th n TT args P} acquireable (TT := TT) γ th (Held n args) P - \bound P' - \bound_existential th' args' - \proving acquireable γ th' args' P' - \instantiate th' := th - \instantiate args' := Held n args - \deduce @tele_app TT (fun _ => mpred) P args - \through @tele_app TT (fun _ => mpred) P' args - \end. - Next Obligation. rewrite acquireable.unlock; work. Qed. - - #[program] - Definition own_P_is_acquireable_C {TT} g n P := - \cancelx - \preserving{th} current_thread th - \consuming own g.(level_gname) (◯E (S n, th)) - \bound n' args - \proving acquireable (TT := TT) g th (Held n' args) P - \through @tele_app TT (fun _ => mpred) P args - \through [| n' = n |] - \end. - Next Obligation. rewrite acquireable.unlock; work. Qed. - - #[global] Instance : `{Learnable - (current_thread th) - (acquireable (TT := TT0) γ th0 args P0) - [th0 = th] }. - Proof. solve_learnable. Qed. - - #[global] Instance learn_inv_rmutex_γ : `{Learnable - (inv_rmutex γ1 P1) - (inv_rmutex γ2 P2) - [γ2 = γ1] }. - Proof. solve_learnable. Qed. - - #[global] Instance learn_inv_rmutex_TT : `{Learnable - (inv_rmutex γ (∃ xs : tele_arg TT1, - @tele_app TT1 (fun _ => mpred) P1 xs)) - (inv_rmutex γ (∃ xs : tele_arg TT2, - @tele_app TT2 (fun _ => mpred) P2 xs)) - [TT2 = TT1] }. - Proof. solve_learnable. Qed. - - #[global] Instance learn_inv_rmutex_P TT : `{Learnable - (inv_rmutex γ1 (∃ xs : tele_arg TT, - @tele_app TT (fun _ => mpred) P1 xs)) - (inv_rmutex γ2 (∃ xs : tele_arg TT, - @tele_app TT (fun _ => mpred) P2 xs)) - [P2 = P1] }. - Proof. solve_learnable. Qed. - - #[global] Instance learn_args - {TT: tele} (t : acquire_state TT) (P : TT -t> mpred) : - `{Learnable - (@tele_app TT (fun _ => mpred) P args ** - own (level_gname γ) (◯E (S n, th))) - (acquireable γ th t P) - [t = Held n args] }. - Proof. solve_learnable. Qed. - - Definition update {TT : tele} (f : TT -t> TT) - (x : acquire_state TT) : acquire_state TT := - match x with - | NotHeld => NotHeld - | Held n xs => Held n (@tele_app TT (fun _ => TT) f xs) - end. + (** Ownership of a thread's acquisition state. Operations separately require + [current_thread th] to establish that the caller is that thread. *) + sl.lock + Definition acquireable `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (g : gname) (qt : cQp.t) + (th : thread_idT) {TT : tele} (s : acquire_state TT) + (P : TT -t> mpred) : mpred := + match s with + | NotHeld => not_locked this g qt th + | Held n xs => locked this g qt th n ** @tele_app TT (fun _ => mpred) P xs + end. + #[global] Hint Opaque acquireable : sl_opacity typeclass_instances. - Lemma update_eq {TT : tele} f t1 t2 : acquire t1 t2 -> - update f t1 = release (TT := TT) (update f t2). - Proof. - by intros ([|] & ? & -> & ->)%is_held; rewrite !release.unlock. - Qed. + Section with_cpp. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + Context {HAS_THREADS : HasStdThreads Σ} {σ : genv}. - (* this is the usable pre-condition *) - cpp.spec "std::recursive_mutex::recursive_mutex()" as ctor_spec' with + Definition lock_spec : ptr -> WpSpec mpred val val := (\this this - \persist{th} current_thread th - \pre{TT P xs} |> @tele_app TT (fun _ => mpred) P xs - \require ∀ xs, WeaklyObjective (@tele_app TT (fun _ => mpred) P xs) - \post - Exists g, - this |-> derivedR g 1$m ** - used_threads g.(lock_gname) empty ** - inv_rmutex g (∃ xs, @tele_app TT (fun _ => mpred) P xs)). - - cpp.spec "std::recursive_mutex::~recursive_mutex()" as dtor_spec' with - (\this this - \pre{g} this |-> derivedR g 1 - \pre used_threads g.(lock_gname) empty - \pre{TT P} inv_rmutex g (∃ xs, - @tele_app TT (fun _ => mpred) P xs) - \post |> (Exists xs, @tele_app TT (fun _ => mpred) P xs)). + \prepost{g TT P q} this |-> R g q (∃ xs, @tele_app TT (fun _ => mpred) P xs) + \persist{th} current_thread th + \pre{qt n} acquireable this g qt th n P + \post Exists n', [| acquire n n' |] ** ▷ acquireable this g qt th n' P). - cpp.spec "std::recursive_mutex::lock()" as lock_spec' with + Definition unlock_spec : ptr -> WpSpec mpred val val := (\this this - \persist{g TT P} inv_rmutex g (∃ xs, - @tele_app TT (fun _ => mpred) P xs) - \prepost{q} this |-> derivedR g q - \pre{th n} acquireable g th n P - \post (Exists n', [| acquire n n' |] ** ▷ acquireable g th n' P)). - (* to prove: this is derivable from lock_spec *) - - cpp.spec "std::recursive_mutex::unlock()" as unlock_spec' with + \prepost{g TT P q} this |-> R g q (∃ xs, @tele_app TT (fun _ => mpred) P xs) + \persist{th} current_thread th + \pre{qt n args} acquireable this g qt th (Held n args) P + \post acquireable this g qt th (release $ Held n args) P). + + Definition try_lock_spec : ptr -> WpSpec mpred val val := (\this this - \persist{g TT P} inv_rmutex g (∃ xs, - @tele_app TT (fun _ => mpred) P xs) - \prepost{q} this |-> derivedR g q - \pre{th n args} acquireable g th (Held n args) P - \post acquireable g th (release $ Held n args) P). - - Definition do_lock g K : mpred := ∃ TT P th n, - inv_rmutex g (∃ xs, @tele_app TT (fun _ => mpred) P xs) - ** acquireable g th n P - ** ( - (* TODO readd *) - (* ▷ *) - (Exists n', [| acquire n n' |] ** ▷ acquireable g th n' P) -* - K). + \prepost{g TT P q} this |-> R g q (∃ xs, @tele_app TT (fun _ => mpred) P xs) + \persist{th} current_thread th + \pre{qt n} acquireable this g qt th n P + \post{b}[Vbool b] + if b then Exists n', [| acquire n n' |] ** ▷ acquireable this g qt th n' P + else acquireable this g qt th n P). + + Definition do_lock (this : ptr) (lk : gname * mpred) (K : mpred) : mpred := + ∃ TT P qt th n, + [| lk.2 = (∃ xs, @tele_app TT (fun _ => mpred) P xs)%I |] ** + current_thread th ** + acquireable this lk.1 qt th n P ** + ((* TODO readd *) + (* ▷ *) + (Exists n', [| acquire n n' |] ** ▷ acquireable this lk.1 qt th n' P) -* K). #[global] Arguments do_lock /. - Definition do_unlock g K : mpred := ∃ TT P th n args, - inv_rmutex g (∃ xs, @tele_app TT (fun _ => mpred) P xs) - ** acquireable g th (Held n args) P - ** ( - (* TODO readd *) + Definition do_unlock (this : ptr) (lk : gname * mpred) (K : mpred) : mpred := + ∃ TT P qt th n args, + [| lk.2 = (∃ xs, @tele_app TT (fun _ => mpred) P xs)%I |] ** + current_thread th ** + acquireable this lk.1 qt th (Held n args) P ** + ((* TODO readd *) (* ▷ *) - acquireable g th (release $ Held n args) P -* - K). + acquireable this lk.1 qt th (release $ Held n args) P -* K). #[global] Arguments do_unlock /. #[global] Instance recursive_mutex_basic_lockable : BasicLockable - (T:=rmutex_gname) "std::recursive_mutex" (λ q g, derivedR g q) := - { do_lock := fun this => do_lock - ; do_unlock := fun this => do_unlock }. - - cpp.spec "std::recursive_mutex::lock()" as lock_spec_alt' with - (\exact Reduce (lock_basic_lockable "std::recursive_mutex" (λ q g, derivedR g q))). - - cpp.spec "std::recursive_mutex::unlock()" as unlock_spec_alt' with - (\exact Reduce (unlock_basic_lockable "std::recursive_mutex" (λ q g, derivedR g q))). - - Lemma lock_spec'_equiv_lock_spec_alt' : - lock_spec' -|- lock_spec_alt'. - Proof. iSplit; iApply specify_mono; ework with br_erefl. Qed. - - Lemma unlock_spec'_equiv_unlock_spec_alt' : - unlock_spec' -|- unlock_spec_alt'. - Proof. iSplit; iApply specify_mono; ework with br_erefl. Qed. - - Definition acquireable_current_thread_F := - ltac:(mk_obs_fwd acquireable_current_thread). - #[local] Hint Resolve acquireable_current_thread_F : br_hints. - - (* TODO AUTO *) - #[global] Instance later_acquireable_learn γ th TT : - LearnEq2 (fun a b => bi_later (acquireable γ th (TT := TT) a b)). - Proof. solve_learnable. Qed. - - Import linearity. - - Context `{HOV : !HasOwnValid mpredI cmraR, HOU : !HasOwnUpd mpredI cmraR}. - - Lemma ctor_spec_impl_ctor_spec' : - ctor_spec |-- ctor_spec'. - Proof using MOD HOV HOU. - apply specify_mono_fupd; work. - iModIntro; work. - rewrite /acquireable /=. - iMod (own_alloc (●E (O, th) ⋅ ◯E (O, th))) as (g) "(? & ?)". - { apply excl_auth_valid. } - wname [used_threads] "u". - wname [_ |-> _ _ _] "a". - iMod (cinv_alloc with "[-u a]") as (ginv) "(#Hinv & Hown)"; last first. - - iExists {| lock_gname := t; level_gname := g; cinv_gname:= ginv |}. - rewrite derivedR.unlock inv_rmutex.unlock /=. - iModIntro. - go with br_erefl $usenamed=true. - - ework with br_erefl. - - apply _. - Qed. + (T := gname * mpred) cpp_ty (fun q gP => R gP.1 q gP.2) := + { do_lock := do_lock + ; do_unlock := do_unlock }. - Lemma dtor_spec_impl_dtor_spec' : - dtor_spec |-- dtor_spec'. - Proof using MOD HOV HOU. - apply specify_mono_fupd; work. - rewrite derivedR.unlock inv_rmutex.unlock. - work. - iMod (cinv_cancel with "[$] [$]") as (n th) "(>? & ?)"; [done..|]. - - destruct n as [|n'] eqn:?; work; first last. - { - rewrite locked.unlock used_threads.unlock. - wapply used_threads_empty_no_not_locked; work with br_erefl. - } - iModIntro. work. iModIntro. ego with br_erefl. - (* _now_ we just need to leak ghost state and that's okay *) - wname [cinv] "I"; wname [own] "L1"; wname [own] "L2"; iCombine "L1 L2" as "L". - iApply (affine with "L"). apply mpred_BiAffine. - Qed. + Definition lock_spec_alt : ptr -> WpSpec mpred val val := + lock_basic_lockable cpp_ty (fun q gP => R gP.1 q gP.2). + Definition unlock_spec_alt : ptr -> WpSpec mpred val val := + unlock_basic_lockable cpp_ty (fun q gP => R gP.1 q gP.2). - Lemma lock_spec_impl_lock_spec' : - lock_spec |-- lock_spec'. - Proof using MOD HOV HOU. - apply specify_mono; rewrite derivedR.unlock; work. - iExists q, (cinv_own g.(cinv_gname) q ** - (∃ t, [| acquire n t |] ∗ ▷ acquireable g th t P))%I. - wname [bi_wand] "W"; wfocus (bi_wand _ _) "W". { work $usenamed=true. } - rewrite inv_rmutex.unlock acquireable.unlock. - work; iAcIntro; rewrite /commit_acc/=; work. - iInv rmutex_namespace as "[(%n' & %th' & >Hn & Hcases) ?]" "Hclose". - destruct n as [|n args]; simpl; [iExists 0 | iExists (S n)]; work. - 2: iDestruct (own_valid_2 with "Hn [$]") as %[=]%excl_auth_agree_L; subst. - all: work $usenamed=true; iApply fupd_mask_intro; first set_solver; - iIntros "Hclose'"; work; iMod "Hclose'" as "_". - - destruct n'; first last. { - iMod "Hcases". - iDestruct (locked_excl_different_thread with "[$]") as (?) "?". - exfalso. lia. - } - rewrite bi.later_sep bi.later_exist_except_0. - iDestruct "Hcases" as "(>(%args & ?) & >Hcase)". - iMod (own_update_2 with "Hn Hcase") as "(Hg & ?)"; - first apply (excl_auth_update _ _ (1, th)). - wname [recursive_mutex.locked _ th _] "Hlocked"; - iMod ("Hclose" with "[$Hg $Hlocked //]") as "_"; iModIntro. - iExists (Held 0 args). work. - - iMod (own_update_2 with "Hn [$]") as "(Hg & ?)"; - first apply (excl_auth_update _ _ (S (S n), th)). - wname [recursive_mutex.locked _ th _] "Hlocked"; - iMod ("Hclose" with "[$Hg $Hlocked //]") as "_"; iModIntro. - iExists (Held (S n) args). work. + Lemma lock_spec_equiv_lock_spec_alt this xs K : + lock_spec this xs K ⊣⊢ lock_spec_alt this xs K. + Proof. + unfold lock_spec, lock_spec_alt, lock_basic_lockable, do_lock. + cbn. iSplit. + - iIntros "H". iDestruct "H" as (g TT P q th qt n) + "(%Hxs & HR & #Hth & HA & HK)". + iExists q, (g, (∃ xs : tele_arg TT, @tele_app TT (fun _ => mpred) P xs)%I), + (Exists n', [| acquire n n' |] ** ▷ acquireable this g qt th n' P)%I. + iFrame "HR HK". iSplit; first done. + iExists TT, P, qt, th, n. iFrame "Hth HA". + iSplit; first done. iIntros "$". + - iIntros "H". iDestruct "H" as (q [g J] K') + "(%Hxs & HR & Hdo & HK)". + iDestruct "Hdo" as (TT P qt th n) "(%Heq & #Hth & HA & Hcont)". + simpl in *. subst J. + iExists g, TT, P, q, th, qt, n. iFrame "HR Hth HA". + iSplit; first done. iIntros "[HR Hac]". + iApply "HK". iFrame "HR". iApply "Hcont". iExact "Hac". Qed. - Lemma unlock_spec_impl_unlock_spec' : - unlock_spec |-- unlock_spec'. - Proof using MOD HOV HOU. - apply specify_mono; rewrite derivedR.unlock; work. - iExists q, (cinv_own g.(cinv_gname) q ** - acquireable g th (release $ Held n args) P)%I. - wname [bi_wand] "W"; wfocus (bi_wand _ _) "W". { work $usenamed=true. } - rewrite inv_rmutex.unlock acquireable.unlock. - work; iAcIntro; rewrite /commit_acc/=; work. - iInv rmutex_namespace as "[(%n' & %th' & >Hn & Hcases) ?]" "Hclose". - iDestruct (own_valid_2 with "Hn [$]") as %[=]%excl_auth_agree_L; subst. - iMod "Hcases". - iApply fupd_mask_intro; first set_solver; iIntros "Hclose'". - iExists n; work $usenamed=true. - iMod "Hclose'" as "_". - iMod (own_update_2 with "Hn [$]") as "(Hg & Hcase)"; - first apply (excl_auth_update _ _ (n, th)). - rewrite release.unlock; destruct n; iFrame "#∗". - all: iMod ("Hclose" with "[-]") as "_"; - ework $usenamed=true with br_erefl; done. + Lemma unlock_spec_equiv_unlock_spec_alt this xs K : + unlock_spec this xs K ⊣⊢ unlock_spec_alt this xs K. + Proof. + unfold unlock_spec, unlock_spec_alt, unlock_basic_lockable, do_unlock. + cbn. iSplit. + - iIntros "H". iDestruct "H" as (g TT P q th qt n args) + "(%Hxs & HR & #Hth & HA & HK)". + iExists q, (g, (∃ xs : tele_arg TT, @tele_app TT (fun _ => mpred) P xs)%I), + (acquireable this g qt th (release $ Held n args) P). + iFrame "HR HK". iSplit; first done. + iExists TT, P, qt, th, n, args. iFrame "Hth HA". + iSplit; first done. iIntros "$". + - iIntros "H". iDestruct "H" as (q [g J] K') + "(%Hxs & HR & Hdo & HK)". + iDestruct "Hdo" as (TT P qt th n args) "(%Heq & #Hth & HA & Hcont)". + simpl in *. subst J. + iExists g, TT, P, q, th, qt, n, args. iFrame "HR Hth HA". + iSplit; first done. iIntros "[HR Hac]". + iApply "HK". iFrame "HR". iApply "Hcont". iExact "Hac". Qed. + Definition ctor_spec : ptr -> WpSpec mpred val val := + (\this this + \pre{N TT P xs} |> @tele_app TT (fun _ => mpred) P xs + \require N <> nroot + \require ∀ xs, Objective (@tele_app TT (fun _ => mpred) P xs) + \post Exists g, + [| rmutex_inv_namespace g = N |] ** + this |-> R g 1$m (∃ xs, @tele_app TT (fun _ => mpred) P xs) ** token g 1$m). + + Definition dtor_spec : ptr -> WpSpec mpred val val := + (\this this + \pre{g TT P} this |-> R g 1 (∃ xs, @tele_app TT (fun _ => mpred) P xs) + \pre token g 1$m + \post |> (Exists xs, @tele_app TT (fun _ => mpred) P xs)). + + Section proof_automation. + #[global] Instance R_learn : + Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). + Proof. solve_learnable. Qed. + (** Recover the resource telescope before matching its arguments. *) + #[global] Instance R_learn_TT : `{Learnable + (R g q1 (∃ xs : tele_arg TT1, @tele_app TT1 (fun _ => mpred) P1 xs)) + (R g q2 (∃ xs : tele_arg TT2, @tele_app TT2 (fun _ => mpred) P2 xs)) + [TT2 = TT1] }. + Proof. solve_learnable. Qed. + + #[global] Instance R_learn_P TT : `{Learnable + (R g q1 (∃ xs : tele_arg TT, @tele_app TT (fun _ => mpred) P1 xs)) + (R g q2 (∃ xs : tele_arg TT, @tele_app TT (fun _ => mpred) P2 xs)) + [P2 = P1] }. + Proof. solve_learnable. Qed. + + (** The mutex representation remembers the resource after clients open it. *) + #[global] Instance R_acquireable_learn_TT (this : ptr) : `{Learnable + (this |-> R g q (∃ xs : tele_arg TT1, @tele_app TT1 (fun _ => mpred) P1 xs)) + (acquireable this g qt th (TT := TT2) t P2) + [TT2 = TT1] }. + Proof. solve_learnable. Qed. + + #[global] Instance R_acquireable_learn_P TT (this : ptr) : `{Learnable + (this |-> R g q (∃ xs : tele_arg TT, @tele_app TT (fun _ => mpred) P1 xs)) + (acquireable this g qt th (TT := TT) t P2) + [P2 = P1] }. + Proof. solve_learnable. Qed. + + #[global] Instance acquireable_learn_TT : `{Learnable + (acquireable this g qt1 th (TT := TT1) t1 P1) + (acquireable this g qt2 th (TT := TT2) t2 P2) + [qt2 = qt1; TT2 = TT1] }. + Proof. solve_learnable. Qed. + + #[global] Instance acquireable_learn this γ th TT : + LearnEq3 (fun qt s P => acquireable this γ qt th (TT := TT) s P). + Proof. solve_learnable. Qed. + #[global] Instance not_locked_learn this γ th : LearnEq1 (fun qt => not_locked this γ qt th). + Proof. solve_learnable. Qed. + #[global] Instance locked_learn this γ th : LearnEq2 (fun qt n => locked this γ qt th n). + Proof. solve_learnable. Qed. + #[global] Instance later_acquireable_learn this γ th TT : + LearnEq3 (fun qt a b => bi_later (acquireable this γ qt th (TT := TT) a b)). + Proof. solve_learnable. Qed. + + #[global] Instance : `{Learnable + (current_thread th) + (acquireable this (TT := TT0) γ qt th0 args P0) + [th0 = th] }. + Proof. solve_learnable. Qed. + + #[global] Instance learn_args (this : ptr) + {TT : tele} (t : acquire_state TT) (P : TT -t> mpred) : + `{Learnable + (@tele_app TT (fun _ => mpred) P args ** locked this γ qt th n) + (acquireable this γ qt' th t P) + [qt' = qt; t = Held n args] }. + Proof. solve_learnable. Qed. + + #[global] Instance locked_acquireable_learn {TT : tele} : `{Learnable + (locked this g qt th n) + (acquireable this (TT := TT) g qt' th (Held n' args) P) + [qt' = qt; n' = n] }. + Proof. solve_learnable. Qed. + + #[program] + Definition acquireable_is_acquired_C {TT} this g qt th t t' P + (_ : acquire (TT := TT) t t') := + \cancelx + \consuming acquireable this g qt th t' P + \deduce{args} @tele_app TT (fun _ => mpred) P args + \deduce{n} [| t' = Held n args /\ t = release t' |] + \deduce locked this g qt th n + \end. + Next Obligation. + intros * (? & ? & -> & ->)%is_held. + rewrite acquireable.unlock /=. ego. + Qed. + + #[program] + Definition acquireable_acquireable_C this γ := + \cancelx + \consuming{qt th n TT args P} acquireable this (TT := TT) γ qt th (Held n args) P + \bound P' + \bound_existential qt' th' args' + \proving acquireable this γ qt' th' args' P' + \instantiate qt' := qt + \instantiate th' := th + \instantiate args' := Held n args + \deduce @tele_app TT (fun _ => mpred) P args + \through @tele_app TT (fun _ => mpred) P' args + \end. + Next Obligation. intros. rewrite acquireable.unlock /=; work. Qed. + + #[program] + Definition own_P_is_acquireable_C {TT} this g qt n P := + \cancelx + \consuming{th} locked this g qt th n + \bound n' args + \proving acquireable this (TT := TT) g qt th (Held n' args) P + \through @tele_app TT (fun _ => mpred) P args + \through [| n' = n |] + \end. + Next Obligation. + intros. iIntros "Htoken" (n' args) "[HP %Heq]". + subst n'. rewrite acquireable.unlock /=. iFrame "#∗". + Qed. + End proof_automation. End with_cpp. - #[global] Hint Resolve acquireable_acquireable_C : br_hints. - #[global] Hint Resolve acquireable_is_acquired_C : br_hints. - #[global] Hint Resolve own_P_is_acquireable_C : br_hints. - #[global] Hint Resolve acquireable_current_thread_F : br_hints. + #[global] Hint Resolve acquireable_acquireable_C acquireable_is_acquired_C + own_P_is_acquireable_C : br_hints. +End recursive_mutex_spec. + +Module StdRecursiveMutexName. + Definition cpp_ty : type := "std::recursive_mutex"%cpp_type. + #[global] Hint Opaque cpp_ty : sl_opacity. + Definition cpp_N : bs := "std::recursive_mutex". +End StdRecursiveMutexName. + + +(** Bind the reusable specifications to std::recursive_mutex names, but with + a parametrized RECURSIVE_MUTEX_PREDS. *) +Module StdRecursiveMutex + (Preds : RECURSIVE_MUTEX_PREDS StdRecursiveMutexName). + Include Preds. + #[global] Hint Opaque cpp_ty : sl_opacity. + Module Spec := recursive_mutex_spec StdRecursiveMutexName Preds. + Include Spec. + + Section with_cpp. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MOD : source ⊧ σ}. + + cpp.spec "std::recursive_mutex::recursive_mutex()" as std_ctor_spec with + (\exact Reduce Spec.ctor_spec). + cpp.spec "std::recursive_mutex::~recursive_mutex()" as std_dtor_spec with + (\exact Reduce Spec.dtor_spec). + cpp.spec "std::recursive_mutex::lock()" as std_lock_spec with + (\exact Reduce Spec.lock_spec). + cpp.spec "std::recursive_mutex::unlock()" as std_unlock_spec with + (\exact Reduce Spec.unlock_spec). + cpp.spec "std::recursive_mutex::lock()" as std_lock_spec_alt with + (\exact Reduce Spec.lock_spec_alt). + cpp.spec "std::recursive_mutex::unlock()" as std_unlock_spec_alt with + (\exact Reduce Spec.unlock_spec_alt). + + Lemma std_lock_spec_equiv_std_lock_spec_alt : std_lock_spec -|- std_lock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.lock_spec_equiv_lock_spec_alt; done. + Qed. + Lemma std_unlock_spec_equiv_std_unlock_spec_alt : std_unlock_spec -|- std_unlock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.unlock_spec_equiv_unlock_spec_alt; done. + Qed. + End with_cpp. +End StdRecursiveMutex. -End recursive_mutex. +(** The standard-library implementation remains abstract; concrete recursive + mutex implementations supply their own [RECURSIVE_MUTEX_PREDS]. *) +Declare Module RecursiveMutexPreds : RECURSIVE_MUTEX_PREDS StdRecursiveMutexName. +Module std_recursive_mutex := StdRecursiveMutex RecursiveMutexPreds. diff --git a/rocq-brick-libstdcpp/proof/mutex/spec/recursive_mutex_atomic.v b/rocq-brick-libstdcpp/proof/mutex/spec/recursive_mutex_atomic.v new file mode 100644 index 00000000..5012990b --- /dev/null +++ b/rocq-brick-libstdcpp/proof/mutex/spec/recursive_mutex_atomic.v @@ -0,0 +1,542 @@ +Require Import iris.algebra.gset. +Require Import iris.algebra.coPset. +Require Import iris.algebra.lib.excl_auth. + +Require Import skylabs.bi.tls_modalities. +Require Import skylabs.bi.tls_modalities_rep. +Require Import skylabs.bi.weakly_objective. +Require Import skylabs.auto.cpp.weakly_local_with. + +Require Import skylabs.auto.cpp.proof. +Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. +Require Export skylabs.brick.libstdcpp.runtime.pred. + +Require Import skylabs.brick.libstdcpp.mutex.requirements. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Require Import skylabs.brick.libstdcpp.mutex.inc_hpp. + +Require Export skylabs.brick.libstdcpp.mutex.spec.recursive_mutex. + +Import linearity. + +(** This file contains: + - Atomic specs for recursive mutex [RecursiveMutexAtomicSpecs]. + - An instantiation of (non-atomic) recursive mutex predicates + (which can be plugged into [recursive_mutex_spec]). + - A proof that the non-atomic specs refine the atomic version. +*) + +(** Atomic specs for recursive mutex. *) +Module RecursiveMutexAtomicSpecs (T : MutexCPPName). + (* FIXME the hardcoded strings can be derived from T.cpp_ty. *) + + (* [locked γ qt th 0] does not identify the thread holding the mutex. *) + (** The exclusive owner/count fragment also records the deposited token share. *) + Canonical Structure phys_stateUR := + authR (optionR (exclR (prodO (prodO thread_idTO natO) (leibnizO cQp.t)))). + + (** [locked γ qt th n] records [n] acquisitions by [th], using token share [qt]. *) + Class lockedG `{Σ : cpp_logic} := { + #[global] tokens_G :: MutexTokens.G Σ; + + #[local] has_phys_state :: HasOwn (iPropI _Σ) phys_stateUR; + #[local] has_phys_state_upd :: HasOwnUpd (iPropI _Σ) phys_stateUR; + #[local] has_phys_state_valid :: HasOwnValid (iPropI _Σ) phys_stateUR; + #[local] has_phys_state_unit :: HasOwnUnit mpredI phys_stateUR; + }. + #[global] Arguments lockedG {_ _} Σ : assert. + + Record gname : Set := MkGname + { owned_count_id : iprop.gname; + mutex_inv_namespace : namespace; + inv_gname : iprop.gname; + token_gname : iprop.gname; + }. + + Definition token `{Σ : cpp_logic, !lockedG Σ} (g : gname) (qt : cQp.t) : mpred := + MutexTokens.token g.(token_gname) qt. + #[global] Instance token_timeless `{Σ : cpp_logic, !lockedG Σ} g qt : + Timeless (token g qt). + Proof. rewrite /token. apply _. Qed. + #[global] Instance token_WeaklyObjective `{Σ : cpp_logic, !lockedG Σ} g qt : + WeaklyObjective (token g qt). + Proof. rewrite /token /MutexTokens.token. apply _. Qed. + #[global] Hint Opaque token : sl_opacity typeclass_instances. + + (** [owned_count_id_auth γ (Some (th, n, qt))] implies that the lock's count is [n + 1]. *) + sl.lock + Definition owned_count_id_auth `{Σ : cpp_logic, !lockedG Σ} + (γ : gname) (om : option (thread_idT * nat * cQp.t)) : mpred := + own γ.(owned_count_id) (● (option_map Excl om)). + #[only(timeless)] derive owned_count_id_auth. + + (** [owned_count_id_frag γ (Some (th, n, qt))] implies that the lock's count is [n + 1]. *) + sl.lock + Definition owned_count_id_frag `{Σ : cpp_logic, !lockedG Σ} + (γ : gname) (om : option (thread_idT * nat * cQp.t)) : mpred := + own γ.(owned_count_id) (◯ (option_map Excl om)). + #[only(timeless)] derive owned_count_id_frag. + + #[local] Open Scope nat_scope. + + (** For positive [n], [locked γ qt th n] records the physical lock count + and exact registration share through [owned_count_id_frag]. At zero it + owns the namespace permission and registration token. The first lock + deposits both in the physical invariant; the final unlock returns them. *) + sl.lock + Definition locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !lockedG Σ} + (γ : gname) (qt : cQp.t) (th : thread_idT) (n : nat) : mpred := + match n with + | 0 => MutexSets.my_mutexes th + (CoPset $ ↑γ.(mutex_inv_namespace)) ** token γ qt + | S n => owned_count_id_frag γ (Some (th, n, qt)) + end. + #[only(timeless)] derive locked. + + Section locked_with_cpp. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!lockedG Σ}. + Context `{!HasStdThreads Σ}. + + #[global] Instance owned_count_id_frag_WeaklyObjective γ om : + WeaklyObjective (PROP := iPropI _) (owned_count_id_frag γ om). + Proof. rewrite owned_count_id_frag.unlock. apply _. Qed. + + #[global] Instance + locked_WeaklyObjective γ qt thr n : + WeaklyObjective (PROP := iPropI _) (locked γ qt thr n). + Proof. rewrite locked.unlock. apply _. Qed. + + Lemma locked_excl_different_thread g qt qt' th th' n m : + locked g qt th n ** locked g qt' th' m |-- [| n = 0 \/ m = 0 |] ** True. + Proof. + rewrite locked.unlock. + iIntros "[H1 H2]". + destruct n, m; try auto. iExFalso. + rewrite owned_count_id_frag.unlock. + iDestruct (own_valid_2 with "H1 H2") as %HV; exfalso. + rewrite -auth_frag_op auth_frag_valid in HV. + done. + Qed. + + End locked_with_cpp. + +(** +Underlying pthread implementation for [PTHREAD_MUTEX_RECURSIVE_NP] case: + + << + /* Check whether we already hold the mutex. */ + if (mutex->__data.__owner == id) + { + /* Just bump the counter. */ + if (__glibc_unlikely (mutex->__data.__count + 1 == 0)) + /* Overflow of the counter. */ + return EAGAIN; + + ++mutex->__data.__count; + + return 0; + } + (* LLL_MUTEX_LOCK_OPTIMIZED (mutex); *) + LLL_MUTEX_LOCK (mutex); + >> + +Informally, we can read mutex->__data.__owner atomically, and we know that +mutex->__data.__owner == id if and only if our thread has completed locking the +recursive mutex; hence, mutex->__data.__owner != id means that nobody is +touching the mutex or other threads are operating on it, but at no point will they set __owner to our ID. + +Hence: +1. [if (mutex->__data.__owner == id)], we can get sequential ownership of +mutex->__data.__count, and of the underlying resources, and complete the lock operation. +2. else, we can attempt to grab the underlying non-recursive lock, and be sure we + won't deadlock against ourselves. + +Formalizing step 1 seems nontrivial, but relatively routine. +But the full pthread implementation would add annoying details. + +The right invariant might resemble the following, but significant details are TBD. +[[ +cinv ( + \exists x, + mutex->__data.__owner |-> x ** + if bool_decide (x <> 0) then + (sequential ownership of count ** ownership of data protected by the lock) \/ + some exclusive token (* needed to take the sequential out *) + else + emp + ) +]] +*) + (* the mask of recursive_mutex *) + Definition mask := nroot .@@ "std" .@@ "recursive_mutex" .@@ "mask". + + (** We base the implementation protocol on + https://github.com/bminor/glibc/blob/04e750e75b73957cf1c791535a3f4319534a52fc/nptl/pthread_mutex_lock.c#L90-L112. + + official mirror: + https://sourceware.org/git/?p=glibc.git;a=blob;f=nptl/pthread_mutex_lock.c;h=a697f2b6ca8dfa9e4557ab3f44b87bc5ceeec014;hb=HEAD#l90 + TODO: revise. + *) + + (* NOTE: Invariant used to protect resource [r] + + [[ + inv (r \\// exists qt th n, locked qt th (S n)) + ]] + *) + + + (** Intended meaning: ownership of physical C++ state for an instance of "std::recursive_mutex". *) + Parameter rawR : ∀ `{Σ : cpp_logic, σ : genv}, option thread_idT -> nat -> Rep. + (* The thread_idT is None (0) if there is no owner. *) + Axiom rawR_type_ptr : ∀ `{Σ : cpp_logic, σ : genv} o n, + Observe (type_ptrR T.cpp_ty) (rawR o n). + #[global] Existing Instance rawR_type_ptr. + + Definition rmutex_N : namespace := + nroot .@@ "std" .@@ "recursive_mutex" .@ "raw_inv". + + sl.lock + Definition R `{Σ : cpp_logic, σ : genv, MUTEX_SETS : !mutex_set_G Σ, !lockedG Σ} (γ : gname) (q : cQp.t) : Rep := + type_ptrR T.cpp_ty ** + as_Rep (fun this => cinv rmutex_N γ.(inv_gname) + (∃ owner count qt, this |-> rawR owner count ** + (* [owned_count_id_auth] stores [counter - 1]. *) + owned_count_id_auth γ ((λ t, (t, Nat.pred count, qt)) <$> owner) ** + match owner with + | None => emp + | Some th => token γ qt ** + MutexSets.my_mutexes th + (CoPset $ ↑γ.(mutex_inv_namespace)) + end)) ** + pureR (cinv_own γ.(inv_gname) q). + (* TODO: add sequential ownership of the physical lock and + [|owner = None <-> count = O|]. *) + #[only(cfractional,cfracvalid,ascfractional)] derive R. + #[global] Instance R_type_ptr `{Σ : cpp_logic, σ : genv, MUTEX_SETS : !mutex_set_G Σ, !lockedG Σ} γ q : + Observe (type_ptrR T.cpp_ty) (R γ q). + Proof. rewrite R.unlock. apply _. Qed. + + + Section base_construction. + Context `{Σ : cpp_logic} `{MOD : source ⊧ σ}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!lockedG Σ}. + + #[global] Instance R_learn : Cbn (Learn (learn_eq ==> any ==> learn_hints.fin) R). + Proof. solve_learnable. Qed. + + Definition ctor_spec_body : ptr -> WpSpec mpred val val := + (\this this + \pre{N} emp + \post Exists g, + [| mutex_inv_namespace g = N |] ** + this |-> R g 1$m ** token g 1$m). + + Definition dtor_spec_body : ptr -> WpSpec mpred val val := + (\this this + \pre{g} this |-> R g 1$m + \pre token g 1$m + \post emp). + + Definition lock_spec_body : ptr -> WpSpec mpred val val := + (\this this + \prepost{q g} this |-> R g q (* part of both pre and post *) + \persist{th} current_thread th + \pre{qt Q} AC << ∀ n , locked g qt th n >> @ top \ ↑ mask , empty + << locked g qt th (S n), COMM Q >> + \post Q). + + Definition unlock_spec_body : ptr -> WpSpec mpred val val := + (\this this + \prepost{q g} this |-> R g q (* part of both pre and post *) + \persist{th} current_thread th + \pre{qt Q} AC << ∀ n , locked g qt th (S n) >> @ top \ ↑ mask , empty + << locked g qt th n, COMM Q >> + \post Q). + + Lemma locked_contradict_full_token (this : ptr) g q qt th n {E : coPset} : + ↑rmutex_N ⊆ E -> + this |-> R g q ** token g 1$m ** locked g qt th (S n) |-- + (|={E}=> False). + Proof. + intros HE. + rewrite R.unlock locked.unlock /token !_at_sep + !_at_pureR _at_as_Rep. + iIntros "((_ & #HI & Hown) & Htoken & Hcount)". + iInv rmutex_N as "[(%owner & %count & %held_qt & _ & >Hauth & Hheld) Hown]". + destruct owner as [owner|]. + - iDestruct "Hheld" as "[>Hheld _]". + iEval (rewrite /token) in "Hheld". + iDestruct (MutexTokens.token_valid_2 with "[$Htoken $Hheld]") as %Hbad. + exfalso. exact (Qp.not_add_le_l 1 (cQp.frac held_qt) Hbad). + - iEval (rewrite owned_count_id_auth.unlock /=) in "Hauth". + iEval (rewrite owned_count_id_frag.unlock) in "Hcount". + iDestruct (own_valid_2 with "Hauth Hcount") as %Hvalid. + exfalso. move: Hvalid. rewrite auth_both_valid_discrete /=. + rewrite option_included. naive_solver. + Qed. + + End base_construction. +End RecursiveMutexAtomicSpecs. + +(** An instantiation of RecursiveMutexPreds. This is built on top of predicates + in RecursiveMutexAtomicSpecs, so the abstraction here is not ideal. + The main result here is that the atomic specs can derive the non-atomic + ones; see the [RecursiveMutexRefinement] module defined later. *) +Module RecursiveMutexPreds (T : MutexCPPName) <: RECURSIVE_MUTEX_PREDS T. + Module at_specs := RecursiveMutexAtomicSpecs T. + Include RecursiveMutexState. + + Definition cpp_ty : type := T.cpp_ty. + #[global] Hint Opaque cpp_ty : sl_opacity. + + Record rmutex_gname := MkGname + { lock_gname : at_specs.gname + ; level_gname : iprop.gname + ; cinv_gname : iprop.gname + }. + Definition gname := rmutex_gname. + Definition rmutex_inv_namespace (g : gname) := + at_specs.mutex_inv_namespace g.(lock_gname). + + (** Remember the acquired share as well as the count and owner, so the + final unlock returns the same share that was registered. *) + Canonical Structure cmraR := + excl_authR (prodO (prodO natO thread_idTO) (leibnizO cQp.t)). + + Class recursive_mutexG `{Σ : cpp_logic} := { + #[global] locked_G :: at_specs.lockedG Σ; + #[global] level_own :: HasOwn (iPropI _Σ) cmraR; + #[global] level_valid :: HasOwnValid (iPropI _Σ) cmraR; + }. + Definition G := @recursive_mutexG. + Existing Class G. + #[global] Arguments G {_ _} Σ : assert. + #[global] Instance recursive_mutex_G `{Σ : cpp_logic} (H : G Σ) : + @recursive_mutexG _ _ Σ := H. + + Definition rmutex_namespace := + nroot .@@ "std" .@@ "recursive_mutex" .@@ "derived". + + sl.lock + Definition inv_rmutex `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} + (g : gname) (P : mpred) : mpred := + cinv rmutex_namespace g.(cinv_gname) + (Exists n th qt, own g.(level_gname) (●E (n, th, qt)) ** + match n with + | 0 => P ** own g.(level_gname) (◯E (n, th, qt)) + | S n => at_specs.locked g.(lock_gname) qt th (S n) + end). + #[only(knowledge)] derive inv_rmutex. + + (** Fractional ownership of the physical mutex, its invariant lifetime, + and the protected resource protocol. *) + Definition R `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {HAS_THREADS : HasStdThreads Σ} + {σ : genv} (g : gname) (q : cQp.t) (P : mpred) : Rep := + at_specs.R g.(lock_gname) q ** + pureR (cinv_own g.(cinv_gname) q) ** pureR (inv_rmutex g P). + #[global] Hint Opaque R : sl_opacity typeclass_instances. + #[only(cfractional,cfracvalid,ascfractional)] derive R. + #[global] Instance R_type_ptr `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} + {HAS_THREADS : HasStdThreads Σ} {σ : genv} g q P : + Observe (type_ptrR cpp_ty) (R g q P). + Proof. rewrite /R. apply _. Qed. + + Definition token `{Σ : cpp_logic, !G Σ} (g : gname) (qt : cQp.t) : mpred := + at_specs.token g.(lock_gname) qt. + #[global] Instance token_fractional `{Σ : cpp_logic, !G Σ} g : + CFractional (token g). + Proof. intros q1 q2. rewrite /token /at_specs.token cQp.frac_add. apply MutexTokens.token_fractional. Qed. + #[global] Instance token_timeless `{Σ : cpp_logic, !G Σ} g qt : Timeless (token g qt). + Proof. rewrite /token /at_specs.token. apply _. Qed. + #[global] Hint Opaque token : sl_opacity typeclass_instances. + + Definition not_locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} + (_this : ptr) (g : gname) (qt : cQp.t) (th : thread_idT) : mpred := + token g qt ** + MutexSets.my_mutexes th (CoPset $ ↑rmutex_inv_namespace g). + Definition locked `{Σ : cpp_logic, !G Σ, σ : genv} + (_this : ptr) (g : gname) (qt : cQp.t) (th : thread_idT) (n : nat) : mpred := + own g.(level_gname) (◯E (S n, th, qt)). + #[global] Instance not_locked_timeless `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} this g qt th : + Timeless (not_locked this g qt th). + Proof. rewrite /not_locked. apply _. Qed. + #[global] Instance locked_timeless `{Σ : cpp_logic, !G Σ, σ : genv} this g qt th n : + Timeless (locked this g qt th n). + Proof. rewrite /locked. apply _. Qed. + #[global] Hint Opaque not_locked locked : sl_opacity typeclass_instances. + + Section rules. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + + Context `{!HasStdThreads Σ} {σ : genv}. + + Lemma locked_contradict_full_token (this : ptr) g q qt th n P : + this |-> R g q P ** token g 1$m ** locked this g qt th n |-- + (|={⊤}=> False). + Proof. + rewrite /R /token /locked !_at_sep !_at_pureR inv_rmutex.unlock. + iIntros "((HR & Hown & #Hinv) & Htoken & Hheld)". + iInv rmutex_namespace as "[(%n' & %th' & %qt' & >Hauth & Hcase) Hown]". + iDestruct (own_valid_2 with "Hauth Hheld") as %[=]%excl_auth_agree_L; subst. + iMod "Hcase" as "Hlocked". + iMod (at_specs.locked_contradict_full_token with "[$HR $Htoken $Hlocked]") as %[]. + have Hdisjoint : (↑at_specs.rmutex_N : coPset) ## ↑rmutex_namespace. + { + rewrite /at_specs.rmutex_N /rmutex_namespace. + set base := nroot .@@ "std" .@@ "recursive_mutex". + have Hraw : base .@ "raw_inv" = base .@ (encode "raw_inv"). + { by rewrite !namespaces.ndot_unseal. } + have Hderived : base .@@ "derived" = base .@ (encode "derived"%bs). + { by rewrite !namespaces.ndot_unseal. } + rewrite Hraw Hderived. apply ndot_ne_disjoint. vm_compute. discriminate. + } + set_solver. + Qed. + + End rules. +End RecursiveMutexPreds. + +(** Atomic specs derive the non-atomic specs. *) +Module RecursiveMutexRefinement (T : MutexCPPName). + Module Preds := RecursiveMutexPreds T. + Import Preds. + Module Spec := recursive_mutex_spec T Preds. + Import Spec (acquireable). + +Section with_cpp. + Context `{Σ : cpp_logic} `{MOD : source ⊧ σ}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + Context `{HOV : !HasOwnValid mpredI cmraR, HOU : !HasOwnUpd mpredI cmraR}. + + (* basically std_ctor_atomic_spec |-- std_ctor_non_atomic_spec *) + Lemma at_spec_impl_na_spec_ctor this : + elaborate.spec_entails_fupd + (Preds.at_specs.ctor_spec_body this) + (Spec.ctor_spec this). + Proof using MOD HOV HOU. + work. + iModIntro; work. + iExists N. work. + (* The owner slot is unused while the initial count is zero. *) + set th : thread_idT := inhabitant. + iMod (own_alloc (●E (O, th, (1$m)%cQp) ⋅ ◯E (O, th, (1$m)%cQp))) as (g) "(? & ?)". + { apply excl_auth_valid. } + wname [_ |-> _ _ _] "a". + wname [at_specs.token] "Htoken". + iMod (cinv_alloc with "[-a Htoken]") as (ginv) "(#Hinv & Hown)"; last first. + - iExists {| lock_gname := t; level_gname := g; cinv_gname:= ginv |}. + rewrite /R /token /rmutex_inv_namespace !_at_sep !_at_pureR inv_rmutex.unlock /=. + iModIntro. + go with br_erefl $usenamed=true. + - ework with br_erefl. + - apply _. + Qed. + + Lemma at_spec_impl_na_spec_dtor this : + elaborate.spec_entails_fupd + (Preds.at_specs.dtor_spec_body this) + (Spec.dtor_spec this). + Proof using MOD HOV HOU. + work. + rewrite /R /token /rmutex_inv_namespace !_at_sep !_at_pureR inv_rmutex.unlock. + work. + iMod (cinv_cancel with "[$] [$]") as (n th qt) "(>? & ?)"; [done..|]. + + destruct n as [|n'] eqn:?; work; first last. + { + iMod (at_specs.locked_contradict_full_token with "[$]") as %[]; first set_solver. + } + iModIntro. work. iModIntro. ego with br_erefl. + (* _now_ we just need to leak ghost state and that's okay *) + iDestruct select (own _ (●E _)) as "L1". + iDestruct select (own _ (◯E _)) as "L2". + iCombine "L1 L2" as "L". + iApply (affine with "L"). apply mpred_BiAffine. + Qed. + + Lemma at_spec_impl_na_spec_lock this (xs : list val) (K : val -> mpred) : + Spec.lock_spec this xs K + |-- Preds.at_specs.lock_spec_body this xs K. + Proof using MOD HOV HOU. + revert xs K. + change (elaborate.spec_entails + (Preds.at_specs.lock_spec_body this) + (Spec.lock_spec this)). + work. + rewrite /R !_at_sep !_at_pureR; work. + iExists q, qt, (cinv_own g.(cinv_gname) q ** + (∃ t, [| acquire n t |] ∗ ▷ acquireable this g qt th t P))%I. + wname [bi_wand] "W"; wfocus (bi_wand _ _) "W". { work $usenamed=true. } + rewrite inv_rmutex.unlock Spec.acquireable.unlock /not_locked /locked + /token /rmutex_inv_namespace. + work; iAcIntro; rewrite /commit_acc/=; work. + iInv rmutex_namespace as "[(%n' & %th' & %qt' & >Hn & Hcases) ?]" "Hclose". + destruct n as [|n args]; simpl; [iExists 0 | iExists (S n)]. + 1: rewrite [in at_specs.locked _ _ _ 0]at_specs.locked.unlock /=. + all: work. + 2: iDestruct (own_valid_2 with "Hn [$]") as %[=]%excl_auth_agree_L; subst. + all: work $usenamed=true; iApply fupd_mask_intro; first set_solver; + iIntros "Hclose'"; work; iMod "Hclose'" as "_". + - destruct n'; first last. { + iMod "Hcases". + iDestruct (Preds.at_specs.locked_excl_different_thread with "[$]") as (?) "?". + exfalso. lia. + } + rewrite bi.later_sep bi.later_exist_except_0. + iDestruct "Hcases" as "(>(%args & ?) & >Hcase)". + iMod (own_update_2 with "Hn Hcase") as "(Hg & ?)"; + first apply (excl_auth_update _ _ (1, th, qt)). + wname [Preds.at_specs.locked _ qt th _] "Hlocked"; + iMod ("Hclose" with "[$Hg $Hlocked //]") as "_"; iModIntro. + iExists (Held 0 args). rewrite /locked /=. work. + - iMod (own_update_2 with "Hn [$]") as "(Hg & ?)"; + first apply (excl_auth_update _ _ (S (S n), th, qt)). + wname [Preds.at_specs.locked _ qt th _] "Hlocked"; + iMod ("Hclose" with "[$Hg $Hlocked //]") as "_"; iModIntro. + iExists (Held (S n) args). rewrite /locked /=. work. + Qed. + + Lemma at_spec_impl_na_spec_unlock this (xs : list val) (K : val -> mpred) : + Spec.unlock_spec this xs K + |-- Preds.at_specs.unlock_spec_body this xs K. + Proof using MOD HOV HOU. + revert xs K. + change (elaborate.spec_entails + (Preds.at_specs.unlock_spec_body this) + (Spec.unlock_spec this)). + work. + rewrite /R !_at_sep !_at_pureR; work. + iExists q, qt, (cinv_own g.(cinv_gname) q ** + acquireable this g qt th (release $ Held n args) P)%I. + wname [bi_wand] "W"; wfocus (bi_wand _ _) "W". { work $usenamed=true. } + rewrite inv_rmutex.unlock Spec.acquireable.unlock /not_locked /locked + /token /rmutex_inv_namespace. + work; iAcIntro; rewrite /commit_acc/=; work. + iInv rmutex_namespace as "[(%n' & %th' & %qt' & >Hn & Hcases) ?]" "Hclose". + iDestruct (own_valid_2 with "Hn [$]") as %[=]%excl_auth_agree_L; subst. + iMod "Hcases". + iApply fupd_mask_intro; first set_solver; iIntros "Hclose'". + iExists n; work $usenamed=true. + iMod "Hclose'" as "_". + iMod (own_update_2 with "Hn [$]") as "(Hg & Hcase)"; + first apply (excl_auth_update _ _ (n, th, qt)). + rewrite /not_locked /locked /token + /rmutex_inv_namespace release.unlock; destruct n. + 1: rewrite [in at_specs.locked _ _ _ 0]at_specs.locked.unlock /=. + 1: iDestruct select (MutexSets.my_mutexes _ _ ** at_specs.token _ _) as "[? ?]". + all: iFrame "#∗". + all: iMod ("Hclose" with "[-]") as "_"; + ework $usenamed=true with br_erefl; done. + Qed. + +End with_cpp. +End RecursiveMutexRefinement. diff --git a/rocq-brick-libstdcpp/proof/mutex/spec/scoped_lock.v b/rocq-brick-libstdcpp/proof/mutex/spec/scoped_lock.v index 7a734e9f..0e30fa45 100644 --- a/rocq-brick-libstdcpp/proof/mutex/spec/scoped_lock.v +++ b/rocq-brick-libstdcpp/proof/mutex/spec/scoped_lock.v @@ -1,62 +1,85 @@ -Require Import skylabs.auto.cpp.prelude.proof. -Require Import skylabs.brick.libstdcpp.mutex.inc_hpp. +Require Import skylabs.auto.cpp.proof. +Require Import skylabs.cpp.spec.specify. Require Export skylabs.brick.libstdcpp.runtime.pred. -Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. +Require Import skylabs.brick.libstdcpp.mutex.spec.prelude. +Require Import skylabs.brick.libstdcpp.mutex.requirements. + +(* Generic specs for "scoped_lock". *) +NES.Begin scoped_lock. + Record M {T : Type} : Type := Mk + { mutex_ptr : ptr + ; mutex_q : Qp + ; mutex_m : T }. + #[global] Arguments M _ : clear implicits. + #[global] Arguments Mk {_} _ _ _. + #[only(lens)] derive M. + + #[global] Hint Opaque mutex_ptr : sl_opacity. + + (* We only write specs in the case of two mutexes in a scoped lock. + More mutexes is not conceptually more difficult. *) + Parameter R : forall + `{Σ : cpp_logic} {σ : genv} ty1 ty2 {T1 T2} mutexR1 mutexR2 + `{!BasicLockable ty1 (T:=T1) mutexR1, !Lockable ty1 mutexR1, + !BasicLockable ty2 (T:=T2) mutexR2, !Lockable ty2 mutexR2}, + cQp.t -> M T1 * M T2 -> Rep. -Module scoped_lock. Section with_cpp. - Context `{Σ : cpp_logic}. - - Parameter R : forall {HAS_THREADS : HasStdThreads Σ} {σ : genv}, - cQp.t -> list (ptr * gname * Qp * mpred) -> Rep. - - #[only(type_ptr="std::scoped_lock")] derive R. - #[only(cfractional,ascfractional,cfracvalid)] derive R. - - Section with_threads. - Context {σ : genv}. - Context `{HAS_THREADS : !HasStdThreads Σ}. - - #[global] Instance: LearnEqF1 R := ltac:(solve_learnable). - - cpp.spec - "std::scoped_lock<...>::scoped_lock(std::mutex&, std::mutex&)" - (* "std::scoped_lock::scoped_lock(std::mutex&, std::mutex&)" *) - as ctor_spec from source with ( - \this this - \arg{mp1} "" (Vptr mp1) - \pre{g1 q1 P1} mp1 |-> mutex.R g1 q1$m P1 - \pre mutex.token g1 q1 - \arg{mp2} "" (Vptr mp2) - \pre{g2 q2 P2} mp2 |-> mutex.R g2 q2$m P2 - \pre mutex.token g2 q2 - \persist{thr} current_thread thr - \post - this |-> R 1$m [ (mp1, g1, q1, P1); (mp2, g2, q2, P2)] ** - P1 ** mutex.locked g1 thr q1 ** - P2 ** mutex.locked g2 thr q2 - ). - - cpp.spec "std::scoped_lock<...>::~scoped_lock()" - as dtor_spec from source with ( - \this this - \persist{thr} current_thread thr - \pre{ - mp1 mp2 - g1 q1 P1 - g2 q2 P2 - } - this |-> R 1$m [ (mp1, g1, q1, P1); (mp2, g2, q2, P2)] - \pre |> P1 - \pre |> P2 - \pre mutex.locked g1 thr q1 - \pre mutex.locked g2 thr q2 - \post - mp1 |-> mutex.R g1 q1$m P1 ** mutex.token g1 q1 ** - mp2 |-> mutex.R g2 q2$m P2 ** mutex.token g2 q2 - ). - End with_threads. - End with_cpp. + Context `{Σ : cpp_logic} {σ : genv}. + Context (ty1 ty2 : type) {mutexT1 mutexT2 : Type}. + Context {mutexR1 : cQp.t -> mutexT1 -> Rep} {mutexR2 : cQp.t -> mutexT2 -> Rep}. + Context `{!BasicLockable ty1 (T:=mutexT1) mutexR1, !Lockable ty1 mutexR1}. + Context `{!BasicLockable ty2 (T:=mutexT2) mutexR2, !Lockable ty2 mutexR2}. + + #[local] Definition scoped_lock_type := + Tnamed ("std::scoped_lock" .<< Apack [Atype ty1; Atype ty2] >>). + #[only(type_ptr=scoped_lock_type,cfracvalid)] derive R. + + Section with_cfrac. + Context `{CFrac1 : !CFractional1 mutexR1, CFrac2 : !CFractional1 mutexR2}. + #[local] Set Default Proof Using "CFrac1 CFrac2". -End scoped_lock. + #[only(cfractional,ascfractional)] derive R. + End with_cfrac. + + #[local] Abbreviation R := (R ty1 ty2 mutexR1 mutexR2). + + #[global] Instance R_learn : LearnEqF1 R := ltac:(solve_learnable). + + (* The generated template metadata does not expand parameter packs, so + register the two-mutex signatures explicitly. *) + #[local] Abbreviation scoped_lock_name := + ("std::scoped_lock" .<< Apack [Atype ty1; Atype ty2] >>) (only parsing). + + Definition ctor_spec : mpred := + specify.template.ctor scoped_lock_name [Tref ty1; Tref ty2] $ + \this this + \arg{mp1} "" (Vptr mp1) + \pre{q1 m1} mp1 |-> mutexR1 q1$m m1 + \pre{K1} do_lock ty1 mp1 m1 K1 + \arg{mp2} "" (Vptr mp2) + \pre{q2 m2} mp2 |-> mutexR2 q2$m m2 + \pre{K2} do_lock ty2 mp2 m2 K2 + \post + this |-> R 1$m (Mk mp1 q1 m1, Mk mp2 q2 m2) ** K1 ** K2. + #[global] Hint Opaque ctor_spec : sl_opacity. + #[global] Arguments ctor_spec : simpl never. + Definition SpecFor_ctor_spec := RegisterSpec ctor_spec. + #[global] Existing Instance SpecFor_ctor_spec. + + Definition dtor_spec : mpred := + specify.template.dtor scoped_lock_name $ + \this this + \pre{m1 m2} this |-> R 1$m (m1, m2) + \pre{K1} do_unlock ty1 m1.(mutex_ptr) m1.(mutex_m) K1 + \pre{K2} do_unlock ty2 m2.(mutex_ptr) m2.(mutex_m) K2 + \post K1 ** K2 ** + m1.(mutex_ptr) |-> mutexR1 m1.(mutex_q)$m m1.(mutex_m) ** + m2.(mutex_ptr) |-> mutexR2 m2.(mutex_q)$m m2.(mutex_m). + #[global] Hint Opaque dtor_spec : sl_opacity. + #[global] Arguments dtor_spec : simpl never. + Definition SpecFor_dtor_spec := RegisterSpec dtor_spec. + #[global] Existing Instance SpecFor_dtor_spec. + End with_cpp. +NES.End scoped_lock. diff --git a/rocq-brick-libstdcpp/proof/shared_mutex/shared_mutex.v b/rocq-brick-libstdcpp/proof/shared_mutex/shared_mutex.v index e5541e62..cc69f0c8 100644 --- a/rocq-brick-libstdcpp/proof/shared_mutex/shared_mutex.v +++ b/rocq-brick-libstdcpp/proof/shared_mutex/shared_mutex.v @@ -1,7 +1,7 @@ +Require Import iris.algebra.coPset. + Require Import skylabs.bi.tls_modalities. Require Import skylabs.bi.tls_modalities_rep. -Require Import skylabs.bi.weakly_objective. -Require Import skylabs.auto.cpp.weakly_local_with. Require Import skylabs.auto.cpp.spec. Require Import skylabs.auto.cpp.proof. @@ -9,327 +9,326 @@ Require Export skylabs.brick.libstdcpp.runtime.pred. Require Import skylabs.brick.libstdcpp.shared_mutex.inc_hpp. Require Import skylabs.brick.libstdcpp.mutex.requirements. -Require Import skylabs.brick.libstdcpp.lib.lock_ghost. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Export lock_ghost2.mutex_set_G (mutex_set_G). Import linearity. -Import lock_ghost. - -(* TODO UPSTREAM. *) -#[global] Instance SplitRecord_prod A B : SplitRecord (@prod A B) := {}. - -Module shared_mutex. -Section with_cpp. - - (* maps th:thread_idT to the fraction of permission borrowed. *) - Canonical Structure phys_stateUR := authR (gmapR thread_idT Qp). - - - Class lockedG `{Σ : cpp_logic} := { - #[local] has_lock_ghost :: lock_ghost.lockG Σ; - - #[local] has_phys_state :: HasOwn (iPropI _Σ) phys_stateUR; - #[local] has_phys_state_upd :: HasOwnUpd (iPropI _Σ) phys_stateUR; - #[local] has_phys_state_valid :: HasOwnValid (iPropI _Σ) phys_stateUR; - }. - - #[global] Arguments lockedG {_ _} Σ : assert. - - Record gname : Set := MkGname - { phys_state_gname : iprop.gname; - login_gname : iprop.gname; - inv_gname : iprop.gname; - }. - - Definition reader_locked `{Σ : cpp_logic, !lockedG Σ} - (γ : gname) (th : thread_idT) (q : Qp) : mpred := - own γ.(phys_state_gname) (◯ {[ th := q ]}). - - (** Adapted from https://gitlab.mpi-sws.org/iris/iris/-/blob/master/iris_heap_lang/lib/rw_spin_lock.v?ref_type=heads - - Allows to prove writer_writer_exclusive and writer_reader_exclusive. - Unlike them, we can't just use ∅ for the reader map, because when the - writer unlocks, the cinv stores its `user γ th`, but `th` is existentially - quanified in cinv and it needs some way to recover that `th` matches with - itself. Note that we still lose writer_reader_exclusiveness for the same, - thread, but that should be fine with the current cinv. - *) - Definition locked `{Σ : cpp_logic, !lockedG Σ} - (γ : gname) (th : thread_idT) : mpred := - own γ.(phys_state_gname) (●{# 3/4} {[ th := 1%Qp ]}). - - Local Lemma writer_writer_exclusive `{Σ : cpp_logic, !lockedG Σ} - γ th1 th2 : - locked γ th1 -∗ locked γ th2 -∗ False. - Proof. - iIntros "H1 H2". - iDestruct (own_valid_2 with "H1 H2") as "%Hvalid". - exfalso. - rewrite - auth_auth_dfrac_op_valid - dfrac_op_own - dfrac_valid_own in Hvalid. - by destruct Hvalid as [? _]. - Qed. - - Local Lemma reader_writer_exclusive `{Σ : cpp_logic, !lockedG Σ} - γ th1 th2 q: - th1 <> th2 -> - reader_locked γ th1 q -∗ locked γ th2 -∗ False. - Proof. - iIntros (Hneq) "H1 H2". - iDestruct (own_valid_2 with "H2 H1") as "%Hvalid". - exfalso. - apply auth_both_dfrac_valid in Hvalid as (_ & Hvalid & _). - generalize (Hvalid 0)=> H. - apply singleton_includedN_l in H as (? & ? & ?). - rewrite lookup_singleton in H. - destruct decide in H; try done. - inv H. - Qed. - - Abbreviation used_threads γ s := - (lock_ghost.used_threads γ.(login_gname) s). - - Abbreviation users γ ths := - (lock_ghost.users γ.(login_gname) ths). - - Abbreviation user γ th := (users γ {[ th ]}). - - Context `{Σ : cpp_logic}. - Context `{!lockedG Σ}. - Context `{!HasStdThreads Σ}. - - #[global] Instance - locked_WeaklyObjective γ thr : - WeaklyObjective (PROP := iPropI _) (locked γ thr). - Proof. (* locked.unlock. *) apply _. Qed. - - #[global] Hint Opaque locked : sl_opacity typeclass_instances. - - #[global] Instance - reader_locked_WeaklyObjective γ thr n : - WeaklyObjective (PROP := iPropI _) (reader_locked γ thr n). - Proof. (* locked.unlock. *) apply _. Qed. - - #[global] Hint Opaque reader_locked : sl_opacity typeclass_instances. - - (** A resource enforcing that the thread calling unlock must be the same thread - that owns the lock - - << - \persist{th} >={ L_TI } th - \pre{j} mutex_locked g j - test_unlock(std::mutex & m) { - m.unlock(); - } - >> - - this succeeds: - - << - \persist{th} >={ L_TI } th - \pre mutex_locked g th - same test_unlock - >> - *) - - Context `{MOD : source ⊧ σ}. - - Definition smutex_N : namespace := - nroot .@@ "std" .@@ "shared_mutex" .@ "raw_inv". - - Section with_Qcanon. - Import Qcanon. - - Definition oqp_to_qc (oqp : option Qp) : Qc := - match oqp with - | Some qP => Qp_to_Qc qP - | None => 0%Qc - end. - - Definition smutex_inv γ (P : Qp -> mpred) : mpred := - ∃ (oqP : option Qp) ths borrow_map, - match oqP with - | Some qP => P qP - | None => emp - end ** - (* the set of borrowers *) - users γ ths ** - (* writer mode: cinv keeps ●{# 1/4} of the borrow map and the frac so - writer can't call shared_unlock(). writer has ●{# 3/4}. - reader mode: cinv keeps the full borrow map, readers has the fracs. *) - (( - own γ.(phys_state_gname) (●{# 1/4} borrow_map) ∗ - own γ.(phys_state_gname) (◯ borrow_map) ∧ - ∃ th, [| borrow_map = {[ th := 1%Qp ]} |] - ) ∨ - own γ.(phys_state_gname) (● borrow_map)) ** - (* the permission borrowed and the permission left in the inv sums to 1 *) - [| map_fold (λ _ qi q, (Qp_to_Qc qi) + q) (oqp_to_qc oqP) borrow_map = 1%Qc |] ** - (* all borrowers have to turn in their `user` handle *) - [| ∀ th, th ∈ ths ↔ th ∈ dom borrow_map |]. - End with_Qcanon. - - (** Convention: - qi: fraction of the invariant - qP: fraction of P - TODO: still relevant? - *) - - Local Lemma writer_reader_excl g th1 th2 qP : - th1 ≠ th2 -> - reader_locked g th1 qP ** locked g th2 |-- False. - Proof. - iIntros (?) "[H1 H2]". - iDestruct (reader_writer_exclusive with "H1 H2") as %[]; done. - Qed. - - Local Lemma writer_writer_excl g th1 th2 : - locked g th1 ** locked g th2 |-- False. - Proof. - iIntros "[H1 H2]". - iDestruct (writer_writer_exclusive with "H1 H2") as %[]; done. - Qed. - - (** Fractional ownership of a <> guarding the predicate <

>. *) - Definition R γ (q : cQp.t) (P : Qp -> mpred) : Rep := - structR "std::shared_mutex" q ** - (* should also have ownership of the underlying fields *) - pureR (cinv smutex_N γ.(inv_gname) (smutex_inv γ P) ** - cinv_own γ.(inv_gname) q). - #[global] Hint Opaque R : sl_opacity typeclass_instances. - #[only(cfractional,cfracvalid,ascfractional,type_ptr="std::shared_mutex")] derive R. - #[global] Declare Instance R_learnable : forall {σ : genv}, - Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). - - (* TODO prove that we can allocate used_threads ** the invariant in R *) - cpp.spec "std::shared_mutex::shared_mutex()" as ctor_spec with ( - \this this - \pre{P} ▷P 1%Qp - \post Exists g, this |-> R g 1$m P ** used_threads g ∅). - - cpp.spec "std::shared_mutex::~shared_mutex()" as dtor_spec with ( - \this this - (** the "user" set being ∅ enforces that there is no reader or write *) - \pre{g P} this |-> R g 1$m P ** used_threads g ∅ - \post P 1%Qp). - - (* "Inline" version of these specs. *) - cpp.spec "std::shared_mutex::lock()" as lock_spec_alt with ( - \this this - \prepost{qi P g} this |-> R g qi P - \persist{thr} current_thread thr - \pre user g thr - \post P 1%Qp ** locked g thr). - - cpp.spec "std::shared_mutex::try_lock()" as try_lock_spec_alt with ( - \this this - \prepost{qi P g} this |-> R g qi P - \persist{thr} current_thread thr - \pre user g thr - \post{b}[Vbool b] if b then P 1%Qp ** locked g thr else user g thr). - - cpp.spec "std::shared_mutex::unlock()" as unlock_spec_alt with ( - \this this - \prepost{qi P g} this |-> R g qi P - \persist{thr} current_thread thr - \pre locked g thr - \pre ▷ P 1%Qp - \post user g thr). - - cpp.spec "std::shared_mutex::lock_shared()" as lock_shared_spec_alt with ( - \this this - \prepost{qi P g} this |-> R g qi P - \persist{thr} current_thread thr - \pre user g thr - \post ∃ qP, P qP ** reader_locked g thr qP). - - cpp.spec "std::shared_mutex::try_lock_shared()" as try_lock_shared_spec_alt with ( - \this this - \prepost{qi P g} this |-> R g qi P - \persist{thr} current_thread thr - \pre user g thr - \post{b}[Vbool b] if b then ∃ qP, P qP ** reader_locked g thr qP else user g thr). - - cpp.spec "std::shared_mutex::unlock_shared()" as unlock_shared_spec_alt with ( - \this this - \prepost{qi P g} this |-> R g qi P - \persist{thr} current_thread thr - \pre{qP} reader_locked g thr qP - \pre ▷P qP - \post user g thr). - - (** Safety Properties (we model violation of these properties as stuckness): - 1. a thread calling lock() or shared_lock() twice should fail. - This is prevented by having a unique `user`, and turn in user in the inv. - 2. `lock(); ~shared_mutex()` should fail. - Assume we have `used_threads g s`. - dtor requires s=∅, but `lock()` puts the unique `user γ th` in inv, so we - can't deallocate that piece, therefore th ∈ s. - 3. `lock(); unlock_shared()` should fail. - This is prevented because locked can't be transformed into reader_locked, - and writer is not given one. - *) - - Definition do_lock (lk : gname * (Qp -> mpred)) (K: mpred) : mpred := - let g := lk.1 in - let P := lk.2 in - ∃ thr, current_thread thr ∗ user g thr ∗ - (* TODO readd *) - (* ▷ *) - (locked g thr ** P 1%Qp -* K). - #[global] Arguments do_lock /. - - Definition do_unlock (lk : gname * (Qp -> mpred)) (Q : mpred) : mpred := - let g := lk.1 in - let P := lk.2 in - ∃ thr, current_thread thr ** locked g thr ** ▷P 1%Qp ** - (* TODO readd *) - (* ▷ *) - (user g thr -* Q). - #[global] Arguments do_unlock /. - - (* Obtain same specs from (Basic)Lockable. *) - (** <> implements [BasicLockable] *) - Definition T : Type := gname * (Qp -> mpred). - - #[global] Instance shared_mutex_basic_lockable : BasicLockable (T:=T) "std::shared_mutex" (λ q γP, R γP.1 q γP.2) := - { do_lock := fun this => do_lock - ; do_unlock := fun this => do_unlock }. - - cpp.spec "std::shared_mutex::lock()" as lock_spec with - (\exact Reduce (lock_basic_lockable "std::shared_mutex" (λ q γP, R γP.1 q γP.2))). - - cpp.spec "std::shared_mutex::unlock()" as unlock_spec with - (\exact Reduce (unlock_basic_lockable "std::shared_mutex" (λ q γP, R γP.1 q γP.2))). - - Definition do_try_lock (lk : gname * (Qp -> mpred)) (Q : bool -> mpred) : mpred := - let g := lk.1 in - let P := lk.2 in - ∃ thr, current_thread thr ∗ user g thr ∗ - ∀ b : bool, - (if b then P 1%Qp ** locked g thr else user g thr) -∗ Q b. - #[global] Arguments do_try_lock /. - - #[global,program] Instance shared_mutex_lockable : Lockable (T:=T) "std::shared_mutex" (λ q γP, R γP.1 q γP.2) := - { do_try_lock := fun this => do_try_lock }. - - cpp.spec "std::shared_mutex::try_lock()" as try_lock_spec with - (\exact Reduce (try_lock_lockable "std::shared_mutex" (λ q γP, R γP.1 q γP.2))). - - Lemma lock_spec_entails_lock_spec_alt : lock_spec -|- lock_spec_alt. - Proof. - iSplit; iApply specify_mono; ework with br_erefl. - Qed. - - Lemma unlock_spec_entails_unlock_spec_alt : unlock_spec -|- unlock_spec_alt. - Proof. - iSplit; iApply specify_mono; ework with br_erefl. - Qed. - - Lemma try_lock_spec_entails_try_lock_spec_alt : try_lock_spec -|- try_lock_spec_alt. - Proof. - iSplit; iApply specify_mono; ework with br_erefl. - Qed. -End with_cpp. -End shared_mutex. + +(** Predicates for shared mutexes, independent of their implementation. *) +Module Type SHARED_MUTEX_PREDS. + Parameter cpp_ty : type. + Parameter gname : Set. + Parameter G : forall `{Σ : cpp_logic}, Type. + Existing Class G. + #[global] Arguments G {_ _} Σ : assert. + + Parameter smutex_inv_namespace : gname -> namespace. + + Parameter token : forall `{Σ : cpp_logic, !G Σ}, + gname -> cQp.t -> mpred. + Definition not_locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (g : gname) (th : thread_idT) (qt : cQp.t) : mpred := + token g qt ** MutexSets.my_mutexes th + (CoPset $ ↑smutex_inv_namespace g). + + Parameter locked : forall `{Σ : cpp_logic, !G Σ} {σ : genv}, + ptr -> gname -> thread_idT -> cQp.t -> mpred. + (** [reader_locked this g th q qP] says it contains [token g q] and has + borrowed [P qP] (but [reader_locked] does not include [P] itself). *) + Parameter reader_locked : forall `{Σ : cpp_logic, !G Σ} {σ : genv}, + ptr -> gname -> thread_idT -> cQp.t -> Qp -> mpred. + + (** A shared mutex guarding a fractional predicate [P]. *) + Parameter R : forall `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} + {HAS_THREADS : HasStdThreads Σ} {σ : genv}, + gname -> cQp.t -> (Qp -> mpred) -> Rep. + #[global] Hint Opaque token not_locked locked reader_locked R : + sl_opacity typeclass_instances. + + Section with_cpp. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + + #[global] Declare Instance token_fractional g : CFractional (token g). + #[global] Declare Instance token_timeless g qt : Timeless (token g qt). + + Context {σ : genv}. + + #[global] Instance not_locked_timeless this g th qt : + Timeless (not_locked this g th qt). + Proof. rewrite /not_locked. apply _. Qed. + #[global] Instance not_locked_exclusive this g th : + Exclusive1 (not_locked this g th). + Proof. + intros qt1 qt2. rewrite /not_locked. + iIntros "[_ H1] [_ H2]". + have Hoverlap : ~ ((↑smutex_inv_namespace g : coPset) ## ↑smutex_inv_namespace g). + { have Hnonempty := nclose_non_empty (smutex_inv_namespace g). set_solver. } + iDestruct (MutexSets.my_mutexes_exclusive th + (↑smutex_inv_namespace g) (↑smutex_inv_namespace g) Hoverlap + with "[$H1 $H2]") as %[]. + Qed. + #[global] Declare Instance locked_timeless this g th qt : + Timeless (locked this g th qt). + #[global] Declare Instance locked_exclusive this g : + Exclusive2 (locked this g). + #[global] Declare Instance locked_objective this g th qt : + Objective (locked this g th qt). + #[global] Declare Instance reader_locked_timeless this g th qt qP : + Timeless (reader_locked this g th qt qP). + #[global] Declare Instance reader_locked_objective this g th qt qP : + Objective (reader_locked this g th qt qP). + + Axiom reader_writer_exclusive : forall this g th1 th2 qt1 qt2 qP, + th1 <> th2 -> + reader_locked this g th1 qt1 qP -∗ locked this g th2 qt2 -∗ False. + + Context {HAS_THREADS : HasStdThreads Σ}. + + #[only(cfractional,cfracvalid,ascfractional)] derive R. + #[global] Declare Instance R_type_ptr g q P : + Observe (type_ptrR cpp_ty) (R g q P). + End with_cpp. +End SHARED_MUTEX_PREDS. + +(** Reusable contracts and their [BasicLockable]/[Lockable] presentation. *) +Module shared_mutex_spec (Preds : SHARED_MUTEX_PREDS). + Import Preds. + Section with_cpp. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + Context {HAS_THREADS : HasStdThreads Σ} {σ : genv}. + + (* ctor and dtor are the same as mutex.v, except that [P] is fractional. *) + Definition ctor_spec : ptr -> WpSpec mpred val val := + (\this this + \pre{N P} ▷P 1%Qp + \require Fractional P + \require ∀ q, Objective (P q) + \post Exists g, + [| smutex_inv_namespace g = N |] ** + this |-> R g 1$m P ** token g 1$m). + + Definition dtor_spec : ptr -> WpSpec mpred val val := + (\this this + \pre{g P} this |-> R g 1$m P ** token g 1$m + \post P 1%Qp). + + Definition lock_spec_alt : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} not_locked this g thr qt + \post P 1%Qp ** locked this g thr qt). + + Definition unlock_spec_alt : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} locked this g thr qt + \pre ▷P 1%Qp + \post not_locked this g thr qt). + + Definition try_lock_spec_alt : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} not_locked this g thr qt + \post{b}[Vbool b] + if b then P 1%Qp ** locked this g thr qt + else not_locked this g thr qt). + + Definition lock_shared_spec : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} not_locked this g thr qt + \post ∃ qP, P qP ** reader_locked this g thr qt qP). + + Definition try_lock_shared_spec : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt} not_locked this g thr qt + \post{b}[Vbool b] + if b then ∃ qP, P qP ** reader_locked this g thr qt qP + else not_locked this g thr qt). + + Definition unlock_shared_spec : ptr -> WpSpec mpred val val := + (\this this + \prepost{q P g} this |-> R g q P + \persist{thr} current_thread thr + \pre{qt qP} reader_locked this g thr qt qP + \pre ▷P qP + \post not_locked this g thr qt). + + Definition do_lock (this : ptr) (lk : gname * (Qp -> mpred)) + (K : mpred) : mpred := + ∃ thr qt, current_thread thr ** not_locked this lk.1 thr qt ** + (locked this lk.1 thr qt ** lk.2 1%Qp -* K). + #[global] Arguments do_lock /. + + Definition do_unlock (this : ptr) (lk : gname * (Qp -> mpred)) + (K : mpred) : mpred := + ∃ thr qt, current_thread thr ** locked this lk.1 thr qt ** ▷lk.2 1%Qp ** + (not_locked this lk.1 thr qt -* K). + #[global] Arguments do_unlock /. + + #[global] Instance shared_mutex_basic_lockable : + BasicLockable (T := gname * (Qp -> mpred)) cpp_ty + (fun q gP => R gP.1 q gP.2) := + { do_lock := do_lock + ; do_unlock := do_unlock }. + + Definition do_try_lock (this : ptr) (lk : gname * (Qp -> mpred)) + (K : bool -> mpred) : mpred := + ∃ thr qt, current_thread thr ** not_locked this lk.1 thr qt ** + ∀ b : bool, + (if b then lk.2 1%Qp ** locked this lk.1 thr qt + else not_locked this lk.1 thr qt) -* K b. + #[global] Arguments do_try_lock /. + + #[global] Instance shared_mutex_lockable : + Lockable (T := gname * (Qp -> mpred)) cpp_ty + (fun q gP => R gP.1 q gP.2) := + { do_try_lock := do_try_lock }. + + Definition lock_spec : ptr -> WpSpec mpred val val := + lock_basic_lockable cpp_ty (fun q gP => R gP.1 q gP.2). + + Definition unlock_spec : ptr -> WpSpec mpred val val := + unlock_basic_lockable cpp_ty (fun q gP => R gP.1 q gP.2). + + Definition try_lock_spec : ptr -> WpSpec mpred val val := + try_lock_lockable cpp_ty (fun q gP => R gP.1 q gP.2). + + Lemma lock_spec_equiv_lock_spec_alt this xs K : + lock_spec this xs K ⊣⊢ lock_spec_alt this xs K. + Proof. + unfold lock_spec, lock_basic_lockable, lock_spec_alt, do_lock. + cbn. iSplit. + - ework with br_erefl. + - iIntros "H". iDestruct "H" as (q P g thr qt) + "(%Hxs & HR & #HT & HNL & HK)". + iExists q, (g, P), (P 1%Qp ** locked this g thr qt)%I. + iFrame "HR HK". iSplit; first done. + iExists thr, qt. iFrame "HT HNL". + iIntros "[HL HP]". iFrame. + Qed. + + Lemma unlock_spec_equiv_unlock_spec_alt this xs K : + unlock_spec this xs K ⊣⊢ unlock_spec_alt this xs K. + Proof. + unfold unlock_spec, unlock_basic_lockable, unlock_spec_alt, do_unlock. + cbn. iSplit. + - ework with br_erefl. + - iIntros "H". iDestruct "H" as (q P g thr qt) + "(%Hxs & HR & #HT & HL & HP & HK)". + iExists q, (g, P), (not_locked this g thr qt). + iFrame "HR HK". iSplit; first done. + iExists thr, qt. iFrame "HT HL HP". + iIntros "$". + Qed. + + Lemma try_lock_spec_equiv_try_lock_spec_alt + (Htry_lock : requirements.do_try_lock cpp_ty = do_try_lock) this xs K : + try_lock_spec this xs K ⊣⊢ try_lock_spec_alt this xs K. + Proof. + unfold try_lock_spec, try_lock_lockable. rewrite Htry_lock. + unfold try_lock_spec_alt, do_try_lock. + cbn. iSplit. + - ework with br_erefl. + - iIntros "H". iDestruct "H" as (q P g thr qt) + "(%Hxs & HR & #HT & HNL & HK)". + iExists q, (g, P), (fun b : bool => + if b then (P 1%Qp ** locked this g thr qt)%I + else not_locked this g thr qt). + iFrame "HR HK". iSplit; first done. + iExists thr, qt. iFrame "HT HNL". + iIntros (b) "$". + Qed. + + Section proof_automation. + #[global] Instance R_learn : + Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). + Proof. solve_learnable. Qed. + #[global] Instance not_locked_learn this g th : + LearnEq1 (not_locked this g th). + Proof. solve_learnable. Qed. + #[global] Instance locked_learn this g th : + LearnEq1 (locked this g th). + Proof. solve_learnable. Qed. + #[global] Instance reader_locked_learn this g th : + LearnEq2 (reader_locked this g th). + Proof. solve_learnable. Qed. + End proof_automation. + End with_cpp. +End shared_mutex_spec. + +(** Bind the reusable specifications to their standard C++ names. *) +Module StdSharedMutex + (Preds : SHARED_MUTEX_PREDS with Definition cpp_ty := "std::shared_mutex"%cpp_type). + Include Preds. + #[global] Hint Opaque cpp_ty : sl_opacity. + Module Spec := shared_mutex_spec Preds. + + Section with_cpp. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MOD : source ⊧ σ}. + + cpp.spec "std::shared_mutex::shared_mutex()" as ctor_spec with + (\exact Reduce Spec.ctor_spec). + cpp.spec "std::shared_mutex::~shared_mutex()" as dtor_spec with + (\exact Reduce Spec.dtor_spec). + cpp.spec "std::shared_mutex::lock()" as lock_spec_alt with + (\exact Reduce Spec.lock_spec_alt). + cpp.spec "std::shared_mutex::try_lock()" as try_lock_spec_alt with + (\exact Reduce Spec.try_lock_spec_alt). + cpp.spec "std::shared_mutex::unlock()" as unlock_spec_alt with + (\exact Reduce Spec.unlock_spec_alt). + cpp.spec "std::shared_mutex::lock_shared()" as lock_shared_spec_alt with + (\exact Reduce Spec.lock_shared_spec). + cpp.spec "std::shared_mutex::try_lock_shared()" as try_lock_shared_spec_alt with + (\exact Reduce Spec.try_lock_shared_spec). + cpp.spec "std::shared_mutex::unlock_shared()" as unlock_shared_spec_alt with + (\exact Reduce Spec.unlock_shared_spec). + cpp.spec "std::shared_mutex::lock()" as lock_spec with + (\exact Reduce Spec.lock_spec). + cpp.spec "std::shared_mutex::unlock()" as unlock_spec with + (\exact Reduce Spec.unlock_spec). + cpp.spec "std::shared_mutex::try_lock()" as try_lock_spec with + (\exact Reduce Spec.try_lock_spec). + + #[global] Instance R_std_type_ptr g q P : + Observe (type_ptrR "std::shared_mutex") (R g q P). + Proof. apply R_type_ptr. Qed. + + Lemma lock_spec_equiv_lock_spec_alt : lock_spec -|- lock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.lock_spec_equiv_lock_spec_alt; done. + Qed. + + Lemma unlock_spec_equiv_unlock_spec_alt : unlock_spec -|- unlock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.unlock_spec_equiv_unlock_spec_alt; done. + Qed. + + Lemma try_lock_spec_equiv_try_lock_spec_alt : try_lock_spec -|- try_lock_spec_alt. + Proof. + iSplit; iApply specify_mono; intros this xs K; + rewrite Spec.try_lock_spec_equiv_try_lock_spec_alt; done. + Qed. + End with_cpp. +End StdSharedMutex. + +(** Concrete implementations supply their own [SHARED_MUTEX_PREDS]. *) +Declare Module SharedMutexPreds : SHARED_MUTEX_PREDS + with Definition cpp_ty := "std::shared_mutex"%cpp_type. +Module shared_mutex := StdSharedMutex SharedMutexPreds. diff --git a/rocq-brick-libstdcpp/proof/thread/pred.v b/rocq-brick-libstdcpp/proof/thread/pred.v new file mode 100644 index 00000000..8f05d106 --- /dev/null +++ b/rocq-brick-libstdcpp/proof/thread/pred.v @@ -0,0 +1,6 @@ +Require Import skylabs.auto.cpp.prelude.pred. +Require Import skylabs.brick.libstdcpp.runtime.pred. + +Section with_cpp. + Context `{Σ : cpp_logic}. +End with_cpp. diff --git a/rocq-brick-libstdcpp/proof/thread/spec.v b/rocq-brick-libstdcpp/proof/thread/spec.v new file mode 100644 index 00000000..1556a9a8 --- /dev/null +++ b/rocq-brick-libstdcpp/proof/thread/spec.v @@ -0,0 +1,299 @@ +Require Import skylabs.bi.tls_modalities. +Require Import skylabs.auto.cpp.prelude.proof. +Require Import skylabs.brick.libstdcpp.runtime.pred. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Export lock_ghost2.mutex_set_G (mutex_set_G). +Require Import skylabs.brick.libstdcpp.thread.thread_hpp. + +Import linearity. + +(** Specs for thread creation and joining. *) +Section with_cpp. + Context `{Σ : cpp_logic} {σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{!mutex_set_G Σ}. + + (** Exclusive ownership of a thread object. + [R $ None] is already joined thread and non-joinable; + [R $ Some (child, Q)] is a handle to join [child] and receive [Q]. + R would require the mpred to be WeaklyObjective. *) + Parameter R : option (thread_idT * mpred) -> Rep. + #[global] Hint Opaque R : sl_opacity typeclass_instances. + #[only(type_ptr="std::thread")] derive R. + #[global] Declare Instance R_exclusive state (this : ptr) : + Exclusive0 (R state). + + (** The mutex-set map tracks spawned thread IDs and supplies each child + with its [MutexSets.my_mutexes] handle. *) + Definition spawned_threads_inv (N : namespace) : mpred := + inv N (Exists tids, MutexSets.mutex_set_map tids). + + (** Install the map allocated with the ambient [mutex_set_G] instance. *) + Lemma spawned_threads_inv_alloc N : + MutexSets.mutex_set_map ∅ |-- (|={⊤}=> spawned_threads_inv N). + Proof. + iIntros "Hmap". + iMod (inv_alloc N _ (Exists tids, MutexSets.mutex_set_map tids) + with "[Hmap]") as "Hinv". + { iNext. iExists ∅. done. } + iModIntro. done. + Qed. + + Definition entry_type : type := Tfunction (FunctionType Tvoid []). + + #[global] Declare Instance wp_fptr_objective (f : ptr) (Q : mpred) + `{!WeaklyObjective Q} : + ObjectiveWith threadTI + (wp_fptr (σ.(genv_tu).(types)) entry_type f [] (fun _ => Q)). + + (* TODO should have a generic thread spawn spec that one can pass any resrouce + to, and does not mention my_mutex. *) + (** Passing a function lvalue binds a function reference directly, without + an intermediate function-pointer object. The child receives its mutex + namespace pool before running the entry point. Resources that the entry + point returns can be included in [Q] and recovered by joining. *) + Definition spawn_spec_body : ptr -> WpSpec mpred val val := + (\this this + \arg{f : ptr} "f" (Vptr f) + \pre{N} spawned_threads_inv N + \persist{parent} current_thread parent + \pre{Q : mpred} [| WeaklyObjective Q |] + \pre + (∀ child, + (MutexSets.my_mutexes child (coPset.CoPset ⊤) -* + (* FIXME is the usage of this wp_fptr correct? *) + wp_fptr (σ.(genv_tu).(types)) entry_type f [] + (* f returns void so Q does not depend on return value *) + (fun _ => Q))) + \post Exists child, + [| child <> parent |] ** this |-> R (Some (child, Q))). + + (** Must be joinable, takes resource and leaves the object non-joinable. *) + Definition join_spec_body : ptr -> WpSpec mpred val val := + (\this this + \persist{parent} current_thread parent + \pre{child Q} this |-> R (Some (child, Q)) + (* error if joining itself *) + \require child <> parent + \post this |-> R None ** Q). + + cpp.spec "std::thread::thread, void>(void ( *&)())" as ctor_spec from source with + (\exact Reduce spawn_spec_body). + + cpp.spec "std::thread::thread, void>(void (&)())" + as ctor_ref_spec from source with + (\exact Reduce spawn_spec_body). + + (** Must be joined (and resources transferred) so can be destroyed. *) + cpp.spec "std::thread::~thread()" as dtor_spec from source with + (\this this + \pre this |-> R None + \post emp). + + cpp.spec "std::thread::join()" as join from source with + (\exact Reduce join_spec_body). +End with_cpp. + +(** Specifications for thread IDs and the current thread's operations. *) +Module thread_id_specs. + (* Assume thread_idT can project an integer. *) + Parameter thread_id_to_Z : thread_idT -> Z. + (* Underlying integer of a thread ID object is unique. *) + Axiom thread_id_to_Z_injective : + forall x y, thread_id_to_Z x = thread_id_to_Z y -> x = y. + (* Value of thread::id(). Does not represent any thread. *) + Parameter default_thread_id : thread_idT. + + (* TODO: justify hash injectivity for each supported standard-library/ABI + implementation, including the default (non-thread) ID. The C++ Hash + requirements allow collisions; injectivity does not follow from them. + MyRecursiveMutex relies on this assumption to distinguish owners. If it + cannot be discharged, use a collision-free thread identity instead. *) + Parameter thread_id_hash : thread_idT -> Z. + Axiom thread_id_hash_injective : + forall x y, thread_id_hash x = thread_id_hash y -> x = y. + + (* we know there is only one field in the implementation; we expose it here. + FIXME or should we abstract it away? *) + sl.lock + Definition thread_idR `{Σ : cpp_logic, σ : genv} (q : cQp.t) + (tid : thread_idT) : Rep := + structR "std::thread::id" q ** + _field "std::thread::id::_M_thread" |-> ulongR q (thread_id_to_Z tid). + #[only(type_ptr,cfractional,ascfractional,timeless)] derive thread_idR. + + (** Weak objectivity of thread-ID storage, as required by invariant clients. *) + #[global] Axiom thread_idR_WeaklyObjective : + ∀ `{Σ : cpp_logic, σ : genv} (q : cQp.t) (tid : thread_idT) (p : ptr), + WeaklyObjective (thread_idR q tid p). + #[global] Existing Instance thread_idR_WeaklyObjective. + +Section with_cpp. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + + cpp.spec (default_ctor "std::thread::id") + as thread_id_ctor_spec from source with ( + \this this + \post this |-> thread_idR 1$m default_thread_id). + + cpp.spec (const_copy_ctor "std::thread::id") + as thread_id_copy_ctor_spec from source with ( + \this this + \arg{other} "" (Vptr other) + \prepost{q o} other |-> thread_idR q o + \post this |-> thread_idR 1$m o). + + cpp.spec (dtor "std::thread::id") as thread_id_dtor_spec from source with ( + \this this + \pre{o} this |-> thread_idR 1$m o + \post emp). + + cpp.spec "std::thread::id::operator=(const std::thread::id&)" + as thread_id_copy_assign_spec from source with ( + \this this + \arg{other} "" (Vptr other) + \pre{old} this |-> thread_idR 1$m old + \prepost{q o} other |-> thread_idR q o + \post[Vref this] this |-> thread_idR 1$m o). + + cpp.spec "std::thread::id::operator=(std::thread::id&&)" + as thread_id_move_assign_spec from source with ( + \this this + \arg{other} "" (Vptr other) + \pre{old} this |-> thread_idR 1$m old + \prepost{o} other |-> thread_idR 1$m o + \post[Vref this] this |-> thread_idR 1$m o). + + cpp.spec "std::operator==(std::thread::id, std::thread::id)" + as thread_id_eq_spec from source with ( + \arg{lhs} "" (Vptr lhs) + \arg{rhs} "" (Vptr rhs) + \prepost{q1 o1} lhs |-> thread_idR q1 o1 + \prepost{q2 o2} rhs |-> thread_idR q2 o2 + \post[Vbool (bool_decide (o1 = o2))] emp). + + (** [thread.thread.this] guarantees a running thread has a non-default ID. *) + cpp.spec "std::this_thread::get_id()" as get_id_spec from source with ( + \persist{th} current_thread th + \post{result}[Vptr result] + result |-> thread_idR 1$m th ** [| th <> default_thread_id |]). + + #[global] Instance thread_idR_learn : + Cbn (Learn (any ==> learn_eq ==> learn_hints.fin) thread_idR). + Proof. solve_learnable. Qed. + + Definition thread_id_layout := Eval vm_compute in types source !! "std::thread::id"%cpp_name. + + #[program] + Definition thread_id_const_C (tu : translation_unit) (p : ptr) := + \cancelx + \consuming{owner from} p |-> thread_idR from owner + \guard (types tu !! "std::thread::id"%cpp_name =[Vm?]=> thread_id_layout) + \proving{to Q} wp_const tu from to p "std::thread::id" Q + \through (p |-> thread_idR to owner -* Q) + \end@{mpredI}. + Next Obligation. + intros. rewrite thread_idR.unlock !_at_sep. + iIntros "[Hstruct Hfield]" (to Q) "HQ". + iApply (wp_const_named_struct from to _ _ _ _ G with "Hstruct"). + cbn. work $usenamed=true with br_erefl. + Qed. + + (** The callable returns the thread-ID hash. Its implementation is verified + against the byte-hash primitive below. *) + cpp.spec "std::hash::operator()(const std::thread::id&) const" + as hash_spec from source with ( + \this this + \arg{value} "__id" (Vptr value) + \persist type_ptr "std::hash" this + \prepost{q owner} value |-> thread_idR q owner + \post[Vint (thread_id_hash owner)] emp). + + (** Hash-object destruction, including its empty base object. *) + cpp.spec "std::__hash_base::~__hash_base()" + as hash_base_dtor_spec from source with ( + \this this + \pre this |-> structR "std::__hash_base" 1$m + \post emp). + + cpp.spec "std::hash::~hash()" + as hash_dtor_spec from source with ( + \this this + \pre this |-> structR "std::hash" 1$m + \pre this ,, _base "std::hash" + "std::__hash_base" |-> + structR "std::__hash_base" 1$m + \post emp). + + cpp.spec "std::this_thread::yield()" as yield_spec from source with ( + \post emp). +End with_cpp. +#[global] Hint Resolve thread_id_const_C : sl_opacity. + +(** NOTE that the hash specs and proofs are all generated by AI. + [hash_bytes_spec] cannot be verified because the spec for + <<__builtin_memcpy>> is missing. Maybe cleaner to just axiomatize + [hash_ulong_spec] and remove the rest. *) +Section hash_proofs. + Context `{Σ : cpp_logic, σ : genv, !HasStdThreads Σ}. + + cpp.spec "std::_Hash_bytes(const void*, unsigned long, unsigned long)" + as hash_bytes_spec from source with ( + \arg{value} "__ptr" (Vptr value) + \arg "__len" (Vint 8) + \arg "__seed" (Vint 3339675911) + \prepost{q owner} value |-> ulongR q (thread_id_to_Z owner) + \post[Vint (thread_id_hash owner)] emp). + + cpp.spec "std::_Hash_impl::hash(const void*, unsigned long, unsigned long)" + as hash_impl_spec from source with ( + \arg{value} "__ptr" (Vptr value) + \arg "__clength" (Vint 8) + \arg "__seed" (Vint 3339675911) + \prepost{q owner} value |-> ulongR q (thread_id_to_Z owner) + \post[Vint (thread_id_hash owner)] emp). + + cpp.spec "std::_Hash_impl::hash(const unsigned long&)" + as hash_ulong_spec from source with ( + \arg{value} "__val" (Vptr value) + \prepost{q owner} value |-> ulongR q (thread_id_to_Z owner) + \post[Vint (thread_id_hash owner)] emp). + + Section with_hash_module. + Context `{MOD : source ⊧ σ}. + + Lemma hash_impl_ok : verify[source] hash_impl_spec. + Proof using MOD. verify_spec; go. iExists owner. go. Qed. + + Lemma hash_ulong_ok : verify[source] hash_ulong_spec. + Proof using MOD. verify_spec; go. iExists owner. go. Qed. + + Lemma hash_ok : verify[source] hash_spec. + Proof using MOD. + verify_spec. rewrite thread_idR.unlock. go. iExists owner. go. + rewrite thread_idR.unlock. go. + Qed. + + Lemma hash_base_dtor_ok : verify[source] hash_base_dtor_spec. + Proof using MOD. verify_spec; go. Qed. + + Lemma hash_dtor_ok : hash_base_dtor_spec |-- + verify[source] hash_dtor_spec. + Proof using MOD. verify_shift; go. iModIntro. go. Qed. + + Lemma hash_link : + denoteModule source ** hash_bytes_spec |-- + hash_spec ** hash_base_dtor_spec ** hash_dtor_spec. + Proof using MOD. + work. + wapply hash_ok. + wapply hash_ulong_ok. + wapply hash_impl_ok. + wapply hash_dtor_ok. + wapply hash_base_dtor_ok. + work $usenamed=true. + Qed. + End with_hash_module. +End hash_proofs. +End thread_id_specs. diff --git a/rocq-brick-libstdcpp/proof/thread/thread.hpp b/rocq-brick-libstdcpp/proof/thread/thread.hpp new file mode 100644 index 00000000..983beeca --- /dev/null +++ b/rocq-brick-libstdcpp/proof/thread/thread.hpp @@ -0,0 +1,4 @@ +#include + +template std::thread::thread(void (*&)()); +template std::thread::thread(void (&)()); diff --git a/rocq-brick-libstdcpp/test/dune b/rocq-brick-libstdcpp/test/dune index 2de85b3c..a536cae8 100644 --- a/rocq-brick-libstdcpp/test/dune +++ b/rocq-brick-libstdcpp/test/dune @@ -37,6 +37,19 @@ )) (include dune.inc) +; An executable counterexample to the current generic scoped_lock contract. +(rule + (alias runtest) + (action + (run python3 %{dep:mutex/scoped_lock_counterexample.py} %{bin:clang++} + %{dep:mutex/scoped_lock_counterexample.cpp}))) + +(rule + (alias runtest) + (action + (run python3 %{dep:mutex/recursive_mutex_odr.py} %{bin:clang++} + %{dep:mutex/custom_recursive_mutex.hpp}))) + (rule (action (with-stdout-to dune.inc.gen (run ./dune-gen.sh))) (deps (glob_files_rec *.{h,c,hpp,cpp,sh}))) diff --git a/rocq-brick-libstdcpp/test/dune-gen.sh b/rocq-brick-libstdcpp/test/dune-gen.sh index afdc9fa2..3b1662c6 100755 --- a/rocq-brick-libstdcpp/test/dune-gen.sh +++ b/rocq-brick-libstdcpp/test/dune-gen.sh @@ -16,8 +16,8 @@ echo "; DO NOT EDIT: Generated by \"$0${1+ $*}\"" # Temporary: `cpp2v --templates` for only those tests in ./templates. -# Blacklist some files for arch_indep -find . -name '*.[ch]pp' | +# The custom mutex conformance suites are compiled and run natively, not by cpp2v. +find . \( -path './mutex/libcpp_tests' -o -path './mutex/scoped_lock_counterexample.cpp' \) -prune -o -name '*.[ch]pp' -print | LC_ALL=C sort | while read i; do ./cpp2v-dune-gen.sh -s -- $i -- -std=c++20 -stdlib=libstdc++ diff --git a/rocq-brick-libstdcpp/test/dune.inc b/rocq-brick-libstdcpp/test/dune.inc index 43e3a819..6495b838 100644 --- a/rocq-brick-libstdcpp/test/dune.inc +++ b/rocq-brick-libstdcpp/test/dune.inc @@ -323,6 +323,18 @@ (with-stderr-to custom_recursive_mutex_hpp.v.stderr (run cpp2v -v %{input} -o custom_recursive_mutex_hpp.v --no-elaborate -- -std=c++20 -stdlib=libstdc++ )))) (alias (name srcs) (deps custom_recursive_mutex.hpp)) ) +(subdir mutex + (rule + (targets double_incr_cpp.v.stderr double_incr_cpp.v) + (alias test_ast) + (deps + (:input double_incr.cpp) + (env_var CPP2V_DOCKER_ENABLED) + (glob_files_rec ../*.hpp)) + (action + (with-stderr-to double_incr_cpp.v.stderr (run cpp2v -v %{input} -o double_incr_cpp.v --no-elaborate -- -std=c++20 -stdlib=libstdc++ )))) + (alias (name srcs) (deps double_incr.cpp)) +) (subdir mutex (rule (targets guard_recursive_cpp.v.stderr guard_recursive_cpp.v) @@ -347,6 +359,18 @@ (with-stderr-to test_cpp.v.stderr (run cpp2v -v %{input} -o test_cpp.v --no-elaborate -- -std=c++20 -stdlib=libstdc++ )))) (alias (name srcs) (deps test.cpp)) ) +(subdir mutex + (rule + (targets transfer_with_mutex_cpp.v.stderr transfer_with_mutex_cpp.v) + (alias test_ast) + (deps + (:input transfer_with_mutex.cpp) + (env_var CPP2V_DOCKER_ENABLED) + (glob_files_rec ../*.hpp)) + (action + (with-stderr-to transfer_with_mutex_cpp.v.stderr (run cpp2v -v %{input} -o transfer_with_mutex_cpp.v --no-elaborate -- -std=c++20 -stdlib=libstdc++ )))) + (alias (name srcs) (deps transfer_with_mutex.cpp)) +) (subdir new (rule (targets demo_cpp.v.stderr demo_cpp.v) @@ -359,6 +383,18 @@ (with-stderr-to demo_cpp.v.stderr (run cpp2v -v %{input} -o demo_cpp.v --no-elaborate -- -std=c++20 -stdlib=libstdc++ )))) (alias (name srcs) (deps demo.cpp)) ) +(subdir shared_mutex + (rule + (targets custom_shared_mutex_hpp.v.stderr custom_shared_mutex_hpp.v) + (alias test_ast) + (deps + (:input custom_shared_mutex.hpp) + (env_var CPP2V_DOCKER_ENABLED) + (glob_files_rec ../*.hpp)) + (action + (with-stderr-to custom_shared_mutex_hpp.v.stderr (run cpp2v -v %{input} -o custom_shared_mutex_hpp.v --no-elaborate -- -std=c++20 -stdlib=libstdc++ )))) + (alias (name srcs) (deps custom_shared_mutex.hpp)) +) (subdir shared_mutex (rule (targets test_cpp.v.stderr test_cpp.v) diff --git a/rocq-brick-libstdcpp/test/mutex/custom_mutex.hpp b/rocq-brick-libstdcpp/test/mutex/custom_mutex.hpp index c5844ede..c949f418 100644 --- a/rocq-brick-libstdcpp/test/mutex/custom_mutex.hpp +++ b/rocq-brick-libstdcpp/test/mutex/custom_mutex.hpp @@ -16,13 +16,14 @@ std::mutex protocol: - can't be destroyed while held. */ class MyMutex { - std::atomic m_lock{false}; + // std::atomic m_lock{false}; + std::atomic m_lock{0}; std::thread::id m_owner{}; // "Actual locking" void do_lock() { - while (m_lock.exchange(true)) { + while (m_lock.exchange(1)) { // Yielding helps scheduling, and makes this loop obviously not UB // (under https://eel.is/c++draft/intro.progress#1.2). // Calling exchange might qualify under @@ -32,10 +33,12 @@ class MyMutex { } void do_unlock() { - m_lock = false; + m_lock = 0; } public: + MyMutex() noexcept {} + void lock() { std::thread::id this_id{std::this_thread::get_id()}; @@ -52,6 +55,16 @@ class MyMutex { m_owner = this_id; } + bool try_lock() noexcept { + std::thread::id this_id{std::this_thread::get_id()}; + if (m_lock.exchange(1)) + return false; + + assert(m_owner == std::thread::id()); + m_owner = this_id; + return true; + } + void unlock(){ assert(m_owner == std::this_thread::get_id()); m_owner = std::thread::id(); // unowned @@ -60,7 +73,7 @@ class MyMutex { } ~MyMutex() { - assert(!m_lock); + assert(m_lock == 0); assert(m_owner == std::thread::id()); //unowned } }; diff --git a/rocq-brick-libstdcpp/test/mutex/custom_mutex_hpp_proof.v b/rocq-brick-libstdcpp/test/mutex/custom_mutex_hpp_proof.v new file mode 100644 index 00000000..95852bae --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/custom_mutex_hpp_proof.v @@ -0,0 +1,654 @@ +(** Provisional *) + +Require Import iris.algebra.coPset. +Require Import iris.algebra.frac. + +Require Import skylabs.auto.cpp.proof. +Require Import skylabs.auto.cpp.hints.base_derived. +Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. +Require Import skylabs.brick.libstdcpp.mutex.requirements. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Require Import skylabs.brick.libstdcpp.atomic.spec. +Require Import skylabs.brick.libstdcpp.cassert.spec. +Require Import skylabs.brick.libstdcpp.thread.spec. +NES.Open std.atomic. + +Import linearity. +Import OwnerTid. +Require Import skylabs.brick.libstdcpp.test.mutex.custom_mutex_hpp. + +(** The concrete mutex supplies both the ghost state and the physical + predicates used by [mutex_spec]. The mutex-set pool is shared, and the + memory-order global remains a separate precondition of the C++ methods. *) +Module CustomMutexPreds <: MUTEX_PREDS. + Import MutexSets. + + Record state_gname : Set := MkStateGname { + token_gname : iprop.gname; + owner_gname : iprop.gname; + state_namespace : namespace; + state_namespace_not_root : state_namespace <> nroot; + }. + + Definition class_name : name := "MyMutex"%cpp_name. + Definition cpp_ty : type := Tnamed class_name. + Record mutex_gname : Set := MkGname { + lock_state_gname :> state_gname; + cinv_gname : iprop.gname; + }. + Definition gname : Set := mutex_gname. + + Definition mutex_inv_namespace (g : gname) : namespace := + g.(state_namespace). + + Lemma mutex_inv_namespace_not_root g : mutex_inv_namespace g <> nroot. + Proof. exact g.(state_namespace_not_root). Qed. + + Canonical Structure token_cmraR : cmra := fracR. + + Class stateG `{Σ : cpp_logic} := { + #[local] has_token_own :: HasOwn (iPropI _Σ) token_cmraR; + #[local] has_token_upd :: HasOwnUpd (iPropI _Σ) token_cmraR; + #[local] has_token_valid :: HasOwnValid (iPropI _Σ) token_cmraR; + #[global] owners_G :: OwnerTid.G Σ; + }. + Definition G := @stateG. + Existing Class G. + #[global] Arguments G {_ _} Σ : assert. + #[global] Instance state_G `{Σ : cpp_logic} (H : G Σ) : @stateG _ _ Σ := H. + + Definition token `{Σ : cpp_logic, !G Σ} (γ : gname) (q : cQp.t) : mpred := + own γ.(token_gname) (cQp.frac q : token_cmraR). + + Definition not_locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (γ : gname) (th : thread_idT) (q : cQp.t) : mpred := + own γ.(token_gname) (cQp.frac q : token_cmraR) ** + MutexSets.my_mutexes th (coPset.CoPset $ ↑mutex_inv_namespace γ). + + Definition state `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (γ : gname) (b : bool) : mpred := + ∃ th q, + owner_tid_auth γ.(owner_gname) (if b then Some (th, q) else None) ** + if b then + (* The mutex owner deposits [not_locked] and takes [owner_tid_frag], + which records the [th] and [q] stored in the invariant. *) + not_locked this γ th q + else + owner_tid_frag γ.(owner_gname) None. + + #[global] Instance state_WeaklyObjective + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this γ b : + WeaklyObjective (state this γ b). + Proof. rewrite /state /not_locked. destruct b; apply _. Qed. + + Section state_laws. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + + Lemma alloc_state {σ : genv} this gi (N : namespace) (HN : N <> nroot) : + ⊢ |==> ∃ γ, [| γ.(state_namespace) = N |] ** + token (MkGname γ gi) 1$m ** + state this (MkGname γ gi) false. + Proof. + iMod (own_alloc (1%Qp : token_cmraR)) as (gt) "T"; first done. + iMod (OwnerTid.alloc None) as (go) "[OA OF]". + iModIntro. iExists (MkStateGname gt go N HN). + iSplit; first done. + rewrite /token /state /=. iFrame "T". + iExists thread_id_specs.default_thread_id, (1$m)%cQp. iFrame. + Qed. + + Lemma state_lock {σ : genv} this (γ : gname) th (q : cQp.t) : + state this γ false ** not_locked this γ th q |-- + (|==> state this γ true ** owner_tid_frag γ.(owner_gname) (Some (th, q))). + Proof. + rewrite /state. + iIntros "[State NL]". + iDestruct "State" as (old_th old_q) "[OA OF]". + iMod (owner_update γ.(owner_gname) None None (Some (th, q)) + with "[$OA $OF]") as "[OA OF]". + iModIntro. iFrame "OF". iExists th, q. iFrame. + Qed. + + Lemma state_unlock {σ : genv} this (γ : gname) th (q : cQp.t) : + state this γ true ** owner_tid_frag γ.(owner_gname) (Some (th, q)) |-- + (|==> state this γ false ** not_locked this γ th q). + Proof. + rewrite /state. + iIntros "[State OF]". + iDestruct "State" as (owner held_q) "[OA NL]". + iDestruct (observe_2 [| Some (owner, held_q) = Some (th, q) |] + with "OA OF") as %Heq. + injection Heq as -> ->. + iMod (owner_update γ.(owner_gname) _ _ None with "[$OA $OF]") as "[OA OF]". + iModIntro. iFrame "NL". iExists th, q. iFrame. + Qed. + + Lemma unlocked_owner_tid_frag {σ : genv} this γ th q : + state this γ false ** owner_tid_frag γ.(owner_gname) (Some (th, q)) |-- False. + Proof. + rewrite /state. iIntros "[State OF]". + iDestruct "State" as (old_th old_q) "[_ OF0]". + iDestruct (owner_tid_frag_exclusive with "OF0 OF") as %[]. + Qed. + + Lemma locked_full_token {σ : genv} this γ : + state this γ true ** token γ 1$m |-- False. + Proof. + rewrite /state /not_locked /token. iIntros "[State T]". + iDestruct "State" as (th q) "(_ & Held & _)". + iDestruct (own_valid_2 with "T Held") as %Hbad. + iPureIntro. exact (Qp.not_add_le_l 1 (cQp.frac q) Hbad). + Qed. + End state_laws. + + #[global] Hint Opaque not_locked state : sl_opacity typeclass_instances. + + Definition globals `{Σ : cpp_logic} {σ : genv} (q : cQp.t) : mpred := + _global "std::memory_order_seq_cst" |-> + primR "enum std::memory_order" q + (memory_order.to_val memory_order.seq_cst). + + #[global] Arguments globals /. + + Definition locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (γ : gname) (th : thread_idT) (q : cQp.t) : mpred := + this ,, _field "MyMutex::m_owner" |-> thread_id_specs.thread_idR 1$m th ** + owner_tid_frag γ.(owner_gname) (Some (th, q)). + #[global] Hint Opaque token : sl_opacity typeclass_instances. + #[global] Hint Opaque locked : typeclass_instances. + #[global] Arguments locked /. + + #[global] Instance token_fractional `{Σ : cpp_logic, !G Σ} γ : + CFractional (token γ). + Proof. + intros q1 q2. rewrite /token cQp.frac_add -own_op. done. + Qed. + #[global] Instance token_timeless `{Σ : cpp_logic, !G Σ} γ q : + Timeless (token γ q). + Proof. rewrite /token. apply _. Qed. + #[global] Instance not_locked_timeless `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + this γ th q : Timeless (not_locked this γ th q). + Proof. rewrite /not_locked. apply _. Qed. + #[global] Instance not_locked_exclusive `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + this γ th : Exclusive1 (not_locked this γ th). + Proof. + intros q1 q2. rewrite /not_locked. + iIntros "[_ F1] [_ F2]". + have Hoverlap : ~ ((↑ mutex_inv_namespace γ : coPset) ## ↑ mutex_inv_namespace γ). + { have Hnonempty := nclose_non_empty (mutex_inv_namespace γ). set_solver. } + iDestruct (MutexSets.my_mutexes_exclusive th + (↑ mutex_inv_namespace γ) (↑ mutex_inv_namespace γ) Hoverlap + with "[$F1 $F2]") as %[]. + Qed. + #[global] Instance locked_timeless `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + this γ th q : Timeless (locked this γ th q). + Proof. rewrite /locked. apply _. Qed. + #[global] Instance locked_exclusive `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + this γ : Exclusive2 (locked this γ). + Proof. intros th1 th2 q1 q2. rewrite /locked. apply observe_2_sep_r. apply _. Qed. + + #[local] Instance at_WeaklyObjective `{Σ : cpp_logic} + (p : ptr) (R : Rep) `{!WeaklyObjective (R p)} : + WeaklyObjective (p |-> R). + Proof. rewrite INTERNAL._at_eq. apply _. Qed. + + #[global] Instance locked_WeaklyObjective `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + this γ th q : WeaklyObjective (locked this γ th q). + Proof. rewrite /locked. apply _. Qed. + + Section with_Σ. + Context `{Σ : cpp_logic}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + + (** The physical lock bit agrees with the abstract ghost state. The + protected resources and cleared owner field are available while free. *) + Definition mutex_inv {σ : genv} (this : ptr) (γ : gname) (P : mpred) : mpred := + ∃ b : bool, + this ,, _field "MyMutex::m_lock" |-> + atomic.R "int" 1$m (if b then 1 else 0)%Z ** + state this γ b ** + if b then emp else + P ** this ,, _field "MyMutex::m_owner" |-> thread_id_specs.thread_idR 1$m thread_id_specs.default_thread_id. + + Definition R {HAS_THREADS : HasStdThreads Σ} {σ : genv} (γ : gname) (q : cQp.t) (P : mpred) : Rep := + structR class_name q$m ** + as_Rep (fun this => + cinv (mutex_inv_namespace γ) (cinv_gname γ) (mutex_inv this γ P) ** + cinv_own (cinv_gname γ) q + ). + #[global] Hint Opaque R : sl_opacity typeclass_instances. + #[only(type_ptr,cfractional,ascfractional,cfracvalid)] derive R. + + #[global] Instance R_learnable {HAS_THREADS : HasStdThreads Σ} {σ : genv} : + Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). + Proof. solve_learnable. Qed. + + Context {σ : genv} {HAS_THREADS : HasStdThreads Σ}. + + Lemma locked_my_mutexes_exclusive (this : ptr) g th q qt P E : + ↑mutex_inv_namespace g ⊆ E -> + this |-> R g q P ** locked this g th qt ** + MutexSets.my_mutexes th + (coPset.CoPset $ ↑mutex_inv_namespace g) |-- (|={E}=> False). + Proof. + iIntros (Hmask) "(HR & Locked & Names)". + iEval (rewrite /R _at_sep _at_as_Rep) in "HR". + iDestruct "HR" as "(_ & #CI & CO)". + iInv (mutex_inv_namespace g) as "[Inv CO]" "Hclose". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (b) "(_ & State & _)". + iEval (rewrite /locked) in "Locked". + iDestruct "Locked" as "[_ OF]". + iEval (rewrite /state /not_locked) in "State". + destruct b. + - iDestruct "State" as (owner held_q) "(>OA & _ & >Sets)". + iDestruct (observe_2 [| Some (owner, held_q) = Some (th, qt) |] + with "OA OF") as %Heq. + injection Heq as -> ->. + iDestruct (MutexSets.my_mutexes_exclusive with "[$Sets $Names]") as %[]. + rewrite /mutex_inv_namespace. + have Hnonempty := nclose_non_empty g.(state_namespace). + set_solver. + - iDestruct "State" as (owner held_q) "[_ >OF0]". + iDestruct (owner_tid_frag_exclusive with "OF0 OF") as %[]. + Qed. + + #[global] Instance locked_learn : + Cbn (Learn (req_eq ==> learn_eq ==> req_eq ==> req_eq ==> learn_hints.fin) locked). + Proof using MUTEX_SETS. solve_learnable. Qed. + + Definition do_lock (this : ptr) (lk : gname * mpred) (K : mpred) : mpred := + ∃ thr qt, current_thread thr ** not_locked this lk.1 thr qt ** + (locked this lk.1 thr qt ** lk.2 -* K). + #[global] Arguments do_lock /. + + Definition do_unlock (this : ptr) (lk : gname * mpred) (K : mpred) : mpred := + ∃ thr qt, current_thread thr ** locked this lk.1 thr qt ** ▷lk.2 ** + (not_locked this lk.1 thr qt -* K). + #[global] Arguments do_unlock /. + + #[global] Instance mutex_basic_lockable : + BasicLockable (T := gname * mpred) cpp_ty + (fun q gP => R gP.1 q gP.2) := + { do_lock := do_lock + ; do_unlock := do_unlock }. + + Definition do_try_lock (this : ptr) (lk : gname * mpred) + (K : bool -> mpred) : mpred := + ∃ thr qt, current_thread thr ** not_locked this lk.1 thr qt ** + ∀ b : bool, + (if b then lk.2 ** locked this lk.1 thr qt + else not_locked this lk.1 thr qt) -* K b. + #[global] Arguments do_try_lock /. + + #[global] Instance mutex_lockable : + Lockable (T := gname * mpred) cpp_ty + (fun q gP => R gP.1 q gP.2) := + { do_try_lock := do_try_lock }. + + Lemma init_R (this : ptr) (old : gname) + (N : namespace) (P : mpred) : + WeaklyObjective P -> + N <> nroot -> + this |-> R old 1$m emp ** token old 1$m ** ▷P |-- + (|={⊤}=> ∃ g, [| mutex_inv_namespace g = N |] ** + this |-> R g 1$m P ** token g 1$m). + Proof. + intros HP HN. iIntros "(HR & T & P)". + iEval (rewrite /R _at_sep _at_as_Rep) in "HR". + iDestruct "HR" as "(S & #CI & CO)". + iMod (cinv_cancel with "CI CO") as "Inv"; [done..|]. + iMod (cinv_alloc_cofinite ∅ ⊤ N) as (gi) "(_ & COnew & Alloc)". + iMod (alloc_state this gi N HN) as (gs) "(%Hnamespace & Tnew & Stnew)". + iMod ("Alloc" $! (mutex_inv this (MkGname gs gi) P) + with "[] [Inv P Stnew T]") as "#CInew". + { iPureIntro. apply _. } + { iNext. rewrite /mutex_inv. + iDestruct "Inv" as (b) "(L & St & Resources)". + destruct b. + - iDestruct (locked_full_token with "[$St $T]") as %[]. + - iDestruct "Resources" as "[_ Owner]". + iExists false. iFrame "L Stnew P Owner". + iApply (affine with "[St T]"); last iAccu. apply mpred_BiAffine. } + iModIntro. iExists (MkGname gs gi). + iSplit; first (iPureIntro; exact Hnamespace). + rewrite /R _at_sep _at_as_Rep /mutex_inv_namespace /= Hnamespace. + iFrame "CInew S COnew Tnew". + Qed. + + End with_Σ. +End CustomMutexPreds. + +(** Verify the implementation using its ghost resources and public state predicates. *) +Module custom_mutex. + Import CustomMutexPreds. + Module Spec := mutex_spec CustomMutexPreds. + + Abbreviation N := "MyMutex"%cpp_name. + #[local] Hint Opaque thread_id_specs.thread_idR : sl_opacity typeclass_instances. + + #[local] Instance at_WeaklyObjective `{Σ : cpp_logic} + (p : ptr) (R : Rep) `{!WeaklyObjective (R p)} : + WeaklyObjective (p |-> R). + Proof. rewrite INTERNAL._at_eq. apply _. Qed. + + Abbreviation IR := CustomMutexPreds.R. + + Section with_Σ. + Context `{Σ : cpp_logic, σ : genv, HAS_THREADS : !HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!CustomMutexPreds.G Σ}. + + Context `{MOD : source ⊧ σ}. + + (* FIXME any reason to keep Reduce? *) + cpp.spec "MyMutex::MyMutex()" as ctor_spec with + (\exact Reduce (Spec.ctor_spec)). + + cpp.spec "MyMutex::~MyMutex()" as dtor_spec with + (\exact Reduce (Spec.dtor_spec)). + + Definition T : Type := gname * mpred. + cpp.spec "MyMutex::do_lock()" as do_lock_spec with ( + \this this + \prepost{g q P} this |-> IR g q P + \persist{thr} current_thread thr + \prepost{qg} globals qg + \pre{qt} not_locked this g thr qt + \post P ** + this ,, _field "MyMutex::m_owner" |-> thread_id_specs.thread_idR 1$m thread_id_specs.default_thread_id ** + owner_tid_frag g.(owner_gname) (Some (thr, qt))). + + cpp.spec "MyMutex::do_unlock()" as do_unlock_spec with ( + \this this + \prepost{g q P} this |-> IR g q P + \persist{thr} current_thread thr + \prepost{qg} globals qg + \pre{(qt : cQp.t)} this ,, _field "MyMutex::m_owner" |-> thread_id_specs.thread_idR 1$m thread_id_specs.default_thread_id ** + owner_tid_frag g.(owner_gname) (Some (thr, qt)) + \pre ▷P + \post not_locked this g thr qt). + + cpp.spec "MyMutex::lock()" as lock_spec_alt with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.lock_spec_alt this)). + + cpp.spec "MyMutex::unlock()" as unlock_spec_alt with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.unlock_spec_alt this)). + + cpp.spec "MyMutex::lock()" as lock_spec with + (\this this + \prepost{qg} globals qg + \exact Reduce + (Spec.lock_spec this)). + + cpp.spec "MyMutex::unlock()" as unlock_spec with + (\this this + \prepost{qg} globals qg + \exact Reduce + (Spec.unlock_spec this)). + + cpp.spec "MyMutex::try_lock()" as try_lock_spec_alt with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.try_lock_spec_alt this)). + + cpp.spec "MyMutex::try_lock()" as try_lock_spec with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.try_lock_spec this)). + + Lemma try_lock_spec_equiv_try_lock_spec_alt : try_lock_spec ⊣⊢ try_lock_spec_alt. + Proof. + rewrite /try_lock_spec /try_lock_spec_alt /specify. + do 2 f_equiv. intros this xs K. + rewrite !add_with_equiv. f_equiv=> qg. + rewrite !add_prepost_equiv Spec.try_lock_spec_equiv_try_lock_spec_alt; done. + Qed. + + Lemma lock_spec_equiv_lock_spec_alt : lock_spec ⊣⊢ lock_spec_alt. + Proof. + rewrite /lock_spec /lock_spec_alt /specify. + do 2 f_equiv. intros this xs K. + rewrite !add_with_equiv. + f_equiv=> qg. + by rewrite !add_prepost_equiv Spec.lock_spec_equiv_lock_spec_alt. + Qed. + + Lemma unlock_spec_equiv_unlock_spec_alt : unlock_spec ⊣⊢ unlock_spec_alt. + Proof. + rewrite /unlock_spec /unlock_spec_alt /specify. + do 2 f_equiv. intros this xs K. + rewrite !add_with_equiv. + f_equiv=> qg. + by rewrite !add_prepost_equiv Spec.unlock_spec_equiv_unlock_spec_alt. + Qed. + + Abbreviation BASE p := (p ,, _base "std::atomic" "std::__atomic_base"). + + Definition bi_later_exist_F := [FWD] @bi.later_exist. + Definition bi_later_sep_F := [FWD] @bi.later_sep. + Hint Resolve bi_later_exist_F bi_later_sep_F : br_hints. + + #[program] + Definition do_exchange_C (p : ptr) := + \cancelx + \using denoteModule source + \using{thr} current_thread thr + \consuming{g q P} p |-> IR g q P + \consuming{qt} not_locked p g thr qt + \proving{K (_ : IsExistential K)} + std.atomic.do_exchange "int" (BASE (p,, o_field σ "MyMutex::m_lock") ) 1%Z K + \instantiate K := (fun res => p |-> IR g q P ** [| res = 0 \/ res = 1 |]%Z ** + if bool_decide (res = 0) then P ** owner_tid_frag g.(owner_gname) (Some (thr, qt)) ** + p ,, _field "MyMutex::m_owner" |-> thread_id_specs.thread_idR 1$m thread_id_specs.default_thread_id + else not_locked p g thr qt) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[#M Hpre]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + iDestruct "Hpre" as "(#Thr & IR & NL)". + iEval (rewrite /IR _at_sep _at_as_Rep) in "IR". + iDestruct "IR" as "(S & #CI & CO)". + rewrite /std.atomic.do_exchange. + iAuIntro1. rewrite /atomic1_acc. + iInv (mutex_inv_namespace g) as "Inv" "Hclose". + iDestruct "Inv" as "[Inv CO]". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (b) "(>L & State & Resources)". + iDestruct (fupd_mask_subseteq) as ">Y"; [ | iModIntro ]; first set_solver. + iExists (if b then 1 else 0)%Z. + iSplitL "L". + { ework $usenamed=true with br_erefl. } + iSplit. + - iIntros "L". iMod "Y" as "_". + iMod ("Hclose" with "[L State Resources]") as "_". + { iNext. rewrite /mutex_inv. iExists b. + iSplitL "L"; first by ework $usenamed=true with br_erefl. + iFrame. } + iModIntro. iFrame. + - iNext. iIntros "L". iMod "Y" as "_". + destruct b. + + iMod ("Hclose" with "[L State Resources]") as "_". + { iNext. rewrite /mutex_inv. iExists true. + iSplitL "L"; first by ework $usenamed=true with br_erefl. + iFrame. } + iModIntro. rewrite /IR _at_sep _at_as_Rep /=. + iFrame "CI". iFrame. iPureIntro. auto. + + iDestruct "Resources" as "[P Owner]". + iMod (state_lock p _ with "[$State $NL]") as "[State Locked]". + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists true. + iSplitL "L"; first by ework $usenamed=true with br_erefl. + iFrame. } + iModIntro. rewrite /IR _at_sep _at_as_Rep /=. + iFrame "CI". iFrame. iPureIntro. auto. + Qed. + Hint Resolve do_exchange_C : sl_opacity. + + #[program] + Definition do_store_C (p : ptr) := + \cancelx + \using denoteModule source + \using{thr} current_thread thr + \consuming{g q P} p |-> IR g q P + \consuming P + \consuming p ,, _field "MyMutex::m_owner" |-> thread_id_specs.thread_idR 1$m thread_id_specs.default_thread_id + \consuming{(qt : cQp.t)} owner_tid_frag g.(owner_gname) (Some (thr, qt)) + \proving{K (_ : IsExistential K)} + std.atomic.do_store "int" (BASE (p ,, o_field σ "MyMutex::m_lock")) 0%Z K + \instantiate K := (p |-> IR g q P ** not_locked p g thr qt) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[#M Hpre]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + iDestruct "Hpre" as "(#Thr & IR & P & Owner & Locked)". + iEval (rewrite /IR _at_sep _at_as_Rep) in "IR". + iDestruct "IR" as "(S & #CI & CO)". + rewrite /std.atomic.do_store. + iAcIntro. rewrite /commit_acc /=. + iInv (mutex_inv_namespace g) as "Inv" "Hclose". + iDestruct "Inv" as "[Inv CO]". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (b) "(>L & State & Resources)". + iDestruct (fupd_mask_subseteq) as ">Y"; [ | iModIntro ]; first set_solver. + iExists (if b then 1 else 0)%Z. + iSplitL "L"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "L". iMod "Y" as "_". + destruct b. + - iMod (state_unlock p _ with "[$State $Locked]") as "[State NL]". + iMod ("Hclose" with "[L State P Owner]") as "_". + { iNext. rewrite /mutex_inv. iExists false. + iSplitL "L"; first by ework $usenamed=true with br_erefl. + iFrame. } + iModIntro. + rewrite /IR _at_sep _at_as_Rep /=. + iFrame "CI". iFrame. + - iDestruct (unlocked_owner_tid_frag with "[$State $Locked]") as %[]. + Qed. + Hint Resolve do_store_C : sl_opacity. + + #[program] + Definition do_load_C (p : ptr) := + \cancelx + \using denoteModule source + \consuming{q (n : Z)} p |-> atomic.R "int" q n + \proving{(K : Z -> mpred) (_ : IsExistential K)} + std.atomic.do_load "int" (BASE p) K + \instantiate K := + (fun x : Z => p |-> atomic.R "int" q n ** [| x = n |]) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[#M ?]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + rewrite /std.atomic.do_load. + iAcIntro. rewrite /commit_acc. + iDestruct (fupd_mask_subseteq) as ">Y"; [ | iModIntro ]; eauto. + work. iExists q. work. + iMod "Y". iModIntro. + work. + Qed. + Hint Resolve do_load_C : sl_opacity. + + Lemma mymutex_do_lock_proof : verify[source] "MyMutex::do_lock()". + Proof using MOD HAS_THREADS. + verify_spec; go. + wp_while (fun _ => emp); go; first by ework. + wp_if; go. + Qed. + + Lemma mymutex_do_unlock_proof : verify[source] "MyMutex::do_unlock()". + Proof using MOD HAS_THREADS. + verify_spec; go. + Qed. + + #[local] Remove Hints CustomMutexPreds.locked_learn : typeclass_instances. + + Lemma mymutex_lock_alt_proof : verify[source] lock_spec_alt. + Proof using MOD HAS_THREADS. + verify_spec; ego. + wname [CustomMutexPreds.not_locked] "NL". + iFrame "NL". ego. + Unshelve. all: exact (1$m)%cQp. + Qed. + + Lemma mymutex_try_lock_alt_proof : verify[source] try_lock_spec_alt. + Proof using MOD HAS_THREADS. + verify_spec; ego. + repeat (first [case_bool_decide | wp_if]; ego). + Qed. + + Lemma mymutex_try_lock_proof : verify[source] try_lock_spec. + Proof using MOD HAS_THREADS. + rewrite try_lock_spec_equiv_try_lock_spec_alt. + exact mymutex_try_lock_alt_proof. + Qed. + + Lemma mymutex_unlock_alt_proof : verify[source] unlock_spec_alt. + Proof using MOD HAS_THREADS. + verify_spec. + repeat (go; ework). + wname [owner_tid_frag] "Owner". + iFrame "Owner". ego. + Unshelve. all: exact (1$m)%cQp. + Qed. + + Lemma mymutex_ctor_proof : verify[source] "MyMutex::MyMutex()". + Proof using MOD HAS_THREADS. + verify_spec; go. + wname [structR] "S". + wname [P] "P". + wname [_ |-> atomic.R _ _ _] "L". + wname [_ |-> thread_id_specs.thread_idR _ _] "Owner". + iMod (cinv_alloc_cofinite ∅ ⊤ N) as (gi) "(_ & CO & Alloc)". + iMod (alloc_state this gi N ltac:(done)) as (gs) "(%Hnamespace & T & State)". + iMod ("Alloc" $! (mutex_inv this (MkGname gs gi) P) + with "[] [L P Owner State]") as "#CI". + { iPureIntro. apply _. } + { iNext. rewrite /mutex_inv. iExists false. iFrame. } + iModIntro. iExists (MkGname gs gi). + iSplit; first (iPureIntro; exact Hnamespace). + rewrite /IR _at_sep _at_as_Rep /mutex_inv_namespace /= Hnamespace. + iFrame "CI". iFrame. + Qed. + + Lemma mymutex_dtor_proof : verify[source] "MyMutex::~MyMutex()". + Proof using MOD HAS_THREADS. + verify_spec. + rewrite /IR /mutex_inv. + work. + wname [cinv] "#CI". + wname [cinv_own] "CO". + wname [CustomMutexPreds.token] "T". + iMod (cinv_cancel with "CI CO") + as "Inv"; [done..|]. + go. + iDestruct "Inv" as (b) "(Lock & State & Resources)". + destruct b. + - iDestruct (locked_full_token with "[$State $T]") as %[]. + - iDestruct "Resources" as "[P Owner]". + iAssert emp with "[State T]" as "_". + { iApply (affine with "[State T]"); last iAccu. apply mpred_BiAffine. } + ego $usenamed=true with br_erefl. + Qed. + + Lemma mymutex_lock_proof : verify[source] lock_spec. + Proof using MOD HAS_THREADS. + rewrite lock_spec_equiv_lock_spec_alt. + exact mymutex_lock_alt_proof. + Qed. + + Lemma mymutex_unlock_proof : verify[source] unlock_spec. + Proof using MOD HAS_THREADS. + rewrite unlock_spec_equiv_unlock_spec_alt. + exact mymutex_unlock_alt_proof. + Qed. + + End with_Σ. +End custom_mutex. diff --git a/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex.hpp b/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex.hpp index cbaf109e..92466b3d 100644 --- a/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex.hpp +++ b/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex.hpp @@ -1,12 +1,14 @@ +#pragma once + #include #include #include #include -#pragma once +inline size_t id_hash = std::hash{}(std::thread::id{}); class MyRecursiveMutex { // not protected by the lock, but atomic - std::atomic m_owner{}; + std::atomic m_owner{id_hash}; // protected by the lock unsigned long long m_count{0}; @@ -15,10 +17,13 @@ class MyRecursiveMutex { std::mutex m_lock{}; public: + // Explicitly define the constructor so that compiler emits constructor code + MyRecursiveMutex() {} + // TODO: add the token/given_token accounting, maybe with q*tickets void lock() { - std::thread::id this_id{std::this_thread::get_id()}; + size_t this_id = std::hash{}(std::this_thread::get_id()); // Open cinv if (m_owner != this_id) { // here, m_owner is: @@ -53,13 +58,29 @@ class MyRecursiveMutex { } m_count++; } + + bool try_lock() noexcept { + size_t this_id = std::hash{}(std::this_thread::get_id()); + if (m_owner != this_id) { + if (!m_lock.try_lock()) + return false; + m_owner = this_id; + } + + // At the recursion limit, leave ownership and the count unchanged. + if (m_count + 1 == 0) + return false; + m_count++; + return true; + } + // TODO: connect token and given token to the exclusive token somehow, so that void unlock() { m_count--; if (m_count == 0) { + m_owner = id_hash; m_lock.unlock(); - m_owner = std::thread::id{}; } } }; diff --git a/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex_hpp_proof.v b/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex_hpp_proof.v index b0929fd4..d44b40f5 100644 --- a/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex_hpp_proof.v +++ b/rocq-brick-libstdcpp/test/mutex/custom_recursive_mutex_hpp_proof.v @@ -1,90 +1,637 @@ -(** Provisional *) +(** Verification of the recursive wrapper around std::mutex. -Require Import skylabs.auto.cpp.proof. + The implementation clears the atomic owner before releasing the inner + mutex. The contracts are partial-correctness contracts: at the largest + counter value, lock does not return. Hash uniqueness is assumed by + [thread.spec] in the shared thread specifications. The lock/unlock + contracts instantiate [recursive_mutex_spec], including its + [BasicLockable] presentation and construction contracts. *) +Require Import iris.algebra.coPset. +Require Import skylabs.auto.cpp.prelude.proof. +Require Import skylabs.brick.libstdcpp.atomic.spec. +Require Import skylabs.brick.libstdcpp.cassert.spec. Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. Require Import skylabs.brick.libstdcpp.mutex.spec.recursive_mutex. +Require Import skylabs.brick.libstdcpp.thread.spec. Require Import skylabs.brick.libstdcpp.test.mutex.custom_recursive_mutex_hpp. -(* -TODO step 1: -split recursive_mutex specs: +NES.Open std.atomic. -* C++- independent predicates vs -* what's bound to C++ names +Import linearity. +Import OwnerTid. -* step2 -specs for atomic +Module CustomRecursiveMutexName <: MutexCPPName. + Definition cpp_ty : type := "MyRecursiveMutex"%cpp_type. +End CustomRecursiveMutexName. -* step3??? +(* A custom recursive mutex implementation built on top of a mutex. *) +Module CustomRecursiveMutexPreds + <: RECURSIVE_MUTEX_PREDS CustomRecursiveMutexName. + Include RecursiveMutexState. - *) -Module custom_recursive_mutex. - Abbreviation N := "MyRecursiveMutex"%cpp_name. + Definition class_name : name := "MyRecursiveMutex"%cpp_name. + Definition cpp_ty : type := CustomRecursiveMutexName.cpp_ty. - (* - sl.lock - Definition countR `{Σ : cpp_logic, σ : genv} (count : nat) : Rep := - (* structR "std::recursive_mutex" 1$m ** *) - _field "MyRecursiveMutex::m_count" |-> ulonglongR 1$m count. *) + Record mutex_gname : Set := MkGname { + inner_gname : mutex.gname; + owner_gname : iprop.gname; + cinv_gname : iprop.gname; + (* the atomic int field associates another invariant with + namespace [owner_namespace]. *) + owner_namespace : namespace; + owner_namespace_disjoint : mutex.mutex_inv_namespace inner_gname ## owner_namespace; + }. + Definition gname : Set := mutex_gname. + Definition rmutex_inv_namespace (g : gname) : namespace := + mutex.mutex_inv_namespace g.(inner_gname). - Parameter atomic_thread_idT : ∀ `{Σ : cpp_logic, σ : genv}, cQp.t -> option thread_idT -> Rep. + Class stateG `{Σ : cpp_logic} := { + #[global] mutex_G :: mutex.G Σ; + #[global] owners_G :: OwnerTid.G Σ; + }. + Definition G := @stateG. + Existing Class G. + #[global] Arguments G {_ _} Σ : assert. + #[global] Instance state_G `{Σ : cpp_logic} (H : G Σ) : @stateG _ _ Σ := H. - Parameter exclusive_token : ∀ `{Σ : cpp_logic}, iprop.gname -> mpred. + Definition token `{Σ : cpp_logic, !G Σ} (g : gname) (q : cQp.t) : mpred := + mutex.token g.(inner_gname) q. + #[global] Hint Opaque token : sl_opacity typeclass_instances. - Record gname : Set := MkGname - { lock_gname : iprop.gname - ; cinv_gname : iprop.gname - ; excl_gname : iprop.gname - ; rec_gname : recursive_mutex.gname - }. + #[global] Instance token_fractional `{Σ : cpp_logic, !G Σ} g : + CFractional (token g). + Proof. intros q1 q2. rewrite /token. apply mutex.token_fractional. Qed. + #[global] Instance token_timeless `{Σ : cpp_logic, !G Σ} g qt : + Timeless (token g qt). + Proof. rewrite /token. apply _. Qed. + + Definition countR `{Σ : cpp_logic, σ : genv} + (this : ptr) (n : Z) : mpred := + this ,, _field "MyRecursiveMutex::m_count" |-> ulonglongR 1$m n. + + Definition protected `{Σ : cpp_logic, !G Σ, σ : genv} + (this : ptr) (go : iprop.gname) (P : mpred) : mpred := + countR this 0 ** owner_tid_frag go None ** P. + + Definition globals `{Σ : cpp_logic, σ : genv} (q : cQp.t) : mpred := + _global "id_hash" |-> ulongR q (thread_id_specs.thread_id_hash thread_id_specs.default_thread_id). + #[global] Arguments globals /. - Definition lock_namespace : namespace := nroot .@@ "MyRecursiveMutex". - (* About cinv. *) - Parameter count_auth : ∀ `{Σ : cpp_logic} (count : nat), mpred. - Parameter count_frag : ∀ `{Σ : cpp_logic} (count : nat), mpred. - - (* Definition IR `{Σ : cpp_logic, σ : genv, !HasStdThreads Σ, !recursive_mutex.lockedG Σ} (γ : gname) (q : cQp.t) : mpred := - ∃ x, recursive_mutex.owned_count_id_auth γ.(rec_gname) x. *) -(* - Definition rawR `{Σ : cpp_logic, σ : genv} (owner : option thread_idT) (count : nat) : Rep := - structR "std::recursive_mutex" 1$m ** - _field "MyRecursiveMutex::m_count" |-> ulonglongR 1$m count. *) - - Section with_Σ. - Context `{Σ : cpp_logic, σ : genv, !HasStdThreads Σ, !recursive_mutex.lockedG Σ}. - - Definition mutex_content (γ : gname) : Rep := - ∃ count, - _field "MyRecursiveMutex::m_count" |-> ulonglongR 1$m (Z.of_nat count) ** - pureR ( - count_auth count - (* ** - exclusive_token γ.(excl_gname) *) - ) . - - Definition IR (γ : gname) (q : cQp.t) : Rep := - structR N q$m ** - (* TODO: mutex could take a [Rep] to save us this [as_Rep] *) + (** Absence of a ghost owner is encoded by the default C++ thread ID. *) + Definition owner_id (owner : option (thread_idT * cQp.t)) : thread_idT := + match owner with + | None => thread_id_specs.default_thread_id + | Some (th, _) => th + end. + + Definition I `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} + (this : ptr) (g : gname) : mpred := + ∃ owner, + this ,, _field "MyRecursiveMutex::m_owner" |-> + atomic.R Tulong 1$m (thread_id_specs.thread_id_hash (owner_id owner)) ** + owner_tid_auth g.(owner_gname) owner ** + match owner with + | Some (th, qt) => + (** Note that this [mutex.locked] is NOT the [locked] precidate that we + instantiate later for the CustomRecursiveMutexPreds. + + Putting [mutex.locked] here is useful when a thread [th] acquires the + lock for the first time to show that [owner ≠ th]: when [th] opens + the invariant, [th] has [CustomRecursiveMutexPreds.not_locked th] + (which coincide with [mutex.not_locked th]), contradicting + [mutex.locked _ _ th _]. *) + mutex.locked + (this ,, _field "MyRecursiveMutex::m_lock") g.(inner_gname) th qt + | None => emp + end. + + Definition R `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} + {HAS_THREADS : HasStdThreads Σ} {σ : genv} + (g : gname) (q : cQp.t) (P : mpred) : Rep := + structR class_name q$m ** as_Rep (fun this => - this ,, _field "MyRecursiveMutex::m_lock" |-> mutex.R γ.(lock_gname) q - (this |-> mutex_content γ) ** - (* *) - (this |-> mutex_content γ - (* ** recursive_mutex.token γ.(rec_gname) (* ? *) *) - \\// - exclusive_token γ.(excl_gname) - (* ** recursive_mutex.given_token γ.(rec_gname) (* ? *) *) - ) ** - cinv lock_namespace γ.(cinv_gname) (∃ count owner, - count_frag count ** - this ,, _field "MyRecursiveMutex::m_owner" |-> atomic_thread_idT 1$m owner ** - recursive_mutex.owned_count_id_auth γ.(rec_gname) ((λ t, (t, Nat.pred count)) <$> owner) ** - [| owner = None <-> count = 0 |] ** - match count with - | 0 => emp - | _ => exclusive_token γ.(excl_gname) - end)). - (* we hold the lock if the owner is not none! *) - End with_Σ. + this ,, _field "MyRecursiveMutex::m_lock" |-> + mutex.R g.(inner_gname) q (protected this g.(owner_gname) P) ** + cinv g.(owner_namespace) g.(cinv_gname) (I this g) ** + cinv_own g.(cinv_gname) q). + #[global] Hint Opaque R : sl_opacity typeclass_instances. + #[only(type_ptr,cfractional,ascfractional,cfracvalid)] derive R. + + #[global] Instance R_learnable + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv, !HasStdThreads Σ} : + Cbn (Learn (learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). + Proof. solve_learnable. Qed. + + Definition not_locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} + (this : ptr) (g : gname) (qt : cQp.t) (th : thread_idT) : mpred := + mutex.not_locked (this ,, _field "MyRecursiveMutex::m_lock") g.(inner_gname) th qt. + + Definition locked `{Σ : cpp_logic, !G Σ, σ : genv} + (this : ptr) (g : gname) (qt : cQp.t) (th : thread_idT) + (n : nat) : mpred := + countR this (Z.of_nat (S n)) ** owner_tid_frag g.(owner_gname) (Some (th, qt)). + + #[global] Hint Opaque not_locked locked : sl_opacity typeclass_instances. + + #[global] Instance I_timeless + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} this g : Timeless (I this g). + Proof. + rewrite /I. apply bi.exist_timeless. + intros [[th qt]|]; apply _. + Qed. + + #[global] Instance locked_timeless + `{Σ : cpp_logic, !G Σ, σ : genv} this g qt th n : + Timeless (locked this g qt th n). + Proof. rewrite /locked /countR. apply _. Qed. + #[global] Instance not_locked_timeless + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} this g qt th : + Timeless (not_locked this g qt th). + Proof. rewrite /not_locked. apply _. Qed. + + #[local] Instance at_WeaklyObjective `{Σ : cpp_logic} + (p : ptr) (R : Rep) `{!WeaklyObjective (R p)} : + WeaklyObjective (p |-> R). + Proof. rewrite INTERNAL._at_eq. apply _. Qed. + + #[global] Instance I_WeaklyObjective + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ, σ : genv} this g : + WeaklyObjective (I this g). + Proof. + rewrite /I. apply exists_weakly_objective. + intros [[th qt]|]; apply _. + Qed. + #[global] Instance protected_WeaklyObjective + `{Σ : cpp_logic, !G Σ, σ : genv} this go P + `{!WeaklyObjective (countR this 0), !WeaklyObjective P} : + WeaklyObjective (protected this go P). + Proof. rewrite /protected. apply _. Qed. + +End CustomRecursiveMutexPreds. + +Module custom_recursive_mutex. + Import CustomRecursiveMutexPreds. + Module Spec := recursive_mutex_spec + CustomRecursiveMutexName CustomRecursiveMutexPreds. + Import Spec (acquireable). + #[local] Hint Opaque acquireable I protected countR : sl_opacity typeclass_instances. + Section with_cpp. + Context `{Σ : cpp_logic, σ : genv, !HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + Context `{MOD : source ⊧ σ}. + + (** Recursive mutex specs with globals. *) + cpp.spec "MyRecursiveMutex::MyRecursiveMutex()" as ctor_spec with ( + \this this + \prepost{qg} globals qg + \exact Reduce (Spec.ctor_spec this)). + + cpp.spec "MyRecursiveMutex::~MyRecursiveMutex()" as dtor_spec with + (\exact Reduce Spec.dtor_spec). + + cpp.spec "MyRecursiveMutex::lock()" as lock_spec from source with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.lock_spec this)). + + cpp.spec "MyRecursiveMutex::unlock()" as unlock_spec from source with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.unlock_spec this)). + + cpp.spec "MyRecursiveMutex::lock()" as lock_spec_alt from source with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.lock_spec_alt this)). + + cpp.spec "MyRecursiveMutex::unlock()" as unlock_spec_alt from source with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.unlock_spec_alt this)). + + cpp.spec "MyRecursiveMutex::try_lock()" as try_lock_spec from source with + (\this this + \prepost{qg} globals qg + \exact Reduce (Spec.try_lock_spec this)). + + (* Helper specs, proved later. *) + cpp.spec "std::mutex::lock()" as inner_lock_spec with ( + \this inner + \prepost{(container : ptr) g q P} container |-> R g q P + \require inner = container ,, _field "MyRecursiveMutex::m_lock" + \persist{th} current_thread th + \pre{qt} not_locked container g qt th + \post protected container g.(owner_gname) P ** mutex.locked inner g.(inner_gname) th qt). + + cpp.spec "std::mutex::try_lock()" as inner_try_lock_spec with ( + \this inner + \prepost{(container : ptr) g q P} container |-> R g q P + \require inner = container ,, _field "MyRecursiveMutex::m_lock" + \persist{th} current_thread th + \pre{qt} not_locked container g qt th + \post{b}[Vbool b] + if b then protected container g.(owner_gname) P ** mutex.locked inner g.(inner_gname) th qt + else not_locked container g qt th). + + cpp.spec "std::mutex::unlock()" as inner_unlock_spec with ( + \this inner + \prepost{(container : ptr) g q P} container |-> R g q P + \require inner = container ,, _field "MyRecursiveMutex::m_lock" + \persist{th} current_thread th + \pre{qt} mutex.locked inner g.(inner_gname) th qt + \pre ▷ protected container g.(owner_gname) P + \post not_locked container g qt th). + + (* The proof bodies in the rest of the section are generated by LLM. *) + (** The heap API leaves weak objectivity of the internal count as an + implementation hypothesis. *) + Lemma ctor_proof + `{count_objective : !forall this, WeaklyObjective (countR this 0)} : + verify[source] ctor_spec. + Proof using MOD. + (* Initialize the physical members first, with an empty inner invariant. *) + verify_spec. + (* Retain the final statement so its update modality can install the + ghost state after the member constructors return. *) + match goal with |- context[wp ?tu ?rho ?s ?K] => + rewrite (lock (wp tu rho s K)) end. + go. iExists N, emp. go. + rewrite -lock. + iApply fupd_wp. + iDestruct select (|={⊤}=> _)%I as "Init". + iMod "Init" as (g) "(_ & Inner & Token)". + (* Install the zero count, empty owner fragment, and client resource in + the inner mutex; keep the matching authority with the atomic owner. *) + iMod (OwnerTid.alloc None) as (go) "[Auth Frag]". + wname [_ |-> ulonglongR _ _] "Count". + iDestruct select (@tele_app TT (fun _ => mpred) P xs) as "P". + iMod (mutex.init_R (this ,, _field "MyRecursiveMutex::m_lock") + g N (protected this go (∃ xs, @tele_app TT (fun _ => mpred) P xs)) ltac:(apply _) ltac:(done) + with "[Inner Token Count Frag P]") as (inner) "(%Hnames & Inner & Token)". + { iFrame "Inner Token". iNext. rewrite /protected /countR. iFrame. } + wname [_ |-> atomic.R _ _ _] "Owner". + have [No Hdisjoint] := mutex.Spec.mutex_inv_namespace_disjoint inner. + iMod (cinv_alloc ⊤ No with "[Owner Auth]") as (gi) "[#CI CO]"; last first. + - iModIntro. go. iExists (MkGname inner go gi No Hdisjoint). + iSplit; first (iPureIntro; reflexivity). + rewrite /token /R _at_sep _at_as_Rep + /I /=. + iFrame "CI". iFrame. + - iNext. rewrite /I /=. iExists None. iFrame. + - change (WeaklyObjective (I this (MkGname inner go 1%positive No Hdisjoint))). + apply _. + Qed. + + Lemma dtor_proof : + mutex.dtor_spec ** std.atomic.dtor Tulong |-- verify[source] dtor_spec. + Proof using MOD. + verify_shift. + rewrite /token /R _at_sep _at_as_Rep. + work. + wname [cinv] "#CI". + wname [cinv_own] "CO". + (* Cancel the owner invariant before destroying the physical members. *) + iMod (cinv_cancel with "CI CO") as "Inv"; [done..|]. + iEval (rewrite /I) in "Inv". + iMod "Inv" as (owner) "[Owner State]". + iModIntro. go. + iAssert emp with "[State]" as "_". + { iApply (affine with "[State]"); last iAccu. apply mpred_BiAffine. } + (* The inner mutex destructor returns the protected count and resource. *) + wname [protected] "Guard". + iEval (rewrite /protected /countR) in "Guard". + iDestruct "Guard" as "(Count & Frag & P)". + iAssert emp with "[Frag]" as "_". + { iApply (affine with "[Frag]"); last iAccu. apply mpred_BiAffine. } + ego $usenamed=true with br_erefl. + Qed. + + #[local] Existing Instance mpred_BiAffine. + #[local] Hint Opaque locked not_locked + : sl_opacity typeclass_instances. + #[local] Instance owner_tid_frag_learn : LearnEq2 owner_tid_frag. + Proof. solve_learnable. Qed. + + Abbreviation BASE p := + (p ,, _base "std::atomic" "std::__atomic_base"). + + Lemma inner_lock_spec_ok : mutex.lock_spec_alt |-- inner_lock_spec. + Proof. + apply specify_mono. rewrite /R /not_locked. work with br_erefl. + iExists qt. work with br_erefl. + Qed. + + Lemma inner_try_lock_spec_ok : mutex.try_lock_spec_alt |-- inner_try_lock_spec. + Proof. + apply specify_mono. rewrite /R /not_locked. work with br_erefl. + iExists qt. work with br_erefl. + Qed. + + Lemma inner_unlock_spec_ok : mutex.unlock_spec_alt |-- inner_unlock_spec. + Proof. + apply specify_mono. rewrite /R /not_locked. work with br_erefl. + iExists qt. work with br_erefl. + Qed. + + #[program] + Definition load_not_held_C (this : ptr) := + \cancelx + \consuming{g q P} this |-> R g q P + \consuming{th qt} not_locked this g qt th + \guard (th <> thread_id_specs.default_thread_id) + \proving{K (_ : IsExistential K)} + std.atomic.do_load Tulong (BASE (this ,, _field "MyRecursiveMutex::m_owner")) K + \instantiate K := (fun owner => this |-> R g q P ** + not_locked this g qt th ** [| owner <> thread_id_specs.thread_id_hash th |]) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[HR Hhandle]" (?? ->). + iEval (rewrite /R _at_sep _at_as_Rep) in "HR". + iDestruct "HR" as "(S & Base & #CI & CO)". + rewrite /std.atomic.do_load. + iAcIntro. rewrite /commit_acc. + iInv (owner_namespace g) as "[>Inv CO]" "Hclose". + iEval (rewrite /I) in "Inv". + iDestruct "Inv" as (owner) "(Owner & Auth & Handle)". + destruct (decide (option_map fst owner = Some th)) as [Heq|Hne]. + { destruct owner as [[owner owner_qt]|]; simpl in Heq; last discriminate. + injection Heq as ->. + iEval (rewrite /not_locked) in "Hhandle". + iMod (mutex.Spec.locked_not_locked_exclusive with "[$Base $Handle $Hhandle]") as %[]. + have Hdisjoint := owner_namespace_disjoint g. solve_ndisj. } + iDestruct (fupd_mask_subseteq) as ">Y"; [| iModIntro]; first set_solver. + iExists (thread_id_specs.thread_id_hash (owner_id owner)), (1$m)%cQp. + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "Owner". iMod "Y" as "_". + iMod ("Hclose" with "[Owner Auth Handle]") as "_". + { iNext. rewrite /I. iExists owner. + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. rewrite /R _at_sep _at_as_Rep. iFrame "#∗". + iPureIntro. intros Heq. + apply thread_id_specs.thread_id_hash_injective in Heq. + destruct owner as [[owner owner_qt]|]; simpl in Heq, Hne; naive_solver. + Qed. + #[local] Hint Resolve load_not_held_C : sl_opacity. + + #[program] + Definition load_held_C (this : ptr) := + \cancelx + \consuming{g q P} this |-> R g q P + \consuming{th qt} owner_tid_frag g.(owner_gname) (Some (th, qt)) + \proving{K (_ : IsExistential K)} + std.atomic.do_load Tulong (BASE (this ,, _field "MyRecursiveMutex::m_owner")) K + \instantiate K := (fun owner => this |-> R g q P ** + owner_tid_frag g.(owner_gname) (Some (th, qt)) ** [| owner = thread_id_specs.thread_id_hash th |]) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[HR Hfrag]" (?? ->). + iEval (rewrite /R _at_sep _at_as_Rep) in "HR". + iDestruct "HR" as "(S & Base & #CI & CO)". + rewrite /std.atomic.do_load. + iAcIntro. rewrite /commit_acc. + iInv (owner_namespace g) as "[>Inv CO]" "Hclose". + iEval (rewrite /I) in "Inv". + iDestruct "Inv" as (owner) "(Owner & Auth & Handle)". + iDestruct (observe_2 [| owner = Some (th, qt) |] with "Auth Hfrag") as %->. + iDestruct (fupd_mask_subseteq) as ">Y"; [| iModIntro]; first set_solver. + iExists (thread_id_specs.thread_id_hash th), (1$m)%cQp. + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "Owner". iMod "Y" as "_". + iMod ("Hclose" with "[Owner Auth Handle]") as "_". + { iNext. rewrite /I. iExists (Some (th, qt)). + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. rewrite /R _at_sep _at_as_Rep. iFrame "#∗". done. + Qed. + #[local] Hint Resolve load_held_C : sl_opacity. + + Lemma store_owner_action (this : ptr) g q P th qt : + this |-> R g q P ** owner_tid_frag g.(owner_gname) None ** + mutex.locked (this ,, _field "MyRecursiveMutex::m_lock") g.(inner_gname) th qt |-- + std.atomic.do_store Tulong (BASE (this ,, _field "MyRecursiveMutex::m_owner")) + (thread_id_specs.thread_id_hash th) (this |-> R g q P ** owner_tid_frag g.(owner_gname) (Some (th, qt))). + Proof using Type MOD. + iIntros "(HR & Hfrag & Hhandle)". + iEval (rewrite /R _at_sep _at_as_Rep) in "HR". + iDestruct "HR" as "(S & Base & #CI & CO)". + rewrite /std.atomic.do_store. + iAcIntro. rewrite /commit_acc. + iInv (owner_namespace g) as "[>Inv CO]" "Hclose". + iEval (rewrite /I) in "Inv". + iDestruct "Inv" as (owner) "(Owner & Auth & Handle)". + iDestruct (observe_2 [| owner = None |] with "Auth Hfrag") as %->. + iDestruct (fupd_mask_subseteq) as ">Y"; [| iModIntro]; first set_solver. + iExists (thread_id_specs.thread_id_hash thread_id_specs.default_thread_id). + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "Owner". iMod "Y" as "_". + iMod (owner_update (owner_gname g) None None (Some (th, qt)) + with "[$Auth $Hfrag]") as "[Auth Hfrag]". + iMod ("Hclose" with "[Owner Auth Hhandle]") as "_". + { iNext. rewrite /I. iExists (Some (th, qt)). + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. rewrite /R _at_sep _at_as_Rep. iFrame "#∗". + Qed. + + #[program] + Definition store_owner_C (this : ptr) := + \cancelx + \consuming{g q P} this |-> R g q P + \consuming owner_tid_frag g.(owner_gname) None + \consuming{th qt} mutex.locked + (this ,, _field "MyRecursiveMutex::m_lock") g.(inner_gname) th qt + \proving{K (_ : IsExistential K)} + std.atomic.do_store Tulong (BASE (this ,, _field "MyRecursiveMutex::m_owner")) (thread_id_specs.thread_id_hash th) K + \instantiate K := (this |-> R g q P ** owner_tid_frag g.(owner_gname) (Some (th, qt))) + \end@{mpredI}. + Next Obligation. + intros. iIntros "Hpre" (?? ->). iApply (store_owner_action with "Hpre"). + Qed. + #[local] Hint Resolve store_owner_C : sl_opacity. + + Lemma clear_owner_action (this : ptr) g q P th qt : + this |-> R g q P ** owner_tid_frag g.(owner_gname) (Some (th, qt)) |-- + std.atomic.do_store Tulong (BASE (this ,, _field "MyRecursiveMutex::m_owner")) + (thread_id_specs.thread_id_hash thread_id_specs.default_thread_id) (this |-> R g q P ** owner_tid_frag g.(owner_gname) None ** + mutex.locked (this ,, _field "MyRecursiveMutex::m_lock") g.(inner_gname) th qt). + Proof using Type MOD. + iIntros "[HR Hfrag]". + iEval (rewrite /R _at_sep _at_as_Rep) in "HR". + iDestruct "HR" as "(S & Base & #CI & CO)". + rewrite /std.atomic.do_store. + iAcIntro. rewrite /commit_acc. + iInv (owner_namespace g) as "[>Inv CO]" "Hclose". + iEval (rewrite /I) in "Inv". + iDestruct "Inv" as (owner) "(Owner & Auth & Handle)". + iDestruct (observe_2 [| owner = Some (th, qt) |] with "Auth Hfrag") as %->. + iDestruct (fupd_mask_subseteq) as ">Y"; [| iModIntro]; first set_solver. + iExists (thread_id_specs.thread_id_hash th). + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "Owner". iMod "Y" as "_". + iMod (owner_update (owner_gname g) (Some (th, qt)) (Some (th, qt)) None + with "[$Auth $Hfrag]") as "[Auth Hfrag]". + iMod ("Hclose" with "[Owner Auth]") as "_". + { iNext. rewrite /I. iExists None. + iSplitL "Owner"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. rewrite /R _at_sep _at_as_Rep. iFrame "#∗". + Qed. + + #[program] + Definition clear_owner_C (this : ptr) := + \cancelx + \consuming{g q P} this |-> R g q P + \consuming{th qt} owner_tid_frag g.(owner_gname) (Some (th, qt)) + \proving{K (_ : IsExistential K)} + std.atomic.do_store Tulong (BASE (this ,, _field "MyRecursiveMutex::m_owner")) (thread_id_specs.thread_id_hash thread_id_specs.default_thread_id) K + \instantiate K := (this |-> R g q P ** owner_tid_frag g.(owner_gname) None ** + mutex.locked (this ,, _field "MyRecursiveMutex::m_lock") g.(inner_gname) th qt) + \end@{mpredI}. + Next Obligation. + intros. iIntros "Hpre" (?? ->). iApply (clear_owner_action with "Hpre"). + Qed. + #[local] Hint Resolve clear_owner_C : sl_opacity. + + Lemma lock_spec_equiv_lock_spec_alt : lock_spec ⊣⊢ lock_spec_alt. + Proof. + rewrite /lock_spec /lock_spec_alt /specify. + do 2 f_equiv. intros this xs K. + rewrite !add_with_equiv. f_equiv=> qg. + by rewrite !add_prepost_equiv Spec.lock_spec_equiv_lock_spec_alt. + Qed. + + Lemma unlock_spec_equiv_unlock_spec_alt : unlock_spec ⊣⊢ unlock_spec_alt. + Proof. + rewrite /unlock_spec /unlock_spec_alt /specify. + do 2 f_equiv. intros this xs K. + rewrite !add_with_equiv. f_equiv=> qg. + by rewrite !add_prepost_equiv Spec.unlock_spec_equiv_unlock_spec_alt. + Qed. + + Lemma lock_proof : verify[source] lock_spec. + Proof using MOD. + verify_spec. + rewrite Spec.acquireable.unlock. destruct n as [|n xs]. + (* First acquisition: exclude our own ID, then acquire the inner mutex. *) + - ego. + (* Publishing ownership transfers the inner held capability into the + owner invariant; its fragment remembers the registration share. *) + rewrite /protected. ego. + iDestruct select (this |-> R _ _ _) as "HR". + iDestruct select (owner_tid_frag _ None) as "Hfrag". + iDestruct select (mutex.locked _ _ _ _) as "Hinner". + iDestruct (store_owner_action with "[$HR $Hfrag $Hinner]") as "Hstore". + iSplitL "Hstore"; first iExact "Hstore". ego. + rewrite /countR. ego. + rewrite Spec.acquireable.unlock /locked /countR. ego. + wname [P] "HP". iExact "HP". + (* Recursive acquisition: the owner fragment identifies this thread. *) + - rewrite /locked /countR. ego. + destruct (decide (Z.of_nat (S n) = 18446744073709551615)%Z) as [Hmax|Hmax]; go. + (* At the counter limit, the loop has no returning execution. *) + + rewrite Hmax. go. wp_for (fun _ => emp); go. + + ego. rewrite Spec.acquireable.unlock /locked /countR. + have Hcount : (Z.of_nat (S n) + 1)%Z = Z.of_nat (S (S n)) by lia. + rewrite Hcount. iFrame "#∗". + Qed. + + Lemma try_lock_proof : verify[source] try_lock_spec. + Proof using MOD. + verify_spec. + rewrite Spec.acquireable.unlock. destruct n as [|n xs]. + - ego. + match goal with b : bool |- _ => destruct b end; ego. + + rewrite /protected. ego. + iDestruct select (this |-> R _ _ _) as "HR". + iDestruct select (owner_tid_frag _ None) as "Hfrag". + iDestruct select (mutex.locked _ _ _ _) as "Hinner". + iDestruct (store_owner_action with "[$HR $Hfrag $Hinner]") as "Hstore". + iSplitL "Hstore"; first iExact "Hstore". ego. + rewrite /countR. ego. + rewrite Spec.acquireable.unlock /locked /countR. ego. + wname [P] "HP". iExact "HP". + + rewrite Spec.acquireable.unlock. ego. + - rewrite /locked /countR. ego. + destruct (decide (Z.of_nat (S n) = 18446744073709551615)%Z) as [Hmax|Hmax]; go. + + rewrite Hmax. ego. + rewrite Spec.acquireable.unlock /locked /countR Hmax. iFrame "#∗". + + ego. rewrite Spec.acquireable.unlock /locked /countR. + have Hcount : (Z.of_nat (S n) + 1)%Z = Z.of_nat (S (S n)) by lia. + rewrite Hcount. iFrame "#∗". + Qed. + + Lemma unlock_proof : verify[source] unlock_spec. + Proof using MOD. + verify_spec. + rewrite Spec.acquireable.unlock /locked /countR. + go. + destruct n as [|n]. + (* Final release: clear the owner to recover the inner held capability; + the inner unlock then returns the acquisition handle. *) + - ego. + (* Return the zero count and client resource to the inner mutex. *) + wname [mutex.locked] "Locked". iFrame "Locked". + wname [P] "P". + wname [_ |-> ulonglongR _ _] "Count". + wname [owner_tid_frag] "OF". + iSplitL "P Count OF". + { iNext. rewrite /protected /countR. + iFrame "Count OF". iExists args. iExact "P". } + ego. rewrite Spec.acquireable.unlock release.unlock. iFrame "#∗". + (* Nested release: decrement the count and keep the resource. *) + - ego. rewrite Spec.acquireable.unlock release.unlock /locked /countR. + have Hcount : (Z.of_nat (S (S n)) - 1)%Z = Z.of_nat (S n) by lia. + rewrite Hcount. iFrame "#∗". + Qed. + + Lemma lock_alt_proof : verify[source] lock_spec_alt. + Proof using MOD. + rewrite -lock_spec_equiv_lock_spec_alt. exact lock_proof. + Qed. + + Lemma unlock_alt_proof : verify[source] unlock_spec_alt. + Proof using MOD. + rewrite -unlock_spec_equiv_unlock_spec_alt. exact unlock_proof. + Qed. + + Local Lemma id_dtor_ok : verify[source] thread_id_specs.thread_id_dtor_spec. + Proof using MOD. verify_spec. rewrite thread_id_specs.thread_idR.unlock. go. Qed. + + (** Only the out-of-line hash primitive and native-ID call remain library + premises for thread IDs. The hash wrappers and destructors are proved + in [thread.spec]. *) + Definition standard_library_specs : mpred := + denoteModule thread_hpp.source ** + mutex.ctor_spec ** mutex.dtor_spec ** + mutex.lock_spec_alt ** mutex.unlock_spec_alt ** mutex.try_lock_spec_alt ** + std.atomic.ctor Tulong ** std.atomic.dtor Tulong ** + std.atomic.cast Tulong ** std.atomic.assign Tulong ** + thread_id_specs.get_id_spec ** + thread_id_specs.hash_bytes_spec ** + std.cassert.assert_fail_spec. + + Lemma link + `{count_objective : !forall this, WeaklyObjective (countR this 0)} : + denoteModule source ** standard_library_specs |-- + ctor_spec ** dtor_spec ** lock_spec ** unlock_spec ** try_lock_spec. + Proof using MOD. + rewrite /standard_library_specs. + iIntros "[Hsource [#Hthread Hspecs]]". + iDestruct (observe [| thread_hpp.source ⊧ σ |] with "Hthread") as "%THREADMOD". + iCombine "Hsource Hthread Hspecs" as "Hpre". iRevert "Hpre". + work. + wapply ctor_proof. + wapply dtor_proof. + wapply lock_proof. + wapply unlock_proof. + wapply try_lock_proof. + wapply inner_lock_spec_ok. + wapply inner_unlock_spec_ok. + wapply inner_try_lock_spec_ok. + wapply id_dtor_ok. + wapply (thread_id_specs.hash_link (MOD := THREADMOD)). + work. + Qed. + + End with_cpp. End custom_recursive_mutex. diff --git a/rocq-brick-libstdcpp/test/mutex/double_incr.cpp b/rocq-brick-libstdcpp/test/mutex/double_incr.cpp new file mode 100644 index 00000000..8a909bc7 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/double_incr.cpp @@ -0,0 +1,24 @@ +#include +#include +#include + +unsigned int x{0}; +std::mutex m{}; + +void double_incr() { + m.lock(); + x++; + x++; + m.unlock(); +} + +int main() { + std::thread t1(double_incr); + std::thread t2(double_incr); + + t1.join(); + t2.join(); + + assert(x % 2 == 0); + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/double_incr_cpp_proof.v b/rocq-brick-libstdcpp/test/mutex/double_incr_cpp_proof.v new file mode 100644 index 00000000..e03e90c7 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/double_incr_cpp_proof.v @@ -0,0 +1,196 @@ +Require Import skylabs.auto.cpp.proof. +Require Import skylabs.bi.tls_modalities. +Require Import skylabs.brick.libstdcpp.mutex.spec.mutex. +Require Import skylabs.brick.libstdcpp.thread.spec. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Require Import skylabs.brick.libstdcpp.cassert.spec. +Require Import skylabs.brick.libstdcpp.test.mutex.double_incr_cpp. +Require Import skylabs.brick.libstdcpp.test.mutex.double_incr_ghost. + +#[local] Hint Opaque double_incr_protocol.frac double_incr_protocol.protected + double_incr_protocol.done double_incr_protocol.evenR : sl_opacity. + +Section with_cpp. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{!double_incr_protocol.G Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!mutex.G Σ}. + + cpp.spec "double_incr()" as double_incr_spec_body from source with ( + \prepost{g q γ} _global "m" |-> mutex.R g q + (double_incr_protocol.protected γ (_global "x")) + \persist{thr} current_thread thr + \prepost{qt} mutex.not_locked (_global "m") g thr qt + \pre double_incr_protocol.frac γ (1 / 2) + \post double_incr_protocol.done γ (_global "x")). + + Lemma double_incr_ok : verify[source] "double_incr()". + Proof. + verify_spec; go. + iExists q, (mutex.locked (_global "m") g thr qt ** + double_incr_protocol.protected γ (_global "x"))%I, qt. + iFrame. iSplitR; first (iIntros "$"). + iIntros "[? [? ?]]". + iDestruct (double_incr_protocol.prepare with "[$]") as "[Hx Hfinish]". + iDestruct "Hx" as (n) "[? %Heven]". + go. + replace (n + 1 + 1)%Z with (n + 2)%Z by lia. + iDestruct select (_global "x" |-> uintR _ _) as "Hx". + iAssert (double_incr_protocol.evenR (_global "x")) with "[Hx]" as "Hx". + { iExists (trim 32 (n + 2)). iFrame. iPureIntro. + apply double_incr_protocol.trim_add_two_even. exact Heven. } + iDestruct ("Hfinish" with "Hx") as "[? ?]". + iExists q, (mutex.not_locked (_global "m") g thr qt), qt; go. + Qed. + + (** The verification below is conditional on these objectivity properties + of the thread model. They are explicit proof hypotheses. *) + Context `{worker_spec_objective : !ObjectiveWith threadTI double_incr_spec_body}. + Context `{even_weakly_objective : + !WeaklyObjective (double_incr_protocol.evenR (_global "x"))}. + Context `{shared_mutex_objective : ∀ g γ, + ObjectiveWith threadTI (_global "m" |-> mutex.R g (1 / 2)$m + (double_incr_protocol.protected γ (_global "x")))}. + Context `{shared_token_objective : ∀ g, + ObjectiveWith threadTI (mutex.token g (1 / 2)$m)}. + + #[local] Instance protected_weakly_objective γ : + WeaklyObjective (double_incr_protocol.protected γ (_global "x")). + Proof using Type even_weakly_objective. + rewrite /double_incr_protocol.protected /double_incr_protocol.auth + /double_incr_protocol.frac. apply _. + Qed. + + (** Each worker receives half of the mutex ownership and a completion + ticket. Its namespace handle comes from spawn. *) + Definition worker_start (g : mutex.gname) + (γ : double_incr_protocol.gname) : mpred := + _global "m" |-> mutex.R g (1 / 2)$m + (double_incr_protocol.protected γ (_global "x")) ** + mutex.token g (1 / 2)$m ** double_incr_protocol.frac γ (1 / 2). + + #[local] Instance worker_start_objective g γ : + ObjectiveWith threadTI (worker_start g γ). + Proof using Type shared_mutex_objective shared_token_objective. + rewrite /worker_start /double_incr_protocol.frac. + apply sep_objective_with; first apply _. + apply sep_objective_with; first apply _. + apply objective_objective_with. apply _. + Qed. + + #[local] Instance done_weakly_objective γ : + WeaklyObjective (double_incr_protocol.done γ (_global "x")). + Proof using Type even_weakly_objective. + rewrite /double_incr_protocol.done /double_incr_protocol.auth. apply _. + Qed. + + (** Register the child's mutex namespace, then invoke the verified worker + contract in that child's thread context. *) + Lemma worker_entry `{MOD : source ⊧ σ} g γ : + double_incr_spec_body ** worker_start g γ |-- + ∀ child, + MutexSets.my_mutexes child (coPset.CoPset ⊤) -* + wp_fptr (σ.(genv_tu).(types)) thread.spec.entry_type + (_global "double_incr()") [] + (fun _ => double_incr_protocol.done γ (_global "x")). + Proof using Type worker_spec_objective even_weakly_objective + shared_mutex_objective shared_token_objective. + iIntros "[#Hcode Hstart]" (child) "Hset". + rewrite (objective_with_intro_exactly_at threadTI + (wp_fptr (σ.(genv_tu).(types)) thread.spec.entry_type + (_global "double_incr()") [] + (fun _ => double_incr_protocol.done γ (_global "x"))) child). + iDestruct (monPred_atleast_exactly_at threadTI child) as "HT". + iModIntro. + iDestruct "Hstart" as "(Hm & Htoken & Hfrac)". + iDestruct (MutexSets.my_mutexes_alloc_mutex_name child ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "Hset") as "[Hrest Hname]". + iAssert (mutex.not_locked (_global "m") g child ((1 / 2)$m)%cQp) + with "[Htoken Hname]" as "Hnotlocked". + { rewrite /mutex.not_locked. iFrame. } + iEval (unfold double_incr_spec_body, specify, unmaterialized_specR) in "Hcode". + iApply (invoke.use_cptrR with "Hcode"). cbn. + iSplitR; first done. + iExists g, ((1 / 2)$m)%cQp, γ, child, ((1 / 2)$m)%cQp. + iSplitR; first done. iFrame. + iFrame "HT". + iIntros "[HR [HNL HD]]" (v) "HV". iExact "HD". + Qed. + + #[local] Hint Opaque worker_start thread.spec.spawned_threads_inv : sl_opacity. + + Definition thread_pool_namespace : namespace := + nroot .@@ "double_incr" .@ "threads". + + (** Global vars are given as preconditions. + Assume the mutex is initialized with an empty invariant (assocaited with + γold), which can later be updated to a new invariant with init_R. *) + cpp.spec "main()" as main_spec from source with ( + (* FIXME if we have fixed the gname for mutex_set_map by the instance, we + have to assume a [mutex_set_map ∅] in the precondition here because if we + allocate a new one, we can't prove that it has the same gname. *) + \pre MutexSets.mutex_set_map ∅ + \pre _global "x" |-> uintR 1$m 0 + \pre{γold} _global "m" |-> mutex.R γold 1$m emp ** mutex.token γold 1$m + \post[Vint 0] double_incr_protocol.evenR (_global "x")). + + Lemma main_ok : verify[source] "main()". + Proof using Type worker_spec_objective even_weakly_objective + shared_mutex_objective shared_token_objective. + verify_spec. + let rec expose_globals := + first [iDestruct select (_global "x" |-> uintR 1$m 0) as "Hx" + | progress go1; expose_globals] in + expose_globals. + iAssert (double_incr_protocol.evenR (_global "x")) with "[Hx]" as "Hx". + { iExists 0. iFrame. done. } + iMod (double_incr_protocol.alloc with "Hx") as (γ) "(HP & Hf1 & Hf2)". + iDestruct select (_global "m" |-> mutex.R γold 1$m emp) as "Hm". + iDestruct select (mutex.token γold 1$m) as "Htoken". + iDestruct select (MutexSets.mutex_set_map ∅) as "Hmap". + iMod (thread.spec.spawned_threads_inv_alloc thread_pool_namespace + with "Hmap") as "#Hpool". + (* Logical invariant setup for the already initialized mutex. *) + iMod (mutex.init_R (_global "m") γold (mutex.mutex_inv_namespace γold) + (double_incr_protocol.protected γ (_global "x")) ltac:(apply _) + (mutex.mutex_inv_namespace_not_root γold) with "[Hm Htoken HP]") as (g) "(%Hnames & Hm & Htoken)". + { iFrame "Hm Htoken". iNext. done. } + iDestruct "Hm" as "[Hm1 Hm2]". + iEval (rewrite (cfractional_split_half (mutex.token g)) + cQp.scale_mut Qp.mul_1_r) in "Htoken". + iDestruct "Htoken" as "[Ht1 Ht2]". + iAssert (worker_start g γ ** worker_start g γ)%I + with "[Hm1 Hm2 Ht1 Ht2 Hf1 Hf2]" as "[Hstart1 Hstart2]". + { rewrite /worker_start. iFrame. } + go. + iDestruct select double_incr_spec_body as "#Hcode". + iExists thread_pool_namespace, + (double_incr_protocol.done γ (_global "x")). + iFrame "#". iSplitR; first (iPureIntro; apply _). + iSplitL "Hstart1". + - iApply worker_entry. iFrame "Hcode Hstart1". + - iIntros "Hthread1". + iDestruct "Hthread1" as (child1) "[% HR1]". + iDestruct (observe (type_ptr "std::thread" t1_addr) with "HR1") as "#?". + iDestruct "HR1" as "?". go. + iExists thread_pool_namespace, + (double_incr_protocol.done γ (_global "x")). + iFrame "#". iSplitR; first (iPureIntro; apply _). + iSplitL "Hstart2". + + iApply worker_entry. iFrame "Hcode Hstart2". + + iIntros "Hthread2". + iDestruct "Hthread2" as (child2) "[% HR2]". + iDestruct (observe (type_ptr "std::thread" t2_addr) with "HR2") as "#?". + iDestruct "HR2" as "?". go. + iExists child1, (double_incr_protocol.done γ (_global "x")). go. + iExists child2, (double_incr_protocol.done γ (_global "x")). go. + iDestruct (double_incr_protocol.join with "[$]") + as (n) "[Hx %Heven]". + go. + iExists (1$m)%cQp; go. + iFrame. + iExists n. iFrame. done. + Qed. +End with_cpp. diff --git a/rocq-brick-libstdcpp/test/mutex/double_incr_ghost.v b/rocq-brick-libstdcpp/test/mutex/double_incr_ghost.v new file mode 100644 index 00000000..8cd82526 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/double_incr_ghost.v @@ -0,0 +1,93 @@ +Require Import skylabs.auto.cpp.prelude.proof. +Require Import iris.algebra.lib.frac_auth. + +(** Each worker receives half of a permission to enter the critical section. + The first worker deposits its half and returns the exclusive authority; + the second gathers both halves and transfers ownership of the counter. + Only the counter's parity is tracked. Joining both workers recovers its + ownership because they cannot both return the exclusive authority. *) +Module double_incr_protocol. + Canonical Structure completionR : cmra := frac_authR unitUR. + + Class G `{Σ : cpp_logic} := { + #[local] completion_own :: HasOwn (iPropI _Σ) completionR; + #[local] completion_upd :: HasOwnUpd (iPropI _Σ) completionR; + #[local] completion_valid :: HasOwnValid (iPropI _Σ) completionR; + }. + #[global] Arguments G {_ _} Σ : assert. + + Definition gname := iprop.gname. + + Lemma trim_add_two_even (n : Z) : + (n mod 2 = 0)%Z -> (trim 32 (n + 2) mod 2 = 0)%Z. + Proof. + intros Hn. rewrite /trim Z.mod_mod_divide. + - rewrite Z.add_mod; last lia. rewrite Hn. reflexivity. + - exists (2 ^ 31)%Z. reflexivity. + Qed. + + Section proof. + Context `{Σ : cpp_logic, σ : genv, !G Σ}. + + Definition auth (γ : iprop.gname) : mpred := + own γ ((●F ()) : completionR). + Definition frac (γ : iprop.gname) (q : Qp) : mpred := + own γ ((◯F{q} ()) : completionR). + + Definition evenR (x : ptr) : mpred := + ∃ n : Z, x |-> uintR 1$m n ** [| (n mod 2 = 0)%Z |]. + + Definition protected (γ : gname) (x : ptr) : mpred := + (auth γ ** evenR x) ∨ + (frac γ (1 / 2) ** evenR x) ∨ frac γ 1. + + Definition done (γ : gname) (x : ptr) : mpred := + auth γ ∨ evenR x. + + Lemma alloc (x : ptr) : + evenR x |-- (|==> ∃ γ, + protected γ x ** frac γ (1 / 2) ** frac γ (1 / 2)). + Proof. + iIntros "Hx". + iMod (own_alloc (((●F ()) ⋅ (◯F ())) : completionR)) + as (γ) "[Ha Hf]"; first (apply frac_auth_valid; done). + have Hsplit : ((◯F ()) : completionR) ≡ + ((◯F{1 / 2} ()) ⋅ (◯F{1 / 2} ())). + { rewrite -frac_auth_frag_op Qp.half_half. done. } + iEval (rewrite Hsplit own_op) in "Hf". + iModIntro. iExists γ. rewrite /frac. iFrame "Hf". + rewrite /protected. iLeft. iFrame. + Qed. + + (** After the physical increments, the first worker deposits its half and + the second gathers both halves. The completed invariant keeps the full + fraction, preventing another worker from presenting an unused half. *) + Lemma prepare γ (x : ptr) : + protected γ x ** frac γ (1 / 2) |-- + evenR x ** (evenR x -* protected γ x ** done γ x). + Proof. + rewrite /protected /done /auth /frac. + iIntros "[[H | [H | H]] Hf]". + - iDestruct "H" as "[Ha Hx]". iFrame "Hx". iIntros "Hx". + iSplitL "Hf Hx". + + iRight. iLeft. iFrame. + + iLeft. done. + - iDestruct "H" as "[Hstored Hx]". iFrame "Hx". iIntros "Hx". + iCombine "Hstored Hf" as "Hfull". + iSplitL "Hfull". + + iRight. iRight. done. + + iRight. done. + - iDestruct (own_valid_2 with "H Hf") as %Hv. + move: Hv. rewrite -frac_auth_frag_op frac_auth_frag_valid. + move=> [Hfrac _]. exfalso. compute in Hfrac. done. + Qed. + + Lemma join γ (x : ptr) : done γ x ** done γ x |-- evenR x. + Proof. + rewrite /done. iIntros "[[H1 | H1] [H2 | H2]]"; try iAssumption. + rewrite /auth. + iDestruct (own_valid_2 with "H1 H2") as %Hv. + move: Hv. rewrite auth_auth_op_valid. done. + Qed. + End proof. +End double_incr_protocol. diff --git a/rocq-brick-libstdcpp/test/mutex/guard_recursive_cpp_proof.v b/rocq-brick-libstdcpp/test/mutex/guard_recursive_cpp_proof.v index 456f5b52..680fe2cc 100644 --- a/rocq-brick-libstdcpp/test/mutex/guard_recursive_cpp_proof.v +++ b/rocq-brick-libstdcpp/test/mutex/guard_recursive_cpp_proof.v @@ -1,6 +1,7 @@ (** Provisional *) Require Import skylabs.auto.cpp.prelude.proof. Require Import skylabs.brick.libstdcpp.mutex.spec. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. Require Import skylabs.brick.libstdcpp.test.mutex.guard_recursive_cpp. Import linearity. @@ -68,7 +69,7 @@ Succeed #[global] Instance: Inhabited TT := _. (** Canonical "constructor" for our telescope. *) Polymorphic Definition mk (a : Z) : TT := {| tele_arg_head := a; tele_arg_tail := () |}. -Succeed Definition b := recursive_mutex.Held 0 (mk 0). +Succeed Definition b := std_recursive_mutex.Held 0 (mk 0). sl.lock Definition CR' `{Σ : cpp_logic, σ : genv} (a : Z) : Rep := @@ -83,14 +84,12 @@ Definition P `{Σ : cpp_logic, σ : genv} (this : ptr) : TT -t> mpred := sl.lock Definition CR `{Σ : cpp_logic, σ : genv} {HAS_THREADS : HasStdThreads Σ} - `{!recursive_mutex.lockedG Σ} - `{!HasOwn (iPropI _) recursive_mutex.cmraR} - (γ : recursive_mutex.rmutex_gname) (q : cQp.t) := + `{MUTEX_SETS : !mutex_set_G Σ, !std_recursive_mutex.G Σ} + (γ : std_recursive_mutex.gname) (q : cQp.t) := structR "C" q ** - _field "C::m" |-> recursive_mutex.derivedR γ q ** as_Rep (fun this : ptr => - recursive_mutex.inv_rmutex γ (∃ a : tele_arg TT, - @tele_app TT (fun _ => mpred) (P this) a)). + this ,, _field "C::m" |-> std_recursive_mutex.R γ q + (∃ a : tele_arg TT, @tele_app TT (fun _ => mpred) (P this) a)). #[only(cfractional,ascfractional,cfracvalid,type_ptr)] derive CR. #[only(lazy_unfold(export))] derive CR. @@ -98,11 +97,10 @@ Definition CR Section with_cpp. Context `{Σ : cpp_logic, σ : genv} {HAS_THREADS : HasStdThreads Σ}. - Context `{!recursive_mutex.lockedG Σ}. - Context `{!HasOwn (iPropI _) recursive_mutex.cmraR}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. - #[local] Instance: - `{WeaklyObjective1 (@tele_app TT (fun _ => mpred) (P p))}. + #[local] Instance: `{Objective1 (@tele_app TT (fun _ => mpred) (P p))}. Proof. Admitted. #[global] Instance P_timeless p a : Timeless (P p a). @@ -114,21 +112,24 @@ Section with_cpp. Timeless (@tele_app TT (fun _ => mpred) (P p) args) := _. cpp.spec "test_one_answer()" from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) \post[Vint 42] emp ). cpp.spec "test_other_answer()" from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) \post[Vint 42] emp ). - Import recursive_mutex(lock_gname). - + (** Initialize the member lock but do not acquire it. *) cpp.spec "C::C()" from source as C_ctor_spec with ( \this this - \persist{thr} current_thread thr + \pre{N} emp + \require N <> nroot \post Exists γ, - this |-> CR γ 1$m ** - recursive_mutex.used_threads (lock_gname γ) {[thr]} ** - recursive_mutex.acquireable (TT := TT) γ thr recursive_mutex.NotHeld (P this) + [| std_recursive_mutex.rmutex_inv_namespace γ = N |] ** + this |-> CR γ 1$m ** std_recursive_mutex.token γ 1$m ). (** XXX Does not appear to help *) @@ -137,42 +138,30 @@ Section with_cpp. Lemma C_ctor_ok : verify[source] "C::C()". Proof. - verify_shift; go. - iExists TT, (P this), (mk 0); go. + verify_shift. + go. + iExists N, TT, (P this), (mk 0); go. rewrite (* Ugh *) -bi.later_intro. rewrite {1}P.unlock. go. - iMod (recursive_mutex.use_thread thr (lock_gname t) ∅ with "[$]") as "?"; first set_solver; iModIntro. - go. - rewrite recursive_mutex.acquireable.unlock (left_id_L _ (∪)). - go. + iModIntro. go. Qed. cpp.spec "C::~C()" from source as C_dtor_spec with ( \this this - \persist{thr} current_thread thr \pre{γ} this |-> CR γ 1$m - (* \pre recursive_mutex.used_threads (lock_gname γ) empty *) - \pre recursive_mutex.used_threads γ.(lock_gname) {[thr]} - \pre recursive_mutex.inv_rmutex γ (∃ xs, - @tele_app TT (fun _ => mpred) (P this) xs) - \pre recursive_mutex.acquireable (TT := TT) γ thr recursive_mutex.NotHeld (P this) + \pre std_recursive_mutex.token γ 1$m \post emp). Lemma C_dtor_ok : - (* We only get [|> recursive_mutex.dtor_spec'], and that's not enough *) - recursive_mutex.dtor_spec' |-- verify[source] "C::~C()". + (* We only get [|> std_recursive_mutex.std_dtor_spec], and that's not enough *) + std_recursive_mutex.std_dtor_spec |-- verify[source] "C::~C()". Proof. verify_shift; go. - wapply (recursive_mutex.logout thr _ ∅); first by set_solver. - rewrite recursive_mutex.acquireable.unlock /= (left_id_L _ (∪)). - go with br_erefl. - iModIntro. - go. + iModIntro. go. progress destruct_tele; rewrite P.unlock /=. go. - iApply affine; [apply mpred_BiAffine|go]. Qed. cpp.spec "C::one_answer()" from source inline. @@ -180,25 +169,39 @@ Section with_cpp. Lemma test_one_answer_ok : verify[source] "test_one_answer()". Proof. + have Hnamespace : nroot .@@ "guard_recursive" <> nroot. + { rewrite namespaces.ndot_unseal. discriminate. } verify_spec; go. + iExists (nroot .@@ "guard_recursive"); go. + iDestruct (MutexSets.my_mutexes_alloc_mutex_name + thr ⊤ + (↑std_recursive_mutex.rmutex_inv_namespace t) ltac:(set_solver) + with "[$]") as "[Hrest Hname]". + wname [std_recursive_mutex.token] "Htoken". + iAssert (std_recursive_mutex.acquireable (c_addr ,, _field "C::m") + (TT := TT) t 1$m thr std_recursive_mutex.NotHeld (P c_addr))%I + with "[Htoken Hname]" as "Hacquireable". + { rewrite std_recursive_mutex.acquireable.unlock /= + /std_recursive_mutex.not_locked. iFrame "#∗". } + iDestruct "Hacquireable" as "?". go. destruct args as [a []]. rewrite P.unlock /=. go. iExists ?[K], (mk 42). go. iSplitL ""; [by go | go]. - have [? [??]] : exists a, n = 1%nat /\ recursive_mutex.acquire a (recursive_mutex.release (recursive_mutex.Held n (mk 42))). { + have [? [??]] : exists a, n = 1%nat /\ std_recursive_mutex.acquire a (std_recursive_mutex.release (std_recursive_mutex.Held n (mk 42))). { lazymatch goal with - | _ : recursive_mutex.Held ?x _ = _ |- _ => rename x into n0 + | _ : std_recursive_mutex.Held ?x _ = _ |- _ => rename x into n0 end. (* This step breaks abstractions, but we have taken the lock yet hints don't give us access to the resouce. *) assert (n0 = 0 /\ n = 1)%nat as [-> ->]. { - rewrite-> recursive_mutex.release.unlock in *. + rewrite-> std_recursive_mutex.release.unlock in *. destruct n0, n; naive_solver. } - rewrite recursive_mutex.release.unlock /=. - exists recursive_mutex.NotHeld. + rewrite std_recursive_mutex.release.unlock /=. + exists std_recursive_mutex.NotHeld. split; first done. typeclasses eauto with br_hints. } @@ -206,14 +209,24 @@ Section with_cpp. rewrite CR'.unlock. destruct args as [a1 []]. go. - have [Ha1 Hrel]: (a1 = 42 /\ recursive_mutex.release (recursive_mutex.Held n (mk a1)) = recursive_mutex.NotHeld). { - rewrite-> recursive_mutex.release.unlock in *; naive_solver. + have [Ha1 Hrel]: (a1 = 42 /\ std_recursive_mutex.release (std_recursive_mutex.Held n (mk a1)) = std_recursive_mutex.NotHeld). { + rewrite-> std_recursive_mutex.release.unlock in *; naive_solver. } subst a1. iExists ?[K], (mk 42). rewrite Hrel /=. go. iSplitL ""; [by go | go]. rewrite P.unlock CR'.unlock; go. + match goal with g : std_recursive_mutex.gname |- _ => rename g into γ end. + iDestruct select (std_recursive_mutex.acquireable _ _ _ _ _ _) as "Hacquireable". + iEval (rewrite std_recursive_mutex.acquireable.unlock /= + /std_recursive_mutex.not_locked) in "Hacquireable". + iDestruct "Hacquireable" as "[Htoken Hname]". + iDestruct (MutexSets.my_mutexes_join_mutex_name + thr ⊤ + (↑std_recursive_mutex.rmutex_inv_namespace γ) ltac:(set_solver) + with "[Hrest Hname]") as "Hnames"; first iFrame. + iFrame "Htoken". go $usenamed=true. Qed. (* TODO: when we project out equalities about Held and NotHeld, project info diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/LICENSE.TXT b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/LICENSE.TXT new file mode 100644 index 00000000..e159d283 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/LICENSE.TXT @@ -0,0 +1,311 @@ +============================================================================== +The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the LLVM Project: +============================================================================== +The LLVM Project contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. + +============================================================================== +Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy): +============================================================================== + +The libc++ library is dual licensed under both the University of Illinois +"BSD-Like" license and the MIT license. As a user of this code you may choose +to use it under either license. As a contributor, you agree to allow your code +to be used under both. + +Full text of the relevant licenses is included below. + +============================================================================== + +University of Illinois/NCSA +Open Source License + +Copyright (c) 2009-2019 by the contributors listed in CREDITS.TXT + +All rights reserved. + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +============================================================================== + +Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/README.txt b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/README.txt new file mode 100644 index 00000000..c95d47a6 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/README.txt @@ -0,0 +1,51 @@ +CAUTION: the [libcpp_tests] directory, including this README and the LLVM + LICENSE.TXT, are generated by LLM. + +Custom mutex runtime tests + +These 17 tests are adapted from LLVM libc++ llvmorg-22.1.8: + ca7933e47d3a3451d81e72ac174dcb5aa28b59d1 + + mutex/ 5 tests of MyMutex + recursive_mutex/ 5 tests of MyRecursiveMutex + shared_mutex/ 7 tests of MySharedMutex + +They check construction, prohibition of copying/assignment, exclusive locking, +recursive ownership, shared locking, and the nonblocking try-lock methods. +The .compile.pass.cpp files check type traits without creating an executable. + +Run from the repository root with Python 3 and Clang: + + python3 rocq-brick-libstdcpp/test/mutex/libcpp_tests/run.py + python3 rocq-brick-libstdcpp/test/mutex/libcpp_tests/run.py --stdlib=libc++ + +The default uses libstdc++. libc++ must be installed for the second command. +Both commands run the same LLVM test sources with different standard libraries. +These are custom-class tests; they do not run the standard-type baseline cases. + +To use GCC's default standard library: + + python3 rocq-brick-libstdcpp/test/mutex/libcpp_tests/run.py --cxx=g++ --stdlib=default + +To select a test or group, add a path substring, for example: + + --filter=recursive_mutex/lock.pass.cpp + +The runner also works from other working directories. It compiles in C++17 mode +with assertions enabled, gives each compilation 60 seconds and each executable +20 seconds, and returns a nonzero status on any failure. Build outputs go into a +temporary directory that is removed after the run. There is no dependency on +LLVM lit, a full LLVM checkout, or the earlier /tmp test workspace. + +UPSTREAM.json records the exact source URLs, original SHA-256 hashes, and the +adaptations. Only the tested mutex types and include directives were changed: +custom headers replace the standard mutex headers, unused includes are removed, +and required standard facilities are included directly. All upstream assertions +and the two support headers are retained. LICENSE.TXT contains the LLVM license. + +These are native C++ runtime tests. test/dune-gen.sh excludes this folder from +cpp2v AST generation; run them using run.py separately from the Rocq build. + +Passing these tests does not establish full standard conformance or cover all +thread schedules. In particular, they do not exercise hash collisions or the +recursive mutex's counter limit. diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/UPSTREAM.json b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/UPSTREAM.json new file mode 100644 index 00000000..cee2d2d0 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/UPSTREAM.json @@ -0,0 +1,93 @@ +{ + "project": "LLVM libc++", + "release": "llvmorg-22.1.8", + "commit": "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", + "adaptations": [ + "Include the repository custom mutex header using a relative path.", + "Replace the tested std:: mutex type with the matching MyMutex, MyRecursiveMutex, or MySharedMutex type.", + "Remove unused includes from test sources and directly include for std::begin/std::end.", + "Preserve every upstream assertion and the support headers." + ], + "files": { + "mutex/assign.compile.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.class/assign.compile.pass.cpp", + "upstream_sha256": "3a5a73a85cfab31871f38a92862838871bd66954fdce4bbeb46f0596fa7cac26" + }, + "mutex/ctor.copy.compile.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.class/ctor.copy.compile.pass.cpp", + "upstream_sha256": "01abb04d22631f7a62fc0bdbc0a832f342d79ee5c5370b51dfa25a6888a4593a" + }, + "mutex/ctor.default.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.class/ctor.default.pass.cpp", + "upstream_sha256": "e47b2dff8b9071238160bdbbd56f6a2437d1c358c5bedf615080465e367950aa" + }, + "mutex/lock.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.class/lock.pass.cpp", + "upstream_sha256": "120e7204cdae0919ff0635bf3acb8839d7a1e7ce63170c5921cf687b841ba7d4" + }, + "mutex/try_lock.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.class/try_lock.pass.cpp", + "upstream_sha256": "81044a279dc66a75aa5e8b39f37576dfff3356a439f7eb842f14e9392f9a29cf" + }, + "recursive_mutex/assign.compile.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.recursive/assign.compile.pass.cpp", + "upstream_sha256": "f38a4c75e7ee06f637e8f81be81c56c4cd231664c2e7c63782d2b1150311d9e0" + }, + "recursive_mutex/ctor.copy.compile.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.recursive/ctor.copy.compile.pass.cpp", + "upstream_sha256": "7e28097fb89e9fe17ce5ff61e128d56827c73b8b6aeba8c2b95fd10ad8b28294" + }, + "recursive_mutex/ctor.default.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.recursive/ctor.default.pass.cpp", + "upstream_sha256": "94230436de215238431d80c16474b019f9b6965350f0ad5429205f4cdb3364f4" + }, + "recursive_mutex/lock.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.recursive/lock.pass.cpp", + "upstream_sha256": "10a631c90893d2ac5a25425b43f7ea70e57c5149ccebc037f02284b10010f747" + }, + "recursive_mutex/try_lock.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.mutex.requirements.mutex/thread.mutex.recursive/try_lock.pass.cpp", + "upstream_sha256": "7555b07e2bfc47bffd02fa9bd7d6551da809d340aced09a16bdb0f58959a39b8" + }, + "shared_mutex/assign.compile.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.shared_mutex.requirements/thread.shared_mutex.class/assign.compile.pass.cpp", + "upstream_sha256": "31c8d543f322febf4609eb0f338799c8359fcc1a7c417822359ba2e9bed7eb84" + }, + "shared_mutex/ctor.copy.compile.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.shared_mutex.requirements/thread.shared_mutex.class/ctor.copy.compile.pass.cpp", + "upstream_sha256": "a6cf086e60c06f093eb82221a8783a4f5547f49810969b16cecef68b42879546" + }, + "shared_mutex/ctor.default.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.shared_mutex.requirements/thread.shared_mutex.class/ctor.default.pass.cpp", + "upstream_sha256": "91e57db57a78002d09ddbdbc95e97266033d6f2cb673a37598a0242d03c63d95" + }, + "shared_mutex/lock.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.shared_mutex.requirements/thread.shared_mutex.class/lock.pass.cpp", + "upstream_sha256": "cc3c4c25c7cf118f23bf6adca6a4acf68fd18c1c065531a87f6b86256fccd985" + }, + "shared_mutex/lock_shared.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.shared_mutex.requirements/thread.shared_mutex.class/lock_shared.pass.cpp", + "upstream_sha256": "7dd1b8e27d15d5a78615fae70c3add1843dd389f1368c0a64c268d46acf05cb5" + }, + "shared_mutex/try_lock.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.shared_mutex.requirements/thread.shared_mutex.class/try_lock.pass.cpp", + "upstream_sha256": "94f90b7d366e10132a09ddf6593239718ca5afd858da9dd3ecd1eeec4439270d" + }, + "shared_mutex/try_lock_shared.pass.cpp": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/std/thread/thread.mutex/thread.mutex.requirements/thread.shared_mutex.requirements/thread.shared_mutex.class/try_lock_shared.pass.cpp", + "upstream_sha256": "0d287f284210a11dba4d5702c8301ebd92aa057320c390c46a8f051611816a9e" + }, + "support/test_macros.h": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/support/test_macros.h", + "upstream_sha256": "ed5fe25dd7d279a5411905dfcddf21aa207d8c39a2c6672de872b270676ba012" + }, + "support/make_test_thread.h": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/test/support/make_test_thread.h", + "upstream_sha256": "5616253d10fa1408faf49a04b5607ba4fd648381522315b5f671690741d3c0f7" + }, + "LICENSE.TXT": { + "upstream_url": "https://github.com/llvm/llvm-project/blob/ca7933e47d3a3451d81e72ac174dcb5aa28b59d1/libcxx/LICENSE.TXT", + "upstream_sha256": "539dd7aed86e8a4f12cbdd0e6c50c189c7d74847e4fecc64ce2c6ee3a01da38b" + } + } +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/assign.compile.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/assign.compile.pass.cpp new file mode 100644 index 00000000..055784de --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/assign.compile.pass.cpp @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads + +// + +// class mutex; + +// mutex& operator=(const mutex&) = delete; + +#include "../../custom_mutex.hpp" + +#include + +static_assert(!std::is_copy_assignable::value, ""); diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/ctor.copy.compile.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/ctor.copy.compile.pass.cpp new file mode 100644 index 00000000..470cd949 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/ctor.copy.compile.pass.cpp @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads + +// + +// class mutex; + +// mutex(const mutex&) = delete; + +#include "../../custom_mutex.hpp" + +#include + +static_assert(!std::is_copy_constructible::value, ""); diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/ctor.default.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/ctor.default.pass.cpp new file mode 100644 index 00000000..831361ea --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/ctor.default.pass.cpp @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads + +// + +// class mutex; + +// mutex() noexcept; + +#include "../../custom_mutex.hpp" + +#include +#include + +static_assert(std::is_nothrow_default_constructible::value, ""); + +int main(int, char**) { + // The mutex is unlocked after default construction + { + MyMutex m; + assert(m.try_lock()); + m.unlock(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/lock.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/lock.pass.cpp new file mode 100644 index 00000000..07f112bb --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/lock.pass.cpp @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03 +// UNSUPPORTED: no-threads + +// + +// class mutex; + +// void lock(); + +#include "../../custom_mutex.hpp" + +#include +#include +#include +#include + +#include "make_test_thread.h" + +int main(int, char**) { + // Lock a mutex that is not locked yet. This should succeed. + { + MyMutex m; + m.lock(); + m.unlock(); + } + + // Lock a mutex that is already locked. This should block until it is unlocked. + { + std::atomic ready(false); + MyMutex m; + m.lock(); + std::atomic is_locked_from_main(true); + + std::thread t = support::make_test_thread([&] { + ready = true; + m.lock(); + assert(!is_locked_from_main); + m.unlock(); + }); + + while (!ready) + /* spin */; + + // We would rather signal this after we unlock, but that would create a race condition. + // We instead signal it before we unlock, which means that it's technically possible for + // the thread to take the lock while main is still holding it yet for the test to still pass. + is_locked_from_main = false; + m.unlock(); + + t.join(); + } + + // Make sure that at most one thread can acquire the mutex concurrently. + { + std::atomic counter(0); + MyMutex mutex; + + std::vector threads; + for (int i = 0; i != 10; ++i) { + threads.push_back(support::make_test_thread([&] { + mutex.lock(); + counter++; + assert(counter == 1); + counter--; + mutex.unlock(); + })); + } + + for (auto& t : threads) + t.join(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/try_lock.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/try_lock.pass.cpp new file mode 100644 index 00000000..344abe0f --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/mutex/try_lock.pass.cpp @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03 +// UNSUPPORTED: no-threads + +// + +// class mutex; + +// bool try_lock(); + +#include "../../custom_mutex.hpp" + +#include +#include + +#include "make_test_thread.h" + +int main(int, char**) { + // Try to lock a mutex that is not locked yet. This should succeed. + { + MyMutex m; + bool succeeded = m.try_lock(); + assert(succeeded); + m.unlock(); + } + + // Try to lock a mutex that is already locked. This should fail. + { + MyMutex m; + m.lock(); + + std::thread t = support::make_test_thread([&] { + for (int i = 0; i != 10; ++i) { + bool succeeded = m.try_lock(); + assert(!succeeded); + } + }); + t.join(); + + m.unlock(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/assign.compile.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/assign.compile.pass.cpp new file mode 100644 index 00000000..57fdc516 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/assign.compile.pass.cpp @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads + +// + +// class recursive_mutex; + +// recursive_mutex& operator=(const recursive_mutex&) = delete; + +#include "../../custom_recursive_mutex.hpp" + +#include + +static_assert(!std::is_copy_assignable::value, ""); diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/ctor.copy.compile.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/ctor.copy.compile.pass.cpp new file mode 100644 index 00000000..ef015514 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/ctor.copy.compile.pass.cpp @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads + +// + +// class recursive_mutex; + +// recursive_mutex(const recursive_mutex&) = delete; + +#include "../../custom_recursive_mutex.hpp" + +#include + +static_assert(!std::is_copy_constructible::value, ""); diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/ctor.default.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/ctor.default.pass.cpp new file mode 100644 index 00000000..3575228e --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/ctor.default.pass.cpp @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads + +// + +// class recursive_mutex; + +// recursive_mutex(); + +#include "../../custom_recursive_mutex.hpp" + +#include + +int main(int, char**) { + // The mutex is unlocked after default construction + { + MyRecursiveMutex m; + assert(m.try_lock()); + m.unlock(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/lock.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/lock.pass.cpp new file mode 100644 index 00000000..cc12193c --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/lock.pass.cpp @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03 +// UNSUPPORTED: no-threads + +// + +// class recursive_mutex; + +// void lock(); + +#include "../../custom_recursive_mutex.hpp" + +#include +#include +#include +#include + +#include "make_test_thread.h" + +bool is_lockable(MyRecursiveMutex& m) { + bool did_lock; + std::thread t = support::make_test_thread([&] { + did_lock = m.try_lock(); + if (did_lock) + m.unlock(); // undo side effects + }); + t.join(); + + return did_lock; +} + +int main(int, char**) { + // Lock a mutex that is not locked yet. This should succeed. + { + MyRecursiveMutex m; + m.lock(); + m.unlock(); + } + + // Lock a mutex that is already locked by this thread. This should succeed and the mutex should only + // be unlocked after a matching number of calls to unlock() on the same thread. + { + MyRecursiveMutex m; + int lock_count = 0; + for (int i = 0; i != 10; ++i) { + m.lock(); + ++lock_count; + } + while (lock_count != 0) { + assert(!is_lockable(m)); + m.unlock(); + --lock_count; + } + assert(is_lockable(m)); + } + + // Lock a mutex that is already locked by another thread. This should block until it is unlocked. + { + std::atomic ready(false); + MyRecursiveMutex m; + m.lock(); + std::atomic is_locked_from_main(true); + + std::thread t = support::make_test_thread([&] { + ready = true; + m.lock(); + assert(!is_locked_from_main); + m.unlock(); + }); + + while (!ready) + /* spin */; + + // We would rather signal this after we unlock, but that would create a race condition. + // We instead signal it before we unlock, which means that it's technically possible for + // the thread to take the lock while main is still holding it yet for the test to still pass. + is_locked_from_main = false; + m.unlock(); + + t.join(); + } + + // Make sure that at most one thread can acquire the mutex concurrently. + { + std::atomic counter(0); + MyRecursiveMutex mutex; + + std::vector threads; + for (int i = 0; i != 10; ++i) { + threads.push_back(support::make_test_thread([&] { + mutex.lock(); + counter++; + assert(counter == 1); + counter--; + mutex.unlock(); + })); + } + + for (auto& t : threads) + t.join(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/try_lock.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/try_lock.pass.cpp new file mode 100644 index 00000000..9966bef4 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/recursive_mutex/try_lock.pass.cpp @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03 +// UNSUPPORTED: no-threads + +// + +// class recursive_mutex; + +// bool try_lock(); + +#include "../../custom_recursive_mutex.hpp" + +#include +#include + +#include "make_test_thread.h" + +bool is_lockable(MyRecursiveMutex& m) { + bool did_lock; + std::thread t = support::make_test_thread([&] { + did_lock = m.try_lock(); + if (did_lock) + m.unlock(); // undo side effects + }); + t.join(); + + return did_lock; +} + +int main(int, char**) { + // Try to lock a mutex that is not locked yet. This should succeed. + { + MyRecursiveMutex m; + bool succeeded = m.try_lock(); + assert(succeeded); + m.unlock(); + } + + // Try to lock a mutex that is already locked by this thread. This should succeed and the mutex should only + // be unlocked after a matching number of calls to unlock() on the same thread. + { + MyRecursiveMutex m; + int lock_count = 0; + for (int i = 0; i != 10; ++i) { + assert(m.try_lock()); + ++lock_count; + } + while (lock_count != 0) { + assert(!is_lockable(m)); + m.unlock(); + --lock_count; + } + assert(is_lockable(m)); + } + + // Try to lock a mutex that is already locked by another thread. This should fail. + { + MyRecursiveMutex m; + m.lock(); + + std::thread t = support::make_test_thread([&] { + for (int i = 0; i != 10; ++i) { + bool succeeded = m.try_lock(); + assert(!succeeded); + } + }); + t.join(); + + m.unlock(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/run.py b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/run.py new file mode 100644 index 00000000..4adf8da7 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/run.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Compile and run the libc++ tests adapted to the custom mutex classes.""" + +import argparse +import os +from pathlib import Path +import shlex +import subprocess +import tempfile + + +ROOT = Path(__file__).resolve().parent + + +def run_step(command, timeout): + try: + result = subprocess.run(command, capture_output=True, text=True, timeout=timeout) + except (OSError, subprocess.TimeoutExpired) as error: + print(f" {shlex.join(command)}\n {error}", flush=True) + return False + if result.returncode: + print(f" {shlex.join(command)}\n{result.stdout}{result.stderr}", flush=True) + return False + return True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default=os.environ.get("CXX", "clang++"), + help="compiler executable (default: CXX or clang++)") + parser.add_argument("--stdlib", choices=("libstdc++", "libc++", "default"), + default="libstdc++", + help="standard library; use default with GCC") + parser.add_argument("--filter", default="", metavar="TEXT", + help="run only tests whose relative path contains TEXT") + args = parser.parse_args() + + tests = sorted(path for path in ROOT.glob("*/*.cpp") + if args.filter in path.relative_to(ROOT).as_posix()) + if not tests: + parser.error("no tests match the filter") + + flags = ["-std=c++17", "-pthread", "-g", "-O1", "-UNDEBUG", + "-I" + str(ROOT / "support")] + if args.stdlib != "default": + flags.append("-stdlib=" + args.stdlib) + + passed = 0 + with tempfile.TemporaryDirectory(prefix="brick-mutex-tests-") as build_dir: + for index, source in enumerate(tests): + name = source.relative_to(ROOT).as_posix() + command = [args.cxx, *flags, str(source)] + compile_only = source.name.endswith(".compile.pass.cpp") + executable = Path(build_dir) / str(index) + command += ["-fsyntax-only"] if compile_only else ["-o", str(executable)] + if not run_step(command, timeout=60): + print(f"FAIL (compile) {name}", flush=True) + continue + if not compile_only and not run_step([str(executable)], timeout=20): + print(f"FAIL (run) {name}", flush=True) + continue + passed += 1 + print(f"PASS {name}", flush=True) + + print(f"{passed}/{len(tests)} passed ({args.cxx}, {args.stdlib})", flush=True) + return 0 if passed == len(tests) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/assign.compile.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/assign.compile.pass.cpp new file mode 100644 index 00000000..17d2bf80 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/assign.compile.pass.cpp @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads +// UNSUPPORTED: c++03, c++11, c++14 + +// + +// class shared_mutex; + +// shared_mutex& operator=(const shared_mutex&) = delete; + +#include "../../../shared_mutex/custom_shared_mutex.hpp" + +#include + +static_assert(!std::is_copy_assignable::value, ""); diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/ctor.copy.compile.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/ctor.copy.compile.pass.cpp new file mode 100644 index 00000000..e66ae61f --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/ctor.copy.compile.pass.cpp @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads +// UNSUPPORTED: c++03, c++11, c++14 + +// + +// class shared_mutex; + +// shared_mutex(const shared_mutex&) = delete; + +#include "../../../shared_mutex/custom_shared_mutex.hpp" + +#include + +static_assert(!std::is_copy_constructible::value, ""); diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/ctor.default.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/ctor.default.pass.cpp new file mode 100644 index 00000000..5d612763 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/ctor.default.pass.cpp @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads +// UNSUPPORTED: c++03, c++11, c++14 + +// + +// class shared_mutex; + +// shared_mutex(); + +#include "../../../shared_mutex/custom_shared_mutex.hpp" + +int main(int, char**) { + MySharedMutex m; + (void)m; + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/lock.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/lock.pass.cpp new file mode 100644 index 00000000..1c2b24de --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/lock.pass.cpp @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads +// UNSUPPORTED: c++03, c++11, c++14 + +// + +// class shared_mutex; + +// void lock(); + +#include "../../../shared_mutex/custom_shared_mutex.hpp" + +#include +#include +#include +#include + +#include "make_test_thread.h" + +int main(int, char**) { + // Exclusive-lock a mutex that is not locked yet. This should succeed. + { + MySharedMutex m; + m.lock(); + m.unlock(); + } + + // Exclusive-lock a mutex that is already locked exclusively. This should block until it is unlocked. + { + std::atomic ready(false); + MySharedMutex m; + m.lock(); + std::atomic is_locked_from_main(true); + + std::thread t = support::make_test_thread([&] { + ready = true; + m.lock(); + assert(!is_locked_from_main); + m.unlock(); + }); + + while (!ready) + /* spin */; + + // We would rather signal this after we unlock, but that would create a race condition. + // We instead signal it before we unlock, which means that it's technically possible for the thread + // to take the lock while we're still holding it and for the test to still pass. + is_locked_from_main = false; + m.unlock(); + + t.join(); + } + + // Exclusive-lock a mutex that is already share-locked. This should block until it is unlocked. + { + std::atomic ready(false); + MySharedMutex m; + m.lock_shared(); + std::atomic is_locked_from_main(true); + + std::thread t = support::make_test_thread([&] { + ready = true; + m.lock(); + assert(!is_locked_from_main); + m.unlock(); + }); + + while (!ready) + /* spin */; + + // We would rather signal this after we unlock, but that would create a race condition. + // We instead signal it before we unlock, which means that it's technically possible for + // the thread to take the lock while we're still holding it and for the test to still pass. + is_locked_from_main = false; + m.unlock_shared(); + + t.join(); + } + + // Make sure that at most one thread can acquire the mutex concurrently. + { + std::atomic counter = 0; + MySharedMutex mutex; + + std::vector threads; + for (int i = 0; i != 10; ++i) { + threads.push_back(support::make_test_thread([&] { + mutex.lock(); + counter++; + assert(counter == 1); + counter--; + mutex.unlock(); + })); + } + + for (auto& t : threads) + t.join(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/lock_shared.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/lock_shared.pass.cpp new file mode 100644 index 00000000..0aca5a13 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/lock_shared.pass.cpp @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads +// UNSUPPORTED: c++03, c++11, c++14 + +// + +// class shared_mutex; + +// void lock_shared(); + +#include "../../../shared_mutex/custom_shared_mutex.hpp" + +#include +#include +#include +#include +#include +#include + +#include "make_test_thread.h" + +int main(int, char**) { + // Lock-shared a mutex that is not locked yet. This should succeed. + { + MySharedMutex m; + std::vector threads; + for (int i = 0; i != 5; ++i) { + threads.push_back(support::make_test_thread([&] { + m.lock_shared(); + m.unlock_shared(); + })); + } + + for (auto& t : threads) + t.join(); + } + + // Lock-shared a mutex that is already exclusively locked. This should block until it is unlocked. + { + std::atomic ready(0); + MySharedMutex m; + m.lock(); + std::atomic is_locked_from_main(true); + + std::vector threads; + for (int i = 0; i != 5; ++i) { + threads.push_back(support::make_test_thread([&] { + ++ready; + while (ready < 5) + /* wait until all threads have been created */; + + m.lock_shared(); + assert(!is_locked_from_main); + m.unlock_shared(); + })); + } + + while (ready < 5) + /* wait until all threads have been created */; + + // We would rather signal this after we unlock, but that would create a race condition. + // We instead signal it before we unlock, which means that it's technically possible for + // the thread to take the lock while we're still holding it and for the test to still pass. + is_locked_from_main = false; + m.unlock(); + + for (auto& t : threads) + t.join(); + } + + // Lock-shared a mutex that is already lock-shared. This should succeed. + { + std::atomic ready(0); + MySharedMutex m; + m.lock_shared(); + + std::vector threads; + for (int i = 0; i != 5; ++i) { + threads.push_back(support::make_test_thread([&] { + ++ready; + while (ready < 5) + /* wait until all threads have been created */; + + m.lock_shared(); + m.unlock_shared(); + })); + } + + while (ready < 5) + /* wait until all threads have been created */; + + m.unlock_shared(); + + for (auto& t : threads) + t.join(); + } + + // Create several threads that all acquire-shared the same mutex and make sure that each + // thread successfully acquires-shared the mutex. + // + // We record how many other threads were holding the mutex when it was acquired, which allows + // us to know whether the test was somewhat effective at causing multiple threads to lock at + // the same time. + { + MySharedMutex mutex; + std::vector threads; + constexpr int n_threads = 5; + std::atomic holders = 0; + int concurrent_holders[n_threads] = {}; + std::atomic ready = false; + + for (int i = 0; i != n_threads; ++i) { + threads.push_back(support::make_test_thread([&, i] { + while (!ready) { + // spin + } + + mutex.lock_shared(); + ++holders; + concurrent_holders[i] = holders; + + mutex.unlock_shared(); + --holders; + })); + } + + ready = true; // let the threads actually start shared-acquiring the mutex + for (auto& t : threads) + t.join(); + + // We can't guarantee that we'll ever have more than 1 concurrent holder so that's what + // we assert, however in principle we should often trigger more than 1 concurrent holder. + int max_concurrent_holders = *std::max_element(std::begin(concurrent_holders), std::end(concurrent_holders)); + assert(max_concurrent_holders >= 1); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/try_lock.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/try_lock.pass.cpp new file mode 100644 index 00000000..ec4f4f82 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/try_lock.pass.cpp @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads +// UNSUPPORTED: c++03, c++11, c++14 + +// + +// class shared_mutex; + +// bool try_lock(); + +#include "../../../shared_mutex/custom_shared_mutex.hpp" + +#include +#include + +#include "make_test_thread.h" + +int main(int, char**) { + // Try to exclusive-lock a mutex that is not locked yet. This should succeed. + { + MySharedMutex m; + bool succeeded = m.try_lock(); + assert(succeeded); + m.unlock(); + } + + // Try to exclusive-lock a mutex that is already locked exclusively. This should fail. + { + MySharedMutex m; + m.lock(); + + std::thread t = support::make_test_thread([&] { + bool succeeded = m.try_lock(); + assert(!succeeded); + }); + t.join(); + + m.unlock(); + } + + // Try to exclusive-lock a mutex that is already share-locked. This should fail. + { + MySharedMutex m; + m.lock_shared(); + + std::thread t = support::make_test_thread([&] { + bool succeeded = m.try_lock(); + assert(!succeeded); + }); + t.join(); + + m.unlock_shared(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/try_lock_shared.pass.cpp b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/try_lock_shared.pass.cpp new file mode 100644 index 00000000..9ba1cfa2 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/shared_mutex/try_lock_shared.pass.cpp @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Modified by SkyLabs AI: adapted the mutex type and includes for the +// custom mutex conformance tests. See README.txt and UPSTREAM.json. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: no-threads +// UNSUPPORTED: c++03, c++11, c++14 + +// + +// class shared_mutex; + +// bool try_lock_shared(); + +#include "../../../shared_mutex/custom_shared_mutex.hpp" + +#include +#include +#include + +#include "make_test_thread.h" + +int main(int, char**) { + // Try to lock-shared a mutex that is not locked yet. This should succeed. + { + MySharedMutex m; + std::vector threads; + for (int i = 0; i != 5; ++i) { + threads.push_back(support::make_test_thread([&] { + bool succeeded = m.try_lock_shared(); + assert(succeeded); + m.unlock_shared(); + })); + } + + for (auto& t : threads) + t.join(); + } + + // Try to lock-shared a mutex that is already exclusively locked. This should fail. + { + MySharedMutex m; + m.lock(); + + std::vector threads; + for (int i = 0; i != 5; ++i) { + threads.push_back(support::make_test_thread([&] { + bool succeeded = m.try_lock_shared(); + assert(!succeeded); + })); + } + + for (auto& t : threads) + t.join(); + + m.unlock(); + } + + // Try to lock-shared a mutex that is already lock-shared. This should succeed. + { + MySharedMutex m; + m.lock_shared(); + std::vector threads; + for (int i = 0; i != 5; ++i) { + threads.push_back(support::make_test_thread([&] { + bool succeeded = m.try_lock_shared(); + assert(succeeded); + m.unlock_shared(); + })); + } + m.unlock_shared(); + + for (auto& t : threads) + t.join(); + } + + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/support/make_test_thread.h b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/support/make_test_thread.h new file mode 100644 index 00000000..7a4e04b4 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/support/make_test_thread.h @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef TEST_SUPPORT_MAKE_TEST_THREAD_H +#define TEST_SUPPORT_MAKE_TEST_THREAD_H + +#include +#include + +#include "test_macros.h" + +namespace support { + +// These functions are used to mock the creation of threads within the test suite. +// +// This provides a vendor-friendly way of making the test suite work even on platforms +// where the standard thread constructors don't work (e.g. embedded environments where +// creating a thread requires additional information like setting attributes). +// +// Vendors can keep a downstream diff in this file to create threads however they +// need on their platform, and the majority of the test suite will work out of the +// box. Of course, tests that exercise the standard thread constructors won't work, +// but any other test that only creates threads as a side effect of testing should +// work if they use the utilities in this file. + +template +std::thread make_test_thread(F&& f, Args&&... args) { + return std::thread(std::forward(f), std::forward(args)...); +} + +#if TEST_STD_VER >= 20 +template +std::jthread make_test_jthread(F&& f, Args&&... args) { + return std::jthread(std::forward(f), std::forward(args)...); +} +#endif + +} // namespace support + +#endif // TEST_SUPPORT_MAKE_TEST_THREAD_H diff --git a/rocq-brick-libstdcpp/test/mutex/libcpp_tests/support/test_macros.h b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/support/test_macros.h new file mode 100644 index 00000000..8d88d6fa --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/libcpp_tests/support/test_macros.h @@ -0,0 +1,550 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef SUPPORT_TEST_MACROS_HPP +#define SUPPORT_TEST_MACROS_HPP + +#ifdef __has_include +# if __has_include() +# include +# else +# include +# endif +#else +# include +#endif + +#define TEST_STRINGIZE_IMPL(...) #__VA_ARGS__ +#define TEST_STRINGIZE(...) TEST_STRINGIZE_IMPL(__VA_ARGS__) + +#define TEST_CONCAT1(X, Y) X##Y +#define TEST_CONCAT(X, Y) TEST_CONCAT1(X, Y) + +#ifdef __has_feature +#define TEST_HAS_FEATURE(X) __has_feature(X) +#else +#define TEST_HAS_FEATURE(X) 0 +#endif + +#ifndef __has_include +#define __has_include(...) 0 +#endif + +#ifdef __has_extension +#define TEST_HAS_EXTENSION(X) __has_extension(X) +#else +#define TEST_HAS_EXTENSION(X) 0 +#endif + +#ifdef __has_warning +#define TEST_HAS_WARNING(X) __has_warning(X) +#else +#define TEST_HAS_WARNING(X) 0 +#endif + +#ifdef __has_builtin +#define TEST_HAS_BUILTIN(X) __has_builtin(X) +#else +#define TEST_HAS_BUILTIN(X) 0 +#endif +#ifdef __is_identifier +// '__is_identifier' returns '0' if '__x' is a reserved identifier provided by +// the compiler and '1' otherwise. +#define TEST_HAS_BUILTIN_IDENTIFIER(X) !__is_identifier(X) +#else +#define TEST_HAS_BUILTIN_IDENTIFIER(X) 0 +#endif + +#if defined(__EDG__) +# define TEST_COMPILER_EDG +#elif defined(__clang__) +# define TEST_COMPILER_CLANG +# if defined(__apple_build_version__) +# define TEST_COMPILER_APPLE_CLANG +# endif +#elif defined(_MSC_VER) +# define TEST_COMPILER_MSVC +#elif defined(__GNUC__) +# define TEST_COMPILER_GCC +#endif + +#if defined(__apple_build_version__) +// Given AppleClang XX.Y.Z, TEST_APPLE_CLANG_VER is XXYZ (e.g. AppleClang 14.0.3 => 1403) +#define TEST_APPLE_CLANG_VER (__apple_build_version__ / 10000) +#elif defined(__clang_major__) +#define TEST_CLANG_VER (__clang_major__ * 100) + __clang_minor__ +#elif defined(__GNUC__) +// Given GCC XX.YY.ZZ, TEST_GCC_VER is XXYYZZ +#define TEST_GCC_VER ((__GNUC__ * 10000) + (__GNUC_MINOR__ * 100) + __GNUC_PATCHLEVEL__) +#endif + +/* Make a nice name for the standard version */ +#ifndef TEST_STD_VER +#if __cplusplus <= 199711L +# define TEST_STD_VER 3 +#elif __cplusplus <= 201103L +# define TEST_STD_VER 11 +#elif __cplusplus <= 201402L +# define TEST_STD_VER 14 +#elif __cplusplus <= 201703L +# define TEST_STD_VER 17 +#elif __cplusplus <= 202002L +# define TEST_STD_VER 20 +#elif __cplusplus <= 202302L +# define TEST_STD_VER 23 +#else +# define TEST_STD_VER 99 // greater than current standard +// This is deliberately different than _LIBCPP_STD_VER to discourage matching them up. +#endif +#endif + +// Attempt to deduce the GLIBC version +#if (defined(__has_include) && __has_include()) || \ + defined(__linux__) +#include +#if defined(__GLIBC_PREREQ) +#define TEST_HAS_GLIBC +#define TEST_GLIBC_PREREQ(major, minor) __GLIBC_PREREQ(major, minor) +#endif +#endif + +#if TEST_STD_VER >= 11 +# define TEST_ALIGNOF(...) alignof(__VA_ARGS__) +# define TEST_ALIGNAS(...) alignas(__VA_ARGS__) +# define TEST_CONSTEXPR constexpr +# define TEST_NOEXCEPT noexcept +# define TEST_NOEXCEPT_FALSE noexcept(false) +# define TEST_NOEXCEPT_COND(...) noexcept(__VA_ARGS__) +#else +# if defined(TEST_COMPILER_CLANG) +# define TEST_ALIGNOF(...) _Alignof(__VA_ARGS__) +# else +# define TEST_ALIGNOF(...) __alignof(__VA_ARGS__) +# endif +# define TEST_ALIGNAS(...) __attribute__((__aligned__(__VA_ARGS__))) +# define TEST_CONSTEXPR +# define TEST_NOEXCEPT throw() +# define TEST_NOEXCEPT_FALSE +# define TEST_NOEXCEPT_COND(...) +#endif + +#if TEST_STD_VER >= 11 +# define TEST_THROW_SPEC(...) +#else +# define TEST_THROW_SPEC(...) throw(__VA_ARGS__) +#endif + +#if defined(__cpp_lib_is_constant_evaluated) && __cpp_lib_is_constant_evaluated >= 201811L +# define TEST_IS_CONSTANT_EVALUATED std::is_constant_evaluated() +#elif TEST_HAS_BUILTIN(__builtin_is_constant_evaluated) +# define TEST_IS_CONSTANT_EVALUATED __builtin_is_constant_evaluated() +#else +# define TEST_IS_CONSTANT_EVALUATED false +#endif + +#if TEST_STD_VER >= 20 +# define TEST_STD_AT_LEAST_20_OR_RUNTIME_EVALUATED true +#else +# define TEST_STD_AT_LEAST_20_OR_RUNTIME_EVALUATED (!TEST_IS_CONSTANT_EVALUATED) +#endif + +#if TEST_STD_VER >= 23 +# define TEST_STD_AT_LEAST_23_OR_RUNTIME_EVALUATED true +#else +# define TEST_STD_AT_LEAST_23_OR_RUNTIME_EVALUATED (!TEST_IS_CONSTANT_EVALUATED) +#endif + +#if TEST_STD_VER >= 26 +# define TEST_STD_AT_LEAST_26_OR_RUNTIME_EVALUATED true +#else +# define TEST_STD_AT_LEAST_26_OR_RUNTIME_EVALUATED (!TEST_IS_CONSTANT_EVALUATED) +#endif + +#if TEST_STD_VER >= 14 +# define TEST_CONSTEXPR_CXX14 constexpr +#else +# define TEST_CONSTEXPR_CXX14 +#endif + +#if TEST_STD_VER >= 17 +# define TEST_CONSTEXPR_CXX17 constexpr +#else +# define TEST_CONSTEXPR_CXX17 +#endif + +#if TEST_STD_VER >= 20 +# define TEST_CONSTEXPR_CXX20 constexpr +#else +# define TEST_CONSTEXPR_CXX20 +#endif + +#if TEST_STD_VER >= 23 +# define TEST_CONSTEXPR_CXX23 constexpr +#else +# define TEST_CONSTEXPR_CXX23 +#endif + +#if TEST_STD_VER >= 26 +# define TEST_CONSTEXPR_CXX26 constexpr +#else +# define TEST_CONSTEXPR_CXX26 +#endif + +#define TEST_ALIGNAS_TYPE(...) TEST_ALIGNAS(TEST_ALIGNOF(__VA_ARGS__)) + +#if !TEST_HAS_FEATURE(cxx_rtti) && !defined(__cpp_rtti) \ + && !defined(__GXX_RTTI) +#define TEST_HAS_NO_RTTI +#endif + +#if !defined(TEST_HAS_NO_RTTI) +# define RTTI_ASSERT(X) assert(X) +#else +# define RTTI_ASSERT(X) +#endif + +#if !TEST_HAS_FEATURE(cxx_exceptions) && !defined(__cpp_exceptions) \ + && !defined(__EXCEPTIONS) +#define TEST_HAS_NO_EXCEPTIONS +#endif + +#if TEST_HAS_FEATURE(address_sanitizer) || TEST_HAS_FEATURE(hwaddress_sanitizer) || \ + TEST_HAS_FEATURE(memory_sanitizer) || TEST_HAS_FEATURE(thread_sanitizer) +#define TEST_HAS_SANITIZERS +#define TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT +#endif + +#ifdef _LIBCPP_USE_FROZEN_CXX03_HEADERS +# ifdef _LIBCPP_HAS_NO_ALIGNED_ALLOCATION +# define TEST_HAS_NO_ALIGNED_ALLOCATION +# endif +#elif defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_ALIGNED_ALLOCATION +# define TEST_HAS_NO_ALIGNED_ALLOCATION +#elif TEST_STD_VER < 17 && (!defined(__cpp_aligned_new) || __cpp_aligned_new < 201606L) +# define TEST_HAS_NO_ALIGNED_ALLOCATION +#endif + +#if TEST_STD_VER > 17 +# define TEST_CONSTINIT constinit +#elif __has_cpp_attribute(clang::require_constant_initialization) +# define TEST_CONSTINIT [[clang::require_constant_initialization]] +#else +# define TEST_CONSTINIT +#endif + +#if TEST_STD_VER < 11 +#define ASSERT_NOEXCEPT(...) +#define ASSERT_NOT_NOEXCEPT(...) +#else +#define ASSERT_NOEXCEPT(...) \ + static_assert(noexcept(__VA_ARGS__), "Operation must be noexcept") + +#define ASSERT_NOT_NOEXCEPT(...) \ + static_assert(!noexcept(__VA_ARGS__), "Operation must NOT be noexcept") +#endif + +/* Macros for testing libc++ specific behavior and extensions */ +#if defined(_LIBCPP_VERSION) +#define LIBCPP_ASSERT(...) assert(__VA_ARGS__) +#define LIBCPP_STATIC_ASSERT(...) static_assert(__VA_ARGS__) +#define LIBCPP_ASSERT_NOEXCEPT(...) ASSERT_NOEXCEPT(__VA_ARGS__) +#define LIBCPP_ASSERT_NOT_NOEXCEPT(...) ASSERT_NOT_NOEXCEPT(__VA_ARGS__) +#define LIBCPP_ONLY(...) __VA_ARGS__ +#else +#define LIBCPP_ASSERT(...) static_assert(true, "") +#define LIBCPP_STATIC_ASSERT(...) static_assert(true, "") +#define LIBCPP_ASSERT_NOEXCEPT(...) static_assert(true, "") +#define LIBCPP_ASSERT_NOT_NOEXCEPT(...) static_assert(true, "") +#define LIBCPP_ONLY(...) static_assert(true, "") +#endif + +#ifdef _LIBCPP_USE_FROZEN_CXX03_HEADERS +# define LIBCPP_NON_FROZEN_ASSERT(...) static_assert(true, "") +#else +# define LIBCPP_NON_FROZEN_ASSERT(...) LIBCPP_ASSERT(__VA_ARGS__) +#endif + +#if __has_cpp_attribute(nodiscard) +# define TEST_NODISCARD [[nodiscard]] +#else +# define TEST_NODISCARD +#endif + +#define TEST_IGNORE_NODISCARD (void) + +#ifdef _LIBCPP_USE_FROZEN_CXX03_HEADERS +// from-chars is a C++17 feature, so it's never available anyways +#elif !defined(_LIBCPP_VERSION) || _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT +# define TEST_HAS_FROM_CHARS_FLOATING_POINT +#endif + +namespace test_macros_detail { +template +struct is_same { enum { value = 0};} ; +template +struct is_same { enum {value = 1}; }; +} // namespace test_macros_detail + +#define ASSERT_SAME_TYPE(...) \ + static_assert((test_macros_detail::is_same<__VA_ARGS__>::value), \ + "Types differ unexpectedly") + +#ifndef TEST_HAS_NO_EXCEPTIONS +#define TEST_THROW(...) throw __VA_ARGS__ +#else +#if defined(__GNUC__) +#define TEST_THROW(...) __builtin_abort() +#else +#include +#define TEST_THROW(...) ::abort() +#endif +#endif + +#if defined(__GNUC__) || defined(__clang__) +// This function can be used to hide some objects from compiler optimizations. +// +// For example, this is useful to hide the result of a call to `new` and ensure +// that the compiler doesn't elide the call to new/delete. Otherwise, elliding +// calls to new/delete is allowed by the Standard and compilers actually do it +// when optimizations are enabled. +template +inline Tp const& DoNotOptimize(Tp const& value) { + // The `m` constraint is invalid in the AMDGPU backend. +# if defined(__AMDGPU__) || defined(__NVPTX__) + asm volatile("" : : "r"(value) : "memory"); +# else + asm volatile("" : : "r,m"(value) : "memory"); +# endif + return value; +} + +template +inline Tp& DoNotOptimize(Tp& value) { + // The `m` and `r` output constraint is invalid in the AMDGPU backend as well + // as i8 / i1 arguments, so we just capture the pointer instead. +# if defined(__AMDGPU__) + Tp* tmp = &value; + asm volatile("" : "+v"(tmp) : : "memory"); +# elif defined(__clang__) + asm volatile("" : "+r,m"(value) : : "memory"); +# else + asm volatile("" : "+m,r"(value) : : "memory"); +# endif + return value; +} +#else +#include +template +inline Tp const& DoNotOptimize(Tp const& value) { + const volatile void* volatile unused = __builtin_addressof(value); + static_cast(unused); + _ReadWriteBarrier(); + return value; +} +#endif + +#if defined(__GNUC__) +#define TEST_ALWAYS_INLINE __attribute__((always_inline)) +#define TEST_NOINLINE __attribute__((noinline)) +#elif defined(_MSC_VER) +#define TEST_ALWAYS_INLINE __forceinline +#define TEST_NOINLINE __declspec(noinline) +#else +#define TEST_ALWAYS_INLINE +#define TEST_NOINLINE +#endif + +#ifdef _WIN32 +#define TEST_NOT_WIN32(...) ((void)0) +#else +#define TEST_NOT_WIN32(...) __VA_ARGS__ +#endif + +#if defined(TEST_WINDOWS_DLL) ||defined(__MVS__) || defined(_AIX) +// Macros for waiving cases when we can't count allocations done within +// the library implementation. +// +// On Windows, when libc++ is built as a DLL, references to operator new/delete +// within the DLL are bound at link time to the operator new/delete within +// the library; replacing them in the user executable doesn't override the +// calls within the library. +// +// The same goes on IBM zOS. +// The same goes on AIX. +#define ASSERT_WITH_LIBRARY_INTERNAL_ALLOCATIONS(...) ((void)(__VA_ARGS__)) +#define TEST_SUPPORTS_LIBRARY_INTERNAL_ALLOCATIONS 0 +#else +#define ASSERT_WITH_LIBRARY_INTERNAL_ALLOCATIONS(...) assert(__VA_ARGS__) +#define TEST_SUPPORTS_LIBRARY_INTERNAL_ALLOCATIONS 1 +#endif + +#if (defined(TEST_WINDOWS_DLL) && !defined(_MSC_VER)) || \ + defined(__MVS__) +// Normally, a replaced e.g. 'operator new' ends up used if the user code +// does a call to e.g. 'operator new[]'; it's enough to replace the base +// versions and have it override all of them. +// +// When the fallback operators are located within the libc++ library and we +// can't override the calls within it (see above), this fallback mechanism +// doesn't work either. +// +// On Windows, when using the MSVC vcruntime, the operator new/delete fallbacks +// are linked separately from the libc++ library, linked statically into +// the end user executable, and these fallbacks work even in DLL configurations. +// In MinGW configurations when built as a DLL, and on zOS, these fallbacks +// don't work though. +#define ASSERT_WITH_OPERATOR_NEW_FALLBACKS(...) ((void)(__VA_ARGS__)) +#else +#define ASSERT_WITH_OPERATOR_NEW_FALLBACKS(...) assert(__VA_ARGS__) +#endif + +#ifdef _WIN32 +#define TEST_WIN_NO_FILESYSTEM_PERMS_NONE +#endif + +// Support for carving out parts of the test suite, like removing wide characters, etc. +#if defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_WIDE_CHARACTERS +# define TEST_HAS_NO_WIDE_CHARACTERS +#endif + +#if defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_UNICODE +# define TEST_HAS_NO_UNICODE +#elif defined(_MSVC_EXECUTION_CHARACTER_SET) && _MSVC_EXECUTION_CHARACTER_SET != 65001 +# define TEST_HAS_NO_UNICODE +#endif + +#ifdef _LIBCPP_USE_FROZEN_CXX03_HEADERS +# ifdef _LIBCPP_HAS_OPEN_WITH_WCHAR +# define TEST_HAS_OPEN_WITH_WCHAR +# endif +#elif defined(_LIBCPP_VERSION) && _LIBCPP_HAS_OPEN_WITH_WCHAR +# define TEST_HAS_OPEN_WITH_WCHAR +#endif + +#ifdef _LIBCPP_USE_FROZEN_CXX03_HEADERS +# ifdef _LIBCPP_HAS_NO_INT128 +# define TEST_HAS_NO_INT128 +# endif +#elif (defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_INT128) || defined(_MSVC_STL_VERSION) +# define TEST_HAS_NO_INT128 +#endif + +#if defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_LOCALIZATION +# define TEST_HAS_NO_LOCALIZATION +#endif + +#if TEST_STD_VER <= 17 || !defined(__cpp_char8_t) +# define TEST_HAS_NO_CHAR8_T +#endif + +#if defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_THREADS +# define TEST_HAS_NO_THREADS +#endif + +#if defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_FILESYSTEM +# define TEST_HAS_NO_FILESYSTEM +#endif + +#ifdef _LIBCPP_USE_FROZEN_CXX03_HEADERS +# ifdef _LIBCPP_HAS_NO_C8RTOMB_MBRTOC8 +# define TEST_HAS_NO_C8RTOMB_MBRTOC8 +# endif +#elif defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_C8RTOMB_MBRTOC8 +# define TEST_HAS_NO_C8RTOMB_MBRTOC8 +#endif + +#if defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_RANDOM_DEVICE +# define TEST_HAS_NO_RANDOM_DEVICE +#endif + +#ifdef _LIBCPP_USE_FROZEN_CXX03_HEADERS +// This is a C++20 feature, so it's never available anyways +# define TEST_HAS_NO_EXPERIMENTAL_TZDB +#elif defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_EXPERIMENTAL_TZDB +# define TEST_HAS_NO_EXPERIMENTAL_TZDB +#endif + +#if defined(_LIBCPP_VERSION) && !_LIBCPP_HAS_TIME_ZONE_DATABASE +# define TEST_HAS_NO_TIME_ZONE_DATABASE +#endif + +#if defined(TEST_COMPILER_CLANG) +# define TEST_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") +# define TEST_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") +# define TEST_CLANG_DIAGNOSTIC_IGNORED(str) _Pragma(TEST_STRINGIZE(clang diagnostic ignored str)) +# define TEST_GCC_DIAGNOSTIC_IGNORED(str) +# define TEST_MSVC_DIAGNOSTIC_IGNORED(num) +#elif defined(TEST_COMPILER_GCC) +# define TEST_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") +# define TEST_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") +# define TEST_CLANG_DIAGNOSTIC_IGNORED(str) +# define TEST_GCC_DIAGNOSTIC_IGNORED(str) _Pragma(TEST_STRINGIZE(GCC diagnostic ignored str)) +# define TEST_MSVC_DIAGNOSTIC_IGNORED(num) +#elif defined(TEST_COMPILER_MSVC) +# define TEST_DIAGNOSTIC_PUSH _Pragma("warning(push)") +# define TEST_DIAGNOSTIC_POP _Pragma("warning(pop)") +# define TEST_CLANG_DIAGNOSTIC_IGNORED(str) +# define TEST_GCC_DIAGNOSTIC_IGNORED(str) +# define TEST_MSVC_DIAGNOSTIC_IGNORED(num) _Pragma(TEST_STRINGIZE(warning(disable: num))) +#else +# define TEST_DIAGNOSTIC_PUSH +# define TEST_DIAGNOSTIC_POP +# define TEST_CLANG_DIAGNOSTIC_IGNORED(str) +# define TEST_GCC_DIAGNOSTIC_IGNORED(str) +# define TEST_MSVC_DIAGNOSTIC_IGNORED(num) +#endif + +#if __has_cpp_attribute(msvc::no_unique_address) +#define TEST_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]] +#elif __has_cpp_attribute(no_unique_address) +#define TEST_NO_UNIQUE_ADDRESS [[no_unique_address]] +#else +#define TEST_NO_UNIQUE_ADDRESS +#endif + +#ifdef _LIBCPP_SHORT_WCHAR +# define TEST_SHORT_WCHAR +#endif + +#ifdef _LIBCPP_ABI_MICROSOFT +# define TEST_ABI_MICROSOFT +#endif + +// This is a temporary workaround for user-defined `operator new` definitions +// not being picked up on Apple platforms in some circumstances. This is under +// investigation and should be short-lived. +#ifdef __APPLE__ +# define TEST_WORKAROUND_BUG_109234844_WEAK __attribute__((weak)) +#else +# define TEST_WORKAROUND_BUG_109234844_WEAK /* nothing */ +#endif + +#ifdef _AIX +# define TEST_IF_AIX(arg_true, arg_false) arg_true +#else +# define TEST_IF_AIX(arg_true, arg_false) arg_false +#endif + +// Placement `operator new`/`operator new[]` are not yet constexpr in C++26 +// when using MS ABI, because they are from . +#if defined(__cpp_lib_constexpr_new) && __cpp_lib_constexpr_new >= 202406L +# define TEST_CONSTEXPR_OPERATOR_NEW constexpr +#else +# define TEST_CONSTEXPR_OPERATOR_NEW +#endif + +#if defined(_MSC_VER) || __SIZEOF_LONG_DOUBLE__ == __SIZEOF_DOUBLE__ +# define TEST_LONG_DOUBLE_IS_DOUBLE +#endif + +#if defined(__LDBL_MANT_DIG__) && __LDBL_MANT_DIG__ == 64 +# define TEST_LONG_DOUBLE_IS_80_BIT +#endif + +#endif // SUPPORT_TEST_MACROS_HPP diff --git a/rocq-brick-libstdcpp/test/mutex/recursive_mutex_odr.py b/rocq-brick-libstdcpp/test/mutex/recursive_mutex_odr.py new file mode 100644 index 00000000..d705948f --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/recursive_mutex_odr.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Check that the recursive mutex header links across translation units.""" + +from pathlib import Path +import subprocess +import sys +import tempfile + + +compiler, header = sys.argv[1:] +header = Path(header).resolve() +with tempfile.TemporaryDirectory(prefix="brick-recursive-mutex-odr-") as directory: + root = Path(directory) + # Include the header in two translation units and check that they share the + # sentinel variable, rather than giving it internal linkage in each unit. + (root / "first.cpp").write_text(''' +#include "custom_recursive_mutex.hpp" +#include "custom_recursive_mutex.hpp" +size_t* sentinel_address() { return &id_hash; } +void exercise_mutex() { + MyRecursiveMutex mutex; + mutex.lock(); + mutex.lock(); + mutex.unlock(); + mutex.unlock(); +} +''') + (root / "second.cpp").write_text(''' +#include "custom_recursive_mutex.hpp" +size_t* sentinel_address(); +void exercise_mutex(); +int main() { + if (sentinel_address() != &id_hash) + return 1; + exercise_mutex(); + MyRecursiveMutex mutex; + if (!mutex.try_lock()) + return 2; + mutex.unlock(); +} +''') + executable = root / "odr-test" + subprocess.run([compiler, "-std=c++17", "-stdlib=libstdc++", "-pthread", + "-I", str(header.parent), str(root / "first.cpp"), + str(root / "second.cpp"), "-o", str(executable)], + check=True, timeout=60) + subprocess.run([str(executable)], check=True, timeout=20) diff --git a/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.cpp b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.cpp new file mode 100644 index 00000000..cbc25622 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.cpp @@ -0,0 +1,49 @@ +// Copyright (c) 2026 SkyLabs AI, Inc. +// Distributed under the BSD 3-Clause license; see LICENSE. + +#include +#include +#include + +// A Lockable that distinguishes lock() from successful try_lock(). Both +// operations acquire the same underlying mutex, but only lock() increments +// lock_calls. Atomic counters preserve the Lockable's thread safety. +struct CountingMutex { + std::mutex native; + std::atomic lock_calls{0}; + std::atomic try_lock_calls{0}; + + void lock() { + native.lock(); + ++lock_calls; + } + + bool try_lock() { + ++try_lock_calls; + return native.try_lock(); + } + + void unlock() { native.unlock(); } +}; + +int main() { + CountingMutex a, b; + { + std::scoped_lock guard(a, b); + const unsigned a_locks = a.lock_calls.load(); + const unsigned b_locks = b.lock_calls.load(); + const unsigned tries = a.try_lock_calls.load() + b.try_lock_calls.load(); + std::printf("lock() calls: %u, %u; try_lock() calls: %u\n", + a_locks, b_locks, tries); + + // The current generic ctor_spec transfers both do_lock continuations. + // Choosing the effect 'lock_calls > 0' would incorrectly promise this: + const bool both_lock_effects = a_locks > 0 && b_locks > 0; + std::printf("both lock() effects: %s\n", both_lock_effects ? "true" : "false"); + + // This test targets the supported libstdc++ implementation. The standard + // leaves the sequence and order unspecified; neither argument order is + // assumed here. A different std::lock algorithm may need another witness. + return both_lock_effects || tries == 0 ? 1 : 0; + } +} diff --git a/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.md b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.md new file mode 100644 index 00000000..67f62523 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.md @@ -0,0 +1,35 @@ +The current generic `scoped_lock.ctor_spec` promises both `do_lock` +continuations for a two-mutex constructor. The C++ constructor calls +`std::lock`, which may use `lock`, `try_lock`, and `unlock` in an unspecified +sequence. The `BasicLockable` and `Lockable` classes currently impose no laws +that make these operations interchangeable with respect to their effects. + +`scoped_lock_counterexample.cpp` supplies a thread-safe Lockable wrapper +around `std::mutex`. Only `lock()` increments `lock_calls`; `try_lock()` +acquires the same mutex without that effect. On the supported libstdc++ +implementation, the scoped constructor acquires one mutex with `lock()` and +the other with `try_lock()`. Thus the continuation `lock_calls > 0` is false +for at least one mutex, contradicting the generic contract. The executable +checks this without assuming argument order. Its expectation is specific to +libstdc++; another conforming algorithm could require a different witness. + +`scoped_lock_counterexample.v` checks the contract issue independently of +the native implementation. It instantiates the existing typeclasses with +distinct flag effects, proves that the constructor's locking premises can +be supplied, checks the expansion of the actual exported constructor +contract, and proves that a false flag conflicts with its promised true +effect. This is a contract-level model: it does not verify the native mutex +implementation or prove a linked C++ program satisfies `False`. It introduces +no additional axiom or admitted lemma. The generic contract remains unfixed. + +The native check runs under the Dune `runtest` alias. It can also be run with: + +``` +python3 scoped_lock_counterexample.py clang++ scoped_lock_counterexample.cpp +``` + +When fixing the contract, replace the diagnostic contract equality and +counterexample expectation with a regression that rejects the false client +claim while accepting ordinary scoped locking. A repair needs coherent +relations between locking, successful/failed try-locking, and rollback; +changing the constructor to request `do_try_lock` alone is insufficient. diff --git a/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.py b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.py new file mode 100644 index 00000000..9694ad45 --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Compile and run the scoped_lock contract counterexample with libstdc++.""" +import pathlib +import subprocess +import sys +import tempfile + + +def main() -> None: + cxx, source = sys.argv[1:] + with tempfile.TemporaryDirectory(prefix="scoped-lock-counterexample-") as tmp: + executable = pathlib.Path(tmp) / "counterexample" + subprocess.run( + [cxx, "-std=c++17", "-stdlib=libstdc++", "-pthread", "-O1", + source, "-o", str(executable)], + check=True, timeout=60, + ) + subprocess.run([str(executable)], check=True, timeout=20) + + +if __name__ == "__main__": + main() diff --git a/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.v b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.v new file mode 100644 index 00000000..b7df874e --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/scoped_lock_counterexample.v @@ -0,0 +1,103 @@ +(* Copyright (c) 2026 SkyLabs AI, Inc. + Distributed under the BSD 3-Clause license; see LICENSE. *) +Require Import skylabs.auto.cpp.prelude.proof. +Require Import skylabs.brick.libstdcpp.mutex.requirements. +Require Import skylabs.brick.libstdcpp.mutex.spec.scoped_lock. + +(** A contract-level counterexample, paired with the native C++ witness. + The model isolates the flag [lock_calls > 0]: [lock()] sets it, whereas + [try_lock()] leaves it alone. It does not verify the underlying mutex + or claim a linked C++ proof of [False]. *) +Section counterexample. + Context `{Σ : cpp_logic, σ : genv}. + + Definition counting_ty : type := "CountingMutex"%cpp_type. + Definition counting_R (q : cQp.t) (_ : ptr) : Rep := + structR "CountingMutex" q. + Definition called (flag : ptr) (b : bool) : mpred := + flag |-> primR Tbool 1$m (Vbool b). + + #[local] Instance counting_basic_lockable : + BasicLockable counting_ty counting_R := + {| do_lock := fun _ flag K => called flag false ** (called flag true -* K) + ; do_unlock := fun _ _ K => K |}. + #[local] Instance counting_lockable : Lockable counting_ty counting_R := + {| do_try_lock := fun _ _ K => (∀ b : bool, K b)%I |}. + + (** Merely registering Lockable imposes no relation between these effects. + Two false flags supply the ctor's do_lock premises with true flags + as continuations. *) + Lemma supplies_lock_effect this flag : + called flag false |-- + do_lock counting_ty this flag (called flag true). + Proof. cbn. iIntros "$". iIntros "$". Qed. + + Lemma try_lock_can_preserve_false this flag : + called flag false |-- + do_try_lock counting_ty this flag (fun _ => called flag false). + Proof. cbn. iIntros "H" (b). iExact "H". Qed. + + (** Check the existing exported ctor_spec, rather than introduce an + assumed scoped_lock spec. It returns K1 and K2 although try_lock + need not establish either lock-specific effect. A repair must + revisit this diagnostic test. *) + Lemma current_ctor_contract : + scoped_lock.ctor_spec counting_ty counting_ty + (mutexR1 := counting_R) (mutexR2 := counting_R) = + specify.template.ctor + ("std::scoped_lock" .<< Apack [Atype counting_ty; Atype counting_ty] >>) + [Tref counting_ty; Tref counting_ty] $ + \this this + \arg{mp1} "" (Vptr mp1) + \pre{q1 flag1} mp1 |-> counting_R q1$m flag1 + \pre{K1} called flag1 false ** (called flag1 true -* K1) + \arg{mp2} "" (Vptr mp2) + \pre{q2 flag2} mp2 |-> counting_R q2$m flag2 + \pre{K2} called flag2 false ** (called flag2 true -* K2) + \post this |-> scoped_lock.R counting_ty counting_ty counting_R counting_R + 1$m (scoped_lock.Mk mp1 q1 flag1, scoped_lock.Mk mp2 q2 flag2) ** K1 ** K2. + Proof. reflexivity. Qed. + + Lemma ctor_promises_both_lock_effects : + scoped_lock.ctor_spec counting_ty counting_ty + (mutexR1 := counting_R) (mutexR2 := counting_R) |-- + specify.template.ctor + ("std::scoped_lock" .<< Apack [Atype counting_ty; Atype counting_ty] >>) + [Tref counting_ty; Tref counting_ty] $ + \this this + \arg{mp1} "" (Vptr mp1) + \pre{q1 flag1} mp1 |-> counting_R q1$m flag1 + \pre called flag1 false + \arg{mp2} "" (Vptr mp2) + \pre{q2 flag2} mp2 |-> counting_R q2$m flag2 + \pre called flag2 false + \post this |-> scoped_lock.R counting_ty counting_ty counting_R counting_R + 1$m (scoped_lock.Mk mp1 q1 flag1, scoped_lock.Mk mp2 q2 flag2) ** + called flag1 true ** called flag2 true. + Proof. + rewrite current_ctor_contract /specify.template.ctor /specify.exact.ctor. + apply specify_mono. intros this args post. cbn. + iIntros "H". + iDestruct "H" as (mp1 q1 flag1 mp2 q2 flag2) + "(%Hargs & R1 & F1 & R2 & F2 & Hpost)". + iExists mp1, q1, flag1, (called flag1 true), + mp2, q2, flag2, (called flag2 true). + iFrame "R1 R2 Hpost". iSplit; first done. + iSplitL "F1". + - iApply (supplies_lock_effect mp1 flag1). iExact "F1". + - iApply (supplies_lock_effect mp2 flag2). iExact "F2". + Qed. + + (** A legal lock/try_lock trace leaves one flag false, inconsistent + with the corresponding true flag promised by the constructor. + The two half permissions add to one, so disagreement of the values, + rather than overfull ownership, causes the contradiction. *) + Lemma false_flag_contradicts_promised_effect (flag : ptr) : + flag |-> primR Tbool (1 / 2)$m (Vbool false) ** + flag |-> primR Tbool (1 / 2)$m (Vbool true) |-- False. + Proof. + iIntros "[Hfalse Htrue]". + iDestruct (observe_2 [| Vbool false = Vbool true |] + with "Hfalse Htrue") as %[=]. + Qed. +End counterexample. diff --git a/rocq-brick-libstdcpp/test/mutex/test_cpp_proof.v b/rocq-brick-libstdcpp/test/mutex/test_cpp_proof.v index 7f263bf2..48038d57 100644 --- a/rocq-brick-libstdcpp/test/mutex/test_cpp_proof.v +++ b/rocq-brick-libstdcpp/test/mutex/test_cpp_proof.v @@ -1,44 +1,164 @@ Require Import skylabs.auto.cpp.prelude.proof. Require Import skylabs.brick.libstdcpp.mutex.spec. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. Require Import skylabs.brick.libstdcpp.test.mutex.test_cpp. Import linearity. Section with_cpp. Context `{Σ : cpp_logic, σ : genv} {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ, !mutex.G Σ}. - cpp.spec "test_mutex()" as test_mutex_spec from source with (\post emp). + cpp.spec "test_mutex()" as test_mutex_spec from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) + \post emp). Theorem test_mutex_ok : verify[source] "test_mutex()". Proof. verify_spec; go. - iExists emp. go. + iExists (nroot .@@ "test_mutex"), emp; go. + iSplitR; first (iPureIntro; rewrite namespaces.ndot_unseal; discriminate). + iIntros "Hinit". + iMod "Hinit" as (g) "(%HN & Hm & Htoken)". + iDestruct (MutexSets.my_mutexes_alloc_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$]") as "[Hrest Hname]". + iAssert (mutex.not_locked m_addr g thr (1$m)%cQp) + with "[Htoken Hname]" as "Hnotlocked". + { rewrite /mutex.not_locked. iFrame. } + go $usenamed=true. + iExists (mutex.locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iExists (mutex.not_locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iDestruct select (mutex.not_locked _ _ _ _) as "Hnotlocked". + iEval (rewrite /mutex.not_locked) in "Hnotlocked". + iDestruct "Hnotlocked" as "[Htoken Hname]". + iDestruct (MutexSets.my_mutexes_join_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$Hrest $Hname]") as "Hnames". + go $usenamed=true. Qed. - cpp.spec "test_lock_guard()" as test_lock_guard_spec from source with (\post emp). + cpp.spec "test_lock_guard()" as test_lock_guard_spec from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) + \post emp). Lemma test_lock_guard_ok : verify[source] "test_lock_guard()". Proof. verify_spec; go. - iExists emp; go. + iExists (nroot .@@ "test_lock_guard"), emp; go. + iSplitR; first (iPureIntro; rewrite namespaces.ndot_unseal; discriminate). + iIntros "Hinit". + iMod "Hinit" as (g) "(%HN & Hm & Htoken)". + iDestruct (MutexSets.my_mutexes_alloc_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$]") as "[Hrest Hname]". + iAssert (mutex.not_locked m_addr g thr (1$m)%cQp) + with "[Htoken Hname]" as "Hnotlocked". + { rewrite /mutex.not_locked. iFrame. } + go $usenamed=true. + iExists (mutex.locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iExists (mutex.not_locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iDestruct select (mutex.not_locked _ _ _ _) as "Hnotlocked". + iEval (rewrite /mutex.not_locked) in "Hnotlocked". + iDestruct "Hnotlocked" as "[Htoken Hname]". + iDestruct (MutexSets.my_mutexes_join_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$Hrest $Hname]") as "Hnames". + go $usenamed=true. Qed. - cpp.spec "test_scoped_lock()" as test_scoped_lock_spec from source with (\post emp). + cpp.spec "test_scoped_lock()" as test_scoped_lock_spec from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) + \post emp). Lemma test_scoped_lock_ok : verify[source] "test_scoped_lock()". Proof. verify_spec; go. - iExists emp; go. - iExists emp; go. + iExists (nroot .@@ "test_scoped_lock" .@ "first"), emp; go. + iSplitR; first (iPureIntro; rewrite namespaces.ndot_unseal; discriminate). + go. + iExists (nroot .@@ "test_scoped_lock" .@ "second"), emp; go. + iSplitR; first (iPureIntro; rewrite namespaces.ndot_unseal; discriminate). + iIntros "Hinit". + iMod "Hinit" as (g2) "(%HN2 & Hm2 & Htoken2)". + iDestruct select ((|={⊤}=> _)%I) as "Hinit". + iMod "Hinit" as (g1) "(%HN1 & Hm1 & Htoken1)". + have Hdisjoint : (↑mutex.mutex_inv_namespace g1 : coPset) ## + ↑mutex.mutex_inv_namespace g2. + { rewrite HN1 HN2. solve_ndisj. } + iDestruct (MutexSets.my_mutexes_alloc_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g1) ltac:(set_solver) + with "[$]") as "[Hrest Hname1]". + iDestruct (MutexSets.my_mutexes_alloc_mutex_name thr + (⊤ ∖ ↑mutex.mutex_inv_namespace g1) + (↑mutex.mutex_inv_namespace g2) ltac:(set_solver) + with "Hrest") as "[Hrest Hname2]". + iAssert (mutex.not_locked m1_addr g1 thr (1$m)%cQp) + with "[Htoken1 Hname1]" as "Hnotlocked1". + { rewrite /mutex.not_locked. iFrame. } + iAssert (mutex.not_locked m2_addr g2 thr (1$m)%cQp) + with "[Htoken2 Hname2]" as "Hnotlocked2". + { rewrite /mutex.not_locked. iFrame. } + go $usenamed=true. + iExists (mutex.locked m1_addr g1 thr (1$m)%cQp), + (mutex.locked m2_addr g2 thr (1$m)%cQp), (1$m)%cQp, (1$m)%cQp. + go $usenamed=true. + iExists (mutex.not_locked m1_addr g1 thr (1$m)%cQp), + (mutex.not_locked m2_addr g2 thr (1$m)%cQp), (1$m)%cQp, (1$m)%cQp. + go $usenamed=true. + iDestruct select (mutex.not_locked m1_addr _ _ _) as "Hnotlocked1". + iDestruct select (mutex.not_locked m2_addr _ _ _) as "Hnotlocked2". + iEval (rewrite /mutex.not_locked) in "Hnotlocked1". + iEval (rewrite /mutex.not_locked) in "Hnotlocked2". + iDestruct "Hnotlocked1" as "[Htoken1 Hname1]". + iDestruct "Hnotlocked2" as "[Htoken2 Hname2]". + iDestruct (MutexSets.my_mutexes_join_mutex_name thr + (⊤ ∖ ↑mutex.mutex_inv_namespace g1) + (↑mutex.mutex_inv_namespace g2) ltac:(set_solver) + with "[$Hrest $Hname2]") as "Hrest". + iDestruct (MutexSets.my_mutexes_join_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g1) ltac:(set_solver) + with "[$Hrest $Hname1]") as "Hnames". + go $usenamed=true. Qed. - cpp.spec "test_unique_lock()" as test_unique_lock_spec from source with (\post emp). + cpp.spec "test_unique_lock()" as test_unique_lock_spec from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) + \post emp). Lemma test_unique_lock_ok : verify[source] "test_unique_lock()". Proof. verify_spec; go. - - iExists emp; go. + iExists (nroot .@@ "test_unique_lock"), emp; go. + iSplitR; first (iPureIntro; rewrite namespaces.ndot_unseal; discriminate). + iIntros "Hinit". + iMod "Hinit" as (g) "(%HN & Hm & Htoken)". + iDestruct (MutexSets.my_mutexes_alloc_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$]") as "[Hrest Hname]". + iAssert (mutex.not_locked m_addr g thr (1$m)%cQp) + with "[Htoken Hname]" as "Hnotlocked". + { rewrite /mutex.not_locked. iFrame. } + go $usenamed=true. + iExists (mutex.locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iExists (mutex.not_locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iDestruct select (mutex.not_locked _ _ _ _) as "Hnotlocked". + iEval (rewrite /mutex.not_locked) in "Hnotlocked". + iDestruct "Hnotlocked" as "[Htoken Hname]". + iDestruct (MutexSets.my_mutexes_join_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$Hrest $Hname]") as "Hnames". + go $usenamed=true. Qed. @@ -49,17 +169,44 @@ Section with_cpp. Lemma test_unique_lock_defer_ok : verify[source] "test_unique_lock_defer()". Proof. verify_spec; go. - iExists emp; go. + iExists (nroot .@@ "test_unique_lock_defer"), emp; go. + iSplitR; first (iPureIntro; rewrite namespaces.ndot_unseal; discriminate). + iIntros "Hinit". + iMod "Hinit" as (g) "(%HN & Hm & Htoken)". + go $usenamed=true. Qed. cpp.spec "std::move&>(std::unique_lock&)" from source inline. cpp.spec "test_unique_lock_move()" as test_unique_lock_move_spec from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) \post emp). Lemma test_unique_lock_move_ok : verify[source] "test_unique_lock_move()". Proof. verify_spec; go. - iExists emp; go. + iExists (nroot .@@ "test_unique_lock_move"), emp; go. + iSplitR; first (iPureIntro; rewrite namespaces.ndot_unseal; discriminate). + iIntros "Hinit". + iMod "Hinit" as (g) "(%HN & Hm & Htoken)". + iDestruct (MutexSets.my_mutexes_alloc_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$]") as "[Hrest Hname]". + iAssert (mutex.not_locked m_addr g thr (1$m)%cQp) + with "[Htoken Hname]" as "Hnotlocked". + { rewrite /mutex.not_locked. iFrame. } + go $usenamed=true. + iExists (mutex.locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iExists (mutex.not_locked m_addr g thr (1$m)%cQp), (1$m)%cQp. + go $usenamed=true. + iDestruct select (mutex.not_locked _ _ _ _) as "Hnotlocked". + iEval (rewrite /mutex.not_locked) in "Hnotlocked". + iDestruct "Hnotlocked" as "[Htoken Hname]". + iDestruct (MutexSets.my_mutexes_join_mutex_name thr ⊤ + (↑mutex.mutex_inv_namespace g) ltac:(set_solver) + with "[$Hrest $Hname]") as "Hnames". + go $usenamed=true. Qed. End with_cpp. diff --git a/rocq-brick-libstdcpp/test/mutex/transfer_with_mutex.cpp b/rocq-brick-libstdcpp/test/mutex/transfer_with_mutex.cpp new file mode 100644 index 00000000..51b8821e --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/transfer_with_mutex.cpp @@ -0,0 +1,69 @@ +#include +#include + +// [R g] says that the account's balance is protected by namespace g. +class C { +private: + unsigned long balance; + std::recursive_mutex mut; // protects balance + +public: + explicit C(unsigned long initial_balance) : balance(initial_balance) {} + + template + + // [| g \subset E |] + // c |-> R g ** my_mutexes th E + // { c,,balance |-> v0 } f {\exists v', c,,balance |-> v' } + void call(F f) { + std::lock_guard lg(mut); + // c,,balance |-> v0 + f(balance); + // c,,balance |-> v' + //~lock_guard() + // c |-> R g ** my_mutexes th E + } + + void print_balance() { + std::lock_guard lg(mut); + std::printf("Balance: %lu\n", balance); + } + + void transfer(C& to, unsigned long i) { + std::lock_guard lg_from(mut); + std::lock_guard lg_to(to.mut); + this->balance -= i; + to.balance += i; + } +}; + + +void transfer_seq(unsigned long& b1, unsigned long& b2, unsigned long i) { + b1 -= i; + b2 += i; +} + +// from |-> R g1 ** to |-> R g2 ** my_mutexes th E +void transfer(C& from, C& to, unsigned long i) { + from.call([&](unsigned long& from_balance) { + // from ,, balance |-> v1 + // ** to |-> R g2 + // ** my_mutexes th E\g1 + to.call([&](unsigned long& to_balance) { + // from ,, balance |-> v1 + // ** to ,, balance |-> v2 + // ** my_mutexes th (E\g1)\g2 + transfer_seq(from_balance, to_balance, i); + // from ,, balance |-> v1-i + // ** to ,, balance |-> v2+i + // ** my_mutexes th (E\g1)\g2 + }); + }); +} + +int main() { + C from{4}; + C to{2}; + transfer(from, to, 3); + return 0; +} diff --git a/rocq-brick-libstdcpp/test/mutex/transfer_with_mutex_cpp_proof.v b/rocq-brick-libstdcpp/test/mutex/transfer_with_mutex_cpp_proof.v new file mode 100644 index 00000000..9c87c9be --- /dev/null +++ b/rocq-brick-libstdcpp/test/mutex/transfer_with_mutex_cpp_proof.v @@ -0,0 +1,606 @@ +Require Import skylabs.auto.cpp.prelude.proof. +Require Import skylabs.brick.libstdcpp.mutex.spec.recursive_mutex. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Require Import skylabs.brick.libstdcpp.test.mutex.transfer_with_mutex_cpp. + +Import linearity. + +(** std::lock_guard specs. + TODO remove this when lock_guard works. *) +Module transfer_lock_guard. + +sl.lock +Definition R `{Σ : cpp_logic, σ : genv, MUTEX_SETS : !mutex_set_G Σ, !std_recursive_mutex.G Σ, !HasStdThreads Σ} + (mp : ptr) (g : std_recursive_mutex.gname) (q : cQp.t) (P : mpred) : Rep := + structR "std::lock_guard" 1$m ** + _field "std::lock_guard::_M_device" + |-> refR<"std::recursive_mutex"> 1$m mp ** + pureR (mp |-> std_recursive_mutex.R g q P). +#[only(type_ptr)] derive R. +#[only(lazy_unfold)] derive R. + +Section with_cpp. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. + + #[local] Hint Opaque std_recursive_mutex.do_lock std_recursive_mutex.do_unlock : sl_opacity. + + #[global] Instance R_learn : + Cbn (Learn (learn_eq ==> learn_eq ==> any ==> learn_eq ==> learn_hints.fin) R). + Proof. solve_learnable. Qed. + + cpp.spec "std::lock_guard::lock_guard(std::recursive_mutex&)" + as ctor_spec from source with ( + \this this + \arg{mp} "m" (Vptr mp) + \pre{g q P} mp |-> std_recursive_mutex.R g q P + \pre{K} std_recursive_mutex.do_lock mp (g, P) K + \post this |-> R mp g q P ** K + ). + + cpp.spec "std::lock_guard::~lock_guard()" + as dtor_spec from source with ( + \this this + \pre{mp g q P} this |-> R mp g q P + \pre{K} std_recursive_mutex.do_unlock mp (g, P) K + \post mp |-> std_recursive_mutex.R g q P ** K + ). + + Lemma ctor_ok : verify[source] ctor_spec. + Proof. verify_spec; go. iExists (std_recursive_mutex.Held n args). go. Qed. + + Lemma dtor_ok : verify[source] dtor_spec. + Proof. verify_spec. rewrite !R.unlock. go. Qed. +End with_cpp. + +End transfer_lock_guard. + +Definition balance_args : tele := [tele (_ : Z)]. +Definition balance_arg (n : Z) : balance_args := + {| tele_arg_head := n; tele_arg_tail := () |}. + +(* sequential ownership of balance *) +sl.lock +Definition account_balanceR `{Σ : cpp_logic, σ : genv} (n : Z) : Rep := + _field "C::balance" |-> ulongR 1$m n. +#[only(lazy_unfold(export),timeless)] derive account_balanceR. + +(* ownership of [this->mutex] and balanceR proctected by the mutex *) +sl.lock +Definition account_mutexR + `{Σ : cpp_logic, σ : genv, MUTEX_SETS : !mutex_set_G Σ, !std_recursive_mutex.G Σ, !HasStdThreads Σ} + (g : std_recursive_mutex.gname) (q : cQp.t) : Rep := + structR "C" q ** + as_Rep (fun this : ptr => + this ,, _field "C::mut" |-> std_recursive_mutex.R g q + (∃ args : balance_args, + @tele_app balance_args (fun _ => mpred) (fun n : Z => this |-> account_balanceR n) args)). +#[only(cfractional,ascfractional,type_ptr,lazy_unfold(export))] derive account_mutexR. + +(** The account rep. Everything that the constructor of [C] returns. + The recursive mutex ctor can allocate a [cinv] assocaited with any given + [N : namespace], and returns (most of) account_mutexR. *) +sl.lock +Definition accountR + `{Σ : cpp_logic, σ : genv, MUTEX_SETS : !mutex_set_G Σ, !std_recursive_mutex.G Σ, !HasStdThreads Σ} + (N : namespace) (q : cQp.t) : Rep := + Exists g, + account_mutexR g q ** pureR (std_recursive_mutex.token g q) ** + pureR [| std_recursive_mutex.rmutex_inv_namespace g = N |]. +#[only(type_ptr="C")] derive accountR. + +(** The two account mutexes use distinct namespaces. *) +Definition from_namespace : namespace := nroot .@@ "transfer" .@ "from". +Definition to_namespace : namespace := nroot .@@ "transfer" .@ "to". + +Lemma transfer_namespaces_disjoint : (↑from_namespace : coPset) ## ↑to_namespace. +Proof. rewrite /from_namespace /to_namespace. solve_ndisj. Qed. + +(** Names of the two closure types and their instantiated [C::call] methods. *) +Definition outer_lambda : name := + Nscoped "transfer(C&, C&, unsigned long)" (Nanon 0). +Definition lambda_call (cl : name) : name := + Nscoped cl (Nop function_qualifiers.Nc OOCall [Tref Tulong]). +Definition inner_lambda : name := Nscoped (lambda_call outer_lambda) (Nanon 0). +Definition account_call (cl : name) : name := + Ninst (Nscoped "C" (Nfunction function_qualifiers.N "call" [Tnamed cl])) + [Atype (Tnamed cl)]. + +Section class_C. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. + + #[local] Instance balanceR_learn : LearnEq2 account_balanceR. + Proof. solve_learnable. Qed. + #[local] Instance account_mutexR_learn : + Cbn (Learn (learn_eq ==> any ==> learn_hints.fin) account_mutexR). + Proof. solve_learnable. Qed. + + #[program] Definition learn_balance_args_C (P : balance_args -t> mpred) := + \cancelx + \bound_existential args + \proving @tele_app balance_args (fun _ => mpred) P args + \exist n + \instantiate args := balance_arg n + \through @tele_app balance_args (fun _ => mpred) P (balance_arg n) + \end. + Next Obligation. work. Qed. + #[local] Hint Resolve learn_balance_args_C : br_hints. + + (** calls [f], gets post condition [Q], and change that to the desired post + condition [K]. *) + Definition balance_callback (cl : name) (f balance : ptr) (K : mpred) : mpred := + ∀ Q : mpred, (K -* Q) -* + wp_minvoke_O source Direct (Qconst (Tnamed cl)) Tvoid + (Tfunction (FunctionType Tvoid [Tref Tulong])) + (lambda_call cl) f [Unmaterialized (Tref Tulong) (Vptr balance)] None + (fun _ => Q). + + #[local] Hint Opaque balance_callback std_recursive_mutex.do_lock + std_recursive_mutex.do_unlock : sl_opacity. + + (* The internal call spec exposes the lock handle, which is supposed to be + hidden to the client of [C]. We define [call_spec_body] later to hide + that. *) + Definition account_call_internal_body (cl : name) : ptr -> WpSpec mpred val val := + (\this this + \arg{f} "f" (Vptr f) + \persist type_ptr (Tnamed cl) f + \prepost{g q} this |-> account_mutexR g q + \pre{K} std_recursive_mutex.do_lock (this ,, _field "C::mut") + (g, (∃ args : balance_args, + @tele_app balance_args (fun _ => mpred) (fun n : Z => this |-> account_balanceR n) args)%I) + (|={⊤}=> balance_callback cl f (this ,, _field "C::balance") + (std_recursive_mutex.do_unlock (this ,, _field "C::mut") + (g, (∃ args : balance_args, + @tele_app balance_args (fun _ => mpred) (fun n : Z => this |-> account_balanceR n) args)%I) K)) + \post K). + + cpp.spec (account_call outer_lambda) as outer_call_internal_spec from source with + (\exact Reduce (account_call_internal_body outer_lambda)). + cpp.spec (account_call inner_lambda) as inner_call_internal_spec from source with + (\exact Reduce (account_call_internal_body inner_lambda)). + + (** The callback invocation is supplied by the precondition, so verification + does not need a separate global specification for its operator(). *) + Ltac prove_account_call := + verify_spec; go; + lazymatch goal with q : cQp.t, qt : cQp.t, K : mpred |- _ => + iExists q, _; go; + wname [bi_wand] "Hcallback"; + iSplitL "Hcallback"; [iExact "Hcallback"|]; + let rec open_callback := + first [wname [balance_callback] "Hcallback"; + iMod "Hcallback" as "Hcallback" + | progress go1; open_callback] in + open_callback; go; + iEval (rewrite /balance_callback) in "Hcallback"; + iApply ("Hcallback" with "[-]"); go; + iExists q, K; go + end. + + Lemma outer_call_internal_ok : verify?[source] outer_call_internal_spec. + Proof. prove_account_call. Qed. + + Lemma inner_call_internal_ok : verify?[source] inner_call_internal_spec. + Proof. prove_account_call. Qed. + + (** call() is a wrapper for function [f] that takes the sequential ownership + of the balance and returns an updated one. *) + Definition call_spec_body (cl : name) : ptr -> WpSpec mpred val val := + (\this this + \arg{f} "f" (Vptr f) + \persist type_ptr (Tnamed cl) f + \persist{th} current_thread th + \prepost{N q} this |-> accountR N q ** + MutexSets.my_mutexes th (coPset.CoPset (↑N)) + \pre{K} ∀ v : Z, + this |-> account_balanceR v -* + balance_callback cl f (this ,, _field "C::balance") + (∃ v' : Z, this |-> account_balanceR v' ** K) + \post K). + + cpp.spec (account_call outer_lambda) as outer_call_spec from source with + (\exact Reduce (call_spec_body outer_lambda)). + cpp.spec (account_call inner_lambda) as inner_call_spec from source with + (\exact Reduce (call_spec_body inner_lambda)). + + Local Lemma account_call_refines cl this args post : + call_spec_body cl this args post |-- + account_call_internal_body cl this args post. + Proof. + rewrite /call_spec_body /account_call_internal_body. + cbn. + iIntros "H". + iDestruct "H" as (f th N q K) + "(%Hargs & #Hf & #Hth & (HR & Hnames) & Hcallback & Hpost)". + iEval (rewrite accountR.unlock _at_exists) in "HR". + iDestruct "HR" as (g) "HR". + iEval (rewrite !_at_sep !_at_pureR) in "HR". + iDestruct "HR" as "(HR & Htoken & %Hg)". + iAssert (std_recursive_mutex.acquireable (this ,, _field "C::mut") (TT := balance_args) g q th + std_recursive_mutex.NotHeld (fun n : Z => this |-> account_balanceR n))%I + with "[Htoken Hnames]" as "Hacquireable". + { rewrite std_recursive_mutex.acquireable.unlock /= /std_recursive_mutex.not_locked. + rewrite Hg. iFrame "#∗". } + iExists f, g, q, + (std_recursive_mutex.acquireable (this ,, _field "C::mut") (TT := balance_args) g q th + std_recursive_mutex.NotHeld (fun n : Z => this |-> account_balanceR n) ** K)%I. + iFrame "Hf HR". iSplit; first done. + iSplitR "Hpost". + - rewrite /std_recursive_mutex.do_lock /=. + iExists balance_args, (fun n : Z => this |-> account_balanceR n)%I, + q, th, std_recursive_mutex.NotHeld. + iSplit; first done. iFrame "Hth Hacquireable". + iIntros "Hlocked". + iDestruct "Hlocked" as (s) "[%Hac Hlocked]". + rewrite std_recursive_mutex.acquire.unlock in Hac. + destruct Hac as [[v []] ->]. + iEval (rewrite std_recursive_mutex.acquireable.unlock /=) in "Hlocked". + iDestruct "Hlocked" as "[Hheld Hbalance]". + iMod "Hheld". iMod "Hbalance". iModIntro. + rewrite /balance_callback. iIntros (Q) "HQ". + iSpecialize ("Hcallback" $! v with "Hbalance"). + iApply ("Hcallback" with "[-]"). + iIntros "Hresult". + iDestruct "Hresult" as (v') "[Hbalance HK]". + iApply "HQ". + iExists balance_args, (fun n : Z => this |-> account_balanceR n)%I, + q, th, 0%nat, (balance_arg v'). + iSplit; first done. iFrame "Hth". iSplitL "Hheld Hbalance". + { rewrite std_recursive_mutex.acquireable.unlock /=. iFrame "#∗". } + iIntros "Hunlocked". + iEval (rewrite std_recursive_mutex.release.unlock) in "Hunlocked". + iFrame. + - iIntros "(HR & Hacquireable & HK)". + iApply "Hpost". iFrame "HK". + iEval (rewrite std_recursive_mutex.acquireable.unlock /= + /std_recursive_mutex.not_locked) in "Hacquireable". + iDestruct "Hacquireable" as "[Htoken Hnames]". + iEval (rewrite Hg) in "Hnames". iFrame "Hnames". + rewrite accountR.unlock _at_exists. iExists g. + rewrite !_at_sep !_at_pureR. iFrame. done. + Qed. + + Local Lemma outer_call_refines : outer_call_internal_spec |-- outer_call_spec. + Proof. apply specify_mono. intros this args post. apply account_call_refines. Qed. + + Local Lemma inner_call_refines : inner_call_internal_spec |-- inner_call_spec. + Proof. apply specify_mono. intros this args post. apply account_call_refines. Qed. + + Lemma outer_call_ok : verify?[source] outer_call_spec. + Proof. work. wapply outer_call_refines. wapply outer_call_internal_ok. work. Qed. + + Lemma inner_call_ok : verify?[source] inner_call_spec. + Proof. work. wapply inner_call_refines. wapply inner_call_internal_ok. work. Qed. + + cpp.spec "C::transfer(C&, unsigned long)" as C_transfer_spec from source with ( + \this from + \arg{to} "to" (Vptr to) + \arg{i} "i" (Vint i) + \persist{th} current_thread th + \prepost{qfrom qto} + from |-> accountR from_namespace qfrom ** + MutexSets.my_mutexes th (coPset.CoPset (↑from_namespace)) ** + to |-> accountR to_namespace qto ** + MutexSets.my_mutexes th (coPset.CoPset (↑to_namespace)) + \post emp + ). + + Lemma C_transfer_ok : verify[source] C_transfer_spec. + Proof. + verify_spec; go. + rewrite accountR.unlock. go. + lazymatch goal with + | |- context[transfer_lock_guard.R (this ,, _) ?gfrom _ _] => + rename gfrom into source_g + end. + match goal with + | H : std_recursive_mutex.rmutex_inv_namespace source_g = from_namespace |- _ => + rename H into Hfrom_namespace + end. + match goal with + | H : std_recursive_mutex.rmutex_inv_namespace g = to_namespace |- _ => + rename H into Hto_namespace + end. + iDestruct select (MutexSets.my_mutexes th + (coPset.CoPset (↑from_namespace))) as "Hfrom_names". + iEval (rewrite -Hfrom_namespace) in "Hfrom_names". + iDestruct select (std_recursive_mutex.token source_g qfrom) as "Hfrom_token". + iAssert (std_recursive_mutex.acquireable (this ,, _field "C::mut") (TT := balance_args) source_g qfrom th + std_recursive_mutex.NotHeld (fun n : Z => this |-> account_balanceR n))%I + with "[Hfrom_names Hfrom_token]" as "?". + { rewrite std_recursive_mutex.acquireable.unlock /= /std_recursive_mutex.not_locked. iFrame "#∗". } + iDestruct select (MutexSets.my_mutexes th + (coPset.CoPset (↑to_namespace))) as "Hto_names". + iEval (rewrite -Hto_namespace) in "Hto_names". + iDestruct select (std_recursive_mutex.token g qto) as "Hto_token". + iAssert (std_recursive_mutex.acquireable (to ,, _field "C::mut") (TT := balance_args) g qto th + std_recursive_mutex.NotHeld (fun n : Z => to |-> account_balanceR n))%I + with "[Hto_names Hto_token]" as "?". + { rewrite std_recursive_mutex.acquireable.unlock /= /std_recursive_mutex.not_locked. iFrame "#∗". } + go. + rewrite std_recursive_mutex.acquire.unlock in H1, H2. + destruct H1 as [[from_balance []] Hfrom]. inversion Hfrom; subst. + destruct H2 as [[to_balance []] Hto]. inversion Hto; subst. + go. + (* Return the updated destination balance when lg_to is destroyed. *) + iExists (cQp.scale (1 / 2) qto), + (std_recursive_mutex.acquireable (to ,, _field "C::mut") (TT := balance_args) g qto th + std_recursive_mutex.NotHeld (fun n : Z => to |-> account_balanceR n))%I. + rewrite std_recursive_mutex.release.unlock /=. go. + (* Return the updated source balance when lg_from is destroyed. *) + iExists (cQp.scale (1 / 2) qfrom), + (std_recursive_mutex.acquireable (this ,, _field "C::mut") (TT := balance_args) source_g qfrom th + std_recursive_mutex.NotHeld (fun n : Z => this |-> account_balanceR n))%I. + rewrite std_recursive_mutex.release.unlock /=. go. + rewrite accountR.unlock std_recursive_mutex.acquireable.unlock /= + /std_recursive_mutex.not_locked. go with br_erefl. + rewrite Hfrom_namespace Hto_namespace. go. + Qed. + + (** Link the C implementation and both lock-guard operations. *) + Lemma C_transfer_link : + denoteModule source + ** std_recursive_mutex.std_lock_spec_alt + ** std_recursive_mutex.std_unlock_spec_alt + |-- C_transfer_spec. + Proof. + work. + wapply C_transfer_ok. + wapply transfer_lock_guard.ctor_ok. + wapply transfer_lock_guard.dtor_ok. + work. + Qed. + + #[local] Instance balanceR_tele_timeless (this : ptr) args : + Timeless (@tele_app balance_args (fun _ => mpred) (fun n : Z => this |-> account_balanceR n) args). + Proof. destruct args as [n []]. apply _. Qed. + + Section construction. + (** FIXME this should be provable *) + Context `{balance_objective : !forall (this : ptr) args, + Objective (@tele_app balance_args (fun _ => mpred) (fun n : Z => this |-> account_balanceR n) args)}. + + cpp.spec "C::C(unsigned long)" as ctor_spec from source with ( + \this this + \arg{initial_balance} "initial_balance" (Vint initial_balance) + \persist{th} current_thread th + \pre{N} emp + \require N <> nroot + \post this |-> accountR N 1$m + ). + + Lemma ctor_ok : verify[source] ctor_spec. + Proof using Type balance_objective. + verify_spec; go. + iExists N, balance_args, (fun n : Z => this |-> account_balanceR n)%I, + (balance_arg initial_balance); go. + rewrite -bi.later_intro. + go. + rewrite accountR.unlock. go with br_erefl. + Qed. + + End construction. + + cpp.spec "C::~C()" as dtor_spec from source with ( + \this this + \pre{N} this |-> accountR N 1$m + \post emp + ). + + Lemma dtor_ok : std_recursive_mutex.std_dtor_spec |-- verify[source] dtor_spec. + Proof. + verify_shift; go. rewrite accountR.unlock. go with br_erefl. + match goal with + | |- context[wp_destroy_named _ _ _ ?K] => + remember K as continuation eqn:Hcontinuation + end. + iModIntro. go. rewrite Hcontinuation. + iDestruct select (bi_later (bi_exist _)) + as "Hbalance". + iApply fupd_wp_destroy_val. + iMod "Hbalance" as ([n []]) "Hbalance". + iModIntro. go $usenamed=true. + Qed. + +End class_C. + +Section clients. + Context `{Σ : cpp_logic, σ : genv}. + Context {HAS_THREADS : HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!std_recursive_mutex.G Σ}. + + #[local] Instance accountR_learn : + Cbn (Learn (learn_eq ==> any ==> learn_hints.fin) accountR). + Proof. solve_learnable. Qed. + + cpp.spec (lambda_call outer_lambda) from source inline. + cpp.spec (lambda_call inner_lambda) from source inline. + cpp.spec (Nscoped outer_lambda Ndtor) from source inline. + cpp.spec (Nscoped inner_lambda Ndtor) from source inline. + + (* transfer balance with sequential ownership of the balances *) + cpp.spec "transfer_seq(unsigned long&, unsigned long&, unsigned long)" + as transfer_seq_spec from source with ( + \arg{b1} "b1" (Vptr b1) + \arg{b2} "b2" (Vptr b2) + \arg{i} "i" (Vint i) + \pre{v1 v2} b1 |-> ulongR 1$m v1 ** b2 |-> ulongR 1$m v2 + \post b1 |-> ulongR 1$m (trim 64 (v1 - i)) ** + b2 |-> ulongR 1$m (trim 64 (v2 + i))). + + Lemma transfer_seq_ok : verify[source] transfer_seq_spec. + Proof. verify_spec; go. Qed. + + (** Transfer balances using account ownership and per-thread namespace resources. *) + cpp.spec "transfer(C&, C&, unsigned long)" as transfer_general_spec from source with ( + \arg{from} "from" (Vptr from) + \arg{to} "to" (Vptr to) + \arg{i} "i" (Vint i) + \persist{th} current_thread th + \prepost{Nfrom Nto qfrom qto} + from |-> accountR Nfrom qfrom ** + MutexSets.my_mutexes th (coPset.CoPset (↑Nfrom)) ** + to |-> accountR Nto qto ** + MutexSets.my_mutexes th (coPset.CoPset (↑Nto)) + \post emp). + + Local Lemma call_with_continuation (A : mpred -> mpred) R Q : + A (R -* Q)%I |-- ∃ K, A K ** (R ** K -* Q). + Proof. + iIntros "HA". iExists (R -* Q)%I. iFrame "HA". + iIntros "[HR HK]". iApply "HK". iFrame. + Qed. + + Lemma transfer_general_ok : inner_call_spec ** transfer_seq_spec |-- verify[source] transfer_general_spec. + Proof. + verify_shift; go. + iExists qfrom. go. + iApply call_with_continuation. + iIntros (v1) "Hfrom_balance". + (* The outer acquisition exposes from's balance; to retains its account and namespace. *) + wname [accountR] "Hto". + iAssert (from |-> account_balanceR v1 ** to |-> accountR Nto qto ** + MutexSets.my_mutexes th (coPset.CoPset (↑Nto)))%I + with "[$]" as "Hafter_from". + iDestruct "Hafter_from" as "[? ?]". + rewrite /balance_callback. iIntros (Qfrom) "HKfrom". go. + iExists qto. go. + iApply call_with_continuation. + iIntros (v2) "Hto_balance". + (* The inner acquisition exposes both balances. *) + wname [account_balanceR] "Hfrom_balance". + iAssert (from |-> account_balanceR v1 ** to |-> account_balanceR v2)%I + with "[$Hfrom_balance $Hto_balance]" as "Hbefore_transfer". + iDestruct "Hbefore_transfer" as "[? ?]". + rewrite /balance_callback. iIntros (Qto) "HKto". go. + (* After transfer_seq: unsigned subtraction/addition. *) + iAssert (from ,, _field "C::balance" |-> ulongR 1$m (trim 64 (v1 - i)) ** + to ,, _field "C::balance" |-> ulongR 1$m (trim 64 (v2 + i)))%I + with "[$]" as "Hafter_transfer". + iDestruct "Hafter_transfer" as "[? ?]". + iApply "HKto". go. + (* Returning from to.call restores its account and namespace resource. *) + iAssert (from ,, _field "C::balance" |-> ulongR 1$m (trim 64 (v1 - i)) ** + to |-> accountR Nto qto ** + MutexSets.my_mutexes th (coPset.CoPset (↑Nto)))%I + with "[$]" as "Hafter_to". + iDestruct "Hafter_to" as "(Hfrom_balance & Hto & Hto_names)". + iApply "HKfrom". iExists (trim 64 (v1 - i)). + iSplitL "Hfrom_balance". + { iDestruct "Hfrom_balance" as "?". go. } + iIntros "(Hfrom & Hfrom_names)". + (* Returning from from.call restores both accounts and namespace resources. *) + iAssert (from |-> accountR Nfrom qfrom ** + MutexSets.my_mutexes th (coPset.CoPset (↑Nfrom)) ** + to |-> accountR Nto qto ** + MutexSets.my_mutexes th (coPset.CoPset (↑Nto)))%I + with "[$Hfrom $Hfrom_names $Hto $Hto_names]" as "Hafter_from". + iDestruct "Hafter_from" as "[? ?]". go. + wname [bi_wand] "Hpost". + iSpecialize ("Hpost" with "[$]"). + iModIntro. iNext. iApply "Hpost". go. + Qed. + + Lemma transfer_general_link : + denoteModule source ** outer_call_spec ** inner_call_spec |-- transfer_general_spec. + Proof. + work. wapply transfer_general_ok. wapply transfer_seq_ok. work. + Qed. + + (** Choose both mutex namespaces for the callback-based transfer. *) + Definition transfer_body : WpSpec mpred val val := + ( + \arg{from} "from" (Vptr from) + \arg{to} "to" (Vptr to) + \arg{i} "i" (Vint i) + \persist{th} current_thread th + \prepost{qfrom qto} + from |-> accountR from_namespace qfrom ** + MutexSets.my_mutexes th (coPset.CoPset (↑from_namespace)) ** + to |-> accountR to_namespace qto ** + MutexSets.my_mutexes th (coPset.CoPset (↑to_namespace)) + \post emp + ). + + cpp.spec "transfer(C&, C&, unsigned long)" as transfer_spec from source with + (\exact Reduce transfer_body). + + Lemma transfer_specialize : transfer_general_spec |-- transfer_spec. + Proof. + apply specify_mono. intros args post. cbn. + work. + iExists qfrom, qto. work. + Qed. + + Lemma transfer_ok : inner_call_spec ** transfer_seq_spec |-- verify[source] transfer_spec. + Proof. work. wapply transfer_specialize. wapply transfer_general_ok. work. Qed. + + Lemma transfer_link : + denoteModule source ** outer_call_spec ** inner_call_spec |-- transfer_spec. + Proof. work. wapply transfer_specialize. wapply transfer_general_link. work. Qed. + + cpp.spec "main()" as main_spec from source with ( + \persist{th} current_thread th + \prepost MutexSets.my_mutexes th (coPset.CoPset ⊤) + \post[Vint 0] emp + ). + + Lemma main_ok : verify[source] main_spec. + Proof. + have Hfrom : from_namespace <> nroot. + { rewrite /from_namespace namespaces.ndot_unseal. discriminate. } + have Hto : to_namespace <> nroot. + { rewrite /to_namespace namespaces.ndot_unseal. discriminate. } + verify_spec. + let rec expose_names := + first [iDestruct select (MutexSets.my_mutexes th _) as "Hnames" + | progress go1; expose_names] in + expose_names. + go. + iExists from_namespace; go. + iExists to_namespace; go. + have Hdisjoint := transfer_namespaces_disjoint. + iDestruct (MutexSets.my_mutexes_alloc_mutex_name th ⊤ + (↑from_namespace) ltac:(set_solver) + with "Hnames") as "[Hnames Hfrom]". + iDestruct (MutexSets.my_mutexes_alloc_mutex_name th + (⊤ ∖ ↑from_namespace) + (↑to_namespace) ltac:(set_solver) + with "Hnames") as "[Hnames Hto]". + iDestruct "Hfrom" as "?". iDestruct "Hto" as "?". + go. iExists (1$m)%cQp, (1$m)%cQp. go. + iDestruct (MutexSets.my_mutexes_join_mutex_name th + (⊤ ∖ ↑from_namespace) + (↑to_namespace) ltac:(set_solver) + with "[$]") as "Hnames". + iDestruct (MutexSets.my_mutexes_join_mutex_name th ⊤ + (↑from_namespace) ltac:(set_solver) + with "[$]") as "Hnames". + iFrame "Hnames". + Qed. + + (* FIXME do we need this? *) + Lemma main_link `{balance_objective : !forall (this : ptr) args, + Objective (@tele_app balance_args (fun _ => mpred) (fun n : Z => this |-> account_balanceR n) args)} : + denoteModule source ** std_recursive_mutex.std_ctor_spec ** std_recursive_mutex.std_dtor_spec ** + std_recursive_mutex.std_lock_spec_alt ** std_recursive_mutex.std_unlock_spec_alt |-- main_spec. + Proof. + work. + wapply main_ok. + wapply ctor_ok. + wapply dtor_ok. + wapply transfer_link. + wapply outer_call_ok. + wapply inner_call_ok. + wapply transfer_lock_guard.ctor_ok. + wapply transfer_lock_guard.dtor_ok. + work. + Qed. +End clients. diff --git a/rocq-brick-libstdcpp/test/shared_mutex/custom_shared_mutex.hpp b/rocq-brick-libstdcpp/test/shared_mutex/custom_shared_mutex.hpp new file mode 100644 index 00000000..bfbd01a2 --- /dev/null +++ b/rocq-brick-libstdcpp/test/shared_mutex/custom_shared_mutex.hpp @@ -0,0 +1,47 @@ +#pragma once +#include +#include + +// Figure 11 of papers/cpp-mutex/stdcpp.pdf, packaged as a class. +// The saturation check avoids signed overflow in the CAS argument val + 1. +class MySharedMutex { + std::atomic sharers{0}; + +public: + MySharedMutex() {} + ~MySharedMutex() {} + + void lock() { + int idle = 0; + while (!sharers.compare_exchange_strong(idle, -1)) + { idle = 0; } + } + + bool try_lock() noexcept { + int idle = 0; + return sharers.compare_exchange_strong(idle, -1); + } + + void unlock() { + sharers = 0; + } + + void lock_shared() { + int val; + do { val = sharers; } + // wait if val=-1 (writer-locked) or val=INT_MAX (so to avoid overflow) + while (val < 0 || val == INT_MAX || + !sharers.compare_exchange_strong(val, val + 1)); + } + + bool try_lock_shared() noexcept { + int val = sharers; + if (val < 0 || val == INT_MAX) + return false; + return sharers.compare_exchange_strong(val, val + 1); + } + + void unlock_shared() { + sharers--; + } +}; diff --git a/rocq-brick-libstdcpp/test/shared_mutex/custom_shared_mutex_hpp_proof.v b/rocq-brick-libstdcpp/test/shared_mutex/custom_shared_mutex_hpp_proof.v new file mode 100644 index 00000000..9e287cab --- /dev/null +++ b/rocq-brick-libstdcpp/test/shared_mutex/custom_shared_mutex_hpp_proof.v @@ -0,0 +1,871 @@ +Require Import iris.algebra.coPset. +Require Import iris.algebra.lib.gmap_view. +Require Import iris.algebra.agree. +Require Import iris.algebra.frac. +Require Import skylabs.auto.cpp.prelude.proof. +Require Import skylabs.brick.libstdcpp.atomic.spec. +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Require Import skylabs.brick.libstdcpp.shared_mutex.shared_mutex. +Require Import skylabs.brick.libstdcpp.test.shared_mutex.custom_shared_mutex_hpp. + +NES.Open std.atomic. + +Import linearity. +#[local] Existing Instance mpred_BiAffine. + +Module CustomSharedMutexPreds <: SHARED_MUTEX_PREDS. + Definition class_name : name := "MySharedMutex"%cpp_name. + Definition cpp_ty : type := Tnamed class_name. + #[global] Arguments cpp_ty /. + #[global] Arguments class_name /. + + (** The invariant holds the auth piece of [holdersR], and each reader/writer + holds a frac. The map is partial, so only active readers & writers are in + the map. An [entry] of holdersR stores (qt, o_qP), where qt is the + caller's token fraction and o_qP is the optional resource fraction: + (Some qP) represents a reader that borrows [P qP] and [None] represents + a writer. + + [reader_map] encodes the fractions when all threads are readers. + + It might be possible to make [holdersR] a map to reader_map and encode + a writer as having a fraction of [1], but we choose to explicitly make + the writer entry (None) to be distinct with reader entry (Some qP) on + the type level. *) + Definition reader_map := gmap thread_idT (cQp.t * Qp). + Definition entry := (cQp.t * option Qp)%type. + Canonical Structure holdersR : cmra := + gmap_viewR thread_idT (agreeR (leibnizO entry)). + + Record mutex_gname : Set := MkGname { + token_gname : iprop.gname; + share_gname : iprop.gname; + holders_gname : iprop.gname; + cinv_gname : iprop.gname; + inv_namespace : namespace; + }. + Definition gname : Set := mutex_gname. + Definition smutex_inv_namespace (g : gname) := g.(inv_namespace). + + Class stateG `{Σ : cpp_logic} := { + #[local] holders_own :: HasOwn (iPropI _Σ) holdersR; + #[local] holders_upd :: HasOwnUpd (iPropI _Σ) holdersR; + #[local] holders_valid :: HasOwnValid (iPropI _Σ) holdersR; + }. + Definition G := @stateG. + Existing Class G. + #[global] Arguments G {_ _} Σ : assert. + #[global] Instance state_G `{Σ : cpp_logic} (H : G Σ) : @stateG _ _ Σ := H. + + Definition token `{Σ : cpp_logic, !G Σ} (g : gname) (q : cQp.t) : mpred := + own g.(token_gname) (q : fracR). + (** [P qP] is always accompanied with [share g qP]: initially the invariant + keeps [share 1 ** P 1]; when a thread borrows [P qP], it also borrows + [share g qP]. This allows to prove writer-writer or writer-reader + exclusivity. *) + Definition share `{Σ : cpp_logic, !G Σ} (g : gname) (q : Qp) : mpred := + own g.(share_gname) (q : fracR). + Definition authority `{Σ : cpp_logic, !G Σ} + (g : gname) (M : gmap thread_idT entry) : mpred := + own g.(holders_gname) + (gmap_view_auth (DfracOwn 1) (to_agree (A := leibnizO entry) <$> M) : holdersR). + Definition holder `{Σ : cpp_logic, !G Σ} + (g : gname) (th : thread_idT) (e : entry) : mpred := + own g.(holders_gname) + (gmap_view_frag th (DfracOwn 1) (to_agree (A := leibnizO entry) e) : holdersR). + + Definition not_locked `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (g : gname) (th : thread_idT) (qt : cQp.t) : mpred := + token g qt ** MutexSets.my_mutexes th + (CoPset $ ↑smutex_inv_namespace g). + Definition locked `{Σ : cpp_logic, !G Σ} {σ : genv} + (this : ptr) (g : gname) (th : thread_idT) (qt : cQp.t) : mpred := + holder g th (qt, None) ** share g 1. + Definition reader_locked `{Σ : cpp_logic, !G Σ} {σ : genv} + (this : ptr) (g : gname) (th : thread_idT) (qt : cQp.t) (qP : Qp) : mpred := + holder g th (qt, Some qP) ** share g qP. + + #[global] Instance token_fractional `{Σ : cpp_logic, !G Σ} g : + CFractional (token g). + Proof. intros q1 q2. rewrite /token cQp.frac_add -frac_op own_op. done. Qed. + #[global] Instance token_timeless `{Σ : cpp_logic, !G Σ} g q : + Timeless (token g q). + Proof. rewrite /token. apply _. Qed. + #[global] Instance share_fractional `{Σ : cpp_logic, !G Σ} g : + Fractional (share g). + Proof. intros q1 q2. rewrite /share -frac_op own_op. done. Qed. + #[global] Instance authority_timeless `{Σ : cpp_logic, !G Σ} g M : + Timeless (authority g M). + Proof. rewrite /authority. apply _. Qed. + #[global] Instance holder_timeless `{Σ : cpp_logic, !G Σ} g th e : + Timeless (holder g th e). + Proof. rewrite /holder. apply _. Qed. + #[global] Instance share_timeless `{Σ : cpp_logic, !G Σ} g q : + Timeless (share g q). + Proof. rewrite /share. apply _. Qed. + + Section ghost_laws. + Context `{Σ : cpp_logic, !G Σ}. + + Lemma full_share_exclusive g q : share g 1 ** share g q |-- False. + Proof. + rewrite /share. iIntros "[A B]". + iDestruct (own_valid_2 with "A B") as %Hvalid. + iPureIntro. move: Hvalid. rewrite frac_op frac_valid. + exact (Qp.not_add_le_l 1 q). + Qed. + + Lemma full_token_exclusive g q : token g 1$m ** token g q |-- False. + Proof. + rewrite /token. iIntros "[A B]". + iDestruct (own_valid_2 with "A B") as %Hvalid. + iPureIntro. move: Hvalid. rewrite frac_op frac_valid. + exact (Qp.not_add_le_l 1 q). + Qed. + + Lemma holder_lookup g M th e : + authority g M ** holder g th e |-- [| M !! th = Some e |]. + Proof. + rewrite /authority /holder. iIntros "[A F]". + iDestruct (own_valid_2 with "A F") as %Hvalid. + apply gmap_view_both_valid in Hvalid as (_ & _ & Heq). + rewrite lookup_fmap in Heq. + destruct (M !! th) as [e'|] eqn:Hlookup; rewrite Hlookup /= in Heq; last inversion Heq. + apply Some_equiv_inj, to_agree_inj, leibniz_equiv in Heq. + subst e'. done. + Qed. + + Lemma holder_insert g M th e : + M !! th = None -> + authority g M |-- (|==> authority g (<[th := e]> M) ** holder g th e). + Proof. + intros Hnone. rewrite /authority /holder fmap_insert. + iIntros "A". iMod (own_update with "A") as "[$ $]"; last done. + apply gmap_view_alloc; [by rewrite lookup_fmap Hnone | done | done]. + Qed. + + Lemma holder_delete g M th e : + authority g M ** holder g th e |-- (|==> authority g (delete th M)). + Proof. + rewrite /authority /holder fmap_delete. iIntros "[A F]". + iMod (own_update_2 with "A F") as "$"; last done. + apply gmap_view_delete. + Qed. + End ghost_laws. + + #[global] Instance not_locked_timeless `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + this g th qt : Timeless (not_locked this g th qt). + Proof. rewrite /not_locked. apply _. Qed. + #[global] Instance not_locked_exclusive `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + this g th : Exclusive1 (not_locked this g th). + Proof. + intros q1 q2. rewrite /not_locked. iIntros "[_ A] [_ B]". + have Hoverlap : ~ ((↑smutex_inv_namespace g : coPset) ## ↑smutex_inv_namespace g). + { have Hne := nclose_non_empty (smutex_inv_namespace g). set_solver. } + iDestruct (MutexSets.my_mutexes_exclusive _ _ _ Hoverlap + with "[$A $B]") as %[]. + Qed. + #[global] Instance locked_timeless `{Σ : cpp_logic, !G Σ} {σ : genv} + this g th qt : Timeless (locked this g th qt). + Proof. rewrite /locked. apply _. Qed. + #[global] Instance reader_locked_timeless `{Σ : cpp_logic, !G Σ} {σ : genv} + this g th qt qP : Timeless (reader_locked this g th qt qP). + Proof. rewrite /reader_locked. apply _. Qed. + #[global] Instance locked_objective `{Σ : cpp_logic, !G Σ} {σ : genv} + this g th qt : Objective (locked this g th qt). + Proof. rewrite /locked /holder /share. apply _. Qed. + #[global] Instance reader_locked_objective `{Σ : cpp_logic, !G Σ} {σ : genv} + this g th qt qP : Objective (reader_locked this g th qt qP). + Proof. rewrite /reader_locked /holder /share. apply _. Qed. + #[global] Instance locked_exclusive `{Σ : cpp_logic, !G Σ} {σ : genv} + this g : Exclusive2 (locked this g). + Proof. + intros th1 th2 q1 q2. rewrite /locked. iIntros "[_ A] [_ B]". + iDestruct (full_share_exclusive with "[$A $B]") as %[]. + Qed. + Lemma reader_writer_exclusive `{Σ : cpp_logic, !G Σ} {σ : genv} + this g th1 th2 qt1 qt2 qP : + th1 <> th2 -> + reader_locked this g th1 qt1 qP -∗ locked this g th2 qt2 -∗ False. + Proof. + intros _. rewrite /reader_locked /locked. iIntros "[_ R] [_ W]". + iApply (full_share_exclusive with "[$W $R]"). + Qed. + + (** sums up the fractions in M and add to initializer [s]. + We need [s] because [Qp] does not have 0. *) + Definition total (M : reader_map) (s : Qp) : Qp := + map_fold (fun _ qr acc => (qr.2 + acc)%Qp) s M. + Definition reader_entries (M : reader_map) : gmap thread_idT entry := + (fun qr => (qr.1, Some qr.2)) <$> M. + + Lemma total_empty s : total ∅ s = s. + Proof. by rewrite /total map_fold_empty. Qed. + Lemma total_insert M th qt r s : M !! th = None -> + total (<[th := (qt,r)]> M) s = (r + total M s)%Qp. + Proof. + intros Hnone. rewrite /total map_fold_insert_L; [done | | done]. + intros j1 j2 [qt1 r1] [qt2 r2] acc Hne H1 H2. simpl. + rewrite !Qp.add_assoc (Qp.add_comm r1 r2). done. + Qed. + Lemma total_add M r s : total M (r+s) = (r + total M s)%Qp. + Proof. + induction M as [|th [qt qP] M Hnone IH] using map_ind. + - by rewrite !total_empty. + - rewrite !total_insert // IH !Qp.add_assoc (Qp.add_comm qP r). done. + Qed. + Lemma total_delete M th qt r s : M !! th = Some (qt,r) -> + total (delete th M) (s+r) = total M s. + Proof. + intros Hlookup. rewrite (Qp.add_comm s r) total_add. + have HM : <[th := (qt,r)]> (delete th M) = M. + { rewrite insert_delete decide_True // insert_id //. } + rewrite -{2}HM. + rewrite total_insert; last apply lookup_delete_eq. + done. + Qed. + + Definition max_readers : Z := 2147483647. + #[global] Arguments max_readers /. + Definition deposits `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (g : gname) (M : reader_map) : mpred := + [∗ map] th ↦ qr ∈ M, not_locked this g th qr.1. + + Definition state `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (g : gname) (P : Qp -> mpred) (n : Z) : mpred := + (* thread [th] is the writer *) + (∃ th qt, [| n = -1 |] ** authority g {[th := (qt,None)]} ** + not_locked this g th qt) ∨ + (* 0 or more readers; [share g s ** P s] remains in the invariant *) + (∃ (M : reader_map) (s : Qp), + [| n = Z.of_nat (size M) /\ n <= max_readers /\ total M s = 1%Qp |] ** + authority g (reader_entries M) ** share g s ** P s ** deposits this g M). + + #[global] Instance state_WeaklyObjective + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this g P n + `{!forall q, Objective (P q)} : WeaklyObjective (state this g P n). + Proof. + rewrite /state /authority /share /deposits /not_locked /token. apply _. + Qed. + + Section state_laws. + Context `{Σ : cpp_logic} {σ : genv}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!G Σ}. + + Lemma state_bounds this g P n : + state this g P n |-- [| -1 <= n <= max_readers |]. + Proof. + rewrite /state. iIntros "[W|R]". + - iDestruct "W" as (th qt) "(% & _)". iPureIntro. unfold max_readers. lia. + - iDestruct "R" as (M s) "(% & _)". iPureIntro. lia. + Qed. + + Lemma deposits_fresh this g M th qt : + deposits this g M ** not_locked this g th qt |-- [| M !! th = None |]. + Proof. + rewrite /deposits. iIntros "[D NL]". + destruct (M !! th) as [qr|] eqn:Hl; last done. + iDestruct (big_sepM_lookup _ _ _ _ Hl with "D") as "Dup". + iDestruct (observe_2 False with "NL Dup") as %[]. + Qed. + + Lemma reader_lookup g M th qt qP : + authority g (reader_entries M) ** holder g th (qt,Some qP) |-- + [| M !! th = Some (qt,qP) |]. + Proof. + iIntros "H". iDestruct (holder_lookup with "H") as %Hl. + rewrite /reader_entries lookup_fmap in Hl. + destruct (M !! th) as [[qt' r]|] eqn:He; rewrite He /= in Hl; simplify_eq; done. + Qed. + + Lemma writer_lookup g th0 qt0 th qt : + authority g {[th0 := (qt0,None)]} ** holder g th (qt,None) |-- + [| th0 = th /\ qt0 = qt |]. + Proof. + iIntros "H". iDestruct (holder_lookup with "H") as %Hl. + apply lookup_singleton_Some in Hl. iPureIntro. naive_solver. + Qed. + + Lemma writer_no_reader g th0 qt0 th qt qP : + authority g {[th0 := (qt0,None)]} ** holder g th (qt,Some qP) |-- False. + Proof. + iIntros "H". iDestruct (holder_lookup with "H") as %Hl. + apply lookup_singleton_Some in Hl. naive_solver. + Qed. + + Lemma state_lock this g P qt th : + state this g P 0 ** not_locked this g th qt |-- + (|==> state this g P (-1) ** P 1%Qp ** locked this g th qt). + Proof. + rewrite /state /locked. iIntros "[[W|R] NL]". + - iDestruct "W" as (th0 qt0) "(% & _)". lia. + - iDestruct "R" as (M s) "(%H & A & S & P & D)". + have Hsize : size M = 0%nat by lia. + have Hempty : M = ∅ by apply map_size_empty_iff. + subst M. rewrite total_empty in H. destruct H as (_ & _ & ->). + iEval (rewrite /reader_entries fmap_empty) in "A". + iMod (holder_insert g ∅ th (qt,None) ltac:(done) with "A") as "[A F]". + iEval (rewrite insert_empty) in "A". iModIntro. iFrame "P F S". + iLeft. iExists th, qt. iFrame. done. + Qed. + + Lemma state_unlock this g P n qt th : + state this g P n ** locked this g th qt ** P 1%Qp |-- + (|==> [| n = -1 |] ** state this g P 0 ** not_locked this g th qt). + Proof. + rewrite /state /locked. iIntros "([W|R] & [F S] & P)". + - iDestruct "W" as (th0 qt0) "(%Hn & A & NL)". + iDestruct (writer_lookup with "[$A $F]") as %[-> ->]. + iMod (holder_delete with "[$A $F]") as "A". + iEval (rewrite delete_singleton decide_True //) in "A". iModIntro. iFrame "NL". iSplit; first done. + iRight. iExists ∅, 1%Qp. + rewrite /reader_entries fmap_empty /deposits big_sepM_empty. + iFrame. iPureIntro. rewrite map_size_empty total_empty. unfold max_readers. repeat split; try done; lia. + - iDestruct "R" as (M s) "(_ & _ & S0 & _)". + iDestruct (full_share_exclusive with "[$S $S0]") as %[]. + Qed. + + Lemma state_lock_shared this g P n qt th : + Fractional P -> 0 <= n < max_readers -> + state this g P n ** not_locked this g th qt |-- + (|==> state this g P (n+1) ** ∃ qP, P qP ** reader_locked this g th qt qP). + Proof. + intros HP Hn. rewrite /state /reader_locked. + iIntros "[[W|R] NL]". + - iDestruct "W" as (th0 qt0) "(% & _)". lia. + - iDestruct "R" as (M s) "(%H & A & S & P & D)". + iDestruct (deposits_fresh with "[$D $NL]") as %Hfresh. + iMod (holder_insert g (reader_entries M) th (qt,Some (s/2)%Qp) + with "A") as "[A F]". + { by rewrite /reader_entries lookup_fmap Hfresh. } + iDestruct "S" as "[S1 S2]". + have Hsplit := @fractional _ _ P HP (s/2)%Qp (s/2)%Qp. + rewrite Qp.div_2 in Hsplit. + iEval (rewrite Hsplit) in "P". + iDestruct "P" as "[P1 P2]". + iModIntro. iSplitR "P2 F S2"; last (iExists (s/2)%Qp; iFrame). + iRight. iExists (<[th := (qt,(s/2)%Qp)]> M), (s/2)%Qp. + rewrite /reader_entries fmap_insert /deposits big_sepM_insert //. + iFrame. iPureIntro. rewrite map_size_insert_None //. + destruct H as (Hcount & Hbound & Htotal). + split; first by rewrite Nat2Z.inj_succ -Hcount; lia. + split; first lia. + rewrite total_insert // -total_add Qp.div_2. exact Htotal. + Qed. + + Lemma state_unlock_shared this g P n qt th qP : + Fractional P -> + state this g P n ** reader_locked this g th qt qP ** P qP |-- + (|==> [| 0 < n <= max_readers |] ** + state this g P (n-1) ** not_locked this g th qt). + Proof. + intros HP. rewrite /state /reader_locked. + iIntros "([W|R] & [F S] & P)". + - iDestruct "W" as (th0 qt0) "(_ & A & _)". + iDestruct (writer_no_reader with "[$A $F]") as %[]. + - iDestruct "R" as (M s) "(%H & A & S0 & P0 & D)". + iDestruct (reader_lookup with "[$A $F]") as %Hl. + iMod (holder_delete with "[$A $F]") as "A". + iEval (rewrite /deposits (big_sepM_delete _ _ _ _ Hl)) in "D". + iDestruct "D" as "[NL D]". + have Hsize : (0 < size M)%nat. + { destruct (decide (M = ∅)) as [->|Hne]; first by rewrite lookup_empty in Hl. + have Hsz : size M <> 0%nat by intro Hz; apply map_size_empty_iff in Hz. + lia. } + iModIntro. iFrame "NL". iSplit; first (iPureIntro; lia). + iRight. iExists (delete th M), (s+qP)%Qp. + rewrite /reader_entries fmap_delete /deposits. + rewrite !fractional. iFrame. + iPureIntro. rewrite map_size_delete_Some; last by eauto. + destruct H as (Hcount & Hbound & Htotal). + split. + + rewrite Nat2Z.inj_pred. + * by rewrite -Z.sub_1_r Hcount. + * exact Hsize. + + split; first lia. + rewrite (total_delete M th qt qP s Hl). exact Htotal. + Qed. + + Lemma deposits_full_token_empty this g M : + token g 1$m ** deposits this g M |-- [| M = ∅ |]. + Proof. + induction M as [|th [qt qP] M Hnone IH] using map_ind; first by iIntros "_". + rewrite /deposits big_sepM_insert // /not_locked /=. + iIntros "[T [[T0 _] _]]". + iDestruct (full_token_exclusive with "[$T $T0]") as %[]. + Qed. + + Lemma state_destroy this g P n : + state this g P n ** token g 1$m |-- [| n = 0 |] ** P 1%Qp. + Proof. + rewrite /state. iIntros "[[W|R] T]". + - iDestruct "W" as (th qt) "(_ & _ & NL)". + iEval (rewrite /not_locked) in "NL". iDestruct "NL" as "[T0 _]". + iDestruct (full_token_exclusive with "[$T $T0]") as %[]. + - iDestruct "R" as (M s) "(%H & _ & _ & P & D)". + iDestruct (deposits_full_token_empty with "[$T $D]") as %->. + rewrite map_size_empty total_empty in H. destruct H as (-> & _ & ->). + iFrame. done. + Qed. + End state_laws. + + Definition mutex_inv `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + (this : ptr) (g : gname) (P : Qp -> mpred) : mpred := + ∃ n : Z, this ,, _field "MySharedMutex::sharers" |-> atomic.R "int" 1$m n ** + state this g P n. + + Definition R `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {HAS_THREADS : HasStdThreads Σ} {σ : genv} + (g : gname) (q : cQp.t) (P : Qp -> mpred) : Rep := + structR class_name q$m ** as_Rep (fun this => + [| Fractional P /\ (forall qP, Objective (P qP)) |] ** + cinv (smutex_inv_namespace g) g.(cinv_gname) (mutex_inv this g P) ** + cinv_own g.(cinv_gname) q). + #[global] Hint Opaque token not_locked locked reader_locked R : + sl_opacity typeclass_instances. + #[only(cfractional,ascfractional,cfracvalid)] derive R. + #[global] Instance R_type_ptr `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} + {HAS_THREADS : HasStdThreads Σ} g q P : + Observe (type_ptrR cpp_ty) (R g q P). + Proof. rewrite /R /cpp_ty. apply _. Qed. + + #[local] Instance at_WeaklyObjective `{Σ : cpp_logic} + (p : ptr) (R : Rep) `{!WeaklyObjective (R p)} : + WeaklyObjective (p |-> R). + Proof. rewrite INTERNAL._at_eq. apply _. Qed. + #[global] Instance mutex_inv_WeaklyObjective + `{Σ : cpp_logic, MUTEX_SETS : !mutex_set_G Σ, !G Σ} {σ : genv} this g P + `{!forall qP, Objective (P qP)} : WeaklyObjective (mutex_inv this g P). + Proof. rewrite /mutex_inv. apply _. Qed. + + Definition globals `{Σ : cpp_logic} {σ : genv} (q : cQp.t) : mpred := + _global "std::memory_order_seq_cst" |-> + primR "enum std::memory_order" q + (memory_order.to_val memory_order.seq_cst). + #[global] Arguments globals /. + +End CustomSharedMutexPreds. + +Module custom_shared_mutex. + Import CustomSharedMutexPreds. + #[local] Existing Instances holders_own holders_upd holders_valid. + Module Spec := shared_mutex_spec CustomSharedMutexPreds. + Abbreviation IR := CustomSharedMutexPreds.R. + Abbreviation BASE p := (p ,, _base "std::atomic" "std::__atomic_base"). + + Section with_Σ. + Context `{Σ : cpp_logic, σ : genv, HAS_THREADS : !HasStdThreads Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!CustomSharedMutexPreds.G Σ}. + Context `{MOD : source ⊧ σ}. + + cpp.spec "MySharedMutex::MySharedMutex()" as ctor_spec with + (\exact Reduce Spec.ctor_spec). + cpp.spec "MySharedMutex::~MySharedMutex()" as dtor_spec with + (\exact Reduce Spec.dtor_spec). + cpp.spec "MySharedMutex::lock()" as lock_spec_alt with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.lock_spec_alt this)). + cpp.spec "MySharedMutex::unlock()" as unlock_spec_alt with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.unlock_spec_alt this)). + cpp.spec "MySharedMutex::lock()" as lock_spec with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.lock_spec this)). + cpp.spec "MySharedMutex::unlock()" as unlock_spec with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.unlock_spec this)). + cpp.spec "MySharedMutex::lock_shared()" as lock_shared_spec with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.lock_shared_spec this)). + cpp.spec "MySharedMutex::unlock_shared()" as unlock_shared_spec with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.unlock_shared_spec this)). + + cpp.spec "MySharedMutex::try_lock()" as try_lock_spec_alt with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.try_lock_spec_alt this)). + cpp.spec "MySharedMutex::try_lock()" as try_lock_spec with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.try_lock_spec this)). + cpp.spec "MySharedMutex::try_lock_shared()" as try_lock_shared_spec with + (\this this \prepost{qg} globals qg + \exact Reduce (Spec.try_lock_shared_spec this)). + + Lemma try_lock_spec_equiv_try_lock_spec_alt : try_lock_spec ⊣⊢ try_lock_spec_alt. + Proof. + rewrite /try_lock_spec /try_lock_spec_alt /specify. + do 2 f_equiv. intros this xs K. rewrite !add_with_equiv. + f_equiv=> qg. + rewrite !add_prepost_equiv Spec.try_lock_spec_equiv_try_lock_spec_alt; done. + Qed. + + Lemma lock_spec_equiv_lock_spec_alt : lock_spec ⊣⊢ lock_spec_alt. + Proof. + rewrite /lock_spec /lock_spec_alt /specify. + do 2 f_equiv. intros this xs K. rewrite !add_with_equiv. + f_equiv=> qg. by rewrite !add_prepost_equiv Spec.lock_spec_equiv_lock_spec_alt. + Qed. + Lemma unlock_spec_equiv_unlock_spec_alt : unlock_spec ⊣⊢ unlock_spec_alt. + Proof. + rewrite /unlock_spec /unlock_spec_alt /specify. + do 2 f_equiv. intros this xs K. rewrite !add_with_equiv. + f_equiv=> qg. by rewrite !add_prepost_equiv Spec.unlock_spec_equiv_unlock_spec_alt. + Qed. + + Definition bi_later_exist_F := [FWD] @bi.later_exist. + Definition bi_later_sep_F := [FWD] @bi.later_sep. + #[local] Hint Resolve bi_later_exist_F bi_later_sep_F : br_hints. + + #[program] + Definition do_load_C (p : ptr) := + \cancelx + \using denoteModule source + \consuming{g q P} p |-> IR g q P + \proving{K (_ : IsExistential K)} + std.atomic.do_load "int" (BASE (p ,, o_field σ "MySharedMutex::sharers")) K + \instantiate K := (fun n : Z => p |-> IR g q P ** + [| -1 <= n <= max_readers |]) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[#M IR]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + iEval (rewrite /IR _at_sep _at_as_Rep) in "IR". + iDestruct "IR" as "(S & %HP & #CI & CO)". + rewrite /std.atomic.do_load. + iAcIntro. rewrite /commit_acc. + iInv (smutex_inv_namespace g) as "Inv" "Hclose". + iDestruct "Inv" as "[Inv CO]". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (n) "(>L & State)". + iDestruct (fupd_mask_subseteq) as ">Y"; [|iModIntro]; first set_solver. + iExists n, (1$m)%cQp. + iSplitL "L"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "L". iMod "Y" as "_". + iDestruct (state_bounds with "State") as %Hbounds. + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists n. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. rewrite /IR _at_sep _at_as_Rep. + iFrame "CI S CO". auto. + Qed. + #[local] Hint Resolve do_load_C : sl_opacity. + + #[program] + Definition do_writer_cas_C (p expected_p : ptr) := + \cancelx + \using denoteModule source + \consuming{g q P} p |-> IR g q P + \consuming{th qt} not_locked p g th qt + \proving{K (_ : IsExistential K)} + std.atomic.do_compare_exchange "int" false + (BASE (p ,, o_field σ "MySharedMutex::sharers")) expected_p 0%Z (-1)%Z K + \instantiate K := (fun b : bool => + (∃ actual : Z, expected_p |-> intR 1$m actual) ** p |-> IR g q P ** + if b then P 1%Qp ** locked p g th qt else not_locked p g th qt) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[#M [IR NL]]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + iEval (rewrite /IR _at_sep _at_as_Rep) in "IR". + iDestruct "IR" as "(S & %HP & #CI & CO)". + rewrite /std.atomic.do_compare_exchange. + iAuIntro1. rewrite /atomic1_acc. + iInv (smutex_inv_namespace g) as "Inv" "Hclose". + iDestruct "Inv" as "[Inv CO]". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (n) "(>L & State)". + iDestruct (fupd_mask_subseteq) as ">Y"; [|iModIntro]; first set_solver. + iExists n. iSplitL "L"; first by ework $usenamed=true with br_erefl. + iSplit. + - iIntros "L". iMod "Y" as "_". + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists n. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. iFrame. + - iNext. iIntros (b n') "[%Hres L]". iMod "Y" as "_". + destruct Hres as [(-> & -> & ->)|(-> & Hne & ->)]. + + iMod (state_lock with "[$State $NL]") as "(State & P & Locked)". + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists (-1)%Z. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. iIntros "Expected". + rewrite /IR _at_sep _at_as_Rep. iFrame "CI S CO P Locked". + iSplitL "Expected"; first (iExists 0%Z; iFrame). done. + + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists n. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. iIntros "Expected". + rewrite /IR _at_sep _at_as_Rep. iFrame "CI S CO NL". + iSplitL "Expected"; first (iExists n; iFrame). done. + Qed. + #[local] Hint Resolve do_writer_cas_C : sl_opacity. + + #[program] + Definition do_reader_cas_C (p expected_p : ptr) (expected : Z) := + \cancelx + \using denoteModule source + \consuming{g q P} p |-> IR g q P + \consuming{th qt} not_locked p g th qt + \guard (0 <= expected < max_readers) + \proving{K (_ : IsExistential K)} + std.atomic.do_compare_exchange "int" false + (BASE (p ,, o_field σ "MySharedMutex::sharers")) + expected_p expected (expected+1)%Z K + \instantiate K := (fun b : bool => + (∃ actual : Z, expected_p |-> intR 1$m actual) ** p |-> IR g q P ** + if b then (∃ qP, P qP ** reader_locked p g th qt qP)%I + else not_locked p g th qt) + \end@{mpredI}. + Next Obligation. + intros. have Hrange : 0 <= expected < max_readers by assumption. + iIntros "[#M [IR NL]]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + iEval (rewrite /IR _at_sep _at_as_Rep) in "IR". + iDestruct "IR" as "(S & %HP & #CI & CO)". + rewrite /std.atomic.do_compare_exchange. + iAuIntro1. rewrite /atomic1_acc. + iInv (smutex_inv_namespace g) as "Inv" "Hclose". + iDestruct "Inv" as "[Inv CO]". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (n) "(>L & State)". + iDestruct (fupd_mask_subseteq) as ">Y"; [|iModIntro]; first set_solver. + iExists n. iSplitL "L"; first by ework $usenamed=true with br_erefl. + iSplit. + - iIntros "L". iMod "Y" as "_". + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists n. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. iFrame. + - iNext. iIntros (b n') "[%Hres L]". iMod "Y" as "_". + destruct Hres as [(-> & -> & ->)|(-> & Hne & ->)]. + + iMod (state_lock_shared _ _ _ _ _ _ (proj1 HP) Hrange + with "[$State $NL]") as "[State Result]". + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists (expected+1)%Z. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. iIntros "Expected". + rewrite /IR _at_sep _at_as_Rep. iFrame "CI S CO Result". + iSplitL "Expected"; first (iExists expected; iFrame). done. + + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists n. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. iIntros "Expected". + rewrite /IR _at_sep _at_as_Rep. iFrame "CI S CO NL". + iSplitL "Expected"; first (iExists n; iFrame). done. + Qed. + #[local] Hint Resolve do_reader_cas_C : sl_opacity. + + #[program] + Definition do_store_C (p : ptr) := + \cancelx + \using denoteModule source + \consuming{g q P} p |-> IR g q P + \consuming{th qt} locked p g th qt + \consuming P 1%Qp + \proving{K (_ : IsExistential K)} + std.atomic.do_store "int" (BASE (p ,, o_field σ "MySharedMutex::sharers")) 0%Z K + \instantiate K := (p |-> IR g q P ** not_locked p g th qt) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[#M (IR & Locked & P)]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + iEval (rewrite /IR _at_sep _at_as_Rep) in "IR". + iDestruct "IR" as "(S & %HP & #CI & CO)". + rewrite /std.atomic.do_store. + iAcIntro. rewrite /commit_acc. + iInv (smutex_inv_namespace g) as "Inv" "Hclose". + iDestruct "Inv" as "[Inv CO]". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (n) "(>L & State)". + iDestruct (fupd_mask_subseteq) as ">Y"; [|iModIntro]; first set_solver. + iExists n. iSplitL "L"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "L". iMod "Y" as "_". + iMod (state_unlock with "[$State $Locked $P]") as "(_ & State & NL)". + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists 0%Z. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. rewrite /IR _at_sep _at_as_Rep. iFrame "CI S CO NL". done. + Qed. + #[local] Hint Resolve do_store_C : sl_opacity. + + #[program] + Definition do_decrement_C (p : ptr) := + \cancelx + \using denoteModule source + \consuming{g q P} p |-> IR g q P + \consuming{th qt qP} reader_locked p g th qt qP + \consuming P qP + \proving{K (_ : IsExistential K)} + (* FIXME std.atomic.do_op is supposed to be local to the atomic module. + Is it fine to use it here directly? *) + std.atomic.do_op "int" (atomic_eval int_rank.Iint Signed Z.add (-1)) + (BASE (p ,, o_field σ "MySharedMutex::sharers")) K + \instantiate K := (fun _ : Z => p |-> IR g q P ** not_locked p g th qt) + \end@{mpredI}. + Next Obligation. + intros. iIntros "[#M (IR & Locked & P)]" (?? ->). + iDestruct (observe [| _ ⊧ _ |] with "M") as "%". + iEval (rewrite /IR _at_sep _at_as_Rep) in "IR". + iDestruct "IR" as "(S & %HP & #CI & CO)". + rewrite /std.atomic.do_op. + iAcIntro. rewrite /commit_acc. + iInv (smutex_inv_namespace g) as "Inv" "Hclose". + iDestruct "Inv" as "[Inv CO]". + iEval (rewrite /mutex_inv) in "Inv". + iDestruct "Inv" as (n) "(>L & State)". + iDestruct (fupd_mask_subseteq) as ">Y"; [|iModIntro]; first set_solver. + iExists n. iSplitL "L"; first by ework $usenamed=true with br_erefl. + iNext. iIntros "L". iMod "Y" as "_". + iMod (state_unlock_shared _ _ _ _ _ _ _ (proj1 HP) + with "[$State $Locked $P]") as "(%Hbounds & State & NL)". + have Hop : atomic_eval int_rank.Iint Signed Z.add (-1) n = (n-1)%Z. + { unfold atomic_eval. rewrite to_signed_id; unfold max_readers in *; simpl in *; lia. } + iEval (rewrite Hop) in "L". + iMod ("Hclose" with "[L State]") as "_". + { iNext. rewrite /mutex_inv. iExists (n-1)%Z. + iSplitL "L"; first by ework $usenamed=true with br_erefl. iFrame. } + iModIntro. rewrite /IR _at_sep _at_as_Rep. iFrame "CI S CO NL". done. + Qed. + #[local] Hint Resolve do_decrement_C : sl_opacity. + + Lemma mysharedmutex_lock_alt_proof : verify[source] lock_spec_alt. + Proof using MOD HAS_THREADS. + verify_spec; go. + wp_while (fun _ => emp); go. + ework with br_erefl. + go. wp_if; go. + Qed. + + Lemma mysharedmutex_unlock_alt_proof : verify[source] unlock_spec_alt. + Proof using MOD HAS_THREADS. + verify_spec; go. + Qed. + + Lemma mysharedmutex_lock_shared_proof : verify[source] lock_shared_spec. + Proof using MOD HAS_THREADS. + verify_spec; go. + wname [_ |-> uninitR _ _] "Val". + iDestruct (_at_mono _ _ (uninitR_anyR "int" 1$m) with "Val") as "?". + wp_do_while (fun _ => emp); go. + repeat (first [case_bool_decide | match goal with b : bool |- _ => destruct b end | wp_if]; go). + all: try (ework with br_erefl; go). + all: repeat (first [case_bool_decide | match goal with b : bool |- _ => destruct b end | wp_if]; go). + all: try match goal with + | Hb : -1 <= ?v |- _ => + have Hrange : 0 <= v < max_readers by unfold max_readers; lia + end. + all: try (ework with br_erefl; go). + all: repeat (first [case_bool_decide | match goal with b : bool |- _ => destruct b end | wp_if]; go). + all: ework with br_erefl. + all: go. + all: repeat (first [case_bool_decide | match goal with b : bool |- _ => destruct b end | wp_if]; go). + all: exfalso; simpl in *; congruence. + Unshelve. all: exact 1%Qp. + Qed. + + Lemma mysharedmutex_try_lock_alt_proof : verify[source] try_lock_spec_alt. + Proof using MOD HAS_THREADS. + verify_spec; go. + Qed. + + Lemma mysharedmutex_try_lock_proof : verify[source] try_lock_spec. + Proof using MOD HAS_THREADS. + rewrite try_lock_spec_equiv_try_lock_spec_alt. exact mysharedmutex_try_lock_alt_proof. + Qed. + + Lemma mysharedmutex_try_lock_shared_proof : verify[source] try_lock_shared_spec. + Proof using MOD HAS_THREADS. + verify_spec; go. + repeat (first [case_bool_decide | match goal with b : bool |- _ => destruct b end | wp_if]; go). + all: try match goal with + | Hb : -1 <= ?v |- _ => + have Hrange : 0 <= v < max_readers by unfold max_readers; lia + end. + all: try (ework with br_erefl; go). + all: repeat (first [case_bool_decide | match goal with b : bool |- _ => destruct b end | wp_if]; go). + Unshelve. all: exact 1%Qp. + Qed. + + Lemma mysharedmutex_unlock_shared_proof : verify[source] unlock_shared_spec. + Proof using MOD HAS_THREADS. + verify_spec; go. + Qed. + + Lemma mysharedmutex_ctor_proof : verify[source] "MySharedMutex::MySharedMutex()". + Proof using MOD HAS_THREADS. + verify_spec. + (* Retain the body while the member constructor initializes the counter; + its update modality permits allocation of the mutex invariant. *) + match goal with |- context[wp ?tu ?rho ?s ?K] => + rewrite (lock (wp tu rho s K)) end. + go. rewrite -lock. iApply fupd_wp. + wname [structR] "S". + wname [P] "P". + wname [_ |-> atomic.R _ _ _] "L". + iMod (own_alloc (1%Qp : fracR)) as (gt) "T"; first done. + iMod (own_alloc (1%Qp : fracR)) as (gs) "Share"; first done. + iMod (own_alloc (gmap_view_auth (DfracOwn 1) ∅ : holdersR)) as (gh) "A". + { apply gmap_view_auth_valid. } + iMod (cinv_alloc ⊤ N + (mutex_inv this (MkGname gt gs gh 1%positive N) P) + with "[L P Share A]") as (gi) "[#CI CO]"; last first. + - iModIntro. go. iExists (MkGname gt gs gh gi N). + rewrite /IR /R _at_sep _at_as_Rep /token /smutex_inv_namespace /=. + iFrame "CI S CO T". auto. + - iNext. rewrite /mutex_inv /state. iExists 0%Z. iFrame "L". + iRight. iExists ∅, 1%Qp. + rewrite /authority /share /reader_entries fmap_empty /deposits big_sepM_empty /=. + iFrame "P Share A". iPureIntro. + rewrite map_size_empty total_empty. unfold max_readers. repeat split; try done; lia. + Qed. + + Lemma mysharedmutex_dtor_proof : verify[source] "MySharedMutex::~MySharedMutex()". + Proof using MOD HAS_THREADS. + verify_spec. + rewrite /IR /R /mutex_inv. + work. + wname [cinv] "#CI". + wname [cinv_own] "CO". + wname [CustomSharedMutexPreds.token] "T". + iMod (cinv_cancel with "CI CO") as "Inv"; [done..|]. + go. + iDestruct "Inv" as (n) "[L State]". + iDestruct (state_destroy with "[$State $T]") as "[%Hn P]". + subst n. ego $usenamed=true with br_erefl. + Qed. + + Lemma mysharedmutex_lock_proof : verify[source] lock_spec. + Proof using MOD HAS_THREADS. + rewrite lock_spec_equiv_lock_spec_alt. exact mysharedmutex_lock_alt_proof. + Qed. + Lemma mysharedmutex_unlock_proof : verify[source] unlock_spec. + Proof using MOD HAS_THREADS. + rewrite unlock_spec_equiv_unlock_spec_alt. exact mysharedmutex_unlock_alt_proof. + Qed. + + Definition standard_library_specs : mpred := + std.atomic.ctor "int" ** std.atomic.dtor "int" ** + std.atomic.compare_exchange_strong_1 "int" ** + std.atomic.cast "int" ** std.atomic.assign "int" ** + std.atomic.fetch_unop "int" (op_name := OOMinusMinus). + + Lemma link : + denoteModule source ** standard_library_specs |-- + ctor_spec ** dtor_spec ** lock_spec ** unlock_spec ** + lock_shared_spec ** unlock_shared_spec ** try_lock_spec ** try_lock_shared_spec. + Proof using MOD HAS_THREADS. + rewrite /standard_library_specs. + work. + wapply mysharedmutex_ctor_proof. + wapply mysharedmutex_dtor_proof. + wapply mysharedmutex_lock_proof. + wapply mysharedmutex_unlock_proof. + wapply mysharedmutex_lock_shared_proof. + wapply mysharedmutex_unlock_shared_proof. + wapply mysharedmutex_try_lock_proof. + wapply mysharedmutex_try_lock_shared_proof. + work. + Qed. + End with_Σ. +End custom_shared_mutex. diff --git a/rocq-brick-libstdcpp/test/shared_mutex/test_cpp_proof.v b/rocq-brick-libstdcpp/test/shared_mutex/test_cpp_proof.v index 2e4c2d8f..ac8679f7 100644 --- a/rocq-brick-libstdcpp/test/shared_mutex/test_cpp_proof.v +++ b/rocq-brick-libstdcpp/test/shared_mutex/test_cpp_proof.v @@ -1,30 +1,78 @@ Require Import skylabs.auto.cpp.prelude.proof. -Require Import skylabs.brick.libstdcpp.lib.lock_ghost. -Require Import skylabs.brick.libstdcpp.shared_mutex.shared_mutex. (* XXX *) +Require Import skylabs.brick.libstdcpp.lib.lock_ghost2. +Require Import skylabs.brick.libstdcpp.shared_mutex.shared_mutex. Require Import skylabs.brick.libstdcpp.test.shared_mutex.test_cpp. Section with_cpp. Context `{Σ : cpp_logic, σ : genv} {HAS_THREADS : HasStdThreads Σ}. - Context `{LOCKED : !shared_mutex.lockedG Σ}. + Context `{MUTEX_SETS : !mutex_set_G Σ}. + Context `{!shared_mutex.G Σ}. - (* TODO: hacky spec *) - cpp.spec "test_shared_mutex()" as test_mutex_spec from source with ( - (* should be unnecessary *) + cpp.spec "test_shared_mutex()" as test_shared_mutex_spec from source with ( \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) \post emp). - Lemma test_mutex_ok : + Lemma test_shared_mutex_ok : verify[source] "test_shared_mutex()". Proof. verify_spec; go. - iExists (const emp); simpl. - iSplit; first work. - iIntros "[% [? H]]". - iMod (lock_ghost.login thr _ empty with "H") as "[??]"; first set_solver. - wuntil Kfree go. - rewrite /Kfree/=. - iMod (lock_ghost.logout thr _ empty with "[$]") as "?"; first set_solver. + iExists (nroot .@@ "test_shared_mutex"), (const emp); go. + rename t into g. + iDestruct (MutexSets.my_mutexes_alloc_mutex_name + thr ⊤ + (↑shared_mutex.smutex_inv_namespace g) ltac:(set_solver) + with "[$]") as "[Hrest Hname]". + wname [shared_mutex.token] "Htoken". + iAssert (shared_mutex.not_locked m_addr g thr 1$m) + with "[Htoken Hname]" as "Hnotlocked". + { rewrite /shared_mutex.not_locked. iFrame. } + iDestruct "Hnotlocked" as "?". + iExists (1$m)%cQp, (g, const emp), + (shared_mutex.locked m_addr g thr 1$m), (1$m)%cQp. go. + iExists (1$m)%cQp, (g, const emp), + (shared_mutex.not_locked m_addr g thr 1$m), (1$m)%cQp. + go. + wname [shared_mutex.not_locked] "Hnotlocked". + iEval (rewrite /shared_mutex.not_locked) in "Hnotlocked". + iDestruct "Hnotlocked" as "[Htoken Hname]". + iDestruct (MutexSets.my_mutexes_join_mutex_name + thr ⊤ + (↑shared_mutex.smutex_inv_namespace g) ltac:(set_solver) + with "[Hrest Hname]") as "Hnames"; first iFrame. + go $usenamed=true. + Qed. + + cpp.spec "test_shared_mutex_shared()" as test_mutex_shared_spec from source with ( + \persist{thr} current_thread thr + \prepost MutexSets.my_mutexes thr (coPset.CoPset ⊤) + \post emp). + + Lemma test_shared_mutex_shared : + verify[source] "test_shared_mutex_shared()". + Proof. + verify_spec; go. + iExists (nroot .@@ "test_shared_mutex_shared"), (const emp); go. + rename t into g. + iDestruct (MutexSets.my_mutexes_alloc_mutex_name + thr ⊤ + (↑shared_mutex.smutex_inv_namespace g) ltac:(set_solver) + with "[$]") as "[Hrest Hname]". + wname [shared_mutex.token] "Htoken". + iAssert (shared_mutex.not_locked m_addr g thr 1$m) + with "[Htoken Hname]" as "Hnotlocked". + { rewrite /shared_mutex.not_locked. iFrame. } + iDestruct "Hnotlocked" as "?". + go. + wname [shared_mutex.not_locked] "Hnotlocked". + iEval (rewrite /shared_mutex.not_locked) in "Hnotlocked". + iDestruct "Hnotlocked" as "[Htoken Hname]". + iDestruct (MutexSets.my_mutexes_join_mutex_name + thr ⊤ + (↑shared_mutex.smutex_inv_namespace g) ltac:(set_solver) + with "[Hrest Hname]") as "Hnames"; first iFrame. + go $usenamed=true. Qed. (* TODO prove the rest of the tests *)