` feature provides a way to split P models for a large system into subprojects that can share models.
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/announcement.md b/Docs/docs/advanced/PVerifierLanguageExtensions/announcement.md
index ba55082cf0..c7cf2250e3 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/announcement.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/announcement.md
@@ -1,3 +1,5 @@
+## PVerifier: Formal Proofs for P Programs
+
# Announcing the New Verification Backend for P
We are excited to announce the release of a new verification backend as part of the 3.0 release of the P programming language! This backend, which we call the P Verifier, allows you to prove that your systems behave correctly under all possible scenarios. The P Verifier is based on Mora et al. ([OOPSLA '23](https://dl.acm.org/doi/10.1145/3622876)) and uses UCLID5 (Polgreen et al., [CAV '22](https://dl.acm.org/doi/10.1007/978-3-031-13185-1_27)).
@@ -8,12 +10,16 @@ The new verification backend allows users to **prove** that their system design
Formal verification is important to AWS’s software correctness program (Brooker and Desai, [Queue '25](https://doi.org/10.1145/3712057)). Several formal tools have had successful applications within AWS in their respective domains. The new verification backend in P gives users the benefits of correctness proofs for the domain of distributed systems design, all while preserving the existing benefits of systematic testing.
+---
+
## Getting Started and Tutorial
To start using the P Verifier, you must install P along with the verification dependencies (UCLID5 and an SMT solver like Z3). Detailed installation instructions are available [here](install-pverifier.md); simple usage instructions are available [here](using-pverifier.md).
To help you get acquainted with the new verification features, we have prepared a comprehensive tutorial that walks you through the formal verification of a simplified two-phase commit (2PC) protocol. This tutorial covers the key concepts and steps of using the verification backend. You can find the tutorial [here](twophasecommitverification.md).
+---
+
## Industrial Application Inside Amazon Web Services
The two-phase commit protocol described in the tutorial is deliberately simplified to help new users get started. In that protocol, one coordinator works with a fixed set of participants to agree on a single boolean value. Industrial systems, however, call for a number of generalizations.
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/init-condition.md b/Docs/docs/advanced/PVerifierLanguageExtensions/init-condition.md
index 331b5b8031..8080f6cd96 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/init-condition.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/init-condition.md
@@ -1,3 +1,5 @@
+## Init Conditions
+
# Initialization Conditions
Initialization conditions let us constrain the kinds of systems that we consider for formal verification. You can think of these as constraints that P test harnesses have to satisfy to be considered valid.
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/install-pverifier.md b/Docs/docs/advanced/PVerifierLanguageExtensions/install-pverifier.md
index e6b19bda1b..d462ff4dfc 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/install-pverifier.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/install-pverifier.md
@@ -1,3 +1,5 @@
+## Installing PVerifier
+
# Install Instructions for Amazon Linux
PVerifier requires several dependencies to be installed. Follow the steps below to set up your environment.
@@ -5,6 +7,8 @@ PVerifier requires several dependencies to be installed. Follow the steps below
!!! success ""
After each step, please use the troubleshooting check to ensure that each installation step succeeded.
+---
+
### [Step 1] Install Java 11
```sh
@@ -20,6 +24,8 @@ sudo yum install java-11-amazon-corretto-devel maven
If you get `java` command not found error, most likely, you need to add the path to `java` in your `PATH`.
+---
+
### [Step 2] Install SBT
```sh
@@ -36,6 +42,8 @@ sudo yum install sbt
If you get `sbt` command not found error, most likely, you need to add the path to `sbt` in your `PATH`.
+---
+
### [Step 3] Install .NET 8.0
Install .NET 8.0 from Microsoft:
@@ -60,6 +68,8 @@ The purpose of copying the .NET distribution into `/usr/share/dotnet` is to make
export DOTNET_ROOT=$HOME/.dotnet
```
+---
+
### [Step 4] Install Z3
```sh
@@ -103,6 +113,8 @@ Note:
to install gcc10-gcc and gcc10-g++ and replace the string `gcc` with `gcc10-` in `config.mk`.
+---
+
### [Step 5] Install UCLID5
```sh
@@ -130,6 +142,8 @@ Note:
If you get `uclid` command not found error, most likely, you need to add the path to `uclid` in your `PATH`.
+---
+
### [Step 6] Install PVerifier
The following steps will build P with PVerifier by running the regular P build on the PVerifier branch of the repository.
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/outline.md b/Docs/docs/advanced/PVerifierLanguageExtensions/outline.md
index 46cc068ecf..440f4bcd42 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/outline.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/outline.md
@@ -1,3 +1,5 @@
+## PVerifier Outline
+
!!! tip ""
**We recommend that you start with the [Tutorials](../../tutsoutline.md) to get familiar with
the P language and its tool chain.**
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/proof.md b/Docs/docs/advanced/PVerifierLanguageExtensions/proof.md
index b64ff04fe4..3416225611 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/proof.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/proof.md
@@ -1,3 +1,5 @@
+## Lemmas and Proofs
+
# Lemmas and Proof Scripts
Lemmas and proof scripts go hand in hand in the P Verifier. Lemmas allow you to decompose specifications and proof scripts allow you to relate lemmas to write larger proofs.
@@ -90,6 +92,8 @@ Where `targetN` are the names of lemmas or invariants to verify, and `helper` is
}
```
+---
+
### Benefits of Proof Scripts
1. **Organization**: Break down complex proofs into manageable parts
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/pure.md b/Docs/docs/advanced/PVerifierLanguageExtensions/pure.md
index 7bce70f766..cc19354e69 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/pure.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/pure.md
@@ -1,3 +1,5 @@
+## Pure Functions
+
??? note "P Pure Function Declaration Grammar"
```
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/specification.md b/Docs/docs/advanced/PVerifierLanguageExtensions/specification.md
index 76f2343f24..3e2dc34149 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/specification.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/specification.md
@@ -1,3 +1,5 @@
+## Specifications
+
# Specifications
The P verifier adds three kinds of specifications: global invariants, loop invariants, and function contracts. We cover each type in its own subsection below.
@@ -83,6 +85,8 @@ P provides special predicates for specifying message state:
- `inflight e`: True if message `e` has been sent but not yet received
- `sent e`: True if message `e` has been sent (regardless of whether it has been received)
+---
+
### Quantifiers
P supports several quantifier expressions for specifying properties over collections:
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/twophasecommitverification.md b/Docs/docs/advanced/PVerifierLanguageExtensions/twophasecommitverification.md
index 7f5889ef66..3834dbb057 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/twophasecommitverification.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/twophasecommitverification.md
@@ -1,4 +1,4 @@
-# Introduction to Formal Verification in P
+## Introduction to Formal Verification in P
This tutorial describes the formal verification features of P through an example. We assume that the reader has P installed along with the verification dependencies (i,e., UCLID5 and Z3). Installation instructions are available [here](install-pverifier.md).
@@ -6,7 +6,9 @@ When using P for formal verification, our goal is to show that no execution of a
To get a sense of these differences, and to cover the new features in P for verification, we will verify a simplified 2PC protocol. The P tutorial already describes a 2PC protocol, but we will make some different modeling choices that will make the verification process easier. In particular, we will follow the modeling decisions made by [Zhang et al.](https://www.usenix.org/system/files/osdi24-zhang-nuda.pdf) in their running example.
-## 2PC Model
+---
+
+## :material-state-machine:{ .lg } 2PC Model
The 2PC protocol consists of two types of machines: coordinator and participant. There is a single coordinator, who receives requests from the outside world, and a set of participants, that must agree on whether to accept or reject the request. If any participant wants to reject the request, they must all agree to reject the request; if all participants want to accept the request, then they must all agree to accept the request. The job of the coordinator is to mediate this agreement.
To accomplish this, the system executes the following steps:
@@ -129,7 +131,9 @@ on eAbort do {goto Rejected;}
We use a function called "preference" to decide whether to vote yes or no on a transaction. We also use a function, "coordinator," to get the address of the coordinator machine.
-## Pure Functions
+---
+
+## :material-function:{ .lg } Pure Functions
The 2PC model described above uses three special functions, `participants`, `coordinator`, and `preference`, that capture the set of participants, the coordinator in charge, and the preference of individual participants for the given request. In this simple system, there is always one coordinator and a fixed set of participants, but we want the proof to work for any function that satisfies those conditions. In P, we can use the new concept of "pure" functions to model this (what SMT-LIB calls functions). Specifically, we can declare the three special functions as follows.
@@ -141,7 +145,9 @@ pure preference(m: machine) : bool;
The participants function is a pure function with no body that takes no argument and returns a set of machines. The coordinator function is similar but only returns a single machine. The preference function, which also has no body, takes a machine and returns a preference. We call these functions "pure" because they can have no side-effects and behave like mathematical functions (e.g., calling the same pure function twice with the same arguments must give the same result). When pure functions do not have bodies, they are like foreign functions that we can guarantee don't have side-effects. When pure functions do have bodies, the bodies must be side-effect-free expressions.
-## Initialization Conditions
+---
+
+## :material-cog-outline:{ .lg } Initialization Conditions
We want our model to capture many different system configurations (e.g., number of participants) but not all configurations are valid. For example, we want to constrain the `participants` function to only point to participant machines. Initialization conditions let us constrain the kinds of systems that we consider. You can think of these as constraints that P test harnesses have to satisfy to be considered valid.
@@ -160,13 +166,17 @@ init-condition forall (c: Coordinator) :: c.yesVotes == default(set[machine]);
When we write a proof of correctness later in this tutorial, we will be restricting the systems that we consider to those that satisfy the initialization conditions listed above.
-## Quantifiers and Machine Types
+---
+
+## :material-code-braces:{ .lg } Quantifiers and Machine Types
Our initialization conditions contain two new P features: the `init` keyword, and quantified expressions (`forall` and `exists`). Even more interesting, one quantifier above is over a machine subtype (`coordinator`).
In P, the only way to dereference a machine variable inside of a specification (like the `init-condition`s above) is by specifically quantifying over that machine type. In other words, `forall (c: Coordinator) :: c.yesVotes == default(set[machine])` is legal but `forall (c: machine) :: c.yesVotes == default(set[machine])` is not, even though they might appear to be similar. The reason for this is that selecting (using the `.` operator) on an incorrect subtype (e.g., trying to get `yesVotes` from a participant machine) is undefined. Undefined behavior in formal verification can lead to surprising results that can be really hard to debug, so in P we syntactically disallow this kind of undefined behavior altogether.
-## P's Builtin Specifications And Our First Proof Attempt
+---
+
+## :material-shield-check:{ .lg } P's Builtin Specifications And Our First Proof Attempt
Using the code described above, and by setting the target to `PVerifier` in the `.pproj` file, you can now run the verification engine for the first time (execute `p compile`). This run will result in a large list of failures, containing items like `❌ Failed to verify that Coordinator never receives eVoteReq in Init`. These failures represent P's builtin requirements that all events are handled. They also give us a glimpse into how verification by induction works.
@@ -176,7 +186,9 @@ When we ran our verification engine it reported that it failed to prove that all
The verification engine is unable to prove these properties not because the system is incorrect, but rather because it needs help from the user to complete it's proof: it needs the user to define the _good_ states. More formally, it needs the user to define an inductive invariant that implies that no builtin P specification is violated.
-## Invariants And Our First Proof
+---
+
+## :material-check-decagram:{ .lg } Invariants And Our First Proof
Users can provide invariants to P using the `invariant` keyword. The goal is to find a set of invariants whose conjunction is inductive and implies the desired property. For now, the desired property is that no builtin P specification is violated.
@@ -248,7 +260,9 @@ After adding similar loop invariants to all three loops in the model, we can re-
Notice that our initial model uses `ignore` statements that we did not describe when introducing the model. If we remove these statements, the verification engine will not be able to prove that no builtin P specification is violated. In some cases, that is because the ignore statements are necessary. For example, it is possible for the coordinator to receive a "yes" or "no" vote when it is in the `Aborted` state. The `ignore` keyword lets us tell the verifier that we have thought of these cases.
-## Invariant Groups, Proof Scripts, and Proof Caching
+---
+
+## :material-check-decagram:{ .lg } Invariant Groups, Proof Scripts, and Proof Caching
When writing larger proofs, it will become useful to group invariants into lemmas, and then to tell the verifier how to use these lemmas for its proof checking. This helps the user organize their proofs; it helps the verifier construct smaller, more stable queries; and it helps avoid checking the same queries over and over, through caching.
@@ -299,7 +313,9 @@ This proof script has two steps. First it says that we need to prove that the le
Notice that the first time you run this verification it will take much longer than the second time. That is because the proof is cached and the solver is not executed the second time. If we don't change our model or our lemma, these queries will never be executed again.
-## First 2PC Specification and Proof
+---
+
+## :material-shield-check:{ .lg } First 2PC Specification and Proof
Proving that the builtin P specifications are satisfied is all well and good, but it isn't exactly the most interesting property. Zhang et al. provide a more exciting specification that we can verify ("2PC-Safety"). Translated into P, their specification looks like the following invariant.
@@ -368,7 +384,9 @@ Showing that the verification passes. Notice that if you remove any of the invar
❓ Failed to verify invariant kondo: a6 at PSrc/System.p:27:12
```
-## Recap and Next Steps
+---
+
+## :material-flag-checkered:{ .lg } Recap and Next Steps
In this tutorial, we formally verified a simplified 2PC protocol in P. The full, final code for the verification is available [here](https://github.com/p-org/P/tree/dev_p3.0/pverifier/Tutorial/Advanced/2_TwoPhaseCommitVerification/Single/PSrc/System.p).
diff --git a/Docs/docs/advanced/PVerifierLanguageExtensions/using-pverifier.md b/Docs/docs/advanced/PVerifierLanguageExtensions/using-pverifier.md
index f5c5ccd7bb..fb2c0fd65d 100644
--- a/Docs/docs/advanced/PVerifierLanguageExtensions/using-pverifier.md
+++ b/Docs/docs/advanced/PVerifierLanguageExtensions/using-pverifier.md
@@ -1,3 +1,5 @@
+## Using PVerifier
+
!!! check ""
Before moving forward, we assume that you have successfully installed
the [PVerifier](install-pverifier.md).
@@ -17,6 +19,8 @@ In this section, we provide an overview of the steps involved in verifying a P p
cd /Tutorial/2_TwoPhaseCommit
```
+---
+
### Verifying a P program
To verify a P program using the PVerifier, you need to:
diff --git a/Docs/docs/advanced/debuggingerror.md b/Docs/docs/advanced/debuggingerror.md
index ff36ae9820..fcbbedf2a7 100644
--- a/Docs/docs/advanced/debuggingerror.md
+++ b/Docs/docs/advanced/debuggingerror.md
@@ -1,6 +1,9 @@
-!!! info "If you are using an older P version 1.x.x, please find the usage guide [here](../old/advanced/debuggingerror.md)"
+## Debugging Error Traces
-As described in the [using P compiler and checker](../getstarted/usingP.md) section, running the following command for the ClientServer example finds an error.
+!!! info "Looking for P 1.x?"
+ If you are using an older P version 1.x.x, please find the usage guide [here](../old/advanced/debuggingerror.md).
+
+As described in the [using P compiler and checker](../getstarted/usingP.md) section, running the following command for the ClientServer example finds an error:
```shell
p check -tc tcAbstractServer -s 100
@@ -11,8 +14,8 @@ p check -tc tcAbstractServer -s 100
$ p check -tc tcAbstractServer -s 100
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
- .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
+ .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
.. Test case :: tcAbstractServer
... Checker is using 'random' strategy (seed:3584517644).
..... Schedule #1
@@ -38,35 +41,45 @@ p check -tc tcAbstractServer -s 100
~~ [PTool]: Thanks for using P! ~~
```
-The P checker on finding a bug generates two artifacts (highlighted in the expected output above):
+---
+
+### Bug Artifacts
+
+The P checker on finding a bug generates two artifacts:
-1. A **textual trace file** (e.g., `ClientServer_0_0.txt`) that has the readable error trace representing the
-sequence of steps from the intial state to the error state.
-2. A **schedule file** (e.g., `ClientServer_0_0.schedule`) that can be used to replay the error
-trace and single step through the P program with the generated error trace for debugging.
+| Artifact | Description |
+|----------|-------------|
+| **Textual trace** (`*.txt`) | Readable error trace showing the sequence of steps (messages sent, received, machines created) from the initial state to the error state |
+| **Schedule file** (`*.schedule`) | Can be used to replay the error trace and single-step through the P program for debugging |
-### Error Trace
+---
-The `*.txt` file contains a textual error trace representing the sequence of steps (i.e, messages sent, messages received, machines created) from the initial state to the final error state. In the end of the error trace is the final error message, for example, in the case of ClientServer example above, you must see the following in the end of the error trace.
+### Reading the Error Trace
-``` xml
- Assertion Failed: PSpec/BankBalanceCorrect.p:76:9 Bank must accept the withdraw request for 1, bank balance is 11!
+The `*.txt` file contains the sequence of steps from the initial state to the final error state. At the end of the trace is the error message, for example:
+
+```xml
+ Assertion Failed: PSpec/BankBalanceCorrect.p:76:9 Bank must accept
+the withdraw request for 1, bank balance is 11!
```
-In most cases, you can ignore the stack trace and information below the `ErrorLog`.
+!!! tip ""
+ In most cases, you can ignore the stack trace and information below the `ErrorLog`.
+
+---
### Replaying the Error Schedule
-One can also replay the error schedule using commandline and enabling verbose feature to dump out the error trace on the commandline.
+You can replay the error schedule using the command line with verbose output enabled:
```shell
p check --replay .schedule -tc -v
```
-For example,
+For example:
```shell
p check --replay PCheckerOutput/BugFinding/ClientServer_0_0.schedule \
- -tc tcAbstractServer \
- -v
+ -tc tcAbstractServer \
+ -v
```
diff --git a/Docs/docs/advanced/importanceliveness.md b/Docs/docs/advanced/importanceliveness.md
index 77195d08f4..889aef7386 100644
--- a/Docs/docs/advanced/importanceliveness.md
+++ b/Docs/docs/advanced/importanceliveness.md
@@ -1,9 +1,24 @@
+## Importance of Liveness Specifications
+
When reasoning about the correctness of a distributed system, **it is really important to specify both safety as well as liveness specifications**.
!!! note ""
- The examples in [Tutorials](../tutsoutline.md) show how to specify both safety and liveness specifications using [P Monitors.](../manual/monitors.md)
+ The examples in [Tutorials](../tutsoutline.md) show how to specify both safety and liveness specifications using [P Monitors](../manual/monitors.md).
+
+!!! warning "Always specify both safety and liveness specifications"
+ Only specifying safety properties is not enough — a system model may be incorrect and in the worst case drop all requests without performing any operation. Such a system trivially satisfies all safety specifications! Hence, it is essential to combine safety with **liveness properties** to check that the system is making progress and servicing requests.
+
+ Running the checker on models that have both safety and liveness properties ensures that for all executions explored by the checker, requests are eventually serviced by the system and all responses satisfy the desired correctness specification. This prevents models from doing something trivially incorrect like always doing nothing :smile:, in which case running the checker adds no value.
+
+---
+
+### Example: Client Server
+
+In the [client server example](../tutorial/clientserver.md):
-!!! danger "Always specify both safety and liveness specifications"
- Only specifying safety property for a system is not enough, this is mainly because, a system model may be incorrect and in the worst case drop all requests on to the ether and not perform any operation. Such a system trivially satisfies all correctness specifications! Hence, it becomes essential to combine that safety property with liveness properties to check that the system is making progress and servicing the requests. Running the checker on models that have both safety and liveness properties ensures that for all executions explored by the checker, requests are eventually serviced by the system (by sending responses potentially) and all the responses sent by the system satisfy the desired correctness safety specification. This helps ensure that your models are not doing something trivially incorrect like always doing nothing :smile:, in which case running the checker on such a model adds no value.
+| Specification | Type | What it checks |
+|--------------|------|----------------|
+| [BankBalanceIsAlwaysCorrect](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSpec/BankBalanceCorrect.p#L4) | **Safety** | The response sent by the bank server is always correct |
+| [GuaranteedWithDrawProgress](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSpec/BankBalanceCorrect.p#L91) | **Liveness** | The system will always eventually send a response |
-For example, in the case of the client server example, the [BankBalanceIsAlwaysCorrect](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSpec/BankBalanceCorrect.p#L4) safety property checks that the response sent by the bank server is always correct and combining it with the [GuaranteedWithDrawProgress](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSpec/BankBalanceCorrect.p#L91) liveness property ensures that system will always eventually send a response which will be checked for correctness by the safety property.
+Combining both ensures that the system **will eventually respond** (liveness) and that **every response is correct** (safety).
diff --git a/Docs/docs/advanced/pobserve/example/lockserver.md b/Docs/docs/advanced/pobserve/example/lockserver.md
index 491600fd23..ecfb59c1f3 100644
--- a/Docs/docs/advanced/pobserve/example/lockserver.md
+++ b/Docs/docs/advanced/pobserve/example/lockserver.md
@@ -1,3 +1,5 @@
+## Example: Lock Server
+
# Lock Server
## Overview
diff --git a/Docs/docs/advanced/pobserve/example/lockserverwithpobservecli.md b/Docs/docs/advanced/pobserve/example/lockserverwithpobservecli.md
index 1fced038a2..c58c6add46 100644
--- a/Docs/docs/advanced/pobserve/example/lockserverwithpobservecli.md
+++ b/Docs/docs/advanced/pobserve/example/lockserverwithpobservecli.md
@@ -1,3 +1,5 @@
+## Lock Server with PObserve CLI
+
# Running PObserve CLI on Lock Server Example
This page demonstrates how to use PObserve CLI to verify system correctness using a lock server as an example.
diff --git a/Docs/docs/advanced/pobserve/example/lockserverwithpobservejunit.md b/Docs/docs/advanced/pobserve/example/lockserverwithpobservejunit.md
index 1358440ae5..de11688f9b 100644
--- a/Docs/docs/advanced/pobserve/example/lockserverwithpobservejunit.md
+++ b/Docs/docs/advanced/pobserve/example/lockserverwithpobservejunit.md
@@ -1,3 +1,5 @@
+## Lock Server with PObserve JUnit
+
# Running PObserve Java Unit Test on Lock Server Example
This page demonstrates how to use PObserve with Java JUnit tests to verify system correctness in real-time during test execution using a lock server as an example.
diff --git a/Docs/docs/advanced/pobserve/gettingstarted.md b/Docs/docs/advanced/pobserve/gettingstarted.md
index 1db7ab47d7..b043c2a420 100644
--- a/Docs/docs/advanced/pobserve/gettingstarted.md
+++ b/Docs/docs/advanced/pobserve/gettingstarted.md
@@ -1,3 +1,5 @@
+## Getting Started with PObserve
+
# Getting Started with PObserve
## [Step 1] Create a PObserve Package
diff --git a/Docs/docs/advanced/pobserve/logparser.md b/Docs/docs/advanced/pobserve/logparser.md
index b856e64791..69c1b9ddf4 100644
--- a/Docs/docs/advanced/pobserve/logparser.md
+++ b/Docs/docs/advanced/pobserve/logparser.md
@@ -1,3 +1,5 @@
+## Writing a Log Parser
+
# PObserve: Log Parser
## What is a PObserve Log Parser?
In order to use PObserve, a *log parser* is an essential component that converts system-generated log lines to PObserve events which are later consumed by a PObserve monitor. The log parser takes a log line (text or JSON) as input and converts it into a PObserveEvent object.
diff --git a/Docs/docs/advanced/pobserve/package-setup.md b/Docs/docs/advanced/pobserve/package-setup.md
index 9e2b56bdf8..c2a2456843 100644
--- a/Docs/docs/advanced/pobserve/package-setup.md
+++ b/Docs/docs/advanced/pobserve/package-setup.md
@@ -1,3 +1,5 @@
+## Setting Up the PObserve Package
+
# Setting Up a PObserve Package with Gradle
This guide walks you through setting up a PObserve package using Gradle. You'll learn how to create an empty Gradle project with the proper structure, configure the build for your intended usage (JUnit integration or PObserve CLI).
@@ -57,6 +59,8 @@ YourPObserveProject/
Configure your `build.gradle.kts` file with the base configuration and add the specific components you need to integrate with different pobserve modes as you need:
+---
+
### Base Configuration
Start with this base configuration that's common to all PObserve packages:
@@ -123,6 +127,8 @@ group = "your.group.id"
version = "1.0.0"
```
+---
+
### Additional Configuration for PObserve JUnit Integration
If you want to use your package to run pobserve with unit tests, add these components to your `build.gradle.kts`:
@@ -157,6 +163,8 @@ publishing {
!!! info ""
Publishing your PObserve package to Maven Local repository allows you to import the parser and spec components in your system implementation's unit tests. Alternatively, you can publish them to your custom Maven repository and import from there to run PObserve in your unit tests."
+---
+
### Additional Configuration for PObserve CLI Usage
To use PObserve CLI, you need to provide both the parser and specification as an uber JAR. Add the following components to your `build.gradle.kts` to package your classes and their dependencies into a single uber JAR:
diff --git a/Docs/docs/advanced/pobserve/pobserve.md b/Docs/docs/advanced/pobserve/pobserve.md
index 0fcf1d0022..22a411c6eb 100644
--- a/Docs/docs/advanced/pobserve/pobserve.md
+++ b/Docs/docs/advanced/pobserve/pobserve.md
@@ -1,3 +1,5 @@
+## PObserve: Runtime Monitoring
+
# PObserve
Teams across AWS have been using P to validate correctness of their system and find critical corner case bugs early on during the design phase itself rather than in testing or production. However, the key question that was repeatedly raised by service teams was: *"Design validation is super valuable, finds critical bugs early! But it's the implementation that gets shipped (and not design), how do we connect our design-level specifications to implementation?"*
@@ -16,7 +18,7 @@ We mold the above mentioned challenge of integrating checking formal specificati
PObserve is a distributed runtime monitoring framework that allows checking formal correctness specifications on systems implementation using service logs. As PObserve operates on service logs, it does not require any additional instrumentation to assert design-level correctness specification on the implementation.
-??? question "[Architecture] How does PObserve work?"
+??? question ":material-chart-tree:{ .lg } [Architecture] How does PObserve work?"
The figure above provides an overview of the PObserve architecture. Given a small collection of service logs (generated during integration testing or in production), PObserve requires two additional inputs (represented in orange): (1) [PObserve Log Parser](./logparser.md) to parse service logs and generate PObserve events, and (2) P specifications (global invariants) that must be checked on these service logs.
diff --git a/Docs/docs/advanced/pobserve/pobservecli.md b/Docs/docs/advanced/pobserve/pobservecli.md
index 066e1ba965..574a6085ca 100644
--- a/Docs/docs/advanced/pobserve/pobservecli.md
+++ b/Docs/docs/advanced/pobserve/pobservecli.md
@@ -1,3 +1,5 @@
+## PObserve CLI Integration
+
# PObserve CLI
The PObserve CLI tool enables you to verify formal specifications against service or test logs on your local machine. It's ideal for processing smaller sets of logs (a few GBs).
diff --git a/Docs/docs/advanced/pobserve/pobservejunit.md b/Docs/docs/advanced/pobserve/pobservejunit.md
index 7a2663785f..e83373afc4 100644
--- a/Docs/docs/advanced/pobserve/pobservejunit.md
+++ b/Docs/docs/advanced/pobserve/pobservejunit.md
@@ -1,3 +1,5 @@
+## PObserve JUnit Integration
+
# PObserve JUnit Integration
PObserveJUnit is a specialized component of the PObserve service designed to seamlessly integrate formal specification checking into your Java unit tests.
diff --git a/Docs/docs/advanced/psemantics.md b/Docs/docs/advanced/psemantics.md
index b5b158c807..2647d86493 100644
--- a/Docs/docs/advanced/psemantics.md
+++ b/Docs/docs/advanced/psemantics.md
@@ -1,33 +1,69 @@
-Before getting started with the tutorials, we provide a quick informal overview of the P language semantics so that the readers can keep it at the back of their mind as they walk through the [tutorials](../tutsoutline.md) and the [language manual](../manualoutline.md).
+## P Language Semantics (Informal)
-**P is a programming language.** P is a state machine based _programming language_ and hence, just like any other imperative programming language it supports basic [data types](../manual/datatypes.md), [expressions](../manual/expressions.md), and [statements](../manual/statemachines.md) that enable programmers to capture complex distributed systems protocol logic as a collection of event-handlers or [functions](../manual/functions.md) (in P state machines).
+!!! tip ""
+ Before getting started with the [tutorials](../tutsoutline.md), we provide a quick informal overview of the P language semantics so that readers can keep it in mind as they walk through the tutorials and the [language manual](../manualoutline.md).
+
+---
+
+### P is a Programming Language
+
+P is a state machine based _programming language_ and hence, just like any other imperative programming language it supports basic [data types](../manual/datatypes.md), [expressions](../manual/expressions.md), and [statements](../manual/statemachines.md) that enable programmers to capture complex distributed systems protocol logic as a collection of event-handlers or [functions](../manual/functions.md) (in P state machines).
+
+---
+
+### P State Machines
+
+The underlying model of computation for P state machines is similar to that of [Gul Agha's](http://osl.cs.illinois.edu/members/agha.html) [Actor-model-of-computation](https://dspace.mit.edu/handle/1721.1/6952) ([wiki](https://en.wikipedia.org/wiki/Actor_model)). A P program is a collection of concurrently executing state machines that communicate with each other by sending events (or messages) asynchronously. Each P state machine has an **unbounded FIFO buffer** associated with it.
-**P State machines.** The underlying model of computation for P state machines is similar to that of [Gul Agha's](http://osl.cs.illinois.edu/members/agha.html) [Actor-model-of-computation](https://dspace.mit.edu/handle/1721.1/6952) ([wiki](https://en.wikipedia.org/wiki/Actor_model)). A P program is a collection of concurrently executing state machines that communicate with eachother by sending events (or messages) asynchronously. Each P state machine has an **unbounded FIFO buffer** associated with it.
Sends are **asynchronous**, i.e., executing a send operation `send t,e,v;` adds event `e` with payload value `v` into the FIFO buffer of the target machine `t`.
-Each state in the P state machine has an entry function associated with it which gets executed when the state machine enters that state. After executing the entry
-function, the machine tries to dequeue an event from the input buffer or blocks if the buffer is empty. Upon dequeuing an event from the input queue of the machine, the attached handler is executed which might transition the machine to a different state. We will provide more details about the P state machines in tutorials as well as the language manual.
-For detailed formal semantics of P state machines, we refer the readers to the [original P paper](https://ankushdesai.github.io/assets/papers/p.pdf) and the [more recent paper](https://ankushdesai.github.io/assets/papers/modp.pdf) with updated semantics.
+Each state in the P state machine has an entry function associated with it which gets executed when the state machine enters that state. After executing the entry function, the machine tries to dequeue an event from the input buffer or blocks if the buffer is empty. Upon dequeuing an event from the input queue of the machine, the attached handler is executed which might transition the machine to a different state.
+
+!!! note "Formal Semantics"
+ For detailed formal semantics of P state machines, see the [original P paper](https://ankushdesai.github.io/assets/papers/p.pdf) and the [more recent paper](https://ankushdesai.github.io/assets/papers/modp.pdf) with updated semantics.
+
+There are **two main distinctions** with the actor model of computation:
+
+1. P adds the **syntactic sugar** of state machines to actors
+2. Each state machine in P has an **unbounded FIFO buffer** associated with it instead of an unbounded bag in actors (semantic difference)
+
+---
+
+### Key Semantics
+
+!!! danger "Send Semantics in P"
+ Sends are **reliable, buffered, non-blocking, and directed** (not broadcast).
+
+ - **Reliable** — executing a send operation adds an event into the target machine's buffer. To model message loss, it must be modeled explicitly (discussed in the Failure Detector tutorial).
+ - **FIFO ordered** — events are dequeued in the **causal order** in which they were sent. Events from two different concurrent machines will be interleaved by the checker, but events from the same machine always appear in the same order.
+ - **No implicit re-ordering** — arbitrary message re-ordering must be explicitly modeled in P.
+
+ To check system correctness against an arbitrary network (with message duplicates, loss, re-order, etc.), model the corresponding send semantics in P explicitly. See the [Paxos tutorial](../tutorial/paxos.md) for an example of modeling unreliable networks with message loss and duplication.
+
+!!! danger "New Semantics in P"
+ State machines in P can be dynamically created during execution using the [`new`](../manual/statements.md#new) primitive. Creation of a state machine is also an **asynchronous, non-blocking** operation.
+
+---
-There are **two main distinctions** with actor model of computation: (1) P adds the **syntactic sugar** of state machines to actors, and (2) each state machine in P has an **unbounded FIFO buffer** associated with it instead of an unbounded bag in actors (semantic difference).
+### P Monitors
-!!! danger "[Important] Send semantics in P"
- Sends are reliable, buffered, non-blocking, and directed (not broadcast). Sends are **reliable** i.e., executing a send operation in P adds an event into the target machines buffer. Hence, if one wants to model message loss it has to be modeled explicitly (discussed in the Failure Detector example in the tutorial). Similarly, as P state machine buffers are FIFO, events are dequeued at the state machine in the **causal order** in which they were sent. Note that events that are sent by two different concurrent machines will be interleaved by the checker and hence, will appear in different order at the target machine but the events sent by the same machine will always appear in the same order at the target state machine. So, just like message loss, arbitrary message re-ordering also has to be explicitly modeled in P (explained in the Paxos example in the tutorials). In general, we find that re-ordering messages/events coming from the same machine is not important and does not lead to any interesting behaviors. More interesting behaviors happen because of interleaving of messages across different state machines which the P checker explores automatically.
+Specifications in P are written as **global runtime monitors**. These monitors observe the execution of the system and can assert any global safety or liveness invariants. Monitors are synchronously composed with the P state machines. Details are explained in the [language manual](../manual/monitors.md) and in the tutorials.
- Summary, by default, the communication between state machines using `send` operation follows the above semantics. If you would like to check your system correctness against an arbitrarily network then one would have to model the corresponding `send` semantics in P explicitly. One can then make the arbitrarily network behave as expected with message duplicates, loss, re-order, etc.
+!!! warning "Always specify both safety and liveness specifications"
+ Only specifying safety properties is not enough — a system model may be incorrect and in the worst case drop all requests without performing any operation. Such a system trivially satisfies all safety specifications! Combining safety with **liveness properties** ensures the system is making progress and servicing requests.
- If you have any further doubts related to this topic and modeling network semantics when reasoning using P, feel free to get in touch with Ankush Desai. We have several examples of such cases.
+---
-!!! danger "[Important] New semantics in P"
- State machines in P can be dynamically created during the execution of the Program using the [`new`](../manual/statements.md#new) primitive. Creation of a state machine is also an asynchronous, non-blocking operation.
+### P Checker
-**P Monitors.** Specifications in P are written as global runtime monitors. These global monitors observe the execution of the system and can assert any global safety or liveness invariants on the system. Note that the monitors are synchronously composed with the P state machines. Details are explained in the [language manual](../manual/monitors.md) and we provide examples in the tutorial.
+The P Checker explores different possible behaviors of the P program arising from:
-When reasoning about the correctness of a distributed system, **it is really important to specify both safety as well as liveness specifications**.
+1. **Concurrency** — different interleavings of events from concurrently executing state machines
+2. **Data nondeterminism** — different data input choices modeled using the [`choose`](../manual/expressions.md#choose) operation
-!!! danger "Always specify both safety and liveness specifications"
- Only specifying safety property for a system is not enough, this is mainly because, a system model may be incorrect and in the worst case drop all requests on to the ether and not perform any operation. Such a system trivially satisfies all correctness specifications! Hence, it becomes essentially to combine that safety property with liveness properties to check that the system is making progress and servicing the requests. Running the checker on models that have both safety and liveness properties ensure that for all executions explored by the checker, requests are eventually serviced by the system (by sending responses potentially) and all the responses sent by the system satisfy the desired correctness safety specification. This helps ensure that your models are not doing something trivially incorrect like always doing nothing :smile:, in which case running the checker on such a model adds no value.
+The checker asserts that for each explored execution, the system satisfies the desired properties specified by the P Monitors.
-**P Checker.** The P Checker explores different possible behaviors of the P program arising out of: (1) **concurrency:** different interleavings of events from concurrently executing state machines as well as (2) **data nondeterminism:** different data input choices in the P program modeled using the `choose` ([see](../manual/expressions.md#choose)) operation. The P checker explores different executions of the system that can happen because of these two forms of nondeterminism and asserts that for each of these executions the system satisfies the desired properties specified by the P Monitors.
+---
-**PObserve** enables validating system correctness against formal specifications using service logs. While formal specifications help catch critical bugs during the design phase, PObserve extends this capability to implementation, testing, and production phases. By operating directly on service logs without requiring additional instrumentation, the tool allows developers to verify if their running systems satisfy the same formal correctness specifications that were validated during design. This bridges the gap between design-time specification checking and runtime behavior verification, making formal methods more accessible throughout the development lifecycle.
+### PObserve
+[PObserve](pobserve/pobserve.md) enables validating system correctness against formal specifications using service logs. While formal specifications help catch critical bugs during the design phase, PObserve extends this capability to implementation, testing, and production phases. By operating directly on service logs without requiring additional instrumentation, PObserve allows developers to verify if their running systems satisfy the same formal correctness specifications that were validated during design.
diff --git a/Docs/docs/advanced/structureOfPProgram.md b/Docs/docs/advanced/structureOfPProgram.md
index f09e1ef053..93ca25a977 100644
--- a/Docs/docs/advanced/structureOfPProgram.md
+++ b/Docs/docs/advanced/structureOfPProgram.md
@@ -1,29 +1,20 @@
-A P program is typically divided into four folders (or parts):
+## Structure of a P Program
-- `PSrc`: contains all the state machines representing the implementation (model) of the
- system or protocol to be verified or tested.
-- `PSpec`: contains all the specifications representing the _correctness_ properties that
- the system must satisfy.
-- `PTst`: contains all the _environment_ or _test harness_ state machines that model the
- non-deterministic scenarios under which we want to check that the system model in `PSrc`
- satisfies the specifications in `PSpec`. P allows writing different model checking
- scenarios as test-cases.
+A P program is typically divided into four folders:
-- `PForeign`: P also supports interfacing with foreign languages like `Java`, `C#`, and
- `C/C++`. P allows programmers to implement a part of their protocol logic in these
- foreign languages and use them in a P program using the foreign types and functions interface ([Foreign](../manual/foriegntypesfunctions.md)).
- The `PForeign` folder contains
- all the foreign code used in the P program.
+| Folder | Purpose |
+|--------|---------|
+| **`PSrc/`** | State machines representing the implementation (model) of the system or protocol to be verified |
+| **`PSpec/`** | Specifications representing the _correctness_ properties the system must satisfy |
+| **`PTst/`** | Environment or test harness state machines that model non-deterministic scenarios for checking the system |
+| **`PForeign/`** | Foreign code (Java, C#, C/C++) used via the [P foreign interface](../manual/foreigntypesfunctions.md) |
-!!! Note "Recommendation"
- The folder structure described above is just a recommendation.
- The P compiler does not require any particular folder structure for a P project. The
- examples in the [Tutorials](../tutsoutline.md) use the same folder structure.
+!!! note "Recommendation"
+ The folder structure described above is just a recommendation. The P compiler does not require any particular folder structure for a P project. The examples in the [Tutorials](../tutsoutline.md) use the same folder structure.
-!!! Tip "Models, Specifications, Model Checking Scenario"
- A quick primer on what a model
- is, versus a specification, and model checking scenarios: (1) A specification says what
- the system should do (correctness properties). (2) A model captures the details of how the
- system does it. (3) A model checking scenario provides the finite non-deterministc
- test-harness or environment under which the model checker should check that the system
- model satisfies its specifications.
+??? tip "Models, Specifications, and Model Checking Scenarios"
+ A quick primer:
+
+ - **Specification** — says _what_ the system should do (correctness properties)
+ - **Model** — captures the details of _how_ the system does it
+ - **Model checking scenario** — provides the finite non-deterministic test-harness under which the model checker verifies that the model satisfies its specifications
diff --git a/Docs/docs/casestudies.md b/Docs/docs/casestudies.md
index 294a184f83..7ace7da3de 100644
--- a/Docs/docs/casestudies.md
+++ b/Docs/docs/casestudies.md
@@ -1,19 +1,32 @@
-### [AWS] Amazon S3 Strong Consistency
+## Case Studies
+
+---
+
+### :material-aws:{ .lg } Systems Correctness Practices at AWS
+
+Teams across AWS that build some of its flagship products — from storage (Amazon S3, EBS), to databases (Amazon DynamoDB, MemoryDB, Aurora), to compute (EC2, IoT) — have been using P to reason about the correctness of their system designs. A 2025 Communications of the ACM article surveys the full portfolio of formal methods used across AWS, with P playing a central role.
+
+!!! quote "From [Systems Correctness Practices at Amazon Web Services](https://cacm.acm.org/practice/systems-correctness-practices-at-amazon-web-services/) (CACM, 2025)"
+ P is a state-machine-based language for modeling and analysis of distributed systems. Using P, developers model their system designs as communicating state machines, a mental model familiar to Amazon's developer population — most of whom develop systems based on microservices and service-oriented architectures. P has been developed at AWS since 2019 and is maintained as a strategic open source project.
+
+The article also describes how **PObserve** bridges the gap between design-time verification and production, by validating structured service logs against P specifications post hoc — making the investment in formal specification valuable throughout the entire development lifecycle.
+
+---
+
+### :material-aws:{ .lg } Amazon S3 Strong Consistency
In Dec 2020, Amazon S3 launched [Strong Consistency](https://aws.amazon.com/s3/consistency/) with guaranteed
[strong read-after-write consistency](https://aws.amazon.com/blogs/aws/amazon-s3-update-strong-read-after-write-consistency/).
The S3 team leveraged automated reasoning for ensuring the correctness of S3's Strong
-Consistency design. Werner had a detailed blog post about the challenges involved.
+Consistency design.
-!!! quote "Qoute from Werners blog: [Diving Deep on S3 Consistency](https://www.allthingsdistributed.com/2021/04/s3-strong-consistency.html)"
+!!! quote "From Werner Vogels' blog: [Diving Deep on S3 Consistency](https://www.allthingsdistributed.com/2021/04/s3-strong-consistency.html)"
Common testing techniques like unit testing and integration testing are valuable,
- necessary tools in any production system. But they aren’t enough when you need to build a
- system with such a high bar for correctness. We want a system that’s “provably correct”,
- not just “probably correct.” So, for strong consistency, we utilized a variety of
+ necessary tools in any production system. But they aren't enough when you need to build a
+ system with such a high bar for correctness. We want a system that's "provably correct",
+ not just "probably correct." So, for strong consistency, we utilized a variety of
techniques for ensuring that what we built is correct, and continues to be correct as the
- system evolves. We employed integration tests, deductive proofs of our proposed cache
- coherence algorithm, model checking to formalize our consistency design and to demonstrate
- its correctness, and we expanded on our model checking to examine actual runnable code.
+ system evolves.
P was used for creating formal models of all the core distributed protocols involved in
S3's strong consistency and checking that the system model satisfies the desired
@@ -21,70 +34,59 @@ correctness guarantees. Details about P and how it is being used by the S3 team
found in the [AWS Pi-Week Talk](https://pages.awscloud.com/pi-week-2021.html):
[**Amazon S3 Strong Consistency**](https://youtu.be/B0yXz6EeCaA?list=PL2yQDdvlhXf8vAnQB10dCPIeWUKdHUgOP).
-### [AWS] Amazon IoT Devices: OTA Protocol
+---
+
+### :material-devices:{ .lg } Amazon IoT Devices: OTA Protocol
AWS FreeRTOS is a real-time operating system designed to run on IoT devices to enable them
to interact easily and reliably with AWS services. The Over the Air (OTA) update
functionality makes it possible to update a device with security fixes quickly and
-reliably. The [OTA Library](https://freertos.org/ota/index.html), a part of the overall
-OTA functionality that runs on the IoT devices, enables customers to learn of available
-updates, download the updates, check their cryptographic signatures, and apply them. The
-OTA system is a complex piece of software that performs firmware updates reliably and
-securely --- keeping all devices in a consistent state --- in the presence of arbitrary
-failures of devices and communication. The heart of the OTA system is an intricate
-distributed protocol, the OTA protocol, that co-ordinates the execution of the different
-agents involved.
+reliably. The [OTA Library](https://freertos.org/ota/index.html) enables customers to learn of available
+updates, download the updates, check their cryptographic signatures, and apply them.
P was used for creating formal models of the OTA protocol and checking its
-correctness. During this process the team found 3 bugs in the model that pointed to
+correctness. During this process the team found **3 bugs** in the model that pointed to
potential issues in the actual implementation itself.
-Related Blog:
-[**Using Formal Methods to validate OTA Protocol**](https://freertos.org/2020/12/using-formal-methods-to-validate-ota-protocol.html)
+!!! info "Related Blog"
+ [**Using Formal Methods to validate OTA Protocol**](https://freertos.org/2020/12/using-formal-methods-to-validate-ota-protocol.html)
+
+---
-### [UC Berkeley] Programming Safe Robotics Systems
+### :material-robot:{ .lg } Programming Safe Robotics Systems (UC Berkeley)
DRONA is a software framework for programming safe distributed mobile robotics systems.
DRONA uses P language for implementing and model-checking the correctness of robotics software stack. The
C code generated from P compiler can be deployed on Robot Operating System (ROS).
-More details about the DRONA framework and simulation videos are available here:
-**[https://drona-org.github.io/Drona/](https://drona-org.github.io/Drona/)**
-See the [fun demo video](https://www.youtube.com/watch?v=R8ztpfMPs5c) using P to control a
-quadrocopter and make sense of the MavLink stream, all visualized in a live DGML diagram.
+:material-play-circle: See the [fun demo video](https://www.youtube.com/watch?v=R8ztpfMPs5c) using P to control a quadrocopter and make sense of the MavLink stream, all visualized in a live DGML diagram.
-### [UC Berkeley] Programming Secure Distributed Systems
+!!! info "More Details"
+ **[https://drona-org.github.io/Drona/](https://drona-org.github.io/Drona/)**
+
+---
+
+### :material-shield-lock:{ .lg } Programming Secure Distributed Systems (UC Berkeley)
Programming secure distributed systems that have a formal guarantee of no information
leakage is challenging. PSec framework extended the P language to enable programming secure
distributed systems. PSec leverages Intel SGX enclaves to ensure that the security guarantees
provided by the P language are enforced at runtime. By combining information flow control
with hardware enclaves, PSec prevents programmers from inadvertently leaking sensitive
-information while sending data securely across machines. PSec was used to program several real-world examples,
-including a One Time Passcode application and a Secure Electronic Voting System. Details
-about the PSec framework can be found [here](https://github.com/ShivKushwah/PSec).
-
-### [Microsoft] Windows USB 3.0 Device Drivers
-
-Event-driven asynchronous programs typically have layers of design, where the higher
-layers reason with how the various components (or machines) interact, and the protocol
-they follow, and where as lower layers manage more data-intensive computations,
-controlling local devices, etc. However, the programs often get written in traditional
-languages that offer no mechanisms to capture these abstractions, and hence over time
-leads to code where the individual layers are no longer discernible. High-level protocols,
-though often first designed on paper using clean graphical state-machine abstractions,
-eventually get lost in code, and hence verification tools for such programs face the
-daunting task of extracting these models from the programs. The natural solution to the
-above problem is to build a programming language for asynchronous event-driven programs
-that preserves the protocol abstractions in code. Apart from the difficulty in designing
-such a language, this task is plagued by the reluctance of programmers to adopt a new
-language of programming and the discipline that it brings. However, this precise solution
-was pioneered by the P programming framework, where, during the development of Windows 8,
-the team building the USB driver stack used P for modeling, implementing, and
-model-checking of the USB 3.0 device drivers
-([paper](https://ankushdesai.github.io/assets/papers/p.pdf))
-
-Related Blog:
-
-- **[Building robust USB 3.0 support](https://blogs.msdn.microsoft.com/b8/2011/08/22/building-robust-usb-3-0-support/)**
-- **[P: A programming language designed for asynchrony, fault-tolerance and uncertainty](https://www.microsoft.com/en-us/research/blog/p-programming-language-asynchrony/)**
+information while sending data securely across machines.
+
+PSec was used to program several real-world examples,
+including a One Time Passcode application and a Secure Electronic Voting System.
+
+!!! info "More Details"
+ **[https://github.com/ShivKushwah/PSec](https://github.com/ShivKushwah/PSec)**
+
+---
+
+### :material-microsoft-windows:{ .lg } Windows USB 3.0 Device Drivers (Microsoft)
+
+P was pioneered during the development of Windows 8, where the team building the USB driver stack used P for modeling, implementing, and model-checking of the USB 3.0 device drivers ([paper](https://ankushdesai.github.io/assets/papers/p.pdf)). High-level protocols, though often first designed on paper using clean graphical state-machine abstractions, eventually get lost in code. P preserves these protocol abstractions in code, making verification tractable.
+
+!!! info "Related Blogs"
+ - [**Building robust USB 3.0 support**](https://blogs.msdn.microsoft.com/b8/2011/08/22/building-robust-usb-3-0-support/)
+ - [**P: A programming language designed for asynchrony, fault-tolerance and uncertainty**](https://www.microsoft.com/en-us/research/blog/p-programming-language-asynchrony/)
diff --git a/Docs/docs/getstarted/PeasyIDE.md b/Docs/docs/getstarted/PeasyIDE.md
index 5d233d7936..6bb915d3e0 100644
--- a/Docs/docs/getstarted/PeasyIDE.md
+++ b/Docs/docs/getstarted/PeasyIDE.md
@@ -1,45 +1,40 @@
-
-
-### Peasy: An easy-to-use development environment for P
+## Peasy: IDE for P
> :mega: **Peasy** is a step towards making application of P in practice **easy-peasy**
Peasy offers an intuitive development environment tailored specifically for the P programming language, making it easier for developers to edit, compile, test, debug, and visualize their P programs.
-Peasy provides syntax highlighting for code clarity and code snippets to reduce development time, automated compilation and testing for hassle-free program development. Moreover, Peasy's trace and state machine visualization tools provide rich analysis to gain insights into your projects and simplifies error debugging process.
+
-[Download Peasy for VS Code](vscode:extension/PLanguage.peasy-extension){ .md-button }
+- :material-palette:{ .lg .middle } **Syntax Highlighting**
-!!! tip "[Recommended] Checkout [Peasy's Github page](https://p-org.github.io/peasy-ide-vscode/) for more information, getting started, and demo videos on the IDE!"
+ ---
+ Code clarity with P-specific syntax highlighting and code snippets
+- :material-cog-play:{ .lg .middle } **Automated Compilation & Testing**
+
+ ---
+
+ Hassle-free program development with integrated compile and test workflows
+
+- :material-chart-timeline-variant:{ .lg .middle } **Trace Visualization**
+
+ ---
+
+ Rich analysis tools to gain insights into your projects and simplify debugging
+
+- :material-state-machine:{ .lg .middle } **State Machine Visualization**
+
+ ---
+
+ Visualize your P state machines for better understanding of system design
+
+
+
+---
+
+[Download Peasy for VS Code](vscode:extension/PLanguage.peasy-extension){ .md-button .md-button--primary }
+
+!!! tip "Recommended"
+ Check out [Peasy's GitHub page](https://p-org.github.io/peasy-ide-vscode/) for more information, getting started guides, and demo videos!
diff --git a/Docs/docs/getstarted/build.md b/Docs/docs/getstarted/build.md
index 53e89d247c..0ebfa39b0c 100644
--- a/Docs/docs/getstarted/build.md
+++ b/Docs/docs/getstarted/build.md
@@ -1,44 +1,64 @@
+## Building from Source
+
If you plan to contribute a Pull Request to P then you need to build the source code
-and run the tests. Please make sure that you have followed the steps in the [installation guide](install.md) to install P dependencies.
+and run the tests.
+
+!!! note "Prerequisites"
+ Make sure you have followed the steps in the [installation guide](install.md) to install P dependencies.
-### Building the P project
+---
-Clone the [P repo](https://github.com/p-org/P) and run the following `build` script.
+### Building the P Project
-=== "on MacOS or Linux"
+Clone the [P repo](https://github.com/p-org/P) and run the build script:
+
+=== "MacOS / Linux"
```shell
cd Bld
./build.sh
```
-=== "On Windows"
+=== "Windows"
```shell
cd Bld
./build.ps1
```
-### Running the tests
+---
+
+### Running the Tests
-You can run the following command to build and run the test regressions for P Compiler. Make sure you are in the root directory of the clone repo that has the `P.sln`.
+Build and run the test regressions for the P Compiler. Make sure you are in the root directory of the cloned repo that has the `P.sln`:
```shell
dotnet build --configuration Release
dotnet test --configuration Release
```
-### Using a local build
+---
-P is distributed as a [dotnet tool](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools). To test changes locally you can:
+### Using a Local Build
-1. run `dotnet tool uninstall --global P`
-2. navigate to `/Src/PCompiler/PCommandLine`
-3. run
- ```
- dotnet pack PCommandLine.csproj --configuration Release --output ./publish
- -p:PackAsTool=true
- -p:ToolCommandName=P
- -p:Version=
- ```
-4. run `dotnet tool install P --global --add-source ./publish`
\ No newline at end of file
+P is distributed as a [dotnet tool](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools). To test changes locally:
+
+1. Uninstall the existing global tool:
+ ```shell
+ dotnet tool uninstall --global P
+ ```
+2. Navigate to the command-line project:
+ ```shell
+ cd Src/PCompiler/PCommandLine
+ ```
+3. Pack a local build:
+ ```shell
+ dotnet pack PCommandLine.csproj --configuration Release --output ./publish \
+ -p:PackAsTool=true \
+ -p:ToolCommandName=P \
+ -p:Version=
+ ```
+4. Install from the local package:
+ ```shell
+ dotnet tool install P --global --add-source ./publish
+ ```
diff --git a/Docs/docs/getstarted/install.md b/Docs/docs/getstarted/install.md
index 7212718188..6f35492cd5 100644
--- a/Docs/docs/getstarted/install.md
+++ b/Docs/docs/getstarted/install.md
@@ -1,11 +1,16 @@
-!!! info "If you want to use older P version 1.x.x, please use the installation steps [here](../old/getstarted/install.md)"
+## Installing P
-P is built to be cross-platform and can be used on MacOS, Linux, and Windows. We provide a step-by-step guide for installing P along with the required dependencies.
+!!! info "Looking for P 1.x?"
+ If you want to use older P version 1.x.x, please use the installation steps [here](../old/getstarted/install.md).
-!!! success ""
- After each step, please use the troubleshooting check to ensure that each installation step succeeded.
+P is built to be **cross-platform** and can be used on MacOS, Linux, and Windows. Follow the steps below to install P along with the required dependencies.
+
+!!! success "Verify each step"
+ After each step, use the troubleshooting check to ensure the installation succeeded.
+
+---
-### [Step 1] Install .Net Core SDK
+### :material-numeric-1-circle:{ .lg } Install .NET SDK
The P compiler is implemented in C# and hence the tool chain requires `dotnet`.
P currently uses the specific version of [.Net SDK 8.0](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
@@ -72,7 +77,9 @@ P currently uses the specific version of [.Net SDK 8.0](https://dotnet.microsoft
-### [Step 2] Install Java Runtime
+---
+
+### :material-numeric-2-circle:{ .lg } Install Java Runtime
The P compiler also requires Java (`java` version 11 or higher).
@@ -112,94 +119,11 @@ The P compiler also requires Java (`java` version 11 or higher).
If you get `java` command not found error, mostly likely, you need to add the path to `java` in your `PATH`.
-[//]: # (### [Step 3] Install Maven)
-
-[//]: # ()
-[//]: # (For compiling the generated Java code, the P compiler using Maven (`mvn` version 3.3 or higher).)
-
-[//]: # ()
-[//]: # (=== "MacOS")
-
-[//]: # ()
-[//]: # ( Installing Maven on MacOS using Homebrew ([details](https://mkyong.com/maven/install-maven-on-mac-osx/)))
-
-[//]: # ()
-[//]: # ( ```)
-
-[//]: # ( brew install maven)
-
-[//]: # ( ```)
-
-[//]: # ()
-[//]: # ( Dont have Homebrew? Directly use [installer](https://maven.apache.org/install.html). )
-
-[//]: # ()
-[//]: # (=== "Ubuntu")
-
-[//]: # ()
-[//]: # ( Installing Maven on Ubuntu ([details](https://phoenixnap.com/kb/install-maven-on-ubuntu)))
-
-[//]: # ( )
-[//]: # ( ```)
-
-[//]: # ( sudo apt install maven)
-
-[//]: # ( ```)
-
-[//]: # ()
-[//]: # (=== "Amazon Linux")
-
-[//]: # ()
-[//]: # ( Visit the [Maven releases](http://maven.apache.org/download.cgi) page and install any Maven 3.3+ release.)
-
-[//]: # ()
-[//]: # ( Steps for installing Maven 3.8.7 on Amazon Linux (you can use any version of Maven 3.3+):)
+---
-[//]: # ()
-[//]: # ( ```)
+### :material-numeric-3-circle:{ .lg } Install P Tool
-[//]: # ( wget https://dlcdn.apache.org/maven/maven-3/3.8.7/binaries/apache-maven-3.8.7-bin.tar.gz)
-
-[//]: # ( tar xfv apache-maven-3.8.7-bin.tar.gz)
-
-[//]: # ( ```)
-
-[//]: # ( )
-[//]: # ( You might do this in your home directory, yielding a folder like `` /home/$USER/apache-maven-3.8.7 ``)
-
-[//]: # ( )
-[//]: # ( Next, install the software into your environment by adding it to your path, and by defining Maven's environment variables:)
-
-[//]: # ( )
-[//]: # ( ```)
-
-[//]: # ( export M2_HOME=/home/$USER/apache-maven-3.8.7)
-
-[//]: # ( export M2=$M2_HOME/bin)
-
-[//]: # ( export PATH=$M2:$PATH)
-
-[//]: # ( ```)
-
-[//]: # ()
-[//]: # (=== "Windows")
-
-[//]: # ()
-[//]: # ( Installing Maven on Windows ([details](https://maven.apache.org/install.html)))
-
-[//]: # ()
-[//]: # (??? hint "Troubleshoot: Confirm that Maven is correctly installed on your machine.")
-
-[//]: # ()
-[//]: # ( `mvn -version`)
-
-[//]: # ()
-[//]: # ( If you get `mvn` command not found error, mostly likely, you need to add the path to `$M2_HOME/bin` in your `PATH`.)
-
-
-### [Step 3] Install P tool
-
-Finally, let's install the P tool as a `dotnet tool` using the following command:
+Finally, install the P tool as a `dotnet tool`:
```shell
dotnet tool install --global P
@@ -222,13 +146,14 @@ dotnet tool install --global P
dotnet tool update --global P
```
-### [Step 4] Recommended IDE (Optional)
+---
-- For developing P programs, we recommend using [Peasy](https://marketplace.visualstudio.com/items?itemName=PLanguage.peasy-extension).
+### :material-numeric-4-circle:{ .lg } Recommended IDE (Optional)
-- For debugging generated C# code, we recommend using [Rider](https://www.jetbrains.com/rider/) for Mac/Linux or [Visual Studio 2019](https://docs.microsoft.com/en-us/visualstudio/install/install-visual-studio) for Windows.
+| Purpose | Recommended Tool |
+|---------|-----------------|
+| Developing P programs | [**Peasy**](https://marketplace.visualstudio.com/items?itemName=PLanguage.peasy-extension) (VS Code extension) |
+| AI-assisted P development | [**PeasyAI**](peasyai.md) (Cursor / Claude Code) |
-- For debugging generated Java code, we recommend using [IntelliJ IDEA](https://www.jetbrains.com/idea/)
-
-!!! note ""
+!!! success ""
Great :smile:! You are all set to compile and check your first P program :mortar_board:!
diff --git a/Docs/docs/getstarted/peasyai.md b/Docs/docs/getstarted/peasyai.md
new file mode 100644
index 0000000000..6438da281c
--- /dev/null
+++ b/Docs/docs/getstarted/peasyai.md
@@ -0,0 +1,263 @@
+## PeasyAI: AI-Assisted P Development
+
+PeasyAI is an AI-powered code generation, compilation, and formal verification assistant for the P programming language. It exposes an MCP (Model Context Protocol) server that works with **Cursor** and **Claude Code**, providing 27 tools and 14 resources for generating P state machines from design documents, compiling them, and verifying correctness with PChecker.
+
+### Features
+
+- **Design Doc to Verified P Code** — Generate types, state machines, safety specs, and test drivers from a plain-text design document
+- **Multi-Provider LLM Support** — Snowflake Cortex, AWS Bedrock, Direct Anthropic
+- **Ensemble Generation** — Best-of-N candidate selection for higher quality code
+- **Auto-Fix Pipeline** — Automatically fix compilation errors and PChecker failures
+- **Human-in-the-Loop** — Falls back to user guidance when automated fixing fails
+- **RAG-Enhanced** — 1,200+ indexed P code examples improve generation quality
+
+---
+
+### Prerequisites
+
+PeasyAI relies on the P toolchain at runtime to compile and model-check your programs:
+
+| Dependency | Why it's needed | Install |
+|------------|-----------------|---------|
+| **Python >= 3.10** | Runs the PeasyAI MCP server | [python.org/downloads](https://www.python.org/downloads/) |
+| **.NET SDK, Java, and P compiler** | Compiles and model-checks P programs | [P installation guide](install.md) |
+
+Verify your setup:
+
+```bash
+python3 --version # >= 3.10
+dotnet --list-sdks # must show 8.0.*
+java -version # >= 11
+p --version # P compiler is on PATH
+```
+
+---
+
+### Installation
+
+Install the latest release directly from GitHub:
+
+```bash
+pip install https://github.com/p-org/P/releases/download/peasyai-v0.2.0/peasyai_mcp-0.2.0-py3-none-any.whl
+```
+
+!!! tip "Check the [Releases page](https://github.com/p-org/P/releases) for the latest version."
+
+To upgrade to a newer release:
+
+```bash
+pip install --force-reinstall https://github.com/p-org/P/releases/download/peasyai-v/peasyai_mcp--py3-none-any.whl
+```
+
+??? info "Alternative: install from source (for development)"
+ ```bash
+ git clone https://github.com/p-org/P.git
+ cd P/Src/PeasyAI
+ pip install -e ".[dev]"
+ ```
+
+---
+
+### Configuration
+
+Run the init command to create the configuration file:
+
+```bash
+peasyai-mcp init
+```
+
+This creates **`~/.peasyai/settings.json`**. Open it and fill in the credentials for the LLM provider you want to use:
+
+```json
+{
+ "llm": {
+ "provider": "snowflake",
+ "model": "claude-sonnet-4-5",
+ "timeout": 600,
+ "providers": {
+ "snowflake": {
+ "api_key": "your-snowflake-pat-token",
+ "base_url": "https://your-account.snowflakecomputing.com/api/v2/cortex/openai"
+ },
+ "anthropic": {
+ "api_key": "your-anthropic-key",
+ "model": "claude-3-5-sonnet-20241022"
+ },
+ "bedrock": {
+ "region": "us-west-2",
+ "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0"
+ }
+ }
+ },
+ "generation": {
+ "ensemble_size": 3,
+ "output_dir": "./PGenerated"
+ }
+}
+```
+
+!!! note "Only fill in the provider you use. Set `provider` to `snowflake`, `anthropic`, or `bedrock`."
+
+Verify everything is set up correctly:
+
+```bash
+peasyai-mcp config
+```
+
+#### Configuration Reference
+
+| Key | Example | Description |
+|-----|---------|-------------|
+| `llm.provider` | `"snowflake"` | Active provider: `snowflake`, `anthropic`, or `bedrock` |
+| `llm.model` | `"claude-sonnet-4-5"` | Model name (uses provider default if omitted) |
+| `llm.timeout` | `600` | Request timeout in seconds |
+| `llm.providers.snowflake.api_key` | | Snowflake Programmatic Access Token |
+| `llm.providers.snowflake.base_url` | | Snowflake Cortex endpoint URL |
+| `llm.providers.anthropic.api_key` | | Anthropic API key |
+| `llm.providers.bedrock.region` | `"us-west-2"` | AWS region |
+| `llm.providers.bedrock.model_id` | | Bedrock model ID |
+| `generation.ensemble_size` | `3` | Best-of-N candidates per file |
+| `generation.output_dir` | `"./PGenerated"` | Default output directory |
+
+**Precedence:** Environment variables > `~/.peasyai/settings.json` > built-in defaults.
+
+#### LLM Providers
+
+=== "Snowflake Cortex"
+
+ 1. Log into your Snowflake account
+ 2. Go to **Admin > Security > Programmatic Access Tokens**
+ 3. Create a token with Cortex API permissions
+ 4. Set `"provider": "snowflake"` and fill in `api_key` and `base_url`
+
+=== "Anthropic (Direct API)"
+
+ Set `"provider": "anthropic"` and fill in `api_key`.
+
+=== "AWS Bedrock"
+
+ Ensure `~/.aws/credentials` is configured, then set `"provider": "bedrock"`.
+
+---
+
+### Add to Cursor
+
+Edit **`~/.cursor/mcp.json`** (create the file if it doesn't exist):
+
+```json
+{
+ "mcpServers": {
+ "peasyai": {
+ "command": "peasyai-mcp",
+ "args": []
+ }
+ }
+}
+```
+
+Restart Cursor — the PeasyAI tools will appear in the MCP panel.
+
+### Add to Claude Code
+
+```bash
+claude mcp add peasyai -- peasyai-mcp
+```
+
+---
+
+### MCP Tools
+
+PeasyAI provides 27 MCP tools organized by category:
+
+| Category | Tool | Description |
+|----------|------|-------------|
+| **Generation** | `peasy-ai-create-project` | Create P project skeleton (PSrc/, PSpec/, PTst/) |
+| | `peasy-ai-gen-types-events` | Generate types, enums, and events file |
+| | `peasy-ai-gen-machine` | Generate a single state machine (two-stage, ensemble) |
+| | `peasy-ai-gen-spec` | Generate safety specification / monitor |
+| | `peasy-ai-gen-test` | Generate test driver |
+| | `peasy-ai-gen-full-project` | One-shot full project generation |
+| | `peasy-ai-save-file` | Save generated code to disk |
+| **Compilation** | `peasy-ai-compile` | Compile a P project |
+| | `peasy-ai-check` | Run PChecker model-checking verification |
+| **Fixing** | `peasy-ai-fix-compile-error` | Fix a single compilation error |
+| | `peasy-ai-fix-checker-error` | Fix a PChecker error from trace analysis |
+| | `peasy-ai-fix-all` | Iteratively fix all compilation errors |
+| | `peasy-ai-fix-bug` | Auto-diagnose and fix PChecker failures |
+| **Workflows** | `peasy-ai-run-workflow` | Execute a multi-step workflow (compile_and_fix, full_verification, etc.) |
+| | `peasy-ai-resume-workflow` | Resume a paused workflow with user guidance |
+| | `peasy-ai-list-workflows` | List available and active workflows |
+| **Query** | `peasy-ai-syntax-help` | P language syntax help by topic |
+| | `peasy-ai-list-files` | List all .p files in a project |
+| | `peasy-ai-read-file` | Read contents of a P file |
+| **RAG** | `peasy-ai-search-examples` | Search the P program database |
+| | `peasy-ai-get-context` | Get examples to improve generation quality |
+| | `peasy-ai-index-examples` | Index your own P files into the corpus |
+| | `peasy-ai-get-protocol-examples` | Get examples for common protocols (Paxos, Raft, etc.) |
+| | `peasy-ai-corpus-stats` | Get corpus statistics |
+| **Trace** | `peasy-ai-explore-trace` | Explore a PChecker execution trace |
+| | `peasy-ai-query-trace` | Query machine state at a point in the trace |
+| **Environment** | `peasy-ai-validate-env` | Check P toolchain, LLM provider, and config |
+
+### MCP Resources
+
+| Resource URI | Description |
+|--------------|-------------|
+| `p://guides/syntax` | Complete P syntax reference |
+| `p://guides/basics` | P language fundamentals |
+| `p://guides/machines` | State machine patterns |
+| `p://guides/types` | Type system guide |
+| `p://guides/events` | Event handling guide |
+| `p://guides/enums` | Enum types guide |
+| `p://guides/statements` | Statements and expressions guide |
+| `p://guides/specs` | Specification monitors guide |
+| `p://guides/tests` | Test cases guide |
+| `p://guides/modules` | Module system guide |
+| `p://guides/compiler` | Compiler usage guide |
+| `p://guides/common_errors` | Common compilation errors and fixes |
+| `p://examples/program` | Complete P program example |
+| `p://about` | About the P language |
+
+---
+
+### Typical Workflow
+
+The recommended step-by-step workflow for generating verified P code:
+
+1. **Create project** — `peasy-ai-create-project(design_doc, output_dir)`
+2. **Generate types** — `peasy-ai-gen-types-events(design_doc, project_path)` then review and `peasy-ai-save-file`
+3. **Generate machines** — `peasy-ai-gen-machine(name, design_doc, project_path)` for each machine, then review and `peasy-ai-save-file`
+4. **Generate spec** — `peasy-ai-gen-spec("Safety", design_doc, project_path)` then review and `peasy-ai-save-file`
+5. **Generate test** — `peasy-ai-gen-test("TestDriver", design_doc, project_path)` then review and `peasy-ai-save-file`
+6. **Compile** — `peasy-ai-compile(project_path)`
+7. **Fix errors** — `peasy-ai-fix-all(project_path)` if compilation fails
+8. **Verify** — `peasy-ai-check(project_path)` to run PChecker
+9. **Fix bugs** — `peasy-ai-fix-bug(project_path)` if PChecker finds issues
+
+Or use **`peasy-ai-run-workflow("full_verification", project_path)`** to automate steps 6-9.
+
+---
+
+### Human-in-the-Loop Error Fixing
+
+The `peasy-ai-fix-compile-error` and `peasy-ai-fix-checker-error` tools try up to 3 automated fixes. If all fail, they return `needs_guidance: true` with diagnostic questions. Call the tool again with `user_guidance` containing the user's hint:
+
+```
+peasy-ai-fix-compile-error(...) # attempt 1 — fails
+peasy-ai-fix-compile-error(...) # attempt 2 — fails
+peasy-ai-fix-compile-error(...) # attempt 3 — fails, returns needs_guidance=true
+Ask user for guidance
+peasy-ai-fix-compile-error(user_guidance="The type should be…") # succeeds
+```
+
+---
+
+### Troubleshooting
+
+| Problem | Fix |
+|---------|-----|
+| `peasyai-mcp: command not found` | Make sure the pip install location is on your `PATH`. Try `python -m site --user-base` to find it, or use `pipx install` instead. |
+| `p: command not found` | Install the P compiler following the [P installation guide](install.md) and ensure `~/.dotnet/tools` is on your `PATH`. |
+| `dotnet: command not found` | Install .NET SDK 8.0 following the [P installation guide](install.md#install-net-sdk). |
+| MCP server not showing in Cursor | Restart Cursor after editing `~/.cursor/mcp.json`. Check the MCP panel for error messages. |
+| LLM calls failing | Run `peasyai-mcp config` to verify your credentials are loaded correctly. |
diff --git a/Docs/docs/getstarted/usingP.md b/Docs/docs/getstarted/usingP.md
index 261a4e9bb9..ad200b6457 100644
--- a/Docs/docs/getstarted/usingP.md
+++ b/Docs/docs/getstarted/usingP.md
@@ -1,15 +1,17 @@
-!!! info "If you are using an older P version 1.x.x, please find the usage guide [here](../old/getstarted/usingP.md)"
+## Using P Compiler and Checker
-!!! check ""
+!!! info "Looking for P 1.x?"
+ If you are using an older P version 1.x.x, please find the usage guide [here](../old/getstarted/usingP.md).
+
+!!! check "Prerequisites"
Before moving forward, we assume that you have successfully installed
[P](install.md) and the [Peasy extension](PeasyIDE.md) :metal:.
-We introduce the P language syntax and semantics in details in the
+We introduce the P language syntax and semantics in detail in the
[Tutorials](../tutsoutline.md) and [Language Manual](../manualoutline.md). In this
section, we provide an overview of the steps involved in compiling and checking a P program
using the [client server](../tutorial/clientserver.md) example in Tutorials.
-
??? info "Get the Client Server Example Locally"
We will use the [ClientServer](https://github.com/p-org/P/tree/master/Tutorial/1_ClientServer) example from Tutorial folder in P repository to describe the process of compiling and checking a P program. Please clone the P repo and navigate to the
ClientServer example in Tutorial.
@@ -25,7 +27,9 @@ using the [client server](../tutorial/clientserver.md) example in Tutorials.
-### Compiling a P program
+---
+
+### :material-cog-play:{ .lg } Compiling a P Program
There are two ways of compiling a P program:
@@ -95,7 +99,7 @@ There are two ways of compiling a P program:
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
Restored P/Tutorial/1_ClientServer/PGenerated/CSharp/ClientServer.csproj (in 102 ms).
- ClientServer -> P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ ClientServer -> P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
Build succeeded.
0 Warning(s)
@@ -113,7 +117,7 @@ There are two ways of compiling a P program:
If you are running `p compile` from outside the P project directory, use the `-pp ` option instead.
??? info "P Project File Details"
- The P compiler does not support advanced project management features like separate compilation and dependency analysis (_coming soon_).
+ The P compiler does not support advanced project management features like separate compilation and dependency analysis.
The current project file interface is a simple mechanism to provide all the required inputs to the compiler in an XML format ([ClientServer.pproj](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/ClientServer.pproj)).
``` xml
@@ -156,7 +160,7 @@ There are two ways of compiling a P program:
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
Restored P/Tutorial/1_ClientServer/PGenerated/CSharp/ClientServer.csproj (in 115 ms).
- ClientServer -> P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ ClientServer -> P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
Build succeeded.
0 Warning(s)
@@ -169,7 +173,9 @@ There are two ways of compiling a P program:
~~ [PTool]: Thanks for using P! ~~
```
-### Checking a P program
+---
+
+### :material-shield-check:{ .lg } Checking a P Program
Compiling the ClientServer program generates a `ClientServer.dll`, this `dll` is
the C# representation of the P program. The P Checker takes as input this `dll` and
@@ -189,8 +195,8 @@ p check
$ p check
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
- .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
+ .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
Error: We found '3' test cases. Please provide a more precise name of the test case you wish to check using (--testcase | -tc).
Possible options are:
tcSingleClient
@@ -221,8 +227,8 @@ p check -tc tcSingleClient -s 100
$ p check -tc tcSingleClient -s 100
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
- .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
+ .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
.. Test case :: tcSingleClient
... Checker is using 'random' strategy (seed:2510398613).
..... Schedule #1
@@ -269,8 +275,8 @@ p check -tc tcAbstractServer -s 100
$ p check -tc tcAbstractServer -s 100
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
- .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
+ .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
.. Test case :: tcAbstractServer
... Checker is using 'random' strategy (seed:490949683).
..... Schedule #1
diff --git a/Docs/docs/index.md b/Docs/docs/index.md
index cb4a9b0db1..33130e826b 100644
--- a/Docs/docs/index.md
+++ b/Docs/docs/index.md
@@ -16,49 +16,156 @@


-**Challenge**:
-Distributed systems are notoriously hard to get right. Programming these systems is challenging because of the need to reason about correctness in the presence of myriad possible interleaving of messages and failures. Unsurprisingly, it is common for service teams to uncover correctness bugs after deployment. Formal methods can play an important role in addressing this challenge!
+---
+P is a state machine based programming language for formally modeling and specifying complex distributed systems. P allows programmers to model their system design as a collection of communicating state machines and provides automated reasoning backends to check that the system satisfies the desired correctness specifications.
-**P Overview:**
-P is a state machine based programming language for formally modeling and specifying complex
-distributed systems. P allows programmers to model their system design as a collection of
-communicating state machines. P supports several backend analysis engines
-(based on automated reasoning techniques like model
-checking and symbolic execution) to check that the distributed system modeled in P
-satisfy the desired correctness specifications.
+{ align=center }
-> If you are wondering **"why do formal methods at all?"** or **"how is AWS using P to gain confidence in correctness of their services?"**, the following re:Invent 2023 talk answers this question, provides an overview of P, and its impact inside AWS:
-[(Re:Invent 2023 Talk) Gain confidence in system correctness & resilience with Formal Methods (Finding Critical Bugs Early!!)](https://youtu.be/FdXZXnkMDxs?si=iFqpl16ONKZuS4C0)
+---
+## :material-new-box:{ .lg } What's New
+
+
+- :material-robot-outline:{ .lg .middle } **PeasyAI — AI-Assisted P Development**
+
+ ---
+
+ Generate P state machines, specifications, and test drivers from design documents using AI. Integrates with **Cursor** and **Claude Code** via MCP with 27 tools, ensemble generation, auto-fix pipeline, and 1,200+ RAG examples.
+
+ [:octicons-arrow-right-24: Get started with PeasyAI](getstarted/peasyai.md)
+
+- :material-monitor-eye:{ .lg .middle } **PObserve — Runtime Monitoring**
+
+ ---
+
+ Validate that your **production system conforms to its formal P specifications** by checking service logs against P monitors — bridging the gap between design-time verification and runtime behavior, without additional instrumentation.
+
+ [:octicons-arrow-right-24: Learn about PObserve](advanced/pobserve/pobserve.md)
+
+
+
+---
+
+## :material-shield-check:{ .lg } The P Framework
+
+
+
+- :material-file-document-edit-outline:{ .lg .middle } **P Language**
+
+ ---
+
+ Model distributed systems as communicating state machines. Specify safety and liveness properties. A programming language — not a mathematical notation.
+
+- :material-robot-outline:{ .lg .middle } **PeasyAI**
+
+ ---
+
+ AI-powered code generation from design documents. Generates types, machines, specs, and tests with auto-fix and human-in-the-loop support.
+
+- :material-shield-check-outline:{ .lg .middle } **P-Checker**
+
+ ---
+
+ Systematically explore interleavings of messages and failures to find deep bugs. Reproducible error traces for debugging. Additional backends: PEx, PVerifier.
+
+- :material-monitor-eye:{ .lg .middle } **PObserve**
+
+ ---
+
+ Validate service logs against P specifications in testing and production. Ensure implementation conforms to the verified design.
-
-**Impact**: P is currently being used extensively inside Amazon (AWS) for analysis of complex distributed systems. For example, Amazon S3 used P to formally reason about the core distributed protocols involved in its strong consistency launch. Teams across AWS are now using P for thinking and reasoning about their systems formally. P is also being used for programming safe robotics systems in Academia. P was first used to implement and validate the USB device driver stack that ships with Microsoft Windows 8 and Windows Phone.
+[:octicons-arrow-right-24: Learn more about the P framework](whatisP.md)
+
+---
+
+## :material-aws:{ .lg } Impact at AWS
+
+Using P, developers model their system designs as communicating state machines — a mental model familiar to developers who build systems based on microservices and service-oriented architectures. Teams across AWS that build some of its flagship products — from storage (Amazon S3, EBS), to databases (Amazon DynamoDB, MemoryDB, Aurora), to compute (EC2, IoT) — have been using P to reason about the correctness of their system designs.
+
+!!! abstract "Further Reading"
+ :material-file-document: [**Systems Correctness Practices at Amazon Web Services**](https://cacm.acm.org/practice/systems-correctness-practices-at-amazon-web-services/) — _Marc Brooker and Ankush Desai_, Communications of the ACM, 2025.
+
+??? question "Why formal methods? How is AWS using P?"
+ The following re:Invent 2023 talk provides an overview of P and its impact inside AWS:
+
+
-**Experience and lessons learned**:
-In our experience of using P inside AWS, Academia, and Microsoft. We have observed that P has helped developers in three critical ways: (1) **P as a thinking tool**: Writing formal specifications in P forces developers to think about their system design rigorously, and in turn helped in bridging gaps in their understanding of the system. A large fraction of the bugs can be eliminated in the process of writing specifications itself! (2) **P as a bug finder**: Model checking helped find corner case bugs in system design that were missed by stress and integration testing. (3) **P helped boost developer velocity**: After the initial overhead of creating the formal models, future updates and feature additions could be rolled out faster as these non-trivial changes are rigorously validated before implementation.
+ [(Re:Invent 2023) Gain confidence in system correctness & resilience with Formal Methods](https://youtu.be/FdXZXnkMDxs?si=iFqpl16ONKZuS4C0)
+
+---
+
+## :material-lightbulb-on:{ .lg } Experience and Lessons Learned
+
+
+
+- :material-head-lightbulb:{ .lg .middle } **P as a Thinking Tool**
+
+ ---
+
+ Writing formal specifications forces developers to think about their system design rigorously, bridging gaps in understanding. A large fraction of bugs can be eliminated in the process of writing specifications itself!
+
+- :material-bug:{ .lg .middle } **P as a Bug Finder**
+
+ ---
+
+ Model checking finds corner-case bugs in system design that are missed by stress and integration testing.
+
+- :material-rocket-launch:{ .lg .middle } **P Boosts Developer Velocity**
+
+ ---
+
+ After the initial overhead of creating formal models, future updates and feature additions can be rolled out faster as non-trivial changes are rigorously validated before implementation.
+
+
!!! quote ""
:sparkles: **_Programming concurrent, distributed systems is fun but challenging, however, a pinch of programming language design with a dash of automated reasoning can go a long way in addressing the challenge and amplifying the fun!._** :sparkles:
+---
+
## Let the fun begin!
-You can find most of the information about the P framework on this webpage:
-[what is P?](whatisP.md),
-[getting started](getstarted/install.md), [tutorials](tutsoutline.md),
-[case studies](casestudies.md) and related [research publications](publications.md). If
-you have any further questions, please feel free to create an
+
+
+- :material-help-circle-outline:{ .lg .middle } **[What is P?](whatisP.md)**
+
+ ---
+
+ Learn about the P framework and its components
+
+- :material-download:{ .lg .middle } **[Getting Started](getstarted/install.md)**
+
+ ---
+
+ Install P and start building your first program
+
+- :material-school:{ .lg .middle } **[Tutorials](tutsoutline.md)**
+
+ ---
+
+ Work through hands-on examples step by step
+
+- :material-flask:{ .lg .middle } **[Case Studies](casestudies.md)**
+
+ ---
+
+ See how P is used at AWS, Microsoft, and UC Berkeley
+
+
+
+If you have any questions, please feel free to create an
[issue](https://github.com/p-org/P/issues), ask on
[discussions](https://github.com/p-org/P/discussions), or
-[email us](mailto:ankushdesai@gmail.com)
+[email us](mailto:ankushdesai@gmail.com).
!!! info "Contributions"
_P has always been a collaborative project between industry and academia (since 2013)
:drum:. The P team welcomes contributions and suggestions from all of you!! :punch:._
-
diff --git a/Docs/docs/manual/datatypes.md b/Docs/docs/manual/datatypes.md
index 7c664697d0..21b5b1cad0 100644
--- a/Docs/docs/manual/datatypes.md
+++ b/Docs/docs/manual/datatypes.md
@@ -185,7 +185,7 @@ P allows programmers to define (or implement) types in the external languages. W
`tName` is the name of the foreign type.
Note that foreign types are disjoint from all other types in P. They are subtype of the `any` type.
-Details about how to define/implement foreign types in P is described [here](foriegntypesfunctions.md).
+Details about how to define/implement foreign types in P is described [here](foreigntypesfunctions.md).
### User Defined
diff --git a/Docs/docs/manual/foriegntypesfunctions.md b/Docs/docs/manual/foreigntypesfunctions.md
similarity index 98%
rename from Docs/docs/manual/foriegntypesfunctions.md
rename to Docs/docs/manual/foreigntypesfunctions.md
index 305d42577d..2df85ec358 100644
--- a/Docs/docs/manual/foriegntypesfunctions.md
+++ b/Docs/docs/manual/foreigntypesfunctions.md
@@ -75,7 +75,7 @@ p compile
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
Restored P/Tutorial/PriorityQueue/PGenerated/CSharp/PriorityQueue.csproj (in 392 ms).
- PriorityQueue -> P/Tutorial/PriorityQueue/PGenerated/CSharp/net6.0/PriorityQueue.dll
+ PriorityQueue -> P/Tutorial/PriorityQueue/PGenerated/CSharp/net8.0/PriorityQueue.dll
Build succeeded.
0 Warning(s)
@@ -101,8 +101,8 @@ p check -v
$ p check -v
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/PriorityQueue/PGenerated/CSharp/net6.0/PriorityQueue.dll
- .. Checking P/Tutorial/PriorityQueue/PGenerated/CSharp/net6.0/PriorityQueue.dll
+ .. Found a P compiled file: P/Tutorial/PriorityQueue/PGenerated/CSharp/net8.0/PriorityQueue.dll
+ .. Checking P/Tutorial/PriorityQueue/PGenerated/CSharp/net8.0/PriorityQueue.dll
.. Test case :: tcCheckPriorityQueue
... Checker is using 'random' strategy (seed:1636311106).
..... Schedule #1
diff --git a/Docs/docs/manual/functions.md b/Docs/docs/manual/functions.md
index f73feafec9..99e53b16f4 100644
--- a/Docs/docs/manual/functions.md
+++ b/Docs/docs/manual/functions.md
@@ -48,7 +48,7 @@ P named function declarations without any function body are referred to as forei
`name` is the name of the named function, `funParamList` is the optional function parameters, and `returnType` is the optional return type of the function.
-To know more about the foreign interface and functions, please look at the [PriorityQueue example](https://p-org.github.io/P/manual/foriegntypesfunctions/).
+To know more about the foreign interface and functions, please look at the [PriorityQueue example](https://p-org.github.io/P/manual/foreigntypesfunctions/).
### Function Body
diff --git a/Docs/docs/manualoutline.md b/Docs/docs/manualoutline.md
index 4aa82e8ece..dc8bbbd74e 100644
--- a/Docs/docs/manualoutline.md
+++ b/Docs/docs/manualoutline.md
@@ -1,6 +1,7 @@
+## P Language Manual
+
!!! tip ""
- **We recommend that you start with the [Tutorials](tutsoutline.md) to get familiar with
- the P language and its tool chain.**
+ **We recommend starting with the [Tutorials](tutsoutline.md) to get familiar with the P language and its tool chain.**
??? note "P Top Level Declarations Grammar"
@@ -17,23 +18,24 @@
;
```
-A P program consists of a collection of following top-level declarations:
-
-| Top Level Declarations | Description |
-| :------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
-| [User Defined Types](manual/datatypes.md) | P supports users defined types as well as foreign types (types that are defined in external language) |
-| [Enums](manual/datatypes.md) | P supports declaring enum values that can be used as int constants (update the link) |
-| [Events](manual/events.md) | Events are used by state machines to communicate with each other |
-| [State Machines](manual/statemachines.md) | P state machines are used to model or implement the behavior of the system |
-| [Specification Monitors](manual/monitors.md) | P specification monitors are used to write the safety and liveness specifications the system must satisfy for correctness |
-| [Global Functions](manual/functions.md) | P supports declaring global functions that can be shared across state machines and spec monitors |
-| [Module System](manual/modulesystem.md) | P supports a module system for implementing and testing the system modularly by dividing it into separate components |
-| [Test Cases](manual/testcases.md) | P test cases help programmers to write different finite scenarios under which they would like to check the correctness of their system |
-
-!!! Tip "Models, Specifications, Model Checking Scenario"
- A quick primer on what a model
- is, versus a specification, and model checking scenarios: (1) A specification says what
- the system should do (correctness properties). (2) A model captures the details of how the
- system does it. (3) A model checking scenario provides the finite non-deterministc
- test-harness or environment under which the model checker should check that the system
- model satisfies its specifications.
+---
+
+A P program consists of a collection of the following top-level declarations:
+
+| Declaration | Description |
+| :---------- | :---------- |
+| :material-code-braces: [**User Defined Types**](manual/datatypes.md) | User-defined types as well as foreign types (defined in external languages) |
+| :material-format-list-numbered: [**Enums**](manual/datatypes.md) | Enum values that can be used as int constants |
+| :material-email-outline: [**Events**](manual/events.md) | Events used by state machines to communicate with each other |
+| :material-state-machine: [**State Machines**](manual/statemachines.md) | State machines that model or implement the behavior of the system |
+| :material-shield-check: [**Specification Monitors**](manual/monitors.md) | Safety and liveness specifications the system must satisfy |
+| :material-function: [**Global Functions**](manual/functions.md) | Functions shared across state machines and spec monitors |
+| :material-puzzle: [**Module System**](manual/modulesystem.md) | Modular system design by dividing into separate components |
+| :material-test-tube: [**Test Cases**](manual/testcases.md) | Finite scenarios for checking system correctness |
+
+??? tip "Models, Specifications, and Model Checking Scenarios"
+ A quick primer:
+
+ - **Specification** — says _what_ the system should do (correctness properties)
+ - **Model** — captures the details of _how_ the system does it
+ - **Model checking scenario** — provides the finite non-deterministic test-harness under which the model checker verifies that the model satisfies its specifications
diff --git a/Docs/docs/old/advanced/debuggingerror.md b/Docs/docs/old/advanced/debuggingerror.md
index 982574c9bd..0a4a6b13fa 100644
--- a/Docs/docs/old/advanced/debuggingerror.md
+++ b/Docs/docs/old/advanced/debuggingerror.md
@@ -1,3 +1,6 @@
+!!! warning "Deprecated: P 1.x Documentation"
+ This page documents P version 1.x and is no longer maintained. For the current guide, see [Debugging Error Traces](../../advanced/debuggingerror.md).
+
As described in the [using P compiler and checker](../getstarted/usingP.md) section, running the following command for the ClientServer example finds an error.
```
diff --git a/Docs/docs/old/getstarted/install.md b/Docs/docs/old/getstarted/install.md
index 4907f1dbb8..9140a0ea59 100644
--- a/Docs/docs/old/getstarted/install.md
+++ b/Docs/docs/old/getstarted/install.md
@@ -1,3 +1,6 @@
+!!! warning "Deprecated: P 1.x Documentation"
+ This page documents P version 1.x and is no longer maintained. For the current installation guide, see [Installing P](../../getstarted/install.md).
+
# Installing P
P is built to be cross-platform and can be used on MacOS, Linux, and Windows. We provide a step-by-step guide for installing P along with its required dependencies.
diff --git a/Docs/docs/old/getstarted/usingP.md b/Docs/docs/old/getstarted/usingP.md
index 8a0b888bc8..5ec085ab44 100644
--- a/Docs/docs/old/getstarted/usingP.md
+++ b/Docs/docs/old/getstarted/usingP.md
@@ -1,3 +1,6 @@
+!!! warning "Deprecated: P 1.x Documentation"
+ This page documents P version 1.x and is no longer maintained. For the current guide, see [Using P Compiler and Checker](../../getstarted/usingP.md).
+
# Using P Compiler and Checker
!!! check ""
@@ -104,7 +107,7 @@ There are two ways of compiling a P program:
```
??? info "P Project File Details"
- The P compiler does not support advanced project management features like separate compilation and dependency analysis (_coming soon_).
+ The P compiler does not support advanced project management features like separate compilation and dependency analysis.
The current project file interface is a simple mechanism to provide all the required inputs to the compiler in an XML format ([ClientServer.pproj](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/ClientServer.pproj)).
``` xml
diff --git a/Docs/docs/publications.md b/Docs/docs/publications.md
index 06e87b8297..117bceff23 100644
--- a/Docs/docs/publications.md
+++ b/Docs/docs/publications.md
@@ -1,60 +1,72 @@
-## P Language and Backend Analysis
+## Publications
-1. **[Compositional Programming and Testing of Dynamic Distributed Systems](https://ankushdesai.github.io/assets/papers/modp.pdf)**.
- _Ankush Desai, Amar Phanishayee, Shaz Qadeer, and Sanjit Seshia_
- International Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA), 2018.
+---
-2. **[Lasso detection using Partial State Caching](https://ankushdesai.github.io/assets/papers/liveness.pdf)**.
- _Rashmi Mudduluru, Pantazis Deligiannis, Ankush Desai, Akash Lal and Shaz Qadeer._
Formal
- Methods in Computer-Aided Design (FMCAD) - 2017
+### :material-book-open-variant:{ .lg } P Language and Backend Analysis
-3. **[Systematic Testing of Asynchronous Reactive Systems](https://ankushdesai.github.io/assets/papers/fse-desai.pdf)**.
- _Ankush Desai, Shaz Qadeer, and Sanjit A. Seshia._
Proceedings of the 2015 10th Joint
- Meeting on Foundations of Software Engineering (ESEC/FSE 2015).
+1. **[Compositional Programming and Testing of Dynamic Distributed Systems](https://ankushdesai.github.io/assets/papers/modp.pdf)**
+ _Ankush Desai, Amar Phanishayee, Shaz Qadeer, and Sanjit Seshia_
+ OOPSLA, 2018.
-4. **[Natural proofs for Asynchronous Programs using Almost-synchronous Invariants](https://ankushdesai.github.io/assets/papers/OOPSLA14.pdf)**.
- _Ankush Desai, Pranav Garg, and P. Madhusudan._
International Conference on Object-Oriented
- Programming, Systems, Languages, and Applications (OOPSLA) - 2014
+2. **[Lasso detection using Partial State Caching](https://ankushdesai.github.io/assets/papers/liveness.pdf)**
+ _Rashmi Mudduluru, Pantazis Deligiannis, Ankush Desai, Akash Lal and Shaz Qadeer._
+ FMCAD, 2017.
-5. **[P: Safe asynchronous event-driven programming](https://ankushdesai.github.io/assets/papers/p.pdf)**.
- _Ankush Desai, Vivek Gupta, Ethan Jackson, Shaz Qadeer, Sriram Rajamani, and Damien
- Zufferey._
Proceedings of ACM SIGPLAN Conference on Programming Language Design and
- Implementation (PLDI), 2013.
+3. **[Systematic Testing of Asynchronous Reactive Systems](https://ankushdesai.github.io/assets/papers/fse-desai.pdf)**
+ _Ankush Desai, Shaz Qadeer, and Sanjit A. Seshia._
+ ESEC/FSE, 2015.
-6. **[Depth bounded explicit-state model checking](https://ankushdesai.github.io/assets/papers/spin2011.pdf)**.
- _Abhishek Udupa, Ankush Desai and Sriram Rajamani._
- International SPIN Symposium on Model Checking of Software (SPIN) - 2011
+4. **[Natural proofs for Asynchronous Programs using Almost-synchronous Invariants](https://ankushdesai.github.io/assets/papers/OOPSLA14.pdf)**
+ _Ankush Desai, Pranav Garg, and P. Madhusudan._
+ OOPSLA, 2014.
-## P Case Studies
+5. **[P: Safe asynchronous event-driven programming](https://ankushdesai.github.io/assets/papers/p.pdf)**
+ _Ankush Desai, Vivek Gupta, Ethan Jackson, Shaz Qadeer, Sriram Rajamani, and Damien Zufferey._
+ PLDI, 2013.
-1. **[PSec: Programming Secure Distributed Systems using Enclaves](https://dl.acm.org/doi/10.1145/3433210.3453113)**.
- _Shivendra Kushwah, Ankush Desai, Pramod Subramanyan, Sanjit A. Seshia._
- Proceedings of the
- 2021 ACM Asia Conference on Computer and Communications Security (AsiaCCS) - 2021
+6. **[Depth bounded explicit-state model checking](https://ankushdesai.github.io/assets/papers/spin2011.pdf)**
+ _Abhishek Udupa, Ankush Desai and Sriram Rajamani._
+ SPIN, 2011.
-2. **[Programming Safe Robotics Systems: Challenges and Advances](https://ankushdesai.github.io/assets/papers/isolapaper.pdf)**.
- Ankush Desai, Shaz Qadeer and Sanjit Seshia.
International Symposium On Leveraging
- Applications of Formal Methods, Verification and Validation (ISoLA) - 2018
+---
-3. **[DRONA: A Framework for Safe Distributed Mobile Robotics](https://ankushdesai.github.io/assets/papers/drona.pdf)**.
- _Ankush Desai, Indranil Saha, Jianqiao Yang, Shaz Qadeer, and Sanjit A. Seshia._
- Proceedings of the 8th ACM/IEEE International Conference on Cyber-Physical Systems
- (ICCPS), 2017.
+### :material-aws:{ .lg } P at AWS
-4. **[Combining Model Checking and Runtime Verification for Safe Robotics](https://link.springer.com/chapter/10.1007/978-3-319-67531-2_11%22)**.
- _Ankush Desai, Tommaso Dreossi and Sanjit Seshia._
The 17th International Conference on
- Runtime Verification (RV) - 2017
+1. **[Systems Correctness Practices at Amazon Web Services](https://cacm.acm.org/practice/systems-correctness-practices-at-amazon-web-services/)**
+ _Marc Brooker and Ankush Desai._
+ Communications of the ACM (Practice), 2025.
-5. **[Approximate Synchrony: An Abstraction for Distributed Almost-synchronous Systems](https://ankushdesai.github.io/assets/papers/as-cav15.pdf)**.
- _Ankush Desai, Sanjit Seshia, Shaz Qadeer, David Broman, and John Eidson._
International
- Conference on Computer Aided Verification (CAV) - 2015
+---
-6. **[Endlessly Circulating Messages in IEEE 1588-2008 Systems](https://ankushdesai.github.io/assets/papers/ispcs14.pdf)**.
- _David Broman, P Derler, Ankush Desai, John Eidson, and Sanjit Seshia._
International
- Symposium on Precision Clock Synchronization for Measurement, Control and Communication
- (ISPCS) - 2014
+### :material-flask:{ .lg } P Case Studies
-## PhD Thesis
+1. **[PSec: Programming Secure Distributed Systems using Enclaves](https://dl.acm.org/doi/10.1145/3433210.3453113)**
+ _Shivendra Kushwah, Ankush Desai, Pramod Subramanyan, Sanjit A. Seshia._
+ AsiaCCS, 2021.
-- **[Modular and Safe Event-Driven Programming](https://www2.eecs.berkeley.edu/Pubs/TechRpts/2020/EECS-2020-3.html)**.
-_Ankush Desai_
University of California, Berkeley - 2019.
+2. **[Programming Safe Robotics Systems: Challenges and Advances](https://ankushdesai.github.io/assets/papers/isolapaper.pdf)**
+ _Ankush Desai, Shaz Qadeer and Sanjit Seshia._
+ ISoLA, 2018.
+
+3. **[DRONA: A Framework for Safe Distributed Mobile Robotics](https://ankushdesai.github.io/assets/papers/drona.pdf)**
+ _Ankush Desai, Indranil Saha, Jianqiao Yang, Shaz Qadeer, and Sanjit A. Seshia._
+ ICCPS, 2017.
+
+4. **[Combining Model Checking and Runtime Verification for Safe Robotics](https://link.springer.com/chapter/10.1007/978-3-319-67531-2_11%22)**
+ _Ankush Desai, Tommaso Dreossi and Sanjit Seshia._
+ RV, 2017.
+
+5. **[Approximate Synchrony: An Abstraction for Distributed Almost-synchronous Systems](https://ankushdesai.github.io/assets/papers/as-cav15.pdf)**
+ _Ankush Desai, Sanjit Seshia, Shaz Qadeer, David Broman, and John Eidson._
+ CAV, 2015.
+
+6. **[Endlessly Circulating Messages in IEEE 1588-2008 Systems](https://ankushdesai.github.io/assets/papers/ispcs14.pdf)**
+ _David Broman, P Derler, Ankush Desai, John Eidson, and Sanjit Seshia._
+ ISPCS, 2014.
+
+---
+
+### :material-school:{ .lg } PhD Thesis
+
+- **[Modular and Safe Event-Driven Programming](https://www2.eecs.berkeley.edu/Pubs/TechRpts/2020/EECS-2020-3.html)**
+ _Ankush Desai_, University of California, Berkeley, 2019.
diff --git a/Docs/docs/toolchain.jpg b/Docs/docs/toolchain.jpg
new file mode 100644
index 0000000000..9eb825184c
Binary files /dev/null and b/Docs/docs/toolchain.jpg differ
diff --git a/Docs/docs/toolchain.png b/Docs/docs/toolchain.png
deleted file mode 100644
index f89aa04971..0000000000
Binary files a/Docs/docs/toolchain.png and /dev/null differ
diff --git a/Docs/docs/tutorial/clientserver.md b/Docs/docs/tutorial/clientserver.md
index b507b5826b..d7b78b2539 100644
--- a/Docs/docs/tutorial/clientserver.md
+++ b/Docs/docs/tutorial/clientserver.md
@@ -1,3 +1,5 @@
+## Example 1: Client Server
+
??? note "How to use this example"
We assume that you have cloned the P repository locally.
@@ -5,24 +7,28 @@
git clone https://github.com/p-org/P.git
```
- The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in IntelliJ side-by-side a browser using which you can simultaneously read the description for each example and browse the P program.
+ The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in [Peasy IDE](../getstarted/PeasyIDE.md) (VS Code extension) or your preferred editor side-by-side with a browser, so you can simultaneously read the description for each example and browse the P program.
To know more about P language primitives used in the example, please look them up in the [language manual](../manualoutline.md).
**System:** We consider a client-server application where clients interact with a bank to withdraw money from their accounts.
-{ align=center }
+{ align=center }
The bank consists of two components: (1) a bank server that services withdraw requests from the client; and (2) a backend database which is used to store the account balance information for each client.
Multiple clients can concurrently send withdraw requests to the bank. On receiving a withdraw request, the bank server reads the current bank balance for the client and if the withdraw request is allowed then performs the withdrawal, updates the account balance, and responds back to the client with the new account balance.
**Correctness Specification:** The bank must maintain the invariant that each account must have at least 10 dollars as its balance. If a withdraw request takes the account balance below 10 then the withdraw request must be rejected by the bank. The correctness property that we would like to check is that in the presence of concurrent client withdraw requests the bank always responds with the correct bank balance for each client and a withdraw request always succeeds if there is enough balance in the account (that is, at least 10).
-### P Project
+---
+
+### :material-folder-outline:{ .lg } P Project
The [1_ClientServer](https://github.com/p-org/P/tree/master/Tutorial/1_ClientServer) folder contains the source code for the [ClientServer](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/ClientServer.pproj) project. Please feel free to read details about the recommended [P program structure](../advanced/structureOfPProgram.md) and [P project file](../advanced/PProject.md).
-### Models
+---
+
+### :material-state-machine:{ .lg } Models
The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/1_ClientServer/PSrc)) for the ClientServer example consist of four files:
@@ -63,7 +69,9 @@ The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/1_ClientSer
- ([L1 - L5](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSrc/ClientServerModules.p#L1-L5)) → Declares the `Client` and `Bank` modules. A module in P is a collection of state machines that together implement that module or component. A system model in P is then a composition or union of modules. The `Client` module consists of a single machine `Client` and the `Bank` module is implemented by machines `BankServer` and `Database` together (manual: [P module system](../manual/modulesystem.md)).
- ([L7 - L8](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSrc/ClientServerModules.p#L7-L8)) → The `AbstractBank` module uses the `binding` feature in P modules to bind the `BankServer` machine to the `AbstractBankServer` machine. Basically, what this implies is that whenever `AbstractBank` module is used the creation of the `BankServer` machine will result in creation of `AbstractBankServer`, replacing the implementation with its abstraction (manual: [primitive modules](../manual/modulesystem.md#primitive-module)).
-### Specifications
+---
+
+### :material-shield-check:{ .lg } Specifications
The P Specifications ([PSpec](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSpec)) for the ClientServer example are implemented in the [BankBalanceCorrect.p](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSpec/BankBalanceCorrect.p) file. We define two specifications:
@@ -79,7 +87,9 @@ The P Specifications ([PSpec](https://github.com/p-org/P/blob/master/Tutorial/1_
- ([L92 - L115](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PSpec/BankBalanceCorrect.p#L92-L115)) → Declares the `GuaranteedWithDrawProgress` liveness spec machine that observes the events `eWithDrawReq` and `eWithDrawResp` to assert the required liveness property that every request is eventually responded by the Bank.
- To understand the semantics of the P spec machines, please read manual: [p monitors](../manual/monitors.md).
-### Test Scenarios
+---
+
+### :material-test-tube:{ .lg } Test Scenarios
The test scenarios folder in P has two parts: TestDrivers and TestScripts. TestDrivers are collections of state machines that implement the test harnesses (or environment state machines) for different test scenarios. TestScripts are collections of test cases that are automatically run by the P checker.
@@ -96,7 +106,9 @@ The test scenarios folder for ClientServer ([PTst](https://github.com/p-org/P/tr
- ([L4 - L16](https://github.com/p-org/P/blob/master/Tutorial/1_ClientServer/PTst/Testscript.p#L4-L16)) → Declares three test cases each checking a different scenario and system. The system under test is the `union` of the modules representing each component in the system (manual: [P module system](../manual/modulesystem.md#union-module)). The `assert` module constructor is used to attach monitors or specifications to be checked on the modules (manual: [assert](../manual/modulesystem.md#assert-monitors-module)).
- In the `tcAbstractServer` test case, instead of composing with the Bank module, we use the AbstractBank module. Hence, in the composed system, whenever the creation of a BankServer machine is invoked the binding will instead create an AbstractBankServer machine.
-### Parameterized Tests
+---
+
+### :material-tune:{ .lg } Parameterized Tests
The ClientServer tutorial demonstrates P's parameterized testing capabilities that allow systematic exploration of different system configurations. These tests enable checking the system with varying numbers of clients to validate scalability and concurrent access patterns.
@@ -110,7 +122,9 @@ The ClientServer tutorial demonstrates P's parameterized testing capabilities th
- This parameterized approach allows verification of the bank's correctness properties under different concurrency levels, ensuring that the `BankBalanceIsAlwaysCorrect` and `GuaranteedWithDrawProgress` specifications hold regardless of client count.
- The test cases generated are: `tcParameterizedMultipleClients___nClients_2`, `tcParameterizedMultipleClients___nClients_3`, and `tcParameterizedMultipleClients___nClients_4`, each testing the system with the corresponding number of clients.
-### Compiling ClientServer
+---
+
+### :material-cog-play:{ .lg } Compiling
Navigate to the [1_ClientServer](https://github.com/p-org/P/tree/master/Tutorial/1_ClientServer) folder and run the following command to compile the ClientServer project:
@@ -143,7 +157,7 @@ p compile
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
Restored P/Tutorial/1_ClientServer/PGenerated/CSharp/ClientServer.csproj (in 102 ms).
- ClientServer -> P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ ClientServer -> P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
Build succeeded.
0 Warning(s)
@@ -156,7 +170,9 @@ p compile
~~ [PTool]: Thanks for using P! ~~
```
-### Checking ClientServer
+---
+
+### :material-shield-check-outline:{ .lg } Checking
You can get the list of test cases defined in the ClientServer project by running the P Checker:
@@ -170,8 +186,8 @@ p check
$ p check
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
- .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net6.0/ClientServer.dll
+ .. Found a P compiled file: P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
+ .. Checking P/Tutorial/1_ClientServer/PGenerated/CSharp/net8.0/ClientServer.dll
Error: We found '6' test cases. Please provide a more precise name of the test case you wish to check using (--testcase | -tc).
Possible options are:
tcSingleClient
@@ -213,7 +229,9 @@ p check -tc tcAbstractServer -s 1000
!!! danger "Error"
`tcAbstractServer` triggers an error in the AbstractBankServer state machine. Please use the [guide](../advanced/debuggingerror.md) to explore how to debug an error trace generated by the P Checker.
-### Exercise Problem
+---
+
+### :material-pencil:{ .lg } Exercise Problem
- [Problem 1] Fix the bug in AbstractBankServer state machine and run the P Checker again on the test case to ensure that there are no more bugs in the models.
- [Problem 2] Extend the ClientServer example with support for depositing money into the bank. This would require implementing events `eDepositReq` and `eDepositResp` which are used to interact between the client and server machine. The Client machine should be updated to deposit money into the account when the balance is low, the BankServer machine implementation would have to be updated to support depositing money into the bank account and finally safety and liveness specifications needs to be updated take disposit events into account. After implementing the deposit feature, run the test-cases again to check if the system still satisfies the desired specifications.
diff --git a/Docs/docs/tutorial/common.md b/Docs/docs/tutorial/common.md
index 4b0498a9fd..328a4ccdba 100644
--- a/Docs/docs/tutorial/common.md
+++ b/Docs/docs/tutorial/common.md
@@ -1,3 +1,23 @@
-We also describe how to model system's interaction with an OS Timer [Timer](https://github.com/p-org/P/blob/master/Tutorial/Common/Timer/), and how to model injecting node failures in the system [Failure Injector](https://github.com/p-org/P/tree/master/Tutorial/Common/FailureInjector). These models are used in the Two Phase Commit, Espresso Machine, and Failure Detector models.
+## Common: Timer, Failure Injector, and Shared Memory
-P is a purely messaging passing based programming language and hence does not support primitives for modeling shared memory based concurrency. But one can always model shared memory concurrency using message passing. We have used this style of modeling when checking the correctness of single node file systems. Please check out [shared memory project](https://github.com/p-org/P/tree/master/Tutorial/Common/SharedMemory) on how to model shared memory concurrency using P.
+The tutorials use several reusable P models for common system components. These are located in [Tutorial/Common](https://github.com/p-org/P/tree/master/Tutorial/Common).
+
+---
+
+### :material-timer-outline:{ .lg } Timer
+
+The [Timer](https://github.com/p-org/P/blob/master/Tutorial/Common/Timer/) model captures a system's interaction with an OS timer. It is used in the Two Phase Commit, Espresso Machine, and Failure Detector examples.
+
+---
+
+### :material-alert-circle-outline:{ .lg } Failure Injector
+
+The [Failure Injector](https://github.com/p-org/P/tree/master/Tutorial/Common/FailureInjector) model allows injecting node failures into the system during model checking. It is used in the Two Phase Commit and Failure Detector examples.
+
+---
+
+### :material-memory:{ .lg } Shared Memory
+
+P is a purely message-passing based programming language and does not support primitives for modeling shared memory concurrency. However, shared memory concurrency can always be modeled using message passing.
+
+The [Shared Memory](https://github.com/p-org/P/tree/master/Tutorial/Common/SharedMemory) example demonstrates this approach, which has been used when checking the correctness of single-node file systems.
diff --git a/Docs/docs/tutorial/espressomachine.md b/Docs/docs/tutorial/espressomachine.md
index 8e0ca36ebf..33bd2f3f4f 100644
--- a/Docs/docs/tutorial/espressomachine.md
+++ b/Docs/docs/tutorial/espressomachine.md
@@ -1,3 +1,5 @@
+## Example 3: Espresso Machine
+
??? note "How to use this example"
We assume that you have cloned the P repository locally.
@@ -5,7 +7,7 @@
git clone https://github.com/p-org/P.git
```
- The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in IntelliJ side-by-side a browser using which you can simultaneously read the description for each example and browse the P program in IntelliJ.
+ The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in [Peasy IDE](../getstarted/PeasyIDE.md) (VS Code extension) or your preferred editor side-by-side with a browser, so you can simultaneously read the description for each example and browse the P program.
To know more about the P language primitives used in the example, please look them up in the [language manual](../manualoutline.md).
@@ -15,18 +17,22 @@ P has been used in the past to implement device drivers and robotics systems (se
**System:** We consider a fun example of modeling an espresso coffee machine and see how we can use P state machines to model a reactive system that must respond correctly to various user inputs. The user interacts with the coffee machine through its control panel. So the Espresso machine basically consists of two parts: the front-end control panel and the backend coffee maker that actually makes the coffee.
-{ width=50% align=center }
+{ width=50% align=center }
The control panel presents an interface to the user to perform operations like `reset` machine, turn `steamer` on and off, request an `espresso`, and clear the `grounds` by opening the container. The control panel interprets these inputs from the user and sends appropriate commands to the coffee maker.
**Correctness Specifications:**
By default, the P checker tests whether any event that is received in a state has a handler defined for it, otherwise, it would result in an unhandled event exception. If the P checker fails to find a bug then it implies that the system model can handle any sequence of events generated by the given environment which in our example's context implies that the coffee machine control panel can appropriately handle any sequence of inputs (button presses) by the user. We would also like to check that the coffee machine moves through a desired sequence of states, i.e., `WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready`.
-### P Project
+---
+
+### :material-folder-outline:{ .lg } P Project
The [3_EspressoMachine](https://github.com/p-org/P/tree/master/Tutorial/3_EspressoMachine) folder contains the source code for the [EspressoMachine](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/EspressoMachine.pproj) project. Please feel free to read details about the recommended [P program structure](../advanced/structureOfPProgram.md) and [P project file](../advanced/PProject.md).
-### Models
+---
+
+### :material-state-machine:{ .lg } Models
The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/3_EspressoMachine/PSrc)) for the EspressoMachine example consist of three files:
@@ -47,7 +53,9 @@ The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/3_EspressoM
- [EspressoMachineModules.p](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PSrc/EspressoMachineModules.p): Declares the P module corresponding to EspressoMachine.
-### Specifications
+---
+
+### :material-shield-check:{ .lg } Specifications
The P Specification ([PSpec](https://github.com/p-org/P/tree/master/Tutorial/3_EspressoMachine/PSpec)) for the EspressoMachine example is implemented in [Safety.p](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PSpec/Safety.p). We define a safety specification `EspressoMachineModesOfOperation` that observes the internal state of the EspressoMachine through the events that are announced as the system moves through different states and asserts that it always moves through the desired sequence of states. Steady operation: `WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready`. If an error occurs in any of the states above then the EspressoMachine stays in the error state until
it is reset and after which it returns to the `Warmup` state.
@@ -57,13 +65,17 @@ it is reset and after which it returns to the `Warmup` state.
- The `EspressoMachineModesOfOperation` spec machine observes these events and ensures that the system moves through the states defined by the monitor. Note that if the system allows (has execution as) a sequence of events that are not accepted by the monitor (i.e., the monitor throws an unhandled event exception) then the system does not satisfy the desired specification. Hence, this monitor can be thought of accepting only those behaviors of the system that follow the sequence of states modelled by the spec machine. For example, if the system moves from Ready to CoffeeMaking state directly without Grinding then the monitor will raise an ALARM!
- To understand the semantics of the P spec machines, please read manual: [p monitors](../manual/monitors.md).
-### Test Scenarios
+---
+
+### :material-test-tube:{ .lg } Test Scenarios
The test scenarios folder in P has two parts: TestDrivers and TestScripts. TestDrivers are collections of state machines that implement the test harnesses (or environment state machines) for different test scenarios. TestScripts are collections of test cases that are automatically run by the P checker.
The test scenarios folder for EspressoMachine ([PTst](https://github.com/p-org/P/tree/master/Tutorial/1_EspressoMachine/PTst)) consists of three files: [TestDriver.p](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PTst/TestDrivers.p) and [TestScript.p](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PTst/TestScripts.p) are just like other previous examples. The [User.p](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PTst/Users.p) declares two machines: (1) a [`SaneUser` machine](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PTst/Users.p#L4-L51) that uses the EspressoMachine with care, pressing the buttons in the right order, and cleaning up the grounds after the coffee is made; and (2) a [`CrazyUser` machine](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PTst/Users.p#L66-L136) who has never used an espresso machine before, gets too excited, and starts pushing random buttons on the control panel. Additionally, (3) a [`SteamerTestUser` machine](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PTst/Users.p#L108-L139) that is specifically designed to test steamer functionality, focusing on steamer operations and subsequent espresso making to validate the machine's handling of steamer-related features.
-### Generated Parameterized Tests
+---
+
+### :material-tune:{ .lg } Generated Parameterized Tests
The EspressoMachine tutorial includes comprehensive examples of parameterized testing that demonstrate P's advanced testing capabilities. These tests allow systematic exploration of different system configurations and user behaviors.
@@ -81,7 +93,9 @@ The EspressoMachine tutorial includes comprehensive examples of parameterized te
- ([L26 - L27](https://github.com/p-org/P/blob/master/Tutorial/3_EspressoMachine/PTst/TestScripts.p#L26-L27)) → Boolean parameter test `tcBooleanConfigs` tests all combinations of feature flags `enableSteamer` and `cleaningMode` to ensure proper feature interaction behavior.
-### Compiling EspressoMachine
+---
+
+### :material-cog-play:{ .lg } Compiling
Run the following command to compile the project:
@@ -114,7 +128,7 @@ p compile
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
Restored P/Tutorial/3_EspressoMachine/PGenerated/CSharp/EspressoMachine.csproj (in 102 ms).
- EspressoMachine -> P/Tutorial/3_EspressoMachine/PGenerated/CSharp/net6.0/EspressoMachine.dll
+ EspressoMachine -> P/Tutorial/3_EspressoMachine/PGenerated/CSharp/net8.0/EspressoMachine.dll
Build succeeded.
0 Warning(s)
@@ -127,7 +141,9 @@ p compile
~~ [PTool]: Thanks for using P! ~~
```
-### Checking EspressoMachine
+---
+
+### :material-shield-check-outline:{ .lg } Checking
You can get the list of test cases defined in the EspressoMachine project by running the P Checker:
@@ -141,8 +157,8 @@ p check
$ p check
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/3_EspressoMachine/PGenerated/CSharp/net6.0/EspressoMachine.dll
- .. Checking P/Tutorial/3_EspressoMachine/PGenerated/CSharp/net6.0/EspressoMachine.dll
+ .. Found a P compiled file: P/Tutorial/3_EspressoMachine/PGenerated/CSharp/net8.0/EspressoMachine.dll
+ .. Checking P/Tutorial/3_EspressoMachine/PGenerated/CSharp/net8.0/EspressoMachine.dll
Error: We found '30' test cases. Please provide a more precise name of the test case you wish to check using (--testcase | -tc).
Possible options are:
tcSaneUserUsingCoffeeMachine
@@ -194,7 +210,9 @@ Check the `tcCrazyUserUsingCoffeeMachine` test case for 10,000 schedules:
p check -tc tcCrazyUserUsingCoffeeMachine -s 10000
```
-### Exercise Problem
+---
+
+### :material-pencil:{ .lg } Exercise Problem
- [Problem 1] Note that the current safety specification `EspressoMachineModesOfOperation` does not capture the case where the CoffeeMaker can move to CoffeeMakerDoorOpened state. Extend the spec to cover those modes of operations as well.
diff --git a/Docs/docs/tutorial/failuredetector.md b/Docs/docs/tutorial/failuredetector.md
index 0df9a8fe90..82246b63f2 100644
--- a/Docs/docs/tutorial/failuredetector.md
+++ b/Docs/docs/tutorial/failuredetector.md
@@ -1,3 +1,4 @@
+## Example 4: Failure Detector
Energized with the Coffee :coffee:, let's get back to modeling distributed systems. After the two phase commit protocol, the next protocol that we will jump to is a simple broadcast-based failure detector!
@@ -10,21 +11,25 @@ By this point in the tutorial, we have gotten familiar with the P language and m
git clone https://github.com/p-org/P.git
```
- The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in IntelliJ side-by-side a browser using which you can simultaneously read the description for each example and browse the P program in IntelliJ.
+ The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in [Peasy IDE](../getstarted/PeasyIDE.md) (VS Code extension) or your preferred editor side-by-side with a browser, so you can simultaneously read the description for each example and browse the P program.
To know more about P language primitives used in the example, please look them up in the [language manual](../manualoutline.md).
**System:** We consider a simple failure detector that basically broadcasts ping messages to all the nodes in the system and uses a timer to wait for pong responses from all nodes. If a node does not respond with a pong message after multiple attempts (either because of network failure or node failure), the failure detector marks the node as down and notifies the clients about the nodes that are potentially down. We use this example to show how to model network message loss in P and discuss how to model other types of network behaviours.
-{ align=center }
+{ align=center }
**Correctness Specification:** We would like to check - using a liveness specification - that if the failure injector shuts down a particular node then the failure detector always eventually detects the node failure and notifies the client.
-### P Project
+---
+
+### :material-folder-outline:{ .lg } P Project
The [4_FailureDetector](https://github.com/p-org/P/tree/master/Tutorial/4_FailureDetector) folder contains the source code for the [FailureDetector](https://github.com/p-org/P/blob/master/Tutorial/4_FailureDetector/FailureDetector.pproj) project. Please feel free to read details about the recommended [P program structure](../advanced/structureOfPProgram.md) and [P project file](../advanced/PProject.md).
-### Models
+---
+
+### :material-state-machine:{ .lg } Models
The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/4_FailureDetector/PSrc)) for the FailureDetector example consist of four files:
@@ -53,7 +58,9 @@ The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/4_FailureDe
-### Specifications
+---
+
+### :material-shield-check:{ .lg } Specifications
The P Specification ([PSpec](https://github.com/p-org/P/tree/master/Tutorial/4_FailureDetector/PSpec)) for the FailureDetector is implemented in [ReliableFailureDetector.p](https://github.com/p-org/P/blob/master/Tutorial/4_FailureDetector/PSpec/ReliableFailureDetector.p). We define a simple `ReliableFailureDetector` liveness specification to assert that all nodes that have been shutdown
by the failure injector will eventually be detected by the failure detector as failed nodes.
@@ -62,7 +69,9 @@ by the failure injector will eventually be detected by the failure detector as f
- ([L6 - L57](https://github.com/p-org/P/blob/master/Tutorial/4_FailureDetector/PSpec/ReliableFailureDetector.p#L6-L57)) → Declares the `ReliableFailureDetector` liveness monitor. `ReliableFailureDetector` spec machine basically maintains two sets `nodesDownDetected` (nodes that are detected as down by the detector) and `nodesShutdownAndNotDetected` (nodes that are shutdown by the failure injector but not yet detected). `ReliableFailureDetector` monitor observes the `eNotifyNodesDown` and `eShutDown` events to update these maps and move between the `hot` state (unstable state) and non-hot states. The system is in a hot state if there are nodes that are shutdown but not yet detected by the failure detector. The system violates a liveness specification if any of its execution paths terminates in a hot state.
- To understand the semantics of the P spec machines and the details about liveness monitors, please read the manual: [p monitors](../manual/monitors.md).
-### Test Scenarios
+---
+
+### :material-test-tube:{ .lg } Test Scenarios
The test scenarios folder in P has two parts: TestDrivers and TestScripts. TestDrivers are collections of state machines that implement the test harnesses (or environment state machines) for different test scenarios. TestScripts are collections of test cases that are automatically run by the P checker.
@@ -89,7 +98,9 @@ The test scenarios folder for FailureDetector ([PTst](https://github.com/p-org/P
- Uses `assume` to ensure clients don't outnumber nodes for better monitoring distribution
- Tests each valid configuration with the `ReliableFailureDetector` specification
-### Compiling FailureDetector
+---
+
+### :material-cog-play:{ .lg } Compiling
Run the following command to compile the FailureDetector project:
@@ -128,7 +139,7 @@ p compile
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
Restored P/Tutorial/4_FailureDetector/PGenerated/CSharp/FailureDetector.csproj (in 93 ms).
- FailureDetector -> P/Tutorial/4_FailureDetector/PGenerated/CSharp/net6.0/FailureDetector.dll
+ FailureDetector -> P/Tutorial/4_FailureDetector/PGenerated/CSharp/net8.0/FailureDetector.dll
Build succeeded.
0 Warning(s)
@@ -141,7 +152,9 @@ p compile
~~ [PTool]: Thanks for using P! ~~
```
-### Checking FailureDetector
+---
+
+### :material-shield-check-outline:{ .lg } Checking
You can get the list of test cases defined in the FailureDetector project by running the P Checker:
@@ -181,11 +194,15 @@ Check the `tcTest_FailureDetector` test case for 10,000 schedules:
p check -tc tcTest_FailureDetector -s 10000
```
-### Discussion: Modeling Message Reordering
+---
+
+### :material-chat-processing:{ .lg } Discussion: Modeling Message Reordering
(to be added soon)
-### Exercise Problem
+---
+
+### :material-pencil:{ .lg } Exercise Problem
!!! success "What did we learn through this example?"
In this example, we saw how to use data nondeterminism to model message loss and unreliable sends. We also discussed how to model other types of network nondeterminism.
diff --git a/Docs/docs/tutorial/paxos.md b/Docs/docs/tutorial/paxos.md
index 7a905ef259..9dc462f2cc 100644
--- a/Docs/docs/tutorial/paxos.md
+++ b/Docs/docs/tutorial/paxos.md
@@ -1,6 +1,289 @@
+## Example 5: Single Decree Paxos
+
How can we finish our tutorials on modeling distributed systems without giving tribute to the Paxos protocol :pray: (and our inspiration [Leslie Lamport](http://www.lamport.org/)). Let's end the tutorial with a simplified **[single decree paxos](https://mwhittaker.github.io/blog/single_decree_paxos/)**.
-In this example, we present a simplified model of the single decree paxos. We say simplified because general paxos is resilient against arbitrary network (lossy, duplicate, re-order, and delay), in our case we only model message loss and delay, and check correctness of paxos in the presence of such a network. This is a fun exercise, we encourage you to play around and create variants of paxos!
+??? note "How to use this example"
+
+ We assume that you have cloned the P repository locally.
+ ```shell
+ git clone https://github.com/p-org/P.git
+ ```
+
+ The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in [Peasy IDE](../getstarted/PeasyIDE.md) (VS Code extension) or your preferred editor side-by-side with a browser, so you can simultaneously read the description for each example and browse the P program.
+
+ To know more about P language primitives used in the example, please look them up in the [language manual](../manualoutline.md).
+
+**System:** We present a simplified model of the [single decree Paxos](https://mwhittaker.github.io/blog/single_decree_paxos/) consensus protocol. In single decree Paxos, a set of proposers attempt to get a single value agreed upon (decided) by a set of acceptors, and the decided value is then taught to a set of learners. The protocol guarantees that **at most one value is ever decided**, even in the presence of concurrent proposers and unreliable networks.
+
+We say simplified because general Paxos is resilient against arbitrary network behavior (lossy, duplicate, re-order, and delay). In our model, we model message loss and delay, and check correctness of Paxos in the presence of such a network. This is a fun exercise — we encourage you to play around and create variants of Paxos!
+
+**Correctness Specification:** The safety property we check is that once a value is decided (taught to a learner), all subsequent decisions must agree on the same value. This is the core consensus guarantee of Paxos.
+
+!!! abstract "What will we learn?"
+ Modeling a classic consensus protocol as communicating state machines, using nondeterminism to model unreliable networks (message loss and duplication), writing a consensus safety specification, and testing with multiple configurations of proposers and acceptors.
+
+---
+
+### P Project
+
+The [5_Paxos](https://github.com/p-org/P/tree/master/Tutorial/5_Paxos) folder contains the source code for this example. The P project file for the Paxos example is available [here](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/SingleDecreePaxos.pproj).
+
+The Paxos protocol has three roles — **Proposer**, **Acceptor**, and **Learner** — each modeled as a P state machine:
+
+| Role | Machine | Description |
+|------|---------|-------------|
+| **Proposer** | [`PSrc/proposer.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSrc/proposer.p) | Drives the protocol through three phases: Prepare, Accept, and Teach |
+| **Acceptor** | [`PSrc/acceptor.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSrc/acceptor.p) | Promises not to accept lower ballots and accepts values from valid leaders |
+| **Learner** | [`PSrc/learner.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSrc/learner.p) | Receives decided values and asserts consistency |
+
+---
+
+### Models
+
+#### Types and Events
+
+The protocol communication is defined through ballot numbers, values, and four message types ([`PSrc/acceptor.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSrc/acceptor.p)):
+
+```
+type tBallot = int;
+type tValue = int;
+
+event ePrepareReq: tPrepareReq; // Proposer → Acceptor (Phase 1a)
+event ePrepareRsp: tPrepareRsp; // Acceptor → Proposer (Phase 1b)
+event eAcceptReq: tAcceptReq; // Proposer → Acceptor (Phase 2a)
+event eAcceptRsp: tAcceptRsp; // Acceptor → Proposer (Phase 2b)
+event eLearn: (ballot: tBallot, v: tValue); // Proposer → Learner (Phase 3)
+```
+
+#### Acceptor Machine
+
+The Acceptor ([`PSrc/acceptor.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSrc/acceptor.p)) tracks three pieces of state: the highest ballot it has promised (`n_prepared`), the value it has accepted (`v_accepted`), and the ballot number of the accepted value (`n_accepted`).
+
+```
+machine Acceptor {
+ var n_prepared: tBallot;
+ var v_accepted: tValue;
+ var n_accepted: tBallot;
+
+ start state Init {
+ entry {
+ n_prepared = -1;
+ v_accepted = -1;
+ n_accepted = -1;
+ goto Accept;
+ }
+ }
+
+ state Accept {
+ on ePrepareReq do (req: tPrepareReq) {
+ if (req.ballot_n > n_prepared) {
+ send req.proposer, ePrepareRsp,
+ (acceptor = this, promised = req.ballot_n,
+ v_accepted = v_accepted, n_accepted = n_accepted);
+ n_prepared = req.ballot_n;
+ }
+ }
+
+ on eAcceptReq do (req: tAcceptReq) {
+ if (req.ballot_n >= n_prepared) {
+ v_accepted = req.v;
+ n_accepted = req.ballot_n;
+ n_prepared = req.ballot_n;
+ send req.proposer, eAcceptRsp,
+ (acceptor = this, accepted = req.ballot_n);
+ }
+ }
+ }
+}
+```
+
+!!! note "Design Decision"
+ The Acceptor treats an accept as an implicit prepare (following Lamport's "Part Time Parliament" formulation), updating `n_prepared` on accept. This simplifies the protocol without affecting correctness.
+
+#### Proposer Machine
+
+The Proposer ([`PSrc/proposer.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSrc/proposer.p)) drives the three phases of the protocol as P states:
+
+- **Prepare** (Phase 1): Sends `ePrepareReq` to all acceptors. Collects promises until a majority responds. If any acceptor has already accepted a value, adopts the value with the highest ballot number.
+- **Accept** (Phase 2): Sends `eAcceptReq` with the chosen value to all acceptors. Waits for a majority of accept acknowledgments.
+- **Teach** (Phase 3): Sends `eLearn` to all learners with the decided value.
+
+```
+machine Proposer {
+ var jury: set[Acceptor];
+ var school: set[Learner];
+ var ballot_n: tBallot;
+ var value_to_propose: tValue;
+ var majority: int;
+ var prepare_acks: set[Acceptor];
+ var accept_acks: set[Acceptor];
+ var highest_proposal_n: tBallot;
+
+ start state Init {
+ entry (cfg: tProposerConfig) {
+ jury = cfg.jury;
+ school = cfg.school;
+ ballot_n = cfg.proposer_id;
+ value_to_propose = cfg.value_to_propose;
+ majority = sizeof(jury) / 2 + 1;
+ goto Prepare;
+ }
+ }
+
+ state Prepare {
+ entry {
+ var acceptor: Acceptor;
+ highest_proposal_n = -1;
+ foreach (acceptor in jury) {
+ send acceptor, ePrepareReq,
+ (proposer = this, ballot_n = ballot_n, v = value_to_propose);
+ }
+ }
+
+ on ePrepareRsp do (rsp: tPrepareRsp) {
+ if (rsp.promised == ballot_n) {
+ if (rsp.n_accepted > highest_proposal_n) {
+ highest_proposal_n = rsp.n_accepted;
+ value_to_propose = rsp.v_accepted;
+ }
+ prepare_acks += (rsp.acceptor);
+ if (sizeof(prepare_acks) >= majority) {
+ goto Accept;
+ }
+ }
+ }
+ }
+
+ state Accept { ... }
+ state Teach { ... }
+}
+```
+
+!!! tip "Key Insight"
+ The Proposer uses **sets** (not counters) to track acknowledgments. This correctly handles duplicate message delivery — if the same acceptor's response arrives twice, the set size doesn't change.
+
+#### Learner Machine
+
+The Learner ([`PSrc/learner.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSrc/learner.p)) is the simplest role. It receives decided values and asserts that the value never changes:
+
+```
+machine Learner {
+ var learned_value: tValue;
+
+ start state Init {
+ entry {
+ learned_value = -1;
+ goto Learn;
+ }
+ }
+
+ state Learn {
+ on eLearn do (payload: (ballot: tBallot, v: tValue)) {
+ assert(payload.v != -1);
+ assert((learned_value == -1) || (learned_value == payload.v));
+ learned_value = payload.v;
+ }
+ }
+}
+```
+
+---
+
+### Modeling Unreliable Networks
+
+A key aspect of this example is modeling network unreliability. The file [`PSpec/common.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSpec/common.p) provides broadcast utility functions that use P's `choose()` to model nondeterminism:
+
+| Function | Behavior |
+|----------|----------|
+| `UnreliableBroadcast` | Each message may or may not be delivered (`choose()` for each) |
+| `UnreliableBroadcastMulti` | Each message may be delivered 0 to 3 times (`choose(3)`) |
+| `ReliableBroadcast` | All messages are delivered exactly once |
+| `ReliableBroadcastMajority` | Reliable delivery to a majority, unreliable to the rest |
+
+!!! note ""
+ The `choose()` expression returns a nondeterministic boolean. The P checker systematically explores both `true` and `false` branches, effectively testing all possible network behaviors.
+
+---
+
+### Specifications
+
+The safety specification ([`PSpec/spec.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PSpec/spec.p)) is the **OneValueTaught** monitor. It observes all `eLearn` events and asserts that once a value is decided, all subsequent decisions agree on the same value:
+
+```
+spec OneValueTaught observes eLearn {
+ var decided: int;
+
+ start state Init {
+ entry {
+ decided = -1;
+ }
+
+ on eLearn do (payload: (ballot: tBallot, v: tValue)) {
+ assert(payload.v != -1);
+ if (decided != -1) {
+ assert(decided == payload.v);
+ }
+ decided = payload.v;
+ }
+ }
+}
+```
+
+This captures the core consensus safety property: **agreement** — no two learners learn different values.
+
+---
+
+### Test Scenarios
+
+The test file ([`PTst/test.p`](https://github.com/p-org/P/blob/master/Tutorial/5_Paxos/PTst/test.p)) defines several configurations with varying numbers of proposers and acceptors:
+
+| Test Case | Proposers | Acceptors | Learners |
+|-----------|-----------|-----------|----------|
+| `testBasicPaxos1on1` | 1 | 1 | 1 |
+| `testBasicPaxos2on2` | 2 | 2 | 1 |
+| `testBasicPaxos2on3` | 2 | 3 | 1 |
+| `testBasicPaxos3on1` | 3 | 1 | 1 |
+| `testBasicPaxos3on3` | 3 | 3 | 1 |
+| `testBasicPaxos3on5` | 3 | 5 | 1 |
+
+Each test case creates the machines and asserts the `OneValueTaught` specification. The proposers are initialized with nondeterministic values (`choose(50)`) and ballot numbers, so the checker explores many different orderings and value choices.
+
+---
+
+### Compiling and Checking
+
+Navigate to the Paxos example folder and compile:
+
+```shell
+cd Tutorial/5_Paxos
+p compile
+```
+
+Run the checker on a test case:
+
+```shell
+p check -tc testBasicPaxos3on5 -s 1000
+```
+
+!!! tip "Try different configurations"
+ The `testBasicPaxos3on5` test (3 proposers, 5 acceptors) is the most interesting because it exercises concurrent proposals with a realistic quorum size. Try increasing the number of schedules (`-s`) to explore more behaviors.
+
+---
+
+### Discussion
+
+!!! info "Why sets for ACK tracking?"
+ In our network model, messages can be delivered multiple times (via `UnreliableBroadcastMulti`). Using a `set[Acceptor]` instead of a counter for tracking acknowledgments means that duplicate responses from the same acceptor don't inflate the count. This is a common pattern when modeling protocols over unreliable networks in P.
+
+!!! info "Extending the model"
+ Some interesting extensions to try:
+
+ - Add message re-ordering by introducing an intermediate network machine
+ - Model proposer failures (a proposer crashes mid-protocol)
+ - Add a liveness specification: if a single proposer is eventually the only one proposing, a value is eventually decided
+ - Implement multi-decree Paxos (Multi-Paxos) by adding a log of decisions
+
+---
-!!! summary "Summary"
- In this example, we present a simplified model of the single decree paxos. (Todo: add details about the properties checked)
\ No newline at end of file
+!!! success "What did we learn through this example?"
+ We modeled the classic single decree Paxos consensus protocol as communicating P state machines. We used `choose()` to model unreliable network behavior (message loss and duplication), wrote a consensus safety specification (`OneValueTaught`), and tested with multiple configurations of proposers and acceptors. The P checker systematically explores different interleavings and network behaviors to verify that the protocol maintains agreement.
diff --git a/Docs/docs/tutorial/twophasecommit.md b/Docs/docs/tutorial/twophasecommit.md
index fa4d785c4f..6bb28150a1 100644
--- a/Docs/docs/tutorial/twophasecommit.md
+++ b/Docs/docs/tutorial/twophasecommit.md
@@ -1,3 +1,5 @@
+## Example 2: Two Phase Commit
+
??? note "How to use this example"
We assume that you have cloned the P repository locally.
@@ -5,7 +7,7 @@
git clone https://github.com/p-org/P.git
```
- The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in IntelliJ side-by-side a browser using which you can simultaneously read the description for each example and browse the P program in IntelliJ.
+ The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in [Peasy IDE](../getstarted/PeasyIDE.md) (VS Code extension) or your preferred editor side-by-side with a browser, so you can simultaneously read the description for each example and browse the P program.
To know more about the P language primitives used in this example, please look them up in the [language manual](../manualoutline.md).
@@ -14,7 +16,7 @@ Now that we understand the basic features of the P language, let's look at the m
**System:** We use a simplified version of the [classic two phase commit protocol](https://s2.smu.edu/~mhd/8330f11/p133-gray.pdf) to model a transaction commit service. The two phase commit protocol uses a coordinator to gain consensus for any transaction spanning across multiple participants. A transaction in our case is simply a `write` operation for a key-value data store where the data store is replicated across multiple participants. More concretely, a `write` transaction must be committed by the coordinator only if it's accepted by all the participant replicas, and must be aborted if any one of the participant replicas rejects the `write` request.
-{ align=center }
+{ align=center }
A two phase commit protocol consists of two phases :laughing: (figure above). On receiving a write transaction, the coordinator starts the first phase in which it sends a `prepare` request to all the participants and waits for a `prepare success` or `prepare failure` response. On receiving prepare responses from all the participants, the coordinator moves to the second phase where it sends a `commit` or `abort` message to the participants and also responds back to the client.
@@ -22,12 +24,16 @@ A two phase commit protocol consists of two phases :laughing: (figure above). On
**Correctness Specification:** We would like our transaction commit service to provide atomicity guarantees for each transaction. That is, if the service responds to the client that a transaction was committed then that transaction must have been committed by each of its participants; and, if a transaction is aborted then at least one of the participants must have rejected it. We would also like to check that under the assumptions above (no node failures and reliable network), each transaction request is eventually responded by the transaction commit service.
-### P Project
+---
+
+### :material-folder-outline:{ .lg } P Project
The [2_TwoPhaseCommit](https://github.com/p-org/P/tree/master/Tutorial/2_TwoPhaseCommit) folder contains the source code for the [TwoPhaseCommit](https://github.com/p-org/P/blob/master/Tutorial/2_TwoPhaseCommit/TwoPhaseCommit.pproj) project.
Please feel free to read details about the recommended [P program structure](../advanced/structureOfPProgram.md) and [P project file](../advanced/PProject.md).
-### Models
+---
+
+### :material-state-machine:{ .lg } Models
The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/2_TwoPhaseCommit/PSrc)) for the TwoPhaseCommit example consists of three files:
@@ -53,7 +59,9 @@ The P models ([PSrc](https://github.com/p-org/P/tree/master/Tutorial/2_TwoPhaseC
- [TwoPhaseCommitModules.p](https://github.com/p-org/P/blob/master/Tutorial/2_TwoPhaseCommit/PSrc/TwoPhaseCommitModules.p): Declares the P module corresponding to the two phase commit system.
-### Timer and Failure Injector
+---
+
+### :material-timer-alert:{ .lg } Timer and Failure Injector
Our two phase commit project depends on two other components:
@@ -61,7 +69,9 @@ Our two phase commit project depends on two other components:
- **Failure Injector:** P allows programmers to explicitly model different types of failures in the system. The [`FailureInjector`](https://github.com/p-org/P/tree/master/Tutorial/Common/FailureInjector) project demonstrates how to model node failures in P using the `halt` event. The [`FailureInjector` machine](https://github.com/p-org/P/blob/master/Tutorial/Common/FailureInjector/PSrc/FailureInjector.p) nondeterministically picks a node and sends a `eShutDown` event. On receiving an `eShutDown` event, the corresponding node must `halt` to destroy itself. To know more about the special `halt` event, please check the manual: [halt event](../manual/expressions.md#primitive).
-### Specifications
+---
+
+### :material-shield-check:{ .lg } Specifications
The P Specifications ([PSpec](https://github.com/p-org/P/blob/master/Tutorial/2_TwoPhaseCommit/PSpec)) for the TwoPhaseCommit example are implemented in the [Atomicity.p](https://github.com/p-org/P/blob/master/Tutorial/2_TwoPhaseCommit/PSpec/Atomicity.p) file. We define two specifications:
@@ -72,7 +82,9 @@ The P Specifications ([PSpec](https://github.com/p-org/P/blob/master/Tutorial/2_
!!! info "Weaker Property"
Note that we have asserted a weaker property than what is required for Atomicity. Ideally, we would like to check that if a transaction is committed by the coordinator then it was committed-locally by all participants, and if the transaction is aborted then at least one participant must have rejected the transaction and all the participants aborted the transaction. We leave implementing this stronger property as an exercise problem, which you can revisit after finishing the other problems in the tutorials.
-### Test Scenarios
+---
+
+### :material-test-tube:{ .lg } Test Scenarios
The test scenarios folder in P has two parts: TestDrivers and TestScripts. TestDrivers are collections of state machines that implement the test harnesses (or environment state machines) for different test scenarios. TestScripts are collections of test cases that are automatically run by the P checker.
@@ -119,9 +131,11 @@ The test scenarios folder for TwoPhaseCommit ([PTst](https://github.com/p-org/P/
??? tip "[Expand]: Let's walk through Client.p"
The `Client` machine implements the client of the two-phase-commit transaction service. Each client issues N non-deterministic write-transactions, if the transaction succeeds then it performs a read-transaction on the same key and asserts that the value read is same as what was written by the write transaction.
- - ([L60](https://github.com/p-org/P/blob/master/Tutorial/2_TwoPhaseCommit/PTst/Client.p#L60)) → Declares a foreign function in P. Foreign functions are functions that are declared in P but implemented in the external foreign language. Please read the example in [P foreign interface](../manual/foriegntypesfunctions.md) to know more about this functionality. In this example, the `ChooseRandomTransaction` function could have been very easily written in P itself but it's implemented as foreign function just to demonstrate that P supports this functionality.
+ - ([L60](https://github.com/p-org/P/blob/master/Tutorial/2_TwoPhaseCommit/PTst/Client.p#L60)) → Declares a foreign function in P. Foreign functions are functions that are declared in P but implemented in the external foreign language. Please read the example in [P foreign interface](../manual/foreigntypesfunctions.md) to know more about this functionality. In this example, the `ChooseRandomTransaction` function could have been very easily written in P itself but it's implemented as foreign function just to demonstrate that P supports this functionality.
+
+---
-### Compiling TwoPhaseCommit
+### :material-cog-play:{ .lg } Compiling
Navigate to the [2_TwoPhaseCommit](https://github.com/p-org/P/tree/master/Tutorial/2_TwoPhaseCommit) folder and run the following command to compile the TwoPhaseCommit project:
@@ -161,7 +175,7 @@ p compile
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
Restored P/Tutorial/2_TwoPhaseCommit/PGenerated/CSharp/TwoPhaseCommit.csproj (in 92 ms).
- TwoPhaseCommit -> P/Tutorial/2_TwoPhaseCommit/PGenerated/CSharp/net6.0/TwoPhaseCommit.dll
+ TwoPhaseCommit -> P/Tutorial/2_TwoPhaseCommit/PGenerated/CSharp/net8.0/TwoPhaseCommit.dll
Build succeeded.
0 Warning(s)
@@ -174,7 +188,9 @@ p compile
~~ [PTool]: Thanks for using P! ~~
```
-### Checking TwoPhaseCommit
+---
+
+### :material-shield-check-outline:{ .lg } Checking
You can get the list of test cases defined in the TwoPhaseCommit project by running the P Checker:
@@ -188,8 +204,8 @@ p check
$ p check
.. Searching for a P compiled file locally in the current folder
- .. Found a P compiled file: P/Tutorial/2_TwoPhaseCommit/PGenerated/CSharp/net6.0/TwoPhaseCommit.dll
- .. Checking P/Tutorial/2_TwoPhaseCommit/PGenerated/CSharp/net6.0/TwoPhaseCommit.dll
+ .. Found a P compiled file: P/Tutorial/2_TwoPhaseCommit/PGenerated/CSharp/net8.0/TwoPhaseCommit.dll
+ .. Checking P/Tutorial/2_TwoPhaseCommit/PGenerated/CSharp/net8.0/TwoPhaseCommit.dll
Error: We found '10' test cases. Please provide a more precise name of the test case you wish to check using (--testcase | -tc).
Possible options are:
tcSingleClientNoFailure
@@ -252,11 +268,13 @@ Check the parameterized generated `tcParametricTests` test cases for 1000 schedu
p check -tc tcParametricTests -s 1000
```
-### Exercise Problem
+---
+
+### :material-pencil:{ .lg } Exercise Problem
- [Problem 1] Based on the hint above, try and fix the concurrency bug in the `Client` state machine and run the test cases again!
- [Problem 2] A really interesting exploratory problem would be to try and combine the two phase commit protocol with the failure detector system to overcome the progress issue faced by the two phase commit protocol in the presence of node failures. Can you really do that? Let's have a discussion and build a variant of the protocol to tolerate failures?
!!! summary "What did we learn through this example?"
- We dived deeper into: (1) modeling non-determinism in distributed systems, in particular, time-outs; (2) writing complex safety properties like atomicity of transactions in P; and finally, (3) modeling node failures in P using a failure injector state machine. We will also show how P allows invoking foreign code from the P programs. More details in [P foreign interface](../manual/foriegntypesfunctions.md).
+ We dived deeper into: (1) modeling non-determinism in distributed systems, in particular, time-outs; (2) writing complex safety properties like atomicity of transactions in P; and finally, (3) modeling node failures in P using a failure injector state machine. We will also show how P allows invoking foreign code from the P programs. More details in [P foreign interface](../manual/foreigntypesfunctions.md).
diff --git a/Docs/docs/tutsoutline.md b/Docs/docs/tutsoutline.md
index edd32ca3fa..63bdf7ceda 100644
--- a/Docs/docs/tutsoutline.md
+++ b/Docs/docs/tutsoutline.md
@@ -1,6 +1,7 @@
+## Tutorials
!!! tip "P Language Semantics"
- Before we get started with the tutorials, please read [{==this==}](advanced/psemantics.md) to get an informal overview of the P language semantics.
+ Before getting started, please read the [informal overview of P language semantics](advanced/psemantics.md).
In this tutorial, we use a series of examples along with exercise problems to help you get familiar with the P language and the associated tool chain.
@@ -8,90 +9,78 @@ In this tutorial, we use a series of examples along with exercise problems to he
We recommend that you work through these examples one-by-one by solving the accompanying exercise problems before moving to the next example. If you have any doubts or questions, **please feel free to ask them in [discussions](https://github.com/p-org/P/discussions/categories/q-a) or create an [issue](https://github.com/p-org/P/issues)**.
- Also, we assume that you have cloned the P repository locally.
+ Also, we assume that you have cloned the P repository locally:
```shell
git clone https://github.com/p-org/P.git
```
- The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in IntelliJ side-by-side a browser using which you can simultaneously read the description for each example and browse the P program.
+ The recommended way to work through this example is to open the [P/Tutorial](https://github.com/p-org/P/tree/master/Tutorial) folder in [Peasy IDE](getstarted/PeasyIDE.md) (VS Code extension) or your preferred editor side-by-side with a browser, so you can simultaneously read the description for each example and browse the P program.
To know more about P language primitives used in the examples, please look them up in the [language manual](manualoutline.md).
------
+---
-### **[[Example 1] Client Server](tutorial/clientserver.md)**
+### :material-numeric-1-circle:{ .lg } [Client Server](tutorial/clientserver.md)
-We start with a simple client-server example consisting of clients interact with a bank server to withdraw money from their account. The bank server uses a backend database service to store the account balance for its clients. We will use this example to demonstrate how to implement such a system as a collection of P state machines and also check the correctness property that the bank always responds with the correct account balance for a client and a withdraw request always succeeds if there is enough balance in the account. We will also use P's capability to write multiple model checking scenarios and demonstrate how one can replace components in P with its abstraction.
+We start with a simple client-server example where clients interact with a bank server to withdraw money from their account. The bank server uses a backend database service to store the account balance for its clients. We use this example to demonstrate how to implement such a system as a collection of P state machines and check the correctness property that the bank always responds with the correct account balance.
-!!! summary "What will we learn through this example?"
- We will learn about P state machines, writing simple safety and liveness specifications as P monitors, writing multiple model checking scenarios to check the correctness of a P program, and finally, replacing complex components in P with their abstractions using P's module system.
+!!! abstract "What will we learn?"
+ P state machines, writing simple safety and liveness specifications as P monitors, writing multiple model checking scenarios, and replacing complex components with their abstractions using P's module system.
------
+---
-Now that we understand the basic features of the P language, lets spice things up by looking at a well-known distributed protocol, and the obvious choice is to start with the textbook example of a two-phase commit protocol :man_juggling:!
+### :material-numeric-2-circle:{ .lg } [Two Phase Commit](tutorial/twophasecommit.md)
------
+We use a simplified version of the [classic two phase commit protocol](https://s2.smu.edu/~mhd/8330f11/p133-gray.pdf) to model a transaction commit service. The protocol uses a (single) coordinator to achieve consensus for a transaction spanning across multiple participants.
-### **[[Example 2] Two Phase Commit](tutorial/twophasecommit.md)**
+!!! warning "Assumptions"
+ Our transaction commit system is ridiculously simplified — it is not fault tolerant to node failures, and failure of either coordinator or any participant will block progress forever. We rely on [P's send semantics](advanced/psemantics.md) to model the underlying network.
-We use a simplified version of the [classic two phase commit protocol](https://s2.smu.edu/~mhd/8330f11/p133-gray.pdf) to model a transaction commit service.
-The protocol uses a (single) coordinator to achieve consensus for a transaction spanning across multiple participants. A transaction in our case is simply a `put` operation for a key-value data store where the data store is replicated across participants.
+!!! abstract "What will we learn?"
+ Modeling non-determinism (time-outs), writing complex safety properties (atomicity of transactions), modeling node failures, and invoking foreign code from P programs via the [P foreign interface](manual/foreigntypesfunctions.md).
-**Assumptions:** Note that our transaction commit system is ridiculously simplified, for example, it is not fault tolerant to node failures, failure of either coordinator or any of the participants will block the progress forever. Also, we rely on [P's send semantics](advanced/psemantics.md) to model the behavior of the underlying network.
+---
-!!! summary "What will we learn through this example?"
- We will use this example to dive deeper into: (1) modeling non-determinism in distributed systems, in particular, time-outs (2) writing complex safety properties like atomicity of transactions in P and finally, (3) modeling node failures in P using a failure injector state machine. We will also show how P allows invoking foreign code from the P programs. More details in [P foreign interface](manual/foriegntypesfunctions.md).
+### :material-numeric-3-circle:{ .lg } [Espresso Machine](tutorial/espressomachine.md)
------
+:coffee: Time for a break! Instead of modeling a distributed protocol, we consider the fun example of modeling an espresso machine — a reactive system that must respond correctly to various user inputs through its control panel.
-Wow! we have reached the middle of our tutorials :yawning_face: :yawning_face: , its time to take a break and have an espresso coffee! :coffee: :coffee:
+!!! abstract "What will we learn?"
+ Modeling a reactive system as a P state machine, and using P monitors to check that the system moves through the correct modes of operation.
-In the next example, instead of modeling a distributed protocol, we consider the fun example of modeling an espresso machine and see how we can use P state machines to model a reactive system that must respond correctly to various user inputs.
+---
------
+### :material-numeric-4-circle:{ .lg } [Failure Detector](tutorial/failuredetector.md)
-### **[[Example 3] Espresso Machine](tutorial/espressomachine.md)**
+We use a broadcast-based failure detector to show how to model lossy networks and node failures in P. The failure detector broadcasts ping messages to all nodes and uses a timer to wait for pong responses. If a node does not respond after multiple attempts, it is marked as down and clients are notified.
-P has been used in the past to implement device drivers and robotics systems ([case studies](casestudies.md) and [publications](publications.md)). One of the many challenges in implementing these systems is that they are reactive system and hence, must handle various streams of events (inputs) appropriately depending on their current mode of operation.
-In this example, we consider the example of an Espresso coffee machine where the user interacts with the coffee machine through its control panel. The control panel must correctly interprets inputs from the user and sends commands to the coffee maker. We use this example to demonstrate how using P state machine, one can capture the required reactive behavior of a coffee maker and define how it must handle different user inputs.
+!!! abstract "What will we learn?"
+ Using data nondeterminism to model message loss, unreliable sends, and node failures. Modeling other types of network nondeterminism. Writing a liveness specification.
-!!! summary "What will we learn through this example?"
- This is a just for fun example to demonstrate how to model a reactive system as a P state machine. We also show how using P monitors we can check that the system moves through the correct modes of operation.
+---
------
+### :material-numeric-5-circle:{ .lg } [Single Decree Paxos](tutorial/paxos.md)
-Energized with the Coffee :coffee:, lets get back to distributed systems. After the two phase commit protocol, the next protocol that we will jump to is a simple broadcast-based failure detector!
-By this point in the tutorial, we have gotten familiar with the P language and most of its features. So, working through this example should be super fast!
+We present a simplified model of the [single decree Paxos](https://mwhittaker.github.io/blog/single_decree_paxos/) consensus protocol. Multiple proposers attempt to get a single value agreed upon by a set of acceptors, with the decided value taught to learners. We model message loss and duplication using P's nondeterminism and check that the protocol maintains the core consensus safety property: agreement.
------
+!!! abstract "What will we learn?"
+ Modeling a classic consensus protocol, using `choose()` to model unreliable networks (message loss and duplication), writing a consensus safety specification, and testing with multiple configurations.
-### **[[Example 4] Failure Detector](tutorial/failuredetector.md)**
+---
- We use a broadcast based failure detector to show how to model lossy network and node failures in P. The failure detector basically broadcasts ping messages to all nodes in the system and uses a timer to wait for a pong response from all nodes. If certain node does not respond with a pong message after multiple attempts, the failure detector marks the node as down and notifies the clients. We check using a liveness specification that if the failure injecter shutsdown a particular node then the failure detector always eventually detects that node has failed and notifies the client.
+### :material-puzzle:{ .lg } [Common: Timer, Failure Injector, and Shared Memory](tutorial/common.md)
-!!! summary "What will we learn through this example?"
- In this example, we demonstrate how to use data nondeterminism to model message loss, unreliable sends, and node failures. We also discuss how to model other types of network nondeterminism. Finally, we give an example of a liveness specification that the failure detector must satisfy.
+Reusable building blocks used across the tutorials:
------
-
-
-### **[[Common] Timer, Failure Injector, and Shared Memory](tutorial/common.md)**
-
-We have described how to model system's interaction with an OS Timer [Timer](https://github.com/p-org/P/blob/master/Tutorial/Common/Timer/), and how to model injecting node failures in the system [Failure Injector](https://github.com/p-org/P/tree/master/Tutorial/Common/FailureInjector). These models are used in the Two Phase Commit, Espresso Machine, and Failure Detector examples.
-
-P is a purely messaging passing based programming language and hence does not support primitives for modeling shared memory based concurrency. But one can always model shared memory concurrency using message passing. We have used this style of modeling when checking the correctness of single node file systems. Please check out [Shared Memory](https://github.com/p-org/P/tree/master/Tutorial/Common/SharedMemory) example for how to model shared memory concurrency using P.
-
------
-
-:face_with_cowboy_hat: :face_with_cowboy_hat: **Alright, alright, alright ... lets go!** :woman_technologist:
+:face_with_cowboy_hat: :face_with_cowboy_hat: **Alright, alright, alright ... let's go!** :woman_technologist:
diff --git a/Docs/docs/videos.md b/Docs/docs/videos.md
index 59c0fb3ff2..34fe0e2319 100644
--- a/Docs/docs/videos.md
+++ b/Docs/docs/videos.md
@@ -1,12 +1,35 @@
-## Introductory tutorial on P
+## Videos and Talks
-Background and motivation behind P :: what worked and what didn't: [Apple Podcast](https://podcasts.apple.com/us/podcast/episode-20-ankush-desai-p-the-modeling-language-that-could/id1537190695?i=1000559001558) and [Youtube Podcast](https://www.youtube.com/watch?v=ivFc79l6VpM).
+---
+### Featured Talk
-## Tech Talks and Presentations
+
-- [AWS Pi-Week Talk on "Amazon S3 Strong Consisteny"](https://www.twitch.tv/aws/video/962963706)
+!!! abstract ""
+ [(Re:Invent 2023) Gain confidence in system correctness & resilience with Formal Methods](https://youtu.be/FdXZXnkMDxs?si=iFqpl16ONKZuS4C0) — Overview of P, why formal methods matter, and how AWS uses P to gain confidence in correctness of their services.
-- [Compositional Programming and Testing of Distributed Systems (OOPSLA, 2018)](https://www.youtube.com/watch?v=IQd7RIQEFTQ&list=PLyrlk8Xaylp4hRBrMJlov4fRy5YH8UgWu&index=53)
+---
-- [Programming Safe Robotics Systems (BAIR Talk)](https://www.youtube.com/watch?v=qOtvxNi6n3w)
\ No newline at end of file
+### :material-microphone:{ .lg } Podcast
+
+Background and motivation behind P — what worked and what didn't:
+
+- :material-apple: [Apple Podcast](https://podcasts.apple.com/us/podcast/episode-20-ankush-desai-p-the-modeling-language-that-could/id1537190695?i=1000559001558)
+- :material-youtube: [YouTube Podcast](https://www.youtube.com/watch?v=ivFc79l6VpM)
+
+---
+
+### :material-presentation:{ .lg } Tech Talks and Presentations
+
+| Talk | Venue |
+|------|-------|
+| [Amazon S3 Strong Consistency](https://www.twitch.tv/aws/video/962963706) | AWS Pi-Week |
+| [Amazon S3 Strong Consistency](https://youtu.be/B0yXz6EeCaA?list=PL2yQDdvlhXf8vAnQB10dCPIeWUKdHUgOP) | AWS Pi-Week |
+| [Compositional Programming and Testing of Distributed Systems](https://www.youtube.com/watch?v=IQd7RIQEFTQ&list=PLyrlk8Xaylp4hRBrMJlov4fRy5YH8UgWu&index=53) | OOPSLA 2018 |
+| [Programming Safe Robotics Systems](https://www.youtube.com/watch?v=qOtvxNi6n3w) | BAIR Talk |
+| [DRONA: Programming Safe Robotics Systems — Demo](https://www.youtube.com/watch?v=R8ztpfMPs5c) | Demo Video |
diff --git a/Docs/docs/whatisP.md b/Docs/docs/whatisP.md
index 51dd2eaa0b..fbb5e262b5 100644
--- a/Docs/docs/whatisP.md
+++ b/Docs/docs/whatisP.md
@@ -5,62 +5,116 @@
}
-{ align=center }
+# What is P?
-Distributed systems are notoriously hard to get right (i.e., guaranteeing correctness) as the
-programmer needs to reason about numerous control paths resulting from the myriad
-interleaving of events (or messages or failures). Unsurprisingly, programmers can easily
-introduce subtle errors when designing these systems. Moreover, it is extremely
-difficult to test distributed systems, as most control paths remain untested, and serious
-bugs lie dormant for months or even years after deployment.
+{ align=center }
-!!! info ""
- _The P programming framework takes several steps towards addressing these challenges by providing
- a unified framework for modeling, specifying, implementing, testing, and verifying complex
- distributed systems._
+Distributed systems are notoriously hard to get right. Programmers must reason about numerous control paths resulting from the myriad interleaving of events, messages, and failures. Subtle errors creep in easily, most control paths remain untested, and serious bugs can lie dormant for months or even years after deployment.
+
+!!! abstract "The P Approach"
+ The P programming framework addresses these challenges by providing a **unified framework** for modeling, specifying, testing, verifying, and monitoring complex distributed systems — from design all the way through production.
+
+---
+
+## The P Framework
+
+{ align=center }
+
+The P framework provides an end-to-end pipeline for formally modeling, verifying, and monitoring distributed systems. The key components are:
+
+---
+
+### :material-file-document-edit-outline:{ .lg } 1. P Language — Specifications & Design
+
+The process begins with a high-level **design document** describing the distributed system's protocol logic, along with **correctness specifications** (safety and liveness properties) that the system must satisfy.
+
+P provides a high-level **state machine based programming language** to formally model and specify distributed systems. Programmers capture their system design as _communicating state machines_ — which is how developers generally think about protocol logic. P is more of a programming language than a mathematical modelling language, making it easier to:
+
+- :octicons-check-16: Create formal models that are **close to the implementation** (sufficiently detailed)
+- :octicons-check-16: **Maintain models** as the system design evolves
+- :octicons-check-16: Specify and check both **safety** and **liveness** specifications (global invariants)
+
+??? tip "Models, Specifications, and Model Checking Scenarios"
+ A quick primer:
+
+ - **Specification** — says _what_ the system should do (correctness properties)
+ - **Model** — captures the details of _how_ the system does it
+ - **Model checking scenario** — provides the finite non-deterministic test-harness under which the model checker verifies that the model satisfies its specifications
+
+The underlying model of computation is communicating state machines (or [actors](https://en.wikipedia.org/wiki/Actor_model)). See the [formal semantics paper](https://ankushdesai.github.io/assets/papers/modp.pdf) or the [informal semantics overview](advanced/psemantics.md) for details.
+
+---
+
+### :material-robot-outline:{ .lg } 2. PeasyAI — AI-Assisted Code Generation
+
+[**PeasyAI**](getstarted/peasyai.md) accelerates P development by automatically generating P program models and specification monitors from design documents.
+
+
-### P Framework
+- :material-code-braces:{ .lg .middle } **Generation**
-{ align=center }
+ ---
-The P framework can be divided into **three** important parts:
+ Generates types, state machines, safety specs, and test drivers from plain-text design documents using ensemble generation (best-of-N candidate selection)
-#### P Language
+- :material-tools:{ .lg .middle } **Auto-Fix Pipeline**
-P provides a high-level **state machine based programming language** to formally model and specify
-distributed systems. The syntactic sugar of state machines allows programmers to capture
-their system design (or _protocol logic_) as communicating state machines, which is how programmers generally
-think about their system's design. P is more of a programming language than a mathematical
-modelling language and hence, making it easier for the programmers to both: (1) create formal models that are closer
-to the implementation (sufficiently detailed) and also (2) maintain these models as the system design evolves.
-P supports specifying and checking both safety as well as liveness specifications (global invariants).
-Programmers can easily write different scenarios under which they would like to check that the system satisfies the desired correctness specification.
-The P module system enables programmers to model their system _modularly_ and
-perform _compositional_ testing to scale the analysis to large distributed systems.
+ ---
+ Iteratively resolves compilation errors and PChecker failures, with human-in-the-loop fallback when automated fixing is insufficient
-!!! Tip "Models, Specifications, Model Checking Scenario"
- A quick primer on what a model
- is, versus a specification, and model checking scenarios: (1) a specification says what
- the system should do (correctness properties); (2) a model captures the details of how the
- system does it; (3) a model checking scenario provides the finite non-deterministc
- test-harness or environment under which the model checker should check that the system
- model satisfies its specifications.
+- :material-database-search:{ .lg .middle } **RAG-Enhanced**
-The underlying model of computation for P programs is communicating state machines (or [actors](https://en.wikipedia.org/wiki/Actor_model)). The detailed formal semantics for P can be found [here](https://ankushdesai.github.io/assets/papers/modp.pdf) and an informal discussion [here](advanced/psemantics.md).
+ ---
-#### Backend Analysis Engines
+ 1,200+ indexed P code examples improve generation quality through retrieval-augmented generation
-P provides a backend analysis engine to systematically explore behaviors of the system model (_resulting from interleaving of messages and failures_) and check that the model satisfies the desired _correctness_ specifications.
-To reason about complex distributed systems, the P checker needs to tackle the well-known problem of _state space explosion_. The P checker employs [search prioritization heuristics](https://ankushdesai.github.io/assets/papers/fse-desai.pdf) to drive the exploration along different parts of the state space that are most likely to have concurrency related issues. The P checker is really **efficient at uncovering deep bugs** (i.e., bugs that require complex interleaving of events) in the system design that have a really low probability of occurrence in real-world. On finding a bug, the checker provides a reproducible error-trace which the programmer can use for debugging.
+- :material-connection:{ .lg .middle } **IDE Integration**
-Although the current P checker is great at finding _deep-hard-to-find_ bugs ("[Heisenbugs](https://en.wikipedia.org/wiki/Heisenbug)"), it **cannot provide a proof** of correctness.
-We are actively working on addressing this challenge and are building two new backends for P. First, a _symbolic execution engine_ that can scale the P checker to models of large
-distributed systems and provide **sound guarantees** of exploring all possible behaviors. Second, a deductive verification engine to perform **mathematical proof** of correctness for P programs. Both these backends will be released publicly soon.
+ ---
-#### Code Generation
+ Integrates with **Cursor** and **Claude Code** via MCP, providing 27 tools and 14 resources
-The P compiler currently generates C# and C code. The generated code when combined with the P Runtime (that executes the P state machines) can be deployed on any target platform.
-The generated C code has been used to program [device drivers](https://ankushdesai.github.io/assets/papers/p.pdf) and [robotics systems](https://ankushdesai.github.io/assets/papers/drona.pdf). The generated C# code has been used to program [distributed systems](https://ankushdesai.github.io/assets/papers/modp.pdf).
+
-We are currently working on adding support for a Java backend for P. We will also be adding support for generating _runtime monitors_ for specifications that can be then used to check if the implementation conforms to the high-level P specifications.
+---
+
+### :material-shield-check-outline:{ .lg } 3. P-Checker — Formal Verification
+
+P provides backend analysis engines to systematically explore behaviors of the system model — resulting from interleaving of messages and failures — and check that the model satisfies the desired correctness specifications.
+
+!!! success "Finding Deep Bugs"
+ The P checker employs [search prioritization heuristics](https://ankushdesai.github.io/assets/papers/fse-desai.pdf) to efficiently uncover **deep bugs** — bugs that require complex interleaving of events and have a very low probability of occurrence in real-world testing. On finding a bug, the checker provides a **reproducible error-trace** for debugging. Once all checks pass, the P model is validated for correctness.
+
+Beyond the default checker, P provides additional analysis backends:
+
+| Engine | Approach | Guarantee |
+|--------|----------|-----------|
+| [**PSym**](advanced/psym/whatisPSym.md) | Symbolic execution | Sound exploration of all possible behaviors |
+| [**PEx**](advanced/pex.md) | Exhaustive model checking | Complete state space coverage |
+| [**PVerifier**](advanced/PVerifierLanguageExtensions/announcement.md) | Deductive verification | Mathematical proof of correctness |
+
+---
+
+### :material-monitor-eye:{ .lg } 4. PObserve — Runtime Monitoring & Conformance
+
+[**PObserve**](advanced/pobserve/pobserve.md) bridges the gap between design-time verification and runtime behavior. It validates **service logs and execution traces** from the running system against the same P specification monitors that were verified during formal verification.
+
+
+
+- :material-text-search:{ .lg .middle } **Log Parsing**
+
+ ---
+
+ Parses service logs and sequences events without requiring additional instrumentation
+
+- :material-check-decagram:{ .lg .middle } **Conformance Checking**
+
+ ---
+
+ Feeds events through P specification monitors to check global conformance and identify violations
+
+
+
+!!! info ""
+ PObserve ensures that the **implementation conforms to the high-level design in deployment** — extending formal verification guarantees from design time into production.
diff --git a/Docs/mkdocs.yml b/Docs/mkdocs.yml
index 6dc1d5b743..81f546d030 100644
--- a/Docs/mkdocs.yml
+++ b/Docs/mkdocs.yml
@@ -1,5 +1,5 @@
site_name: "P"
-site_url: http://p-org.github.io/P/
+site_url: https://p-org.github.io/P/
site_description: "P: Modular and Safe Programming of Distributed Systems"
site_author: "Ankush Desai"
@@ -11,9 +11,18 @@ theme:
language: en
name: material
palette:
- scheme: default
- primary: black
- accent: deep orange
+ - scheme: default
+ primary: black
+ accent: deep orange
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - scheme: slate
+ primary: black
+ accent: deep orange
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
logo: ./icon.png
favicon: ./icon.png
icon:
@@ -46,12 +55,12 @@ theme:
- content.tabs.link
- toc.follow
-# This should be renamed to nav for mkdocs 1.0+
nav:
- Home: index.md
- What is P?: whatisP.md
- Getting Started:
- Installing P: getstarted/install.md
+ - PeasyAI - AI for P: getstarted/peasyai.md
- Peasy - IDE for P: getstarted/PeasyIDE.md
- Using P Compiler and Checker: getstarted/usingP.md
@@ -61,27 +70,27 @@ nav:
- Two Phase Commit: tutorial/twophasecommit.md
- Espresso Machine: tutorial/espressomachine.md
- Failure Detector: tutorial/failuredetector.md
- # - Paxos Made Simple: tutorial/paxos.md
+ - Single Decree Paxos: tutorial/paxos.md
- Timer, Failure, and Shared Memory: tutorial/common.md
- - Advanced User Guide:
+ - Advanced:
- P Semantics: advanced/psemantics.md
- - Importance of Liveness Specifications: advanced/importanceliveness.md
- - Structure of a P Program: advanced/structureOfPProgram.md
- - P Project File: advanced/PProject.md
- - Debugging Error Traces (counter examples): advanced/debuggingerror.md
- - PObserve:
+ - Liveness Specifications: advanced/importanceliveness.md
+ - Program Structure: advanced/structureOfPProgram.md
+ - Project File: advanced/PProject.md
+ - Debugging Error Traces: advanced/debuggingerror.md
+ - PEx (Exhaustive Checking): advanced/pex.md
+ - "PObserve: Runtime Monitoring":
- Overview: advanced/pobserve/pobserve.md
- Getting Started: advanced/pobserve/gettingstarted.md
- - Setting Up PObserve Package: advanced/pobserve/package-setup.md
+ - Package Setup: advanced/pobserve/package-setup.md
- Writing a Log Parser: advanced/pobserve/logparser.md
- - PObserve Java Unit Test: advanced/pobserve/pobservejunit.md
- - PObserve CLI: advanced/pobserve/pobservecli.md
- - "[Example] Lock Server":
+ - JUnit Integration: advanced/pobserve/pobservejunit.md
+ - CLI Integration: advanced/pobserve/pobservecli.md
+ - "Example: Lock Server":
- Overview: advanced/pobserve/example/lockserver.md
- - Running with PObserve JUnit: advanced/pobserve/example/lockserverwithpobservejunit.md
- - Running with PObserve CLI: advanced/pobserve/example/lockserverwithpobservecli.md
- - PExhaustive Mode: advanced/pex.md
- - PVerifier:
+ - With PObserve JUnit: advanced/pobserve/example/lockserverwithpobservejunit.md
+ - With PObserve CLI: advanced/pobserve/example/lockserverwithpobservecli.md
+ - "PVerifier: Formal Proofs":
- Announcement: advanced/PVerifierLanguageExtensions/announcement.md
- Outline: advanced/PVerifierLanguageExtensions/outline.md
- Installation: advanced/PVerifierLanguageExtensions/install-pverifier.md
@@ -90,7 +99,7 @@ nav:
- Init Conditions: advanced/PVerifierLanguageExtensions/init-condition.md
- Specifications: advanced/PVerifierLanguageExtensions/specification.md
- Lemmas and Proofs: advanced/PVerifierLanguageExtensions/proof.md
- - Two Phase Commit Verification: advanced/PVerifierLanguageExtensions/twophasecommitverification.md
+ - "Example: Two Phase Commit": advanced/PVerifierLanguageExtensions/twophasecommitverification.md
- Language Manual:
- P Program (Outline): manualoutline.md
- P DataTypes: manual/datatypes.md
@@ -102,22 +111,21 @@ nav:
- P Statements: manual/statements.md
- P Module System: manual/modulesystem.md
- P Test cases: manual/testcases.md
- - P Foreign Interface: manual/foriegntypesfunctions.md
+ - P Foreign Interface: manual/foreigntypesfunctions.md
- Case Studies: casestudies.md
- - Videos: videos.md
- - Publications: publications.md
- - Contributing to P:
+ - Publications:
+ - Papers: publications.md
+ - Videos: videos.md
+ - Contributing:
- Building from Source: getstarted/build.md
markdown_extensions:
- attr_list
- md_in_html
- - pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.snippets
- admonition
- - pymdownx.tabbed
- pymdownx.details
- pymdownx.highlight
- pymdownx.critic
@@ -128,8 +136,8 @@ markdown_extensions:
format: !!python/name:pymdownx.superfences.fence_code_format
- footnotes
- pymdownx.emoji:
- emoji_index: !!python/name:materialx.emoji.twemoji
- emoji_generator: !!python/name:materialx.emoji.to_svg
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
plugins:
@@ -148,4 +156,4 @@ extra:
provider: google
property: G-0WWG87T6BN
-copyright: Copyright © 2023 P Developers
+copyright: Copyright © 2026 P Developers
diff --git a/Docs/requirements.txt b/Docs/requirements.txt
new file mode 100644
index 0000000000..e59f43c89c
--- /dev/null
+++ b/Docs/requirements.txt
@@ -0,0 +1,3 @@
+mkdocs==1.6.1
+mkdocs-material==9.6.23
+mkdocs-macros-plugin==1.3.7
diff --git a/README.md b/README.md
index 6b30ceff12..8a0df19da0 100644
--- a/README.md
+++ b/README.md
@@ -44,9 +44,9 @@ In our experience of using P inside AWS, Academia, and Microsoft. We have observ
## Let the fun begin!
-You can find most of the information about the P framework on: **[http://p-org.github.io/P/](http://p-org.github.io/P/)**.
+You can find most of the information about the P framework on: **[https://p-org.github.io/P/](https://p-org.github.io/P/)**.
-[What is P?](http://p-org.github.io/P/whatisP/), [Getting Started](http://p-org.github.io/P/getstarted/install/), [Tutorials](http://p-org.github.io/P/tutsoutline/), [Case Studies](http://p-org.github.io/P/casestudies/) and related [Research Publications](http://p-org.github.io/P/publications/).
+[What is P?](https://p-org.github.io/P/whatisP/), [Getting Started](https://p-org.github.io/P/getstarted/install/), [PeasyAI](https://p-org.github.io/P/getstarted/peasyai/), [Tutorials](https://p-org.github.io/P/tutsoutline/), [Case Studies](https://p-org.github.io/P/casestudies/) and related [Research Publications](https://p-org.github.io/P/publications/).
If you have any further questions, please feel free to create an [issue](https://github.com/p-org/P/issues), ask on
[discussions](https://github.com/p-org/P/discussions), or [email us](mailto:ankushdesai@gmail.com)
diff --git a/Src/CMakeLists.txt b/Src/CMakeLists.txt
deleted file mode 100644
index 55d145315c..0000000000
--- a/Src/CMakeLists.txt
+++ /dev/null
@@ -1,56 +0,0 @@
-cmake_minimum_required (VERSION 3.1)
-
-project (P)
-
-if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
- set(LINUX ON)
-endif()
-
-if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
- set(MACOSX ON)
-endif()
-
-if (NOT Win32)
- if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
- add_definitions( -DPRT_USE_CLANG)
- elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
- add_definitions( -DPRT_USE_GCC )
- endif()
-endif()
-
-if(SGX)
- add_definitions( -DPRT_PLAT_SGXUSER )
-elseif(Win32)
- add_definitions( -DPRT_PLAT_WINUSER )
-elseif(LINUX OR MACOSX)
- add_definitions( -DPRT_PLAT_LINUXUSER )
-endif()
-
-macro ( Publish_Library_Header target )
- set (extra_macro_args ${ARGN})
- list(LENGTH extra_macro_args num_extra_args)
- if(${num_extra_args} EQUAL 0)
- get_property(Published_Headers_PATHS TARGET ${target} PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
- else()
- set(Published_Headers_PATHS "${ARGN}")
- endif()
- add_custom_command(TARGET ${target} POST_BUILD
- COMMENT "Moving header files to Bld/include/"
- COMMAND ${CMAKE_COMMAND} ARGS -E
- make_directory ${LIBRARY_OUTPUT_INCLUDE_PATH}
- )
-
- foreach(incl_file_path ${Published_Headers_PATHS})
- file ( GLOB incl_files ${incl_file_path}/*.h )
- foreach(incl_file ${incl_files})
- add_custom_command(TARGET ${target} POST_BUILD
- COMMAND ${CMAKE_COMMAND} -E copy_if_different
- ${incl_file}
- ${LIBRARY_OUTPUT_INCLUDE_PATH}
- )
- endforeach()
- endforeach()
-endmacro()
-
-add_subdirectory ( Prt )
-
diff --git a/Src/PeasyAI/.gitignore b/Src/PeasyAI/.gitignore
new file mode 100644
index 0000000000..e47a752eb6
--- /dev/null
+++ b/Src/PeasyAI/.gitignore
@@ -0,0 +1,56 @@
+# Environment files with secrets
+.env
+.env.local
+.env.*.local
+
+# PeasyAI config (lives at ~/.peasyai/ — never in the repo)
+.peasyai/
+
+# Local caches and indexes
+.embedding_cache/
+.p_corpus/
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+venv/
+env/
+.venv/
+ENV/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+.tox/
+.nox/
+
+# Build
+build/
+dist/
+*.egg-info/
+*.egg
+
+# Logs
+*.log
+logs/
+
+# Generated code / runtime output
+generated_code/
+generated_projects/
+PCheckerOutput/
+
+# Workflow runtime state
+.peasyai_workflows.json
+
+# Test artifacts
+test_mcp_paxos.py
diff --git a/Src/PeasyAI/.peasyai-schema.json b/Src/PeasyAI/.peasyai-schema.json
new file mode 100644
index 0000000000..9f4c42aafc
--- /dev/null
+++ b/Src/PeasyAI/.peasyai-schema.json
@@ -0,0 +1,77 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "PeasyAI Settings",
+ "description": "Configuration for the PeasyAI MCP server. Stored at ~/.peasyai/settings.json",
+ "type": "object",
+ "properties": {
+ "$schema": {
+ "type": "string"
+ },
+ "llm": {
+ "type": "object",
+ "description": "LLM provider configuration",
+ "properties": {
+ "provider": {
+ "type": "string",
+ "description": "Active LLM provider",
+ "enum": ["snowflake", "anthropic", "bedrock"],
+ "default": "bedrock"
+ },
+ "model": {
+ "type": "string",
+ "description": "Model name override (uses provider default if omitted)"
+ },
+ "timeout": {
+ "type": "number",
+ "description": "Request timeout in seconds",
+ "default": 600
+ },
+ "providers": {
+ "type": "object",
+ "description": "Per-provider credentials and settings",
+ "properties": {
+ "snowflake": {
+ "type": "object",
+ "properties": {
+ "api_key": { "type": "string", "description": "Snowflake programmatic access token" },
+ "base_url": { "type": "string", "description": "Snowflake Cortex OpenAI-compatible endpoint" }
+ }
+ },
+ "anthropic": {
+ "type": "object",
+ "properties": {
+ "api_key": { "type": "string", "description": "Anthropic API key" },
+ "model": { "type": "string", "description": "Anthropic model name", "default": "claude-3-5-sonnet-20241022" },
+ "base_url": { "type": "string", "description": "Optional custom base URL" }
+ }
+ },
+ "bedrock": {
+ "type": "object",
+ "properties": {
+ "region": { "type": "string", "description": "AWS region", "default": "us-west-2" },
+ "model_id": { "type": "string", "description": "Bedrock model ID", "default": "anthropic.claude-3-5-sonnet-20241022-v2:0" }
+ }
+ }
+ }
+ }
+ }
+ },
+ "p_compiler": {
+ "type": "object",
+ "description": "P compiler paths (auto-detected if omitted)",
+ "properties": {
+ "path": { "type": ["string", "null"], "description": "Path to P compiler binary" },
+ "dotnet_path": { "type": ["string", "null"], "description": "Path to dotnet SDK" }
+ }
+ },
+ "generation": {
+ "type": "object",
+ "description": "Code generation defaults",
+ "properties": {
+ "ensemble_size": { "type": "integer", "description": "Number of candidates for best-of-N selection", "default": 3 },
+ "output_dir": { "type": "string", "description": "Default output directory for generated projects", "default": "./PGenerated" }
+ }
+ }
+ }
+}
+
diff --git a/Src/PeasyAI/LICENSE b/Src/PeasyAI/LICENSE
new file mode 100644
index 0000000000..ec880fd28d
--- /dev/null
+++ b/Src/PeasyAI/LICENSE
@@ -0,0 +1,20 @@
+The MIT License
+
+Copyright (c) 2015 P Developers
+
+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/Src/PeasyAI/Makefile b/Src/PeasyAI/Makefile
new file mode 100644
index 0000000000..daaee63170
--- /dev/null
+++ b/Src/PeasyAI/Makefile
@@ -0,0 +1,18 @@
+.PHONY: deps-check test-contracts test-unit test regression regression-baseline
+
+deps-check:
+ python3 scripts/check_dependencies.py
+
+test-contracts:
+ bash scripts/run_contract_tests.sh
+
+test-unit:
+ PYTHONPATH=src python3 -m pytest tests/ -v --ignore=tests/pipeline
+
+test: test-unit test-contracts
+
+regression:
+ TOKENIZERS_PARALLELISM=false python3 scripts/regression_test.py
+
+regression-baseline:
+ TOKENIZERS_PARALLELISM=false python3 scripts/regression_test.py --save-baseline
diff --git a/Src/PeasyAI/README.md b/Src/PeasyAI/README.md
new file mode 100644
index 0000000000..510c2019e8
--- /dev/null
+++ b/Src/PeasyAI/README.md
@@ -0,0 +1,331 @@
+# PeasyAI
+
+AI-powered code generation, compilation, and formal verification for the [P programming language](https://p-org.github.io/P/).
+
+PeasyAI exposes an MCP (Model Context Protocol) server that works with **Cursor** and **Claude Code**, giving you 27 tools and 14 resources for generating P state machines from design documents, compiling them, and verifying correctness with PChecker.
+
+## Features
+
+- **Design Doc → Verified P Code** — Generate types, state machines, safety specs, and test drivers from a plain-text design document
+- **Multi-Provider LLM Support** — Snowflake Cortex, AWS Bedrock, Direct Anthropic
+- **Ensemble Generation** — Best-of-N candidate selection for higher quality code
+- **Auto-Fix Pipeline** — Automatically fix compilation errors and PChecker failures
+- **Human-in-the-Loop** — Falls back to user guidance when automated fixing fails
+- **RAG-Enhanced** — 1 200+ indexed P code examples improve generation quality
+
+---
+
+## Quick Start
+
+> **Prerequisite:** Install Python ≥ 3.10, .NET SDK 8.0, Java ≥ 11, and the P compiler first.
+> See the [P installation guide](https://p-org.github.io/P/getstarted/install/) for details.
+
+```bash
+# 1. Install PeasyAI from the latest GitHub release
+pip install https://github.com/p-org/P/releases/download/peasyai-v0.2.0/peasyai_mcp-0.2.0-py3-none-any.whl
+
+# 2. Configure LLM credentials
+peasyai-mcp init # creates ~/.peasyai/settings.json — edit with your keys
+
+# 3. Add to Cursor or Claude Code (pick one)
+# Cursor: add the snippet below to ~/.cursor/mcp.json
+# Claude: claude mcp add peasyai -- peasyai-mcp
+```
+
+---
+
+## Installation
+
+### Prerequisites
+
+PeasyAI relies on the P toolchain at runtime to compile and model-check your programs. Make sure the following are installed **before** using PeasyAI:
+
+| Dependency | Why it's needed | Install |
+|------------|-----------------|---------|
+| **Python ≥ 3.10** | Runs the PeasyAI MCP server | [python.org/downloads](https://www.python.org/downloads/) |
+| **.NET SDK, Java, and P compiler** | Compiles and model-checks P programs | [**Follow the P installation guide**](https://p-org.github.io/P/getstarted/install/) |
+
+> P requires a specific version of .NET SDK (8.0) and Java (≥ 11). The [P installation guide](https://p-org.github.io/P/getstarted/install/) has platform-specific instructions for macOS, Ubuntu, Amazon Linux, and Windows.
+
+Verify your setup:
+
+```bash
+python3 --version # ≥ 3.10
+dotnet --list-sdks # must show 8.0.*
+java -version # ≥ 11
+p --version # P compiler is on PATH
+```
+
+### Install PeasyAI
+
+Install the latest release directly from GitHub — **no git clone required**:
+
+```bash
+pip install https://github.com/p-org/P/releases/download/peasyai-v0.2.0/peasyai_mcp-0.2.0-py3-none-any.whl
+```
+
+> Check the [Releases page](https://github.com/p-org/P/releases) for the latest version.
+
+To upgrade to a newer release:
+
+```bash
+pip install --force-reinstall https://github.com/p-org/P/releases/download/peasyai-v
/peasyai_mcp--py3-none-any.whl
+```
+
+
+Alternative: install from source (for development)
+
+```bash
+git clone https://github.com/p-org/P.git
+cd P/Src/PeasyAI
+pip install -e ".[dev]" # editable install — local changes take effect immediately
+```
+
+
+
+### Configure
+
+```bash
+peasyai-mcp init
+```
+
+This creates **`~/.peasyai/settings.json`** (similar to `~/.claude/settings.json`).
+Open it and fill in the credentials for the LLM provider you want to use:
+
+```json
+{
+ "llm": {
+ "provider": "snowflake",
+ "model": "claude-sonnet-4-5",
+ "timeout": 600,
+ "providers": {
+ "snowflake": {
+ "api_key": "your-snowflake-pat-token",
+ "base_url": "https://your-account.snowflakecomputing.com/api/v2/cortex/openai"
+ },
+ "anthropic": {
+ "api_key": "your-anthropic-key",
+ "model": "claude-3-5-sonnet-20241022"
+ },
+ "bedrock": {
+ "region": "us-west-2",
+ "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0"
+ }
+ }
+ },
+ "generation": {
+ "ensemble_size": 3,
+ "output_dir": "./PGenerated"
+ }
+}
+```
+
+> **Only fill in the provider you use.** Set `"provider"` to `"snowflake"`, `"anthropic"`, or `"bedrock"`.
+
+Verify everything is set up correctly:
+
+```bash
+peasyai-mcp config
+```
+
+---
+
+## Add to Cursor
+
+Edit **`~/.cursor/mcp.json`** (create the file if it doesn't exist):
+
+```json
+{
+ "mcpServers": {
+ "peasyai": {
+ "command": "peasyai-mcp",
+ "args": []
+ }
+ }
+}
+```
+
+Restart Cursor — the PeasyAI tools will appear in the MCP panel.
+
+## Add to Claude Code
+
+```bash
+claude mcp add peasyai -- peasyai-mcp
+```
+
+---
+
+## Configuration Reference
+
+All configuration lives in **`~/.peasyai/settings.json`**.
+
+| Key | Example | Description |
+|-----|---------|-------------|
+| `llm.provider` | `"snowflake"` | Active provider: `snowflake`, `anthropic`, or `bedrock` |
+| `llm.model` | `"claude-sonnet-4-5"` | Model name (uses provider default if omitted) |
+| `llm.timeout` | `600` | Request timeout in seconds |
+| `llm.providers.snowflake.api_key` | | Snowflake Programmatic Access Token |
+| `llm.providers.snowflake.base_url` | | Snowflake Cortex endpoint URL |
+| `llm.providers.anthropic.api_key` | | Anthropic API key |
+| `llm.providers.bedrock.region` | `"us-west-2"` | AWS region |
+| `llm.providers.bedrock.model_id` | | Bedrock model ID |
+| `generation.ensemble_size` | `3` | Best-of-N candidates per file |
+| `generation.output_dir` | `"./PGenerated"` | Default output directory |
+
+**Precedence:** Environment variables > `~/.peasyai/settings.json` > built-in defaults.
+
+---
+
+## LLM Providers
+
+### Snowflake Cortex
+
+1. Log into your Snowflake account
+2. Go to **Admin → Security → Programmatic Access Tokens**
+3. Create a token with Cortex API permissions
+4. Set `"provider": "snowflake"` and fill in `api_key` and `base_url`
+
+### Anthropic (Direct API)
+
+Set `"provider": "anthropic"` and fill in `api_key`.
+
+### AWS Bedrock
+
+Ensure `~/.aws/credentials` is configured, then set `"provider": "bedrock"`.
+
+---
+
+## MCP Tools (27)
+
+| Category | Tool | Description |
+|----------|------|-------------|
+| **Generation** | `peasy-ai-create-project` | Create P project skeleton (PSrc/, PSpec/, PTst/) |
+| | `peasy-ai-gen-types-events` | Generate types, enums, and events file |
+| | `peasy-ai-gen-machine` | Generate a single state machine (two-stage, ensemble) |
+| | `peasy-ai-gen-spec` | Generate safety specification / monitor |
+| | `peasy-ai-gen-test` | Generate test driver |
+| | `peasy-ai-gen-full-project` | One-shot full project generation |
+| | `peasy-ai-save-file` | Save generated code to disk |
+| **Compilation** | `peasy-ai-compile` | Compile a P project |
+| | `peasy-ai-check` | Run PChecker model-checking verification |
+| **Fixing** | `peasy-ai-fix-compile-error` | Fix a single compilation error |
+| | `peasy-ai-fix-checker-error` | Fix a PChecker error from trace analysis |
+| | `peasy-ai-fix-all` | Iteratively fix all compilation errors |
+| | `peasy-ai-fix-bug` | Auto-diagnose and fix PChecker failures |
+| **Workflows** | `peasy-ai-run-workflow` | Execute a multi-step workflow (compile_and_fix, full_verification, etc.) |
+| | `peasy-ai-resume-workflow` | Resume a paused workflow with user guidance |
+| | `peasy-ai-list-workflows` | List available and active workflows |
+| **Query** | `peasy-ai-syntax-help` | P language syntax help by topic |
+| | `peasy-ai-list-files` | List all .p files in a project |
+| | `peasy-ai-read-file` | Read contents of a P file |
+| **RAG** | `peasy-ai-search-examples` | Search the P program database |
+| | `peasy-ai-get-context` | Get examples to improve generation quality |
+| | `peasy-ai-index-examples` | Index your own P files into the corpus |
+| | `peasy-ai-get-protocol-examples` | Get examples for common protocols (Paxos, Raft, …) |
+| | `peasy-ai-corpus-stats` | Get corpus statistics |
+| **Trace** | `peasy-ai-explore-trace` | Explore a PChecker execution trace |
+| | `peasy-ai-query-trace` | Query machine state at a point in the trace |
+| **Environment** | `peasy-ai-validate-env` | Check P toolchain, LLM provider, and config |
+
+## MCP Resources (14)
+
+| Resource URI | Description |
+|--------------|-------------|
+| `p://guides/syntax` | Complete P syntax reference |
+| `p://guides/basics` | P language fundamentals |
+| `p://guides/machines` | State machine patterns |
+| `p://guides/types` | Type system guide |
+| `p://guides/events` | Event handling guide |
+| `p://guides/enums` | Enum types guide |
+| `p://guides/statements` | Statements and expressions guide |
+| `p://guides/specs` | Specification monitors guide |
+| `p://guides/tests` | Test cases guide |
+| `p://guides/modules` | Module system guide |
+| `p://guides/compiler` | Compiler usage guide |
+| `p://guides/common_errors` | Common compilation errors and fixes |
+| `p://examples/program` | Complete P program example |
+| `p://about` | About the P language |
+
+---
+
+## Typical Workflow
+
+The recommended step-by-step workflow for generating verified P code:
+
+1. **Create project** — `peasy-ai-create-project(design_doc, output_dir)`
+2. **Generate types** — `peasy-ai-gen-types-events(design_doc, project_path)` → review → `peasy-ai-save-file`
+3. **Generate machines** — `peasy-ai-gen-machine(name, design_doc, project_path)` for each → review → `peasy-ai-save-file`
+4. **Generate spec** — `peasy-ai-gen-spec("Safety", design_doc, project_path)` → review → `peasy-ai-save-file`
+5. **Generate test** — `peasy-ai-gen-test("TestDriver", design_doc, project_path)` → review → `peasy-ai-save-file`
+6. **Compile** — `peasy-ai-compile(project_path)`
+7. **Fix errors** — `peasy-ai-fix-all(project_path)` if compilation fails
+8. **Verify** — `peasy-ai-check(project_path)` to run PChecker
+9. **Fix bugs** — `peasy-ai-fix-bug(project_path)` if PChecker finds issues
+
+Or use **`peasy-ai-run-workflow("full_verification", project_path)`** to automate steps 6–9.
+
+---
+
+## Human-in-the-Loop Error Fixing
+
+The `peasy-ai-fix-compile-error` and `peasy-ai-fix-checker-error` tools try up to 3 automated fixes. If all fail, they return `needs_guidance: true` with diagnostic questions. Call the tool again with `user_guidance` containing the user's hint:
+
+```
+→ peasy-ai-fix-compile-error(...) # attempt 1 — fails
+→ peasy-ai-fix-compile-error(...) # attempt 2 — fails
+→ peasy-ai-fix-compile-error(...) # attempt 3 — fails, returns needs_guidance=true
+→ Ask user for guidance
+→ peasy-ai-fix-compile-error(user_guidance="The type should be…") # succeeds
+```
+
+---
+
+## Troubleshooting
+
+| Problem | Fix |
+|---------|-----|
+| `peasyai-mcp: command not found` | Make sure the pip install location is on your `PATH`. Try `python -m site --user-base` to find it, or use `pipx install` instead. |
+| `p: command not found` | Install the P compiler following the [P installation guide](https://p-org.github.io/P/getstarted/install/) and ensure `~/.dotnet/tools` is on your `PATH`. |
+| `dotnet: command not found` | Install .NET SDK 8.0 following the [P installation guide](https://p-org.github.io/P/getstarted/install/#step-1-install-net-core-sdk). |
+| MCP server not showing in Cursor | Restart Cursor after editing `~/.cursor/mcp.json`. Check the MCP panel for error messages. |
+| LLM calls failing | Run `peasyai-mcp config` to verify your credentials are loaded correctly. |
+
+---
+
+## Development
+
+### Running the MCP Server Standalone
+
+```bash
+# Installed
+peasyai-mcp
+
+# Development (from source)
+cd Src/PeasyAI
+.venv/bin/python -m ui.mcp.entry
+```
+
+### Streamlit Web App
+
+```bash
+cd Src/PeasyAI
+pip install ".[streamlit]"
+streamlit run src/app.py
+```
+
+### Running Tests
+
+```bash
+cd Src/PeasyAI
+make test-contracts # MCP contract tests
+make regression # full regression suite
+```
+
+### Releasing a New Version
+
+Tag the commit and push — GitHub Actions will build the wheel and create a release:
+
+```bash
+git tag peasyai-v
+git push origin peasyai-v
+```
diff --git a/Src/PeasyAI/configuration/providers.yaml b/Src/PeasyAI/configuration/providers.yaml
new file mode 100644
index 0000000000..84d9acee7f
--- /dev/null
+++ b/Src/PeasyAI/configuration/providers.yaml
@@ -0,0 +1,60 @@
+# PeasyAI LLM Provider Configuration
+#
+# This file configures the LLM providers available to PeasyAI.
+# Environment variables can be referenced using ${VAR_NAME} syntax.
+
+providers:
+ # Snowflake Cortex (OpenAI-compatible API)
+ snowflake_cortex:
+ type: snowflake
+ base_url: "${OPENAI_BASE_URL}"
+ api_key: "${OPENAI_API_KEY}"
+ default_model: claude-3-5-sonnet
+ models:
+ - claude-3-5-sonnet
+ - claude-3-5-haiku
+ timeout: 600
+ description: "Snowflake Cortex with Claude models via OpenAI-compatible API"
+
+ # AWS Bedrock
+ bedrock:
+ type: bedrock
+ region: "${AWS_REGION:us-west-2}"
+ default_model: anthropic.claude-3-5-sonnet-20241022-v2:0
+ models:
+ - anthropic.claude-3-5-sonnet-20241022-v2:0
+ - anthropic.claude-3-haiku-20240307-v1:0
+ - anthropic.claude-3-opus-20240229-v1:0
+ timeout: 1000
+ description: "AWS Bedrock with Claude models"
+
+ # Direct Anthropic API
+ anthropic_direct:
+ type: anthropic
+ api_key: "${ANTHROPIC_API_KEY}"
+ base_url: "${ANTHROPIC_BASE_URL:}"
+ default_model: claude-3-5-sonnet-20241022
+ models:
+ - claude-3-5-sonnet-20241022
+ - claude-3-5-haiku-20241022
+ - claude-3-opus-20240229
+ timeout: 600
+ description: "Direct Anthropic API"
+
+# Default provider selection
+# The factory will auto-detect based on available environment variables,
+# but you can override by setting LLM_PROVIDER environment variable.
+#
+# Auto-detection priority:
+# 1. Snowflake Cortex (if OPENAI_BASE_URL contains 'snowflake')
+# 2. Direct Anthropic (if ANTHROPIC_API_KEY is set)
+# 3. AWS Bedrock (default fallback)
+
+default_provider: auto
+
+# Fallback chain (if primary provider fails)
+fallback_providers:
+ - anthropic_direct
+ - bedrock
+
+
diff --git a/Src/PeasyAI/configuration/workflows.yaml b/Src/PeasyAI/configuration/workflows.yaml
new file mode 100644
index 0000000000..b37506550d
--- /dev/null
+++ b/Src/PeasyAI/configuration/workflows.yaml
@@ -0,0 +1,170 @@
+# Workflow Definitions for PeasyAI
+#
+# Workflows define sequences of steps for generating, compiling, and verifying P code.
+# Each workflow can be executed via the WorkflowEngine.
+
+workflows:
+ # Full project generation from design document
+ full_generation:
+ name: "Full P Project Generation"
+ description: "Generate a complete P project from a design document"
+ continue_on_failure: false
+ steps:
+ - name: create_project_structure
+ description: "Create PSrc, PSpec, PTst directories and .pproj file"
+ max_retries: 1
+
+ - name: generate_types_events
+ description: "Generate Enums_Types_Events.p with shared types"
+ max_retries: 3
+
+ - name: generate_machines
+ description: "Generate all state machines (parallel)"
+ parallel: true
+ max_retries: 3
+ # Note: Machine names extracted from design doc at runtime
+
+ - name: generate_spec_safety
+ description: "Generate Safety.p specification/monitor"
+ max_retries: 3
+
+ - name: generate_test_driver
+ description: "Generate TestDriver.p test file"
+ max_retries: 3
+
+ - name: save_generated_files
+ description: "Save all generated code to disk"
+ max_retries: 1
+
+ - name: compile_project
+ description: "Compile the P project"
+ max_retries: 1
+
+ - name: fix_compilation_errors
+ description: "Fix any compilation errors"
+ max_retries: 5
+
+ # Incremental: Add a single machine to existing project
+ add_machine:
+ name: "Add Machine to Project"
+ description: "Add a new state machine to an existing P project"
+ continue_on_failure: false
+ steps:
+ - name: generate_machine
+ description: "Generate the new state machine"
+ max_retries: 3
+
+ - name: save_generated_files
+ description: "Save the new machine file"
+ max_retries: 1
+
+ - name: compile_project
+ description: "Recompile the project"
+ max_retries: 1
+
+ - name: fix_compilation_errors
+ description: "Fix any compilation errors"
+ max_retries: 5
+
+ # Incremental: Add specification to project
+ add_spec:
+ name: "Add Specification to Project"
+ description: "Add a new specification/monitor to an existing P project"
+ continue_on_failure: false
+ steps:
+ - name: generate_spec
+ description: "Generate the specification"
+ max_retries: 3
+
+ - name: save_generated_files
+ description: "Save the spec file"
+ max_retries: 1
+
+ - name: compile_project
+ description: "Recompile the project"
+ max_retries: 1
+
+ - name: fix_compilation_errors
+ description: "Fix any compilation errors"
+ max_retries: 5
+
+ # Compile and fix errors
+ compile_and_fix:
+ name: "Compile and Fix Errors"
+ description: "Compile project and automatically fix any errors"
+ continue_on_failure: false
+ steps:
+ - name: compile_project
+ description: "Compile the P project"
+ max_retries: 1
+
+ - name: fix_compilation_errors
+ description: "Fix compilation errors iteratively"
+ max_retries: 10
+
+ # Full verification: compile, fix, and run checker
+ full_verification:
+ name: "Full Verification"
+ description: "Compile, fix errors, and run PChecker"
+ continue_on_failure: false
+ steps:
+ - name: compile_project
+ description: "Compile the P project"
+ max_retries: 1
+
+ - name: fix_compilation_errors
+ description: "Fix compilation errors"
+ max_retries: 5
+
+ - name: run_checker
+ description: "Run PChecker"
+ max_retries: 1
+ config:
+ schedules: 100
+ timeout: 60
+
+ - name: fix_checker_errors
+ description: "Fix PChecker errors"
+ max_retries: 3
+
+ # Quick check: just run PChecker (assumes project compiles)
+ quick_check:
+ name: "Quick PChecker Run"
+ description: "Run PChecker on an already-compiled project"
+ continue_on_failure: false
+ steps:
+ - name: run_checker
+ description: "Run PChecker"
+ max_retries: 1
+ config:
+ schedules: 100
+ timeout: 60
+
+ # Types-only generation
+ generate_types_only:
+ name: "Generate Types/Events Only"
+ description: "Generate only the Enums_Types_Events.p file"
+ steps:
+ - name: create_project_structure
+ description: "Ensure project structure exists"
+ max_retries: 1
+
+ - name: generate_types_events
+ description: "Generate types and events"
+ max_retries: 3
+
+ - name: save_generated_files
+ description: "Save the types file"
+ max_retries: 1
+
+# Default workflow settings
+defaults:
+ max_retries: 3
+ continue_on_failure: false
+
+# Checker configuration
+checker:
+ default_schedules: 100
+ default_timeout: 60
+ max_schedules: 10000
+ max_timeout: 3600
diff --git a/Src/PeasyAI/pyproject.toml b/Src/PeasyAI/pyproject.toml
new file mode 100644
index 0000000000..9f12a5f9dd
--- /dev/null
+++ b/Src/PeasyAI/pyproject.toml
@@ -0,0 +1,72 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "peasyai-mcp"
+version = "0.1.0"
+description = "PeasyAI – MCP server for P language code generation, compilation, and verification"
+readme = "README.md"
+license = { text = "MIT" }
+requires-python = ">=3.10"
+authors = [
+ { name = "P-Org" },
+]
+keywords = ["mcp", "p-language", "formal-verification", "code-generation", "model-checking"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python :: 3",
+ "Topic :: Software Development :: Code Generators",
+ "Topic :: Software Development :: Testing",
+]
+
+dependencies = [
+ "fastmcp>=0.4.0",
+ "mcp[cli]>=1.10.1",
+ "openai>=1.0.0",
+ "anthropic>=0.30.0",
+ "pydantic>=2.0",
+ "python-dotenv",
+ "whatthepatch",
+ "sentence-transformers",
+ "pyyaml",
+]
+
+[project.optional-dependencies]
+bedrock = ["boto3", "botocore"]
+streamlit = ["streamlit", "st-diff-viewer", "streamlit_scrollable_textbox"]
+dev = ["pytest", "matplotlib"]
+
+[project.scripts]
+peasyai-mcp = "ui.mcp.entry:main"
+
+[project.urls]
+Homepage = "https://github.com/p-org/P"
+Repository = "https://github.com/p-org/P"
+Documentation = "https://p-org.github.io/P/"
+
+# ---------------------------------------------------------------------------
+# Hatch build configuration
+# ---------------------------------------------------------------------------
+[tool.hatch.build.targets.wheel]
+# The installable Python code lives under src/
+packages = ["src/core", "src/ui", "src/utils"]
+
+[tool.hatch.build.targets.wheel.sources]
+# Map src/core → core, src/ui → ui so import paths stay the same
+"src" = ""
+
+[tool.hatch.build.targets.wheel.force-include]
+# Bundle the resources directory inside the wheel so prompts, examples,
+# and syntax guides are available after pip install.
+"resources" = "peasyai_resources"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "src/",
+ "resources/",
+ "pyproject.toml",
+ "README.md",
+ "LICENSE",
+]
+
diff --git a/Src/PeasyAI/requirements.txt b/Src/PeasyAI/requirements.txt
new file mode 100644
index 0000000000..abc0e1ee16
--- /dev/null
+++ b/Src/PeasyAI/requirements.txt
@@ -0,0 +1,16 @@
+flask
+botocore
+boto3
+flask_socketio
+streamlit
+pytest
+mcp[cli]>=1.10.1
+fastmcp>=0.4.0
+matplotlib
+sentence-transformers
+whatthepatch
+st-diff-viewer
+streamlit_scrollable_textbox
+anthropic>=0.30.0
+openai>=1.0.0
+python-dotenv
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/assets/p_icon.ico b/Src/PeasyAI/resources/assets/p_icon.ico
new file mode 100644
index 0000000000..3031859059
Binary files /dev/null and b/Src/PeasyAI/resources/assets/p_icon.ico differ
diff --git a/Src/PeasyAI/resources/assets/pproj_template.txt b/Src/PeasyAI/resources/assets/pproj_template.txt
new file mode 100644
index 0000000000..8d0a3af4dc
--- /dev/null
+++ b/Src/PeasyAI/resources/assets/pproj_template.txt
@@ -0,0 +1,10 @@
+
+
+{project_name}
+
+ ./PSrc/
+ ./PSpec/
+ ./PTst/
+
+./PGenerated/
+
diff --git a/Src/PeasyAI/resources/compile_analysis/errors.json b/Src/PeasyAI/resources/compile_analysis/errors.json
new file mode 100644
index 0000000000..ea311a6bd5
--- /dev/null
+++ b/Src/PeasyAI/resources/compile_analysis/errors.json
@@ -0,0 +1,13 @@
+{
+ "extraneous input 'var' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'while', '{', '}', ';', Iden}": "Variable declaration is not placed correctly. Place it at the start of the machine body or the function body, depending on the need.",
+ "extraneous input 'defer' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'var', 'while', '{', '}', ';', Iden}": "Defer statement should be written in a state. Do not write it in a function.",
+ "extraneous input 'goto' expecting {'defer', 'entry', 'exit', 'ignore', 'on', '}'}": "Goto statement should always be written within a function body.",
+ "functions at entry or exit and do or goto transitions cannot take more than 1 parameter, provided function expects 3 parameters": "If you need to pass multiple values into a named function at entry or do or goto transitions, package them into a SINGLE parameter using a named tuple or a use a previously declared type.",
+ "mismatched input '(' expecting ';'": "Correct the syntax. If you are trying to access functions contained inside other machines, you should not do it.",
+ "mismatched input 'with' expecting {';', ','}": "Goto statement with payload is written incorrectly.",
+ "mismatched input 'entry' expecting Iden": "Entry is a reserved keyword. Do not use it for naming a variable.",
+ "mismatched input 'state' expecting {'any', 'bool', 'event', 'float', 'int', 'machine', 'map', 'set', 'string', 'seq', 'data', '(', Iden}": "State is a reserved keyword. You cannot have variables of type state.",
+ "missing ')' at 'is'": "is keyword is not supported in P. Use == or != for comparison.",
+ "could not find function 'distinct'": "distinct function is not supported in P.",
+ "no viable alternative at input '(all(": "all function is not supported in P."
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/compile_analysis/general_errors.json b/Src/PeasyAI/resources/compile_analysis/general_errors.json
new file mode 100644
index 0000000000..06b2595d7d
--- /dev/null
+++ b/Src/PeasyAI/resources/compile_analysis/general_errors.json
@@ -0,0 +1,31 @@
+{
+ "duplicates declaration": "There are duplicate declarations in this file. Remove all duplicates except for one or change their names. Remember that enum values in P are considered as global constants and must have unique names. Also, states inside of machines must have unique names. Foo this. ",
+ "could not find foreach iterator variable": " In P, when creating a foreach loop, the code needs to declare the iterator variable first at the very beginning of the function body or machine body they are in. var iterator_var: type_name; Do not initialize the iterator variable. The iterator variable is the word right before the 'in' keyword in the foreach loop. Keep the same type definitions or declarations. Keep the foreach loop the same. ",
+ "could not find variable, enum element, or event": "In this line, you are attempting to use a variable, enum element, or event that hasn't been defined yet. Do not change this line!! {file_line} Add a single line to the P file: add a new declaration of a variable, enum, or event called {variable_name}. If it is an event, it must be at the very beginnning of the P file. If it is a variable, define the variable at the very beginning of a machine body or at the very beginning of a function body. Make sure that the variable you are trying to use is not a machine type. If it is a machine type, change the variable's name. Here are all the machines in the P program: {machine_names}. Do not add any other types, enums, events, variables, or machines to the file. Keep the rest of the file the same. ",
+ "could not find enum, typedef, event set, machine, or interface": "This line is incorrect: {file_line} {error_message}. The last word in the error message is the missing definition. If it is missing a type declaration, add one single type declaration at the very beginning of the P file and DO NOT add any new events or enums. If you are defining a new machine, DO NOT add any events, types, or enums. Only add a machine that has a start state. {error_message} Here are all the machines in the P program: {machine_names}",
+ "could not find event": "You are attempting to use an event that does not exist. Please declare an event called {event_name} in the file of all the declarations. Here are all of the type, event, and enum declarations for this P program. {declarations} Here are all the machines in the P program: {machine_names}",
+ "functions at entry or exit and do or goto transitions cannot take more than 1 parameter": "Functions at entry or exit and do or goto transitions cannot take more than 1 parameter, but you have provided more than one parameter. Please fix this so that the function takes in one parameter. Make sure that the single parameter isn't using a P keyword. ",
+ "expected: namedtuple": " You are incorrectly trying to access variables that are part of another machine with dot notation. Note down all machines being used in the current function. Do not try to access any machine fields. If this is a P machine's code, you can use send statements. If this is a P spec's code, remove all variables of machines. After removing the variables, remove all places where those variables were used. Here is a list of the machines in this project: {machine_names}",
+ "could not find function": "The function you are using does not exist. Create a new function or delete this line. {error_message}",
+ "Module not closed. test module is not closed with respect to created interfaces;": "This is the test case: {test_case} . The last line of the test case contains all of the test modules. You need to add the missing machine {interface} to the test module in order for the test case to work. Do not add the test case in the returned code or change the test case at all. ",
+ "extraneous input '['": "There is an extra [ on this line for no reason. Fix this. ",
+ "got type:": "There was a type mismatch on this line, where it expected one type and received another type. ",
+ "could not find module" : "Your task is to add one single line to the file and keep all other lines unchanged. Write a line generating a module called {module} with the correct machine. Check the machines_list for machines that are the same name as the missing module: {machine_names} . If a machine in the machines list is the same name as the missing module's name, it is required to use that name to fill the module curly brackets. For example, module Participant = {{Participant}}; Keep all other modules the same. ",
+ "duplicates handler": "In a single state, you cannot have multiple event handlers handling the same event. Defer statements and ignore statements count as event handlers too. ",
+ "goto, function or constructor call expected": "{instruction}: {file_line} . Your goto, function, or constructor call is expecting {prev_parameters} arguments, but it was given {now_parameters} arguments. Provide a variable as the new argument. For goto statements, in order to add arguments, you need to use the following syntax: goto WaitForRequests, new_argument; .",
+ "field": "This line's variable has the missing field: {file_line} in this file: {prev_file_contents} . Do not add any machines to the returned code. {error_message} . Your type is missing a field. Please add it to the type. Do not remove any other type or event or enum definitions. Only return types, enums, and events. Do not add any =thing to the file. Only add the missing field to the type name. ",
+ "+=": "In P, += is only used to insert elements into a collection, such as a sequence, map, or set. If you would like to add numbers, you need to use x = x+1 syntax. If you are trying to insert elements into a collection, the element you are adding must have parentheses surrounding it. Add parentheses around it. Here is an example of inserting into a collection: set_var += (element); ",
+ "expecting {';', ','}": "Something is very wrong with the syntax of this line. Use the P Syntax document to correct the error with this line. {error_message}",
+ "mismatched input 'set' expecting Iden": "Delete this line.",
+ "10000": "The maximum number of choices allowed in a choose expression is 10000.",
+ "does not exist in the test module": "Here is the test case: {test_case} Error: {error_message} The main machine specified after 'main=' must exist within the test module. Fix by adding the main machine to the test module using union syntax: (union ExistingModule, {MainMachineName}). Example: change 'test name [main=TestDriver] : assert specs in SystemModule;' to 'test name [main=TestDriver] : assert specs in (union SystemModule, {TestDriver});'. Return the corrected test case with the main machine included in the test module using union syntax.",
+ "expecting Iden": "You are using a P keyword in this line when it was expecting a normal variable name. Please replace the P keyword with another word. ",
+ "could not find interface": "Create a new machine where the name is the missing interface's name. ",
+ "is used as a transition function, but might change state here.": "This method should not change state. Do not change state. ",
+ "$, $$, this, new, send, announce, receive, and pop are not allowed in monitors": "Specification monitors cannot have $, $$, this, new, send, announce, receive, case, and pop statements. You can put assert statements. Also, do not put event handlers in functions.",
+ "could not find state": "When using goto in an event handler, remember that the goto must be to another state not another function. ",
+ "incomparable": "Remove the 'to' keyword. You are attempting to coerce types with the keyword 'to' incorrectly. P only supports coercing of any value of type float to int and also any enum element to int with the keyword 'to.' We currently support only coercing of type float to int and also any enum element to int. All other types are impossible to convert to each other. You need to fix this.",
+ "expected either both float or both int;": "When using >, <, ==, or != operators, both sides must be either both floats or both ints. ",
+ "no viable alternative at input": "This line is incorrect. The syntax is incorrect. Use the P syntax document to fix the line. ",
+ "could not find variable": "{error_message} You need to declare a variable like this at the beginning of the function body or the beginning of the machine body: var example_var: some_type; "
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/compile_analysis/generic_errors.json b/Src/PeasyAI/resources/compile_analysis/generic_errors.json
new file mode 100644
index 0000000000..9e020e78a4
--- /dev/null
+++ b/Src/PeasyAI/resources/compile_analysis/generic_errors.json
@@ -0,0 +1,13 @@
+{
+ "could not find state": "Goto should be followed by a state name.",
+ "could not find foreach iterator variable": "Could not find foreach iterator variable '{name}'.",
+ "could not find variable, enum element, or event": "Could not find '{name}'. Declare it. If it is already declared, check if it is being used correctly.",
+ "could not find event": "Could not find event '{name}'. Declare it in the file Enums_Types_Events.p.",
+ "could not find enum, typedef, event set, machine, or interface": "Could not find '{name}'. Declare it.",
+ "expected: namedtuple": "You cannot access variables contained inside other machines. Correct the code.",
+ "no viable alternative at input '(union'": "Modules cannot be declared or used in the file TestDriver.p.",
+ "invalid composition operation. bound interfaces after composition are not disjoint": "You cannot union a module with modules that it already contains through previous unions.",
+ "mismatched input '.+' expecting {, 'enum', 'event', 'eventset', 'machine', 'interface', 'fun', 'spec', 'type', 'module', 'implementation', 'test'}": "Variable declaration is written incorrectly. And variables cannot be declared outside a machine.",
+ "extraneous input '.+' expecting {, 'enum', 'event', 'eventset', 'machine', 'interface', 'fun', 'spec', 'type', 'module', 'implementation', 'test'}": "Variable declaration is not placed correctly.",
+ "mismatched input '.+' expecting '\\)'": "Incorrect syntax. Strictly follow the P language syntax here."
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/compile_analysis/specific_errors.json b/Src/PeasyAI/resources/compile_analysis/specific_errors.json
new file mode 100644
index 0000000000..09c715b5ba
--- /dev/null
+++ b/Src/PeasyAI/resources/compile_analysis/specific_errors.json
@@ -0,0 +1,30 @@
+{
+ "extraneous input 'var' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'while', '{', '}', ';', Iden}": "Locate all variable declarations in this file that are not at the very beginning of their function's bodies. This file has variables incorrectly declared under other lines of code. The variables may be nested within a while loop or inside of an if statement or under an if statement or under other statements. All variables must be declared at the very beginning of the function body they are in, above all other lines of code. Move all variables that aren't declared at the beginning of the function to the beginning of the function. Do not move variables into states. They must be declared at the beginning of the functions. Here is the line where the var is in the wrong place. {file_line} ",
+ "extraneous input 'on' expecting {'cold', 'fun', 'hot', 'start', 'state', 'var', '}'}": "This code has an event handler in an incorrect place. All event handlers must be inside of states. ",
+ "extraneous input 'on' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'var', 'while', '{', '}', ';', Iden}": "This code has an event handler in an incorrect place. All event handlers must be inside of states. ",
+ "mismatched input ',' expecting 'in'": "When iterating through a foreach loop, you only need one variable to do so. If you are iterating through a map, you would use keys(expr) or values(expr) in order to do so.",
+ "mismatched input '{' expecting 'observes'": "In P, the syntax for a spec is like this: spec iden observes eventsList statemachineBody.,",
+ "extraneous input 'goto' expecting {'defer', 'entry', 'exit', 'ignore', 'on', '}'}": "Goto statements can only be placed inside of other functions or entry functions or event handlers. Move or remove any goto statements that are outside of these. ",
+ "extraneous input 'event' expecting {'cold', 'fun', 'hot', 'start', 'state', 'var', '}'}": "The following line is in the wrong place: {file_line}. The event is in the wrong place. It should be declared above the machine. Events are always declared at the very beginning of a P file. Do not add types or enums or machines. Do one task: move the event to the beginning of the P file. ",
+ "String expr format placeholders must contain only digits. Escape braces by doubling them.": "If the format function takes in multiple arguments, you need to place the digits starting from 0 inside of the curly brackets. There must be one digit inside a pair of curly brackets per argument after the main string. String expression format placeholders must contain digits or be doubled up. If you would like to include curly braces without using them as format placeholders, you can escape braces by doubling them. If you would like to use curly braces as format placeholders, place digits starting from 0 inside of the curly braces representing what number placeholder it is.",
+ "no viable alternative at input '({'": "Creating a map, sequence, or set in P with ({variable}) is completely incorrect. In order to create a map, sequence, or set in P, you need to declare a variable at the top of the function or machine. Never make the map, sequence, or set equal to anything. If you want the map, sequence, or set to contain items, you need to add to it with collection_var += (item_var);. Then, you can set collection_var equal to that variable. ",
+ "extraneous input 'var' expecting {, 'enum', 'event', 'eventset', 'machine', 'interface', 'fun', 'spec', 'type', 'module', 'implementation', 'test'}": "This code has variables declared at the wrong place. Variables cannot be declared at the top of the program. They must be declared at the very top of a machine or the top of a function. Move the variable. Move the variables that aren't declared at the top of the function or the top of a machine.",
+ "mismatched input 'var' expecting {, 'enum', 'event', 'eventset', 'machine', 'interface', 'fun', 'spec', 'type', 'module', 'implementation', 'test'}": "This code has variables declared at the wrong place. Variables cannot be declared at the top of the program. They must be declared at the very top of a machine or the top of a function. Move the variables that aren't declared at the top of the function.",
+ "mismatched input 'var' expecting {'defer', 'entry', 'exit', 'ignore', 'on', '}'}": "This code has variables declared at the wrong place. They must be declared at the very top of a machine or the top of a function. Move the variables that aren't declared at the top of the function or to the top of the machine. Variables declared inside of states is illegal. ",
+ "extraneous input 'defer' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'while', '{', '}', ';', Iden}": "Defer statements can only be placed within a state body because defer statements are a type of event handler. They cannot be placed inside of functions.",
+ "mismatched input 'type' expecting {'cold', 'fun', 'hot', 'start', 'state', 'var', '}'}": "This type declaration {file_line} was declared in an incorrect place. All type and enum declarations are declared at the very top of a P program. Only move the type declaration to the right place. Do not add new type, enum, or event declarations. ",
+ "extraneous input 'enum' expecting {'cold', 'fun', 'hot', 'start', 'state', 'var', '}'}": "This enum declaration {file_line} was declared in an incorrect place. All type and enum declarations are declared at the very top of a P program. Only move the type declaration to the right place. Do not add new type, enum, or event declarations. ",
+ "extraneous input 'type' expecting {'cold', 'fun', 'hot', 'start', 'state', 'var', '}'}": "This type declaration {file_line} was declared in an incorrect place. All type and enum declarations are declared at the very top of a P program. Only move the type declaration to the right place. Do not add new type, enum, or event declarations. ",
+ "extraneous input 'var' expecting {'defer', 'entry', 'exit', 'ignore', 'on', '}'}": "This code has variables declared at the wrong place. They must be declared at the very top of a machine or the top of a function. Move the variables that aren't declared at the top of the function. ",
+ "expected an interface or int or float type": "You are attempting to coerce types with the keyword 'to' incorrectly. P only supports coercing of any value of type float to int and also any enum element to int with the keyword 'to.' We currently support only coercing of type float to int and also any enum element to int. All other types are impossible to convert to each other. You need to fix this. If you would like to convert an int or float to a string, try using the format function. Here is the line that is erroring: {file_line}",
+ "extraneous input '{' expecting {'float', 'default', 'format', 'halt', 'keys', 'new', 'sizeof', 'this', 'values', 'choose', BoolLiteral, IntLiteral, 'null', StringLiteral, '$$', '$', '!', '-', '(', ')', '.', Iden}": "It is incorrect to call a function on a value with curly brackets. If you would like to call a function on a collection type, you need to declare a variable of that collection type. Then, you need to add the values to that variables. Then, call the function on that variable. Also, you can't initialize a collection with curly brackets. This is how you declare a collection and add values. var st : set[T];\n var x: T;\n // adds x into the set st\n st += (x);\n",
+ "extraneous input 'ignore' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'while', '{', '}', ';', Iden}": "Ignore statements can only be placed within a state body because defer statements are a type of event handler. They cannot be placed inside of functions.",
+ "missing ')' at 'is'": "In P, comparisons are done with == or !=. Using 'is' is incorrect.",
+ "extraneous input 'case' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'var', 'while', '{', '}', ';', Iden}": "Case statements must be inside of receive statements. ",
+ "mismatched input '=' expecting ';'": "This line is wrong: {file_line}. Variables cannot be declared and initialized on the same line. You need to separate declaration and initialization of variables. Variables must be declared at the very beginning of a P machine or the very beginning of a P function. Then, variables must be initalized with values inside of a function or inside of P states. ",
+ "mismatched input ',' expecting ')'": "This line is expecting a single parameter. Please change it. {file_line}",
+ "extraneous input '{' expecting {'float', 'default', 'format', 'halt', 'keys', 'new', 'sizeof', 'this', 'values', 'choose', BoolLiteral, IntLiteral, 'null', StringLiteral, '$$', '$', '!', '-', '(', '.', Iden}": "It is incorrect to call a function on a value with curly brackets. If you would like to call a function on a collection type, you need to declare a variable of that collection type. Then, you need to add the values to that variables. Then, call the function on that variable. Also, you can't initialize a collection with curly brackets. This is how you declare a collection and add values. var st : set[T];\n var x: T;\n // adds x into the set st\n st += (x);\n",
+ "mismatched input '(' expecting ';'": "Fix the following line that is using dot notation to call functions: {file_line}. You are not allowed to access other machines' functions or use other machines' fields. You are also not allowed to call functions without setting the result equal to a variable. Avoid using dot notation to call functions on variables. Instead, pass the variable as a parameter inside the function's parentheses. You cannot place identifiers with dot notation in front when calling functions. x = Foo(); y = Bar(10, \"haha\");
+A P program consists of a collection of following top-level declarations:
+1. Enums
+2. User Defined Types
+3. Events
+4. State Machines
+5. Specification Monitors
+6. Global Functions
+7. Module System
+
+Here is the list of all words reserved by the P language. These words have a special meaning and purpose, and they cannot be used as identifiers for variables, enums, types, events, machines, function parameters, etc.:
+
+var, type, enum, event, on, do, goto, data, send, announce, receive, case, raise, machine, state, hot, cold, start, spec, module, test, main, fun, observes, entry, exit, with, union, foreach, else, while, return, break, continue, ignore, defer, assert, print, new, sizeof, keys, values, choose, format, if, halt, this, as, to, in, default, Interface, true, false, int, bool, float, string, seq, map, set, any
+
+
+Here is the P enums guide:
+
+1. Enum values in P are considered as global constants and must have unique name.
+2. Enums by default are given integer values starting from 0 (if no values are assigned to the elements).
+3. Enums in P can be coerced to int.
+4. If an enum is used in a user defined type declaration, the enum identifier cannot be a reserved keyword listed in the tags.
+
+Syntax:: enum enumName { enumElemList | numberedEnumElemList }
+
+Here is an example of how enum declaration without values:
+
+// enum representing the response status for the withdraw request
+enum tWithDrawRespStatus { WITHDRAW_SUCCESS, WITHDRAW_ERROR }
+
+// User Defined Type
+type tWithDrawResp = (status: tWithDrawRespStatus, accountId: int, balance: int, rId: int);
+
+
+Here is an example of enum declaration with values:
+
+enum tResponseStatus { ERROR = 500, SUCCESS = 200, TIMEOUT = 400; }
+
+// usage of enums
+var status: tResponseStatus;
+status = ERROR;
+
+// you can coerce an enum to int
+assert (ERROR to int) == 500;
+
+
+
+Here is the P types guide:
+
+P Supports the following data types:
+1. Primitive: int, bool, float, string, machine, and event.
+2. Record: tuple and named tuple.
+3. Collection: map, seq, and set.
+4. User Defined: These are user defined types that are constructed using any of the P types listed above.
+5. Universal Supertypes: any and data.
+
+Here are the syntactical details of each of these data types:
+
+
+Here is an example of how primitive data types are declared:
+
+// some function body in the P program
+{
+ var i: int;
+ var j: float;
+ var k: string;
+ var l: bool;
+
+ i = 10;
+ j = 10.0;
+ k = "Failure!!";
+ l = (i == (j to int));
+}
+
+
+
+
+1. The fields of a tuple can be accessed by using the . operation followed by the field index.
+2. Named tuples are similar to tuples with each field having an associated name.
+3. The fields of a named tuple can be accessed by using the . operation followed by the field name.
+
+Here is an example of a tuple:
+
+// tuple with three fields
+var tupleEx: (int, bool, int);
+
+// constructing a value of tuple type.
+tupleEx = (20, false, 21);
+
+// accessing the first and third element of the tupleEx
+tupleEx.0 = tupleEx.0 + tupleEx.2;
+
+
+Here is an example of a named tuple:
+
+// named tuple with three fields
+var namedTupleEx: (x1: int, x2: bool, x3: int);
+
+// constructing a value of named tuple type.
+namedTupleEx = (x1 = 20, x2 = false, x3 = 21);
+
+// accessing the first and third element of the namedTupleEx
+namedTupleEx.x1 = namedTupleEx.x1 + namedTupleEx.x3;
+
+
+
+A tuple value can be created using the following expressions:
+Syntax:: (rvalue,) for a single field tuple value, or (rvalue (, rvalue)+) for tuple with multiple fields.
+
+Here is an example of creating tuple value:
+
+// tuple value of type (int,)
+(10,)
+
+// tuple value of type (string, (string, string))
+("Hello", ("World", "!"))
+
+// assume x: int and y: string
+// tuple value of type (int, string)
+(x, y)
+
+
+
+
+A named tuple value can be created using the following syntax:
+Syntax:: (iden = rvalue,) for a single field named tuple value, or (iden = rvalue (, iden = rvalue)+) for a named tuple with multiple fields.
+A trailing comma is required after the value assignment in a single field named tuple.
+
+Here is an example of creating a single field named tuple value:
+
+// named tuple value of type (reqId: int, )
+(reqId = 10,)
+
+
+Here is an incorrect example of creating a single field named tuple value:
+
+(reqId = 10) // Missing comma
+
+
+Here is an example of creating a named tuple with multiple fields:
+
+// assume x: int and y: string
+// named tuple value of type (val:int, str:string)
+(val = x, str = y)
+
+// named tuple value of type (h: string, (w: string, a: string))
+(h = "Hello", (w = "World", a = "!"))
+
+
+Here is an incorrect example of creating a named tuple with multiple fields:
+
+// assume x: int and y: string
+(x, y)
+
+
+
+
+
+P supports three collection types:
+1. map[K, V] represents a map type with keys of type K and values of type V.
+2. seq[T] represents a sequence type with elements of type T.
+3. set[T] represents a set type with elements of type T.
+
+
+Here is the syntax to index into collections to access its elements:
+Syntax:: expr_c[expr_i]
+1. If expr_c is a value of sequence type, then expr_i must be an integer expression and expr_c[expr_i] represents the element at index expr_i.
+2. If expr_c is a value of set type, then expr_i must be an integer expression and expr_c[expr_i] represents the element at index expr_i but note that for a set there is no guarantee for the order in which elements are stored in the set.
+3. If expr_c is a value of map type, then expr_i represents the key to look up and expr_c[expr_i] represents the value for the key expr_i.
+
+
+
+foreach statement can be used to iterate over a collection in P.
+Syntax:: foreach (iden in expr)
+Declare iden at the top of the function body in EACH function. Type of iden must be same as the elements type in the collection.
+
+1. iden is the name of the variable that stores the element from the collection during iterations.
+2. expr represents the collection over which we want to iterate.
+3. One can mutate the collection represented by expr when iterating over it as the foreach statement enumerates over a clone of the collection.
+
+Here is an example of foreach over a sequence:
+
+var sq : seq[string];
+var str : string; // str is declared
+
+foreach(str in sq) {
+ print str;
+}
+
+
+Here is an INCORRECT example of foreach over a sequence:
+
+var sq : seq[string];
+// Missing declaration of str
+foreach(str in sq) {
+ print str;
+}
+
+
+Here is an example of foreach over a set:
+
+var ints: set[int];
+var iter, sum: int;
+// iterate over a set of integers
+foreach(iter in ints)
+{
+ sum = sum + iter;
+}
+
+
+Here is an example of foreach over a map:
+
+var intsM: map[int, int];
+var key: int;
+foreach(key in keys(intsM))
+{
+ intsM[key] = intsM[key] + delta;
+}
+
+
+Here is an example of mutating a collection with foreach:
+
+var ints: set[int];
+var i: int;
+foreach(i in ints)
+{
+ ints -= (i);
+}
+assert sizeof(ints) == 0;
+
+
+
+
+Here are the details on how to add an element into a sequence:
+
+1. For a sequence sq, the value of index i should be between 0 <= i <= sizeof(sq).
+2. Index i = 0 inserts x at the start of sq and i = sizeof(sq) appends x at the end of sq.
+3. To insert an element in an empty sequence, reference the details given in tags.
+Syntax:: lvalue += (expr, rvalue);
+expr is the index of the sequence and rvalue is the value to be inserted. Round brackets are important.
+
+Here is an example of inserting an element into a sequence:
+
+type Task = (description: string, assignedTo: string, dueDate: int);
+
+machine TaskManager {
+ var allTasks: seq[Task];
+
+ start state Idle {
+ entry Idle_Entry;
+ }
+
+ fun Idle_Entry() {
+ // Initialize the sequence to be empty
+ allTasks = default(seq[Task]);
+ }
+
+ // Function to add a new task to the sequence
+ fun AddTask(task: Task) {
+ allTasks += (sizeof(allTasks), task);
+ }
+}
+
+
+Here is an incorrect example of inserting an element in a sequence:
+
+var sq : seq[T];
+var x: int;
+// round brackets should contain a pair of values
+sq += (x);
+
+
+
+
+Here is the syntax to add or insert an element in a map:
+Syntax: lvalue += (expr, rvalue);
+lvalue is a value of map in P. expr is the key to be inserted and rvalue is its corresponding value.
+round brackets surrounding rvalue are important.
+
+Here is an example of inserting an element into a map:
+
+var mp : map[K,V];
+var x: K, y: V;
+
+// adds (x, y) into the map
+mp += (x, y);
+
+// adds (x, y) into the map, if key x already exists then updates its value to y.
+mp[x] = y;
+
+
+
+
+Here is the syntax to add or insert an element in a set:
+Syntax:: lvalue += (rvalue);
+lvalue is a value of set in P. round brackets surrounding rvalue are important.
+
+Here is an example of inserting an element into a set:
+
+var st : set[T];
+var x: T;
+
+// adds x into the set st
+// x is surrounded by round brackets
+st += (x);
+
+
+Here is an incorrect example of inserting an element into a set:
+
+var st : set[T];
+var x: T;
+
+// Missing round brackets surrounding x
+st += x;
+
+
+
+
+
+1. The default feature in P can be used to obtain the default value of a collection.
+2. P variables on declaration are automatically initialized to their default values.
+
+Syntax:: default(type)
+type is any P type and default(type) represents the default value for the type
+
+Here is an example of initializing an empty sequence:
+
+var sq : seq[int];
+// by default a seq type is initialized to an empty seq
+assert sizeof(sq) == 0;
+
+// set the variable to an empty seq
+sq = default(seq[int]);
+
+
+Here is an example of initializing an empty map:
+
+var myMap: map[int, int];
+
+// by default a map type is initialized to an empty map
+assert sizeof(myMap) == 0;
+
+// set the variable to an empty map
+myMap = default(map[int, int]);
+
+
+
+var s : set[int];
+// by default a set type is initialized to an empty set
+assert sizeof(s) == 0;
+
+// set the variable to an empty set
+s = default(set[int]);
+
+
+
+
+In P language, a collection when declared is automatically initialized by default to empty collection. Do NOT set it to empty again.
+
+
+In P, a sequence when declared is initialized by default to empty sequence. Do NOT set the sequence to empty again.
+
+Here is an example of initializing a non-empty sequence:
+
+var mySeq: seq[int];
+var i: int;
+var numOfIterations: int;
+
+i = 0;
+numOfIterations = 5;
+
+// initialize mySeq
+while(index < numOfIterations) {
+ mySeq += (sizeof(mySeq), i); // Append i to the end of mySeq
+ i = i + 1;
+}
+
+// At this point, mySeq contains [0, 1, 2, 3, 4]
+
+
+
+
+In P, a map when declared is initialized by default to empty map. Do NOT set the map to empty again.
+
+Here is an example of how to initialize a non-empty map:
+
+var bankBalance: map[int, int];
+var i: int;
+while(i < 6) {
+ bankBalance[i] = choose(100) + 10;
+ i = i + 1;
+}
+
+
+
+
+In P, a set when declared is initialized by default to empty set. Do NOT set the set to empty again.
+
+Here is an example of how to initialize a non-empty set:
+
+var participants: set[Participant];
+var i : int;
+var num: int;
+
+num = 7;
+while (i < num) {
+ participants += (new Participant());
+ i = i + 1;
+}
+
+
+
+
+Remove statement is used to remove an element from a collection.
+Syntax:: lvalue -= rvalue;
+
+
+For a sequence sq, the value of index i above should be between 0 <= i <= sizeof(sq) - 1.
+
+Here is an example of removing an element at an index from a sequence:
+
+var sq : seq[T];
+var i : int;
+
+// i is the index in sq, NOT an element of sq
+sq -= (i);
+
+
+Here is an incorrect example of removing an element at an index from a sequence:
+
+var sq : seq[T];
+var i : int;
+
+// Missing round brackets surrounding i
+sq -= i;
+
+
+
+
+Here is an example of removing an element from a map:
+
+var mp : map[K,V];
+var x: K;
+
+// Removes the element (x, _) from the map i.e., removes the element with key x from mp
+mp -= (x);
+
+
+
+
+Here is an example of removing an element from a set:
+
+var st : set[T];
+var x: T;
+
+// removes x from the set st
+st -= (x);
+
+
+Here is an INCORRECT example of removing an element from a set:
+
+var st : set[T];
+var x: T;
+
+// Missing round brackets surrounding x
+st -= x;
+
+
+
+
+
+P supports four operations on collection types:
+1. sizeof
+2. keys
+3. values
+4. in (to check containment)
+
+
+Syntax:: sizeof(expr)
+expr is a value of type set, seq or map, returns an integer value representing the size or length of the collection.
+
+Here is an example of sizeof:
+
+var sq: seq[int];
+while (i < sizeof(sq)) {
+ i = i + 1;
+}
+
+
+
+
+keys function is used to get access to a sequence of all the keys in map and then operate over it.
+Syntax:: keys(expr)
+1. expr must be a map value
+2. If expr: map[K, V], then keys(expr) returns a sequence (of type seq[K]) of all keys in the map.
+
+Here is an example of keys function:
+
+var iter: int;
+foreach(iter in keys(mapI))
+{
+ assert iter in mapI, "Key should be in the map";
+ mapI[iter] = 0;
+}
+
+
+
+
+values function is used to get access to a sequence of all the values in map and then operate over it.
+Syntax:: values(expr)
+1. expr must be a map value
+2. If expr: map[K, V], then values(expr) returns a sequence (of type seq[V]) of all values in the map.
+
+Here is an example of values function:
+
+var iter: int;
+var rooms: map[int, tRoomInfo];
+foreach(iter in values(rooms))
+{
+ assert iter == 0, "All values must be zero!";
+}
+
+
+
+
+1. P provides the in operation to check if an element (or key in the case of a map) belongs to a collection.
+2. The in expression evaluates to true if the collection contains the element and false otherwise.
+3. !in is NOT supported in P.
+4. 'not in' is NOT supported in P.
+
+Syntax:: expr_e in expr_c
+expr_e is the element (or key in the case of map) and expr_c is the collection value.
+
+Here is an example of checking whether an element is contained in a collection:
+
+var sq: seq[tRequest];
+var mp: map[int, tRequest];
+var rr: tRequest;
+var i: int;
+if(rr in sq && rr in values(mp) && i in mp) {
+ // do something
+}
+if (!(rr in sq)) {
+ // do something else
+}
+
+
+
+
+
+
+
+1. P supports assigning names to types i.e., creating typedef.
+2. NOTE that these typedefs are simply assigning names to P types and does not effect the sub-typing relation.
+3. User defined types are assigned values using named tuples as detailed in tags.
+
+Syntax:: type typeName = typedef;
+typeName should not be named as any of the reserved keywords listed in the tags.
+
+Here is an example of declaring a user defined type:
+
+// defining a type tLookUpRequest
+type tLookUpRequest = (client: machine, requestId: int, key: string);
+
+// defining a type tLookUpRequestX
+type tLookUpRequestX = (client: machine, requestId: int, key: string);
+
+// Note that the types tLookUpRequest and tLookUpRequestX are same, the compiler does not distinguish between the two types.
+
+
+Here is another example of declaring a user defined type:
+
+enum tTransStatus { SUCCESS, ERROR, TIMEOUT }
+
+// defining a type tWriteTransResp
+type tWriteTransResp = (transId: int, status: tTransStatus);
+
+
+
+Here is an example of assigning value to a user defined type:
+
+var resp: tWriteTransResp;
+// named tuple
+resp = (transId = trans.transId, status = TIMEOUT);
+
+
+
+
+1. any type in P is the supertype of all types. Also, note that in P, seq[any] is a super type of seq[int] and similarly for other collection types.
+2. data type in P is the supertype of all types in P that do not have a machine type embedded in it.
+3. For example, data is a supertype of (key: string, value: int) but not (key: string, client: machine).
+
+
+
+1. The default feature in P can be used to obtain the default value of any P type.
+2. P variables on declaration are automatically initialized to their default values.
+
+Syntax:: default(type)
+type is any P type and default(type) represents the default value for the type
+
+Here is an example of default value:
+
+type tRequest = (client: machine, requestId: int);
+
+// somewhere inside a function
+x = default(tRequest);
+
+assert x.client == default(machine);
+
+
+Here's a table of P Types and their corresponding default values:
+
+
+ P Types
+ Default Value
+
+
+ int
+ 0
+
+
+ float
+ 0.0
+
+
+ bool
+ false
+
+
+ string
+ ""
+
+
+
+
+
+Here is the P events guide:
+
+1. A P program is a collection of state machines communicating with each other by exchanging events.
+2. An event in P has two parts: an event name and a payload value (optional) that can be sent along with the event.
+3. Event name in P should be unique and different from Enum and Type names.
+4. There should EXIST a user defined type for EACH event payload and that declared type should be used in the event declaration.
+5. IMPORTANT: Declaring events with named tuple payloads is INCORRECT.
+
+When declaring P events with payloads, always follow this EXACT syntax:
+First, declare a user defined type for the payload as detailed in the tags:
+type payloadTypeName = ((fieldName: typeName)*);
+Then, declare the event using the declared payload type payloadTypeName:
+event eventName: payloadTypeName;
+
+Here is a correct example of events:
+
+// declarations of events with no payloads
+event ePing;
+event ePong;
+
+// declaration of events that have payloads
+type tRequest = (client: machine, requestId: int, key: string);
+// eRequest event with payload of user defined type tRequest
+event eRequest: tRequest;
+
+type tWriteTransResp = (transId: int, status: tTransStatus);
+// eWriteTransResp event with payload of user defined type tWriteTransResp
+event eWriteTransResp : tWriteTransResp;
+
+
+
+Here is the P functions guide:
+
+
+1. Named functions can either be declared inside a state machine or globally as a top-level declaration.
+2. The named functions declared within a state machine are local to that machine and hence can access the local variables of the machine.
+3. Global named functions are shared across state machines.
+4. Named functions at entry or exit and do or goto transitions CANNOT take more than 1 parameter.
+5. Named functions at exit CANNOT have a parameter.
+6. If you need to pass multiple values into a named function at entry or exit and do or goto transitions, package them into a SINGLE parameter using a named tuple or a custom type.
+7. Do NOT use reserved keywords listed in the tags to name a parameter.
+
+Syntax:: fun name (funParamList?) (: returnType)? functionBody
+name is the name of the named function, funParamList is the optional function parameters, and returnType is the optional return type of the function.
+
+
+Here are the P syntax rules to follow when writing any function body:
+1. Function body in P is a sequence of variable declarations followed by a sequence of statements. Syntax:: { varDecl* statement* }.
+2. Declare ALL local variables at the beginning of the function body before any other statements.
+3. Do NOT combine variable declaration with variable initialization or variable assignment in the same line.
+4. To initialize/assign a variable to a value, you can do it in a separate statement after ALL variables in the function body are declared.
+5. If a function has a returnType, the function should necessarily return a value using the return statement described in tags.
+
+Here is a correct example of function body:
+
+var availableServer: machine;
+var i: int;
+availableServer = ChooseAvailableServer();
+i = 0;
+// other statements
+<
+
+Here is an incorrect example of a function body:
+
+// Do not combine variable declaration and initialization
+var availableServer: machine = ChooseAvailableServer();
+// other statements
+
+
+
+Here is an example of named function:
+
+ fun BroadcastToAllParticipants(message: event, payload: any)
+ {
+ // function body is a sequence of variable declarations followed by a sequence of statements
+ var isComplete: bool;
+ var i: int;
+
+ isComplete = true;
+ i = 1;
+ while (i < sizeof(participants)) {
+ send participants[i], message, payload;
+ i = i + 1;
+ }
+ }
+
+
+Here is an example of a named function at entry or do or goto transitions:
+
+fun UpdateBankBalance(query: (database: Database, accId: int, bal: int)) {
+ // Function to update the account balance for the account Id
+}
+
+
+Here is an example of an incorrect named function at entry or exit and do or goto transitions:
+
+// function has more than 1 parameter, which is incorrect
+fun UpdateBankBalance(database: Database, accId: int, bal: int) {
+ // Function to update the account balance for the account Id
+}
+
+
+
+
+1. Function calls in P are similar to any other imperative programming languages.
+2. Note that the parameters passed to the functions and the return values are pass-by-value!
+3. Note that . operation CANNOT be used to make function calls.
+Syntax:: iden (rvalue?);
+
+Here is an example of a function call:
+
+x = Foo();
+Bar(10, "haha");
+
+
+
+
+Here is the P expressions guide:
+
+
+There are three unique primitive expressions in P:
+1. $
+2. halt
+3. this
+
+
+$ represents a nondeterministic boolean choice. It is a short hand for choose() which randomly returns a boolean value.
+
+
+
+1. halt is a special event in P used for destroying an instance of a P machine.
+2. The semantics of an halt event is that whenever a P machine throws an unhandled event exception because of a halt event then the machine is automatically destroyed or halted and all events sent to that machine instance there after are equivalent to being dropped to ether.
+3. There are two ways of using the halt event:
+(a) self-halt by doing raise halt; raising a halt event which is not handled in the state machine will lead to that machine being halted or
+(b) by sending the halt event to a machine, and that machine on dequeueing halt, would halt itself.
+
+
+
+1. this represents the self machine reference of the current machine.
+2. It can be used to send self reference to other machines in the program so that they can send messages to this machine.
+3. this should NOT be used to access variables in a machine.
+
+Here is an example of this expression in P language:
+
+database = new Database((server = this, initialBalance = initialBalance));
+
+
+
+
+
+P allows creating formatted strings.
+Syntax:: format ( formatString (, rvalueList)? )
+formatString is the format string and rvalueList is a comma separated list of arguments for the formatted string.
+`formatString` should have numbers inside the curly braces `{}`, corresponding to the positions of the arguments in the `rvalueList`.
+
+Here is an example of a formatted string:
+
+var hw, h, w: string;
+var tup: (string, int);
+
+h = "Hello"; w = "World";
+tup = ("tup value", 100);
+hw = format("{0} {1}, and {2} is {3}!", h, w, tup.0, tup.1);
+// hw value is "Hello World, and tup value is 100!"
+
+print format("{0} {1}, and {2} is {3}!", h, w, tup.0, tup.1);
+// prints "Hello World, and tup value is 100!"
+
+
+
+
+P supports two unary operations:
+1. - on integers and floats values (i.e., negation) and
+2. ! on boolean values (i.e., logical not).
+3. !in is NOT supported in P.
+
+Here is an example that shows an incorrect usage of the logical not operation with the in expression:
+
+assert resp.roomNum !in roomAssignments, "Room double booked!";
+
+
+Here is an example that shows the correct way of using the logical not operation with the in expression:
+
+assert !(resp.roomNum in roomAssignments), "Room double booked!";
+
+
+
+
+P supports the following arithmetic binary operations on integers or floats:
+1. + (i.e., addition)
+2. - (i.e., subtraction)
+3. * (i.e., multiplication)
+4. % (i.e., modulo)
+5. / (i.e., division)
+
+
+
+P supports the following comparison binary operations on integers or floats:
+1. < (i.e., less-than)
+2. <= (i.e., less-than-equal)
+3. > (i.e., greater-than)
+4. >= (i.e., greater-than-equal)
+
+
+
+P supports the “==” operator, also known as the equality operator.
+The operator will return “true” if both the operands are equal.
+However, it should not be confused with the “=” operator. “=” works as an assignment operator.
+
+
+
+1. P supports two super types any and data, as described in the tags.
+2. To cast values from these supertypes to the actual types, P supports the as cast expression.
+Syntax:: expr as T
+expr expression is cast to type T and if the cast is not valid, then it leads to dynamic type-cast error.
+
+Here is an example of cast expression:
+
+type tRecord = (key: int, val: any);
+
+// inside machine
+var st: set[tRecord];
+var x: any;
+var x_i: string;
+var st_i: set[(key: int, val: string)];
+
+x_i = x as string;
+st += ((key = 1, val = "hello"));
+st_i = st as set[(key: int, val: string)];
+
+
+
+
+P supports coercing of any value of type float to int and also any enum element to int.
+Syntax:: expr to T
+expr expression is coerced to type T. ONLY coercing of type float to int and also any enum element to int is supported.
+
+Here is an example of coerce:
+
+enum Status { ERROR = 101, SUCCESS = 102 }
+
+// inside machine body
+var x_f : float;
+var x_i: int;
+
+x_f = 101.0;
+x_i = x_f to int;
+assert x_i == ERROR to int;
+
+
+
+
+P provides the choose primitive to model data nondeterminism in P programs.
+
+Syntax:: choose() or choose(expr)
+expr should either be a int value or a collection.
+1. For choose(x), when x is an integer, choose(x) returns a random value between 0 to x (excluding x).
+2. When x is a collection, then choose(x) returns a random element from the collection.
+3. Performing a choose over an empty collection leads to an error. Also, choose from a map value returns a random key from the map.
+4. NOTE that the maximum number of choices allowed in a choose(expr) is 10,000. Performing a choose with an int value greater than 10000 or over a collection with more than 10000 elements leads to an error.
+5. Another use case of choose() could be to model nondeterministic behavior within the system itself where the system can randomly choose to timeout or fail or drop messages.
+
+Here is an example of choose:
+
+choose() // returns true or false, is equivalent to $
+choose(10) // returns an integer x, 0 <= x < 10
+choose(x) // if x is set or seq then returns a value from that collection
+
+choose(10001) // throws a compile-time error
+choose(x) // throws a runtime error if x is of integer type and has value greater than 10000
+choose(x) // throws a runtime error if x is of seq/set/map type and has size greater than 10000 elements
+
+
+
+
+
+Here is the P statements guide:
+
+
+Here is the detailed description of various P statements:
+
+P allows writing local assertions using the assert statement.
+Syntax:: assert expr (, expr)?
+The assert statement must have a boolean expression followed by an optional string message that is printed in the error trace.
+1. If the condition inside the assert statement evaluates to True, the program continues its execution as usual, without any interruption.
+2. If the condition inside the assert statement evaluates to False, the optional message is printed and the program exits
+
+Here is an example of assert statement that asserts that the requestId is always greater than 1 and is in the set of all requests:
+
+assert (requestId > 1) && (requestId in allRequestsSet);
+
+
+Here is an example of assert statement with error message:
+
+assert x >= 0, "Expected x to be always positive";
+
+
+Here is an example of assert with formatted error message:
+assert (requestId in allRequestsSet),
+format ("requestId {0} is not in the requests set = {1}", requestId, allRequestsSet);
+
+
+
+
+Print statements can be used for writing or printing log messages into the error traces (especially for debugging purposes).
+Syntax:: print expr;
+The print statement must have an expression of type string.
+
+Here is an example of print statement that prints "Hello World!" in the execution trace log:
+
+print "Hello World!";
+
+
+Here is an example of print statement that prints formatted string message "Hello World to You!!" in the execution trace log:
+
+x = "You";
+print format("Hello World to {0}!!", x);
+
+
+
+
+While statement in P is just like while loops in other popular programming languages like C, C# or Java.
+Syntax:: while (expr) statement
+expr is the conditional boolean expression and statement could be any P statement.
+
+Here is an example of while loop:
+
+i = 0;
+while (i < 10)
+{
+ i = i + 1;
+}
+
+
+Here is an example of while loop iterating over collection:
+
+i = 0;
+while (i < sizeof(s))
+{
+ print s[i];
+ i = i + 1;
+}
+
+
+
+
+IfThenElse statement in P is just like conditional if statements in other popular programming languages like C, C# or Java.
+Syntax:: if(expr) statement (else statement)?
+expr is the conditional boolean expression and statement could be any P statement. The else block is optional.
+
+
+if(x > 10) {
+ x = x + 20;
+}
+
+
+
+if(x > 10)
+{
+ x = 0;
+}
+else
+{
+ x = x + 1;
+}
+
+
+
+
+break and continue statements in P are just like in other popular programming languages like C, C# or Java to break out of the while loop or to continue to the next iteration of the loop respectively.
+
+while(true) {
+ if(x == 10)
+ break;
+ x = x + 1;
+}
+
+
+
+while(true) {
+ if(x == 10) // skip the loop when x is 10
+ continue;
+}
+
+
+
+
+1. return statement in P can be used to return (or return a value) from any function.
+2. return statement is written in the function body.
+3. If a function has a returnType, the function should necessarily contain a return statement in the function body.
+
+Here is an example of return statement:
+
+fun IncrementX() {
+ if(x > MAX_INT)
+ return;
+ x = x + 1;
+}
+
+
+Here is an example of return value statement:
+
+fun Max(x: int, y: int) : int{
+if(x > y)
+ return x;
+else
+ return y;
+}
+
+
+
+
+P has value semantics or copy-by-value semantics and does not support any notion of references.
+
+Syntax:: leftvalue = rightvalue;
+1. Note that because of value semantics, assignment in P copies the value of the rightvalue into leftvalue.
+2. leftvalue could be any variable, a tuple field access, or an element in a collection.
+3. rightvalue could be any expression that evaluates to the same type as lvalue.
+4. In P language syntax, variable assignment CANNOT be combined with variable declaration.
+5. Compound assignment operators (+=, -=) are NOT supported for primitive type variables.
+
+Here is an example of assignment:
+
+var a: seq[string];
+var b: seq[string];
+b += (0, "b");
+a = b; // copy value
+a += (1, "a");
+print a; // will print ["b", "a"]
+print b; // will print ["b"]
+
+
+Here is another example of assignment:
+
+a = 10;
+s[i] = 20;
+tup1.a = "x";
+tup2.0 = 10;
+t = foo();
+
+
+Here is an INCORRECT example of assignment:
+
+var i: int;
+var requestId = 5; // Direct assignment without variable declaration is incorrect
+var availableServer: machine = ChooseAvailableServer(); // Incorrect to combine declaration and assignment
+i += 1; // operator += not supported for int
+
+
+
+
+New statement is used to create an instance of a machine in P.
+Syntax:: new iden (rvalue?);
+
+Here is an example that uses new to create a dynamic instance of a Client machine
+new Client();
+
+
+
+
+1. The statement raise e, v; terminates the evaluation of the function raising an event e with payload v.
+2. The control of the state machine jumps to end of the function and the state machine immediately handles the raised event.
+
+Syntax:: raise expr (, rvalue)?;
+rvalue should be same as the payloadType of expr and should STRICTLY be a NAMED TUPLE as detailed in tags.
+
+Here is an example of raise event:
+
+fun handleRequest(req: tRequest)
+{
+ // ohh, this is a Add request and I have a event handler for it
+ if(req.type == "Add")
+ raise eAddOperation; // terminates function
+
+ assert req.type != "Add"; // valid
+}
+
+state HandleRequest {
+ on eAddOperation do AddOperation;
+}
+
+
+
+
+1. Send statement is one of the most important statements in P as it is used to send messages to other state machines.
+2. Send takes as argument a triple send t, e, v, where t is a reference to the target state machine, e is the event sent and v is the associated payload.
+3. Sends in P are asynchronous and non-blocking. Statement send t, e, v enqueues the event e with payload v into the target machine t's message buffer.
+4. Within EACH machine where you write the send statement, declare variable for the target machine t with the same type as the target machine. If the type for target machine does not exist, declare t as a machine.
+5. t should ALWAYS be a machine and v should ALWAYS be of the same type as the payload type defined for the event e. Do NOT use nested named tuples as the payload v when the payload type is a single field named tuple.
+
+Syntax:: send lvalue, expr (, rvalue)?;
+lvalue should STRICTLY be of the same type as the target machine t.
+rvalue should STRICTLY be of the same type as the payload type defined for the event. Do NOT use nested named tuples as the payload when the payload type is a single field named tuple.
+
+Here is an example of send event with no payload:
+
+machine BankServer {
+ // inside machine
+}
+
+machine Database {
+ // BankServer is a state machine
+ var server: BankServer;
+
+ fun AfterReadQuery(query: (accountId: int)) {
+ assert query.accountId in balance, "Invalid accountId received in the read query!";
+ send server, eReadQueryResp;
+ }
+}
+
+
+Here is an example of send event with payload:
+
+var server: BankServer;
+send server, eReadQueryResp, (accountId = query.accountId, balance = balance[query.accountId]);
+
+
+Here is an INCORRECT example of send event with payload:
+
+// Missing names in the tuple
+send server, eReadQueryResp, (query.accountId, balance[query.accountId]);
+
+
+
+
+1. Announce is used to publish messages to specification monitors in P.
+2. Each monitor observes a set of events and whenever a machine sends an event that is in the observes set of a monitor then it is synchronously delivered to the monitor. Announce can be used to publish an event to all the monitors that are observing that event.
+3. Announce only delivers events to specification monitors (not state machines) and hence has no side effect on the system behavior.
+Syntax:: annouce eventName (, rvalue)?;
+rvalue should STRICTLY be of the same type as the payload type defined for the event. Do NOT use nested named tuples as the payload when the payload type is a single field named tuple.
+
+Here is an example of announce event with payload:
+
+// Consider a specification monitor that continuously observes eStateUpdate event to keep track of the system state and then asserts a required property when the system converges.
+spec CheckConvergedState observes eStateUpdate, eSystemConverged {
+ // something inside spec
+}
+
+//announce statement can be used to inform the monitor when the system has converged and that we should assert the global specification.
+announce eSystemConverged, payload;
+
+
+
+
+1. Goto statement can be used to jump to a particular state.
+2. On executing a goto, the state machine exits the current state (terminating the execution of the current function) and enters the target state.
+3. Goto statement should ALWAYS be written WITHIN a function body.
+4. The optional payload accompanying the goto statement becomes the input parameter to the function at entry of the target state.
+Syntax:: goto stateName (, rvalue)?;
+
+Here is an example of goto:
+
+start state Init {
+ on eProcessRequest goto SendPingsToAllNodes;
+}
+
+state SendPingsToAllNodes {
+ // do something
+}
+
+state {
+ on eFailure, eCancelled goto Done;
+}
+
+state Done {
+ // do something
+}
+
+
+Here is an example of goto with payload:
+
+state ServiceRequests {
+ fun processRequest(req: tRequest) {
+ // process request with some logic
+ lastReqId = req.Id;
+ goto WaitForRequests, lastReqId;
+ }
+}
+
+state WaitForRequests {
+ entry AfterRequest_Entry;
+}
+
+fun AfterRequest_Entry(lastReqId: int) {
+ // do something
+}
+
+
+
+
+1. Receive statements in P are used to perform blocking await/receive for a set of events inside a function.
+2. Each receive statement can block or wait on a set of events, all other events are automatically deferred by the state machine.
+3. On receiving an event that the receive is blocking on (case blocks), the state machine unblocks, executes the corresponding case-handler and resumes executing the next statement after receive.
+
+Syntax::
+receive { recvCase+ }
+/* case block inside a receive statement */
+recvCase : case eventList : anonFunction
+
+Here is an example of receive awaiting a single event:
+
+fun AcquireLock(lock: machine)
+{
+ send lock, eAcquireLock;
+ receive {
+ case eLockGranted: (result: tResponse) { /* case handler */ }
+ }
+ print "Lock Acquired!"
+ // Note that when executing the AcquireLock function the state machine blocks at the receive statement, it automatically defers all the events except the eLockGranted event.
+ // On receiving the eLockGranted, the case-handler is executed and then the print statement.
+}
+
+
+Here is an example of receive awaiting multiple events:
+
+fun WaitForTime(timer: Timer, time: int)
+{
+ var success: bool;
+ send timer, eStartTimer, time;
+ receive {
+ case eTimeOut: { success = true; }
+ case eStartTimerFailed: { success = false; }
+ }
+ if (success) print "Successfully waited!"
+ // Note that when executing the WaitForTime function the state machine blocks at the receive statement, it automatically defers all the events except the eTimeOut and eStartTimerFailed events.
+}
+
+
+
+
+1. Switch case statements are NOT supported in P. Instead, use IfThenElse statements as described in tags.
+2. Usage of 'with' keyword is VALID ONLY in the syntax of event handlers as described in tags.
+3. 'const' keyword is NOT supported in P. Constants in P are defined as described in tags.
+4. 'is' operator is NOT supported in the P language.
+5. 'self' keyword is NOT supported in the P language.
+6. values() or indexOf() functions are NOT supported in P. Do not use library functions from other programming languages.
+7. to string is NOT supported in P.
+
+Here is a non-exhaustive list of the P syntax rules that you should strictly adhere to when writing P code:
+1. Variable declaration and assignment must be done in separate statements.
+2. All variables in a function body must be declared at the top before any other statements.
+3. The `foreach` loop must declare the iteration variable at the top of the function body.
+4. Do not use 'self' or 'this' for accessing variables inside a machine.
+5. Named function at entry CANNOT take more than 1 parameter, as described in tags.
+6. Exit functions cannot have parameters.
+7. The target machine in a `send` statement must be a variable of the same type as the target machine.
+8. The logical not operator `!` must be used with parentheses: `!(expr in expr)`.
+9. '!in' and 'not in' are not supported for collection membership checks.
+10. Default values for types are obtained using 'default(type)' syntax.
+11. Collections are initialized to empty by default and should not be reassigned to default values.
+12. Initializing non-empty collections requires specific syntax (e.g., `seq += (index, value)`, `map[key] = value`).
+13. The `ignore` statement must list the event names: `ignore eventList;`.
+14. Formatted strings use a specific syntax: `format("formatString {0} {1}", arg1, arg2)`.
+15. Creating a single field named tuple requires a trailing comma after the value assignment, as described in tags.
+16. User defined types are assigned values using named tuples as detailed in tags.
+17. Do NOT access functions contained inside of other machines.
+18. Entry functions in spec machines CANNOT take any parameter.
+19. $, $$, this, new, send, announce, receive, and pop are not allowed in monitor.
+
+
+
+Here is the P state machine guide:
+
+A P program is a collection of concurrently executing state machines that communicate with each other by sending events (or messages) asynchronously.
+
+Here is a summary of the important semantic details of P State Machine:
+1. Each P state machine has an unbounded FIFO buffer associated with it.
+2. Sends are asynchronous, i.e., executing a send operation send t,e,v; adds event e with payload value v into the FIFO buffer of the target machine t.
+3. Variables and functions declared within a machine are local, i.e., they are accessible ONLY from within that machine.
+4. Each state in a machine has an entry and an exit function associated with it. The entry function gets executed when the machine enters that state, and similarly, the exit function gets executed when the machine exits that state on an outgoing transition.
+5. After executing the entry function, a machine tries to dequeue an event from its input buffer or blocks if the buffer is empty. Upon dequeuing an event from its input queue, a machine executes the attached event handler which might transition the machine to a different state.
+
+Do NOT leave a machine empty.
+Do NOT explicitly initialize a machine to null.
+Do NOT access functions contained inside of other machines.
+
+A machine can define a set of local variables that are accessible only from within that machine.
+Syntax:: var iden: type;
+iden is the name of the variable and type is the variable datatype.
+
+Here is an example of variable:
+
+type tWithDrawResp = (status: tWithDrawRespStatus, accountId: int, balance: int, rId: int);
+// some function body in the P program
+{
+ // all variables declared at the top of the function body
+ var currentBalance: int;
+ var response: tWithDrawResp;
+
+ // variables are assigned a value in separate statements after all variables are declared
+ currentBalance = ReadBankBalance(database, wReq.accountId);
+ if(currentBalance - wReq.amount >= 10) {
+ response = (status = WITHDRAW_SUCCESS, accountId = wReq.accountId, balance = currentBalance - wReq.amount, rId = wReq.rId);
+ }
+}
+
+
+Within EACH machine x where other machines are referenced, declare variables for the other machines with the same type as those machines.
+
+machine RWLBSharedObject {
+ // LBSharedObject machine declared
+ var sharedObj: LBSharedObject;
+}
+
+machine LBSharedObject {
+ // RWLBSharedObject machine declared
+ var currHolder: RWLBSharedObject;
+ // inside machine
+}
+
+
+
+
+1. A machine can have a set of local named functions that are accessible only from within that machine.
+2. It is INVALID to access named functions from other machines.
+3. If a named function uses any variable declared WITHIN a machine, it should ALWAYS be written WITHIN the machine. NEVER write it outside the machine.
+4. Named functions at entry or exit and do or goto transitions are written OUTSIDE the state. NEVER write them inside a state.
+5. Named functions at entry or exit and do or goto transitions CANNOT take more than 1 parameter. These functions SHOULD have a single named tuple parameter.
+6. Named functions at exit CANNOT have input parameters.
+7. If you need to pass multiple values into a named function at entry or exit and do or goto transitions, package them into a SINGLE parameter using a named tuple or a custom type.
+8. Refer the P syntax rules to write a named function from the tags.
+
+Here is an example of an incorrect function call:
+
+machine EspressoCoffeeMaker{
+ fun HasWater() : bool {
+ return $;
+ }
+}
+machine CoffeeMakerControlPanel {
+ var currentUser: EspressoCoffeeMaker;
+ fun WaitForUser() : bool {
+ // Incorrect syntax. HasWater() cannot be accessed outside of EspressoCoffeeMaker machine
+ return currentUser.HasWater();
+ }
+}
+
+
+
+
+A state can be declared in a machine with a name and a stateBody.
+Syntax:: start? (hot | cold)? state name { stateBody* }
+1. A single state in each machine should be marked as the start state to identify this state as the starting state on machine creation.
+2. A machine should contain at least the start state.
+3. Each state in the machine SHOULD have a UNIQUE name.
+4. ONLY a state declared inside a P Monitor can be marked as hot or cold.
+
+
+
+1. The state body for a state defines its entry/exit functions and attached event handlers supported by that state.
+2. Additionally, the state body can mark certain events as deferred or ignored in the state.
+
+Here are details about the constituents of a state body:
+
+1. The entry function gets executed when a machine enters that state.
+2. If the corresponding state is marked as the start state, the entry function gets executed at machine creation.
+3. Defining the entry function for a state is optional.
+4. The entry function MUST be a named function defined separately.
+Syntax:: entry funName;
+
+Here is an example of entry function:
+
+state CoffeeMakerRunSteam {
+ entry StartSteamer;
+}
+
+fun StartSteamer() {
+ // send an event to maker to start steaming
+ send coffeeMaker, eStartSteamerReq;
+}
+
+
+Here is an incorrect example of entry function:
+
+// entry should NOT be written as anonymous function as follows:
+entry {
+ // send an event to maker to start steaming
+ send coffeeMaker, eStartSteamerReq;
+}
+
+
+
+
+1. The exit function gets executed when a machine exits that state to transition to another state.
+2. Defining the exit function for a state is optional.
+3. The exit function must be a named function defined separately.
+Syntax:: exit funName;
+
+Here is an example of exit function:
+
+state EncounteredError {
+ exit PrintExitingState;
+}
+
+fun PrintExitingState {
+ print format ("Exiting state");
+}
+
+
+Here is an incorrect example of exit function:
+
+// exit function should NOT be anonymous function
+exit {
+ print format ("Exiting state");
+}
+
+
+
+
+1. An event handler defined for event E in state S describes what statements are executed when a machine dequeues event E in state S.
+2. An event handler is ALWAYS defined WITHIN a state where you expect the corresponding event to be handled. It is NOT ALLOWED to define event handlers OUTSIDE of any state in the state machine.
+3. An event handler is NOT a named function.
+Syntax:: on eNameList do funName;
+eNameList should STRICTLY contain only one or more event names.
+The parameter of funName should STRICTLY be same as the payloadType of eName.
+
+Here is an example of event handler:
+
+event eRead: machine;
+
+state WaitForRelease {
+ on eRead do SendReadResponse;
+}
+
+fun SendReadResponse(client: machine) {
+ // do something
+}
+
+state WaitForResponse {
+ on eFailure, eCancel do printResponse;
+}
+
+fun printResponse() {
+ // do something
+}
+
+
+Here is an incorrect example of event handler:
+
+event eRead: machine;
+
+on eRead do {
+ // do something
+}
+
+
+
+
+An event handler can be further combined with a goto statement. After the attached event handler statements are executed, the machine transitions to the goto state.
+Syntax:: on eNameList goto stateName (; | with funName;)
+The parameter of funName should be same as the payloadType of eName.
+
+Here is an example of an event handler with goto:
+
+on eWarmUpCompleted goto CoffeeMakerReady;
+
+
+Here is an example of an event handler combined with goto and function:
+
+event eTimeOut: tTransStatus;
+
+state WaitForPrepareResponses {
+ on eTimeOut goto WaitForTransactions with DoGlobalAbort;
+}
+
+state WaitForTransactions {
+ // when in this state it is fine to drop these messages as
+ // they are from the previous transaction
+ ignore ePrepareResp, eTimeOut;
+}
+
+fun DoGlobalAbort(respStatus: tTransStatus) {
+ // ask all participants to abort and fail the transaction
+}
+
+
+
+
+1. An event can be deferred in a state.
+2. Defer basically defers the dequeue of the event E in state S until the machine transitions to a state that does not defer the event E.
+3. The position of the event E that is deferred in state S does not change the FIFO buffer of the machine.
+4. NOTE that whenever a machine encounters a dequeue event, the machine goes over its unbounded FIFO buffer from the front and removes the first event that is not deferred in its current state, keeping rest of the buffer unchanged.
+5. Defer statement should be written in a state. Do not write it in a function.
+
+Here is the syntax of defer statement:
+Syntax:: defer eventList;
+
+Here is an example of defer statement:
+
+start state Init {
+ defer eDefer;
+}
+
+
+
+
+1. An event E can be ignored in a state, which basically drops the event E.
+2. When ignoring an event, always follow this EXACT syntax:
+Syntax:: ignore eventList;
+3. Writing ignore as 'on eventList ignore;' is NOT supported in P.
+
+Here is an example of ignore statement:
+
+state WaitForTimerRequests {
+ ignore eCancelTimer, eDelayedTimeOut;
+}
+
+state ResetAndStartAgain {
+ ignore ePong;
+}
+
+
+Here is an incorrect example of ignore statement:
+
+on eSteamerButtonOff ignore;
+
+
+
+
+
+
+
+Here is the P specification monitors guide:
+
+P specification monitors or spec machines are used to write the safety and liveness specifications the system must satisfy for correctness.
+
+Syntactically, machines and spec machines in P are very similar in terms of the state machine structure. But, they have some key differences:
+1. spec machines in P are observer machines (imagine runtime monitors); they observe a set of events in the execution of the system and, based on these observed events (may keep track of local states), assert the desired global safety and liveness specifications.
+2. Since spec machines are observer machines, they cannot have any side effects on the system behavior and hence, spec machines cannot perform send, receive, new, and annouce.
+3. spec machines are global machines; in other words, there is only a single instance of each monitor created at the start of the execution of the system.
+4. Since dynamic creation of monitors is not supported, spec machines cannot use this expression described in tags.
+5. spec machines are synchronously composed with the system that is monitored. The way this is achieved is: each time there is a send or announce of an event during the execution of a system, all the monitors or specifications that are observing that event are executed synchronously at that point.
+6. Another way to imagine this is: just before send or annouce of an event, we deliver this event to all the monitors that are observing the event and synchronously execute the monitors at that point.
+7. spec machines can have hot and cold annotations on their states to model liveness specifications.
+8. $, $$, this, new, send, announce, receive, and pop are NOT allowed in monitors.
+9. Entry functions in spec machines CANNOT take any parameter.
+
+Syntax: spec iden observes eventsList statemachineBody;
+iden is the name of the spec machine,
+eventsList is the comma separated list of events observed by the spec machine, and
+statemachineBody is the implementation of the specification and its grammar is similar to the grammar in the tags.
+
+
+Here is a specification that checks a very simple global invariant that all eRequest events that are being sent by clients in the system have a globally monotonically increasing rId:
+
+/*******************************************************************
+ReqIdsAreMonotonicallyIncreasing observes the eRequest event and
+checks that the payload (Id) associated with the requests sent
+by all concurrent clients in the system is always globally
+monotonically increasing by 1
+*******************************************************************/
+spec ReqIdsAreMonotonicallyIncreasing observes eRequest {
+ // keep track of the Id in the previous request
+ var previousId : int;
+ start state Init {
+ on eRequest do AfterRequest;
+ }
+
+ fun CheckIfReqIdsAreMonotonicallyIncreasing(req: tRequest) {
+ assert req.rId > previousId, format ("Request Ids not monotonically increasing, got {0}, previously seen Id was {1}", req.rId, previousId);
+ previousId = req.rId;
+ }
+}
+
+
+
+
+1. hot annotation can be used on states to mark them as intermediate or error states.
+2. The key idea is that the system satisfies a liveness specification if, at the end of the execution, the monitor is not in a hot state.
+3. Properties like 'eventually something holds' or 'every event X is eventually followed by Y' or 'eventually the system enters a convergence state' can be specified by marking the intermediate state as hot states and the checker checks that all the executions of the system eventually end in a non-hot state.
+4. If there exists an execution that fails to come out of a hot state eventually, then it is flagged as a potential liveness violation.
+
+Here is a specification that checks the global liveness property that every event eRequest is eventually followed by a corresponding successful eResponse event:
+
+/**************************************************************************
+GuaranteedProgress observes the eRequest and eResponse events;
+it asserts that every request is always responded by a successful response.
+***************************************************************************/
+spec GuaranteedProgress observes eRequest, eResponse {
+ // keep track of the pending requests
+ var pendingReqs: set[int];
+ start state NopendingRequests {
+ on eRequest goto PendingReqs with AddPendingRequest;
+ }
+
+ hot state PendingReqs {
+ on eResponse do AfterResponse;
+ on eRequest goto PendingReqs with AddPendingRequest;
+ }
+
+ fun AddPendingRequest(req: tRequest) {
+ pendingReqs += (req.rId);
+ }
+
+ fun AfterResponse(resp: tResponse) {
+ assert resp.rId in pendingReqs, format ("unexpected rId: {0} received, expected one of {1}", resp.rId, pendingReqs);
+ if(resp.status == SUCCESS)
+ {
+ pendingReqs -= (resp.rId);
+ if(sizeof(pendingReqs) == 0) // requests already responded
+ goto NopendingRequests;
+ }
+ }
+}
+
+
+
+
+
+Here is the P module systems guide:
+
+1. The P module system allows programmers to decompose their complex system into modules to implement and test the system compositionally.
+2. In its simplest form, a module in P is a collection of state machines.
+3. The P module system allows constructing larger modules by composing or unioning modules together.
+4. Hence, a distributed system under test which is a composition of multiple components can be constructed by composing (or unioning) modules corresponding to those components.
+5. You cannot union a module with modules that it already contains through previous unions.
+
+There are four types of modules:
+1. Named module
+2. Primitive module
+3. Union module
+4. Assert Monitors module
+
+Here are the details of each of these modules:
+
+A named module declaration simply assigns a name to a module expression.
+Syntax: module mName = modExpr;
+mName is the assiged name for the module and modExpr is any of the Primitive, Union, or Assert Monitors modules.
+
+Here is an example of a named module:
+
+// assigns the name serverModule to a primitive module consisting of machines Server and Timer.
+module serverModule = { Server, Timer };
+
+
+
+
+A primitive module is a (annonymous) collection of state machines.
+Syntax: { bindExpr (, bindExpr)* }
+bindExpr is a binding expression which could either be
+1. the name of a machine iden, or
+2. a mapping mName -> replaceName that maps a machine mName to a machine name replaceName that we want to replace.
+
+The binding enforces that whenever a machine replaceName is created in the module, it leads to the creation of machine mName.
+In most cases, a primitive module is simply a list of state machines that together implement that component.
+
+Here is an example of a primitive module:
+
+// Let's say there are three machines in the P program: Client, Server, and Timer
+
+// client is a primitive module consisting of the Client machine and the server module is a primitive module consisting of machines Server and Timer.
+module client = { Client };
+module server = { Server, Timer };
+
+
+Here is an example of a primitive module with bindings:
+
+// Let's say there are four machines in the P program: Client, Server, AbstractServer and Timer
+module client = { Client };
+module server = { Server, Timer };
+
+// module serverAbs represents a primitive module consisting of machines AbstractServer and Timer machines, with the difference that wherever the serverAbs module is used, the creation of machine Server will in turn lead to creation of the AbstractServer machine.
+module serverAbs = { AbstractServer -> Server, Timer };
+
+
+
+
+1. P supports unioning multiple modules together to create larger, more complex modules.
+2. The union of two modules is simply a creation of a new module which is a union of the machines of the component modules.
+3. You cannot union a module with modules that it already contains through previous unions.
+
+Syntax:: union modExpr (, modExpr)+
+modExpr is any P module.
+
+Here is an example of a union module:
+
+// system is a module which is a union of the modules client and server.
+module system = (union client, server);
+
+// systemAbs is a module which is a union of the module client and the serverAbs where the Client machine interacts with the AbstractServer machine instead of the Server machine in the system module.
+module systemAbs = (union client, serverAbs);
+
+
+
+
+1. P allows attaching monitors (or specifications) to modules.
+2. When attaching monitors to a module, the events observed by the monitors must be sent by some machine in the module.
+3. The way to think about assert monitors module is that: attaching these monitors to the module asserts (during P checker exploration) that each execution of the module satisfies the global properties specified by the monitors.
+
+Syntax: assert idenList in modExpr
+idenList is a comma separated list of monitor (spec machine) names that are being asserted on the executions of the module modExpr.
+
+Here is an example of assert monitors module:
+
+// module asserts that the executions of the module TwoPhaseCommit satisfy the properties specified by the monitors AtomicitySpec and EventualResponse.
+assert AtomicitySpec, EventualResponse in TwoPhaseCommit
+
+
+
+
+
+
+Here is the P test cases guide:
+
+1. P Test cases are used to define different finite scenarios under which we would like to check the correctness of the module (or system) under test.
+2. More concretely, the system module to be tested is unioned with different environment modules (or test harnesses/drivers) to check its correctness for different inputs scenarios generated by the environment modules.
+3. Here is the syntax for test case declaration:
+Syntax:: test tName [main=mName] : module_under_test ;
+tName is the name of the test case, mName is the name of the main machine where the execution of the system starts, and module_under_test is the module to be tested.
+
+
+For each testcase, the P checker by default asserts that for each execution of the system (i.e., module_under_test):
+1. there are no unhandled event exceptions
+2. all local assertions in the program hold
+3. there are no deadlocks, and finally
+4. based on the specification monitors that are attached to the module, the safety and liveness properties asserted by the monitors always hold.
+
+
+
+Here is the structure of a P program guide:
+
+
+A specification says what the system should do (correctness properties).
+
+
+
+A model captures the details of how the system does it.
+
+
+
+A model checking scenario provides the finite non-deterministic test-harness or environment under which the model checker should check that the system model satisfies its specifications.
+
+
+
+A P program is typically divided into three folders:
+1. PSrc: contains all the state machines representing the implementation (model) of the system or protocol to be verified or tested. Additionally, it contains a P modules file.
+2. PSpec: contains all the specifications representing the correctness properties that the system must satisfy.
+3. PTst: contains all the environment or test harness state machines that model the non-deterministic scenarios under which we want to check that the system model in PSrc satisfies the specifications in PSpec. P allows writing different model checking scenarios as test-cases.
+The PTst folder has two files:
+1. TestDriver.p: TestDrivers are collections of state machines that implement the test harnesses (or environment state machines) for different test scenarios.
+2. TestScript.p: TestScripts are collections of test cases that are automatically run by the P checker. P allows programmers to write different test cases.
+Each test case is checked separately and can use a different test driver. Using different test drivers triggers different behaviors in the system under test, as it implies different system configurations and input generators.
+
+
+
+
+Here is an example of a P program:
+
+
+System:
+Consider a client-server application where clients interact with a bank to withdraw money from their accounts.
+The bank consists of two components:
+1. a bank server that services withdraw requests from the client, and
+2. a backend database that is used to store the account balance information for each client.
+Multiple clients can concurrently send withdraw requests to the bank.
+On receiving a withdraw request, the bank server reads the current bank balance for the client.
+If the withdraw request is allowed, then the server performs the withdrawal, updates the account balance, and responds back to the client with the new account balance.
+
+Correctness Specification:
+The bank must maintain the invariant that each account must have at least 10 dollars as its balance.
+If a withdrawal request takes the account balance below 10, then the withdrawal request must be rejected by the bank.
+The correctness property that we would like to check is that in the presence of concurrent client withdrawal requests, the bank always responds with the correct bank balance for each client, and a withdraw request always succeeds if there is enough balance in the account (i.e., at least 10).
+
+
+
+The ClientServer folder contains the source code for the ClientServer project.
+
+The P models (PSrc) for the ClientServer example consist of four files:
+1. Client.p: Implements the Client state machine that has a set of local variables used to store the local-state of the state machine.
+2. Server.p: Implements the BankServer and the backend Database state machines.
+3. AbstractBankServer.p: Implements the AbstractBankServer state machine that provides a simplified abstraction that unifies the BankServer and Database machines.
+4. ClientServerModules.p: Declares the P modules corresponding to each component in the system.
+
+
+
+The P Specifications (PSpec) for the ClientServer project are implemented in the BankBalanceCorrect.p file.
+
+We define two specifications:
+1. BankBalanceIsAlwaysCorrect (safety property):
+The BankBalanceIsAlwaysCorrect specification checks the global invariant that the account-balance communicated to the client by the bank is always correct and the bank never removes more money from the account than that withdrawn by the client.
+Also, if the bank denies a withdraw request, then it is only because the withdrawal would reduce the account balance to below 10.
+
+2. GuaranteedWithDrawProgress (liveness property):
+The GuaranteedWithDrawProgress specification checks the liveness (or progress) property that all withdraw requests submitted by the client are eventually responded.
+
+The two properties BankBalanceIsAlwaysCorrect and GuaranteedWithDrawProgress together ensure that every withdraw request if allowed will eventually succeed and the bank cannot block correct withdrawal requests.
+
+
+
+The test scenarios folder for ClientServer (PTst) consists of two files: TestDriver.p and TestScript.p.
+
+
+
+
+Here is the folder PSrc:
+
+Here is the file Client.p:
+
+/* User Defined Types */
+
+// payload type associated with the eWithDrawReq, where `source`: client sending the withdraw request,
+// `accountId`: account to withdraw from, `amount`: amount to withdraw, and
+// `rId`: unique request Id associated with each request.
+type tWithDrawReq = (source: Client, accountId: int, amount: int, rId:int);
+
+// payload type associated with the eWithDrawResp, where `status`: response status (below),
+// `accountId`: account withdrawn from, `balance`: bank balance after withdrawal, and
+// `rId`: request id for which this is the response.
+type tWithDrawResp = (status: tWithDrawRespStatus, accountId: int, balance: int, rId: int);
+
+// enum representing the response status for the withdraw request
+enum tWithDrawRespStatus {
+ WITHDRAW_SUCCESS,
+ WITHDRAW_ERROR
+}
+
+// event: withdraw request (from client to bank server)
+event eWithDrawReq : tWithDrawReq;
+// event: withdraw response (from bank server to client)
+event eWithDrawResp: tWithDrawResp;
+
+
+machine Client
+{
+ var server : BankServer;
+ var accountId: int;
+ var nextReqId : int;
+ var numOfWithdrawOps: int;
+ var currentBalance: int;
+
+ /*************************************************************
+ Init state is the start state of the machine where the machine starts executions on being created.
+ The entry function of the Init state initializes the local variables based on the parameters received
+ on creation and jumps to the WithdrawMoney state.
+ *************************************************************/
+ start state Init {
+ entry Init_Entry;
+ }
+
+ /*************************************************************
+ In the WithdrawMoney state, the state machine checks if there is enough money in the account.
+ If the balance is greater than 10, then it issues a random withdraw request to the bank by sending the eWithDrawReq event, otherwise, it jumps to the NoMoneyToWithDraw state.
+ After sending a withdraw request, the machine waits for the eWithDrawResp event.
+ On receiving the eWithDrawResp event, the machine executes the corresponding event handler that confirms if the bank response is as expected and if there is still money in the account
+ and then jumps back to the WithdrawMoney state. Note that each time we (re-)enter a state (through a transition or goto statement), its entry function is executed.
+ *************************************************************/
+ state WithdrawMoney {
+ entry WithdrawMoney_Entry;
+
+ on eWithDrawResp do AfterWithDrawResp;
+ }
+
+ fun AfterWithDrawResp(resp: tWithDrawResp) {
+ // bank always ensures that a client has atleast 10 dollars in the account
+ assert resp.balance >= 10, "Bank balance must be greater than 10!!";
+ if(resp.status == WITHDRAW_SUCCESS) // withdraw succeeded
+ {
+ print format ("Withdrawal with rId = {0} succeeded, new account balance = {1}", resp.rId, resp.balance);
+ currentBalance = resp.balance;
+ }
+ else // withdraw failed
+ {
+ // if withdraw failed then the account balance must remain the same
+ assert currentBalance == resp.balance,
+ format ("Withdraw failed BUT the account balance changed! client thinks: {0}, bank balance: {1}", currentBalance, resp.balance);
+ print format ("Withdrawal with rId = {0} failed, account balance = {1}", resp.rId, resp.balance);
+ }
+
+ if(currentBalance > 10)
+ {
+ print format ("Still have account balance = {0}, lets try and withdraw more", currentBalance);
+ goto WithdrawMoney;
+ }
+ }
+
+ fun Init_Entry(input : (serv : BankServer, accountId: int, balance : int)) {
+ server = input.serv;
+ currentBalance = input.balance;
+ accountId = input.accountId;
+ // hacky: we would like request id's to be unique across all requests from clients
+ nextReqId = accountId*100 + 1; // each client has a unique account id
+ goto WithdrawMoney;
+ }
+
+ fun WithdrawMoney_Entry() {
+ // If current balance is <= 10 then we need more deposits before any more withdrawal
+ if(currentBalance <= 10)
+ goto NoMoneyToWithDraw;
+
+ // send withdraw request to the bank for a random amount between (1 to current balance + 1)
+ send server, eWithDrawReq, (source = this, accountId = accountId, amount = WithdrawAmount(), rId = nextReqId);
+ nextReqId = nextReqId + 1;
+ }
+
+ // function that returns a random integer between (1 to current balance + 1)
+ fun WithdrawAmount() : int {
+ return choose(currentBalance) + 1;
+ }
+
+ state NoMoneyToWithDraw {
+ entry NoMoneyToWithDrawEntry;
+ }
+
+ fun NoMoneyToWithDrawEntry() {
+ // if I am here then the amount of money in my account should be exactly 10
+ assert currentBalance == 10, "Hmm, I still have money that I can withdraw but I have reached NoMoneyToWithDraw state!";
+ print format ("No Money to withdraw, waiting for more deposits!");
+ }
+}
+
+
+Here is the file Server.p:
+
+// payload type associated with eUpdateQuery
+type tUpdateQuery = (accountId: int, balance: int);
+
+// payload type associated with eReadQuery
+type tReadQuery = (accountId: int);
+
+// payload type associated with eReadQueryResp
+type tReadQueryResp = (accountId: int, balance: int);
+
+/** Events used to communicate between the bank server and the backend database **/
+// event: send update the database, i.e. the `balance` associated with the `accountId`
+event eUpdateQuery: tUpdateQuery;
+// event: send a read request for the `accountId`.
+event eReadQuery: tReadQuery;
+// event: send a response (`balance`) corresponding to the read request for an `accountId`
+event eReadQueryResp: tReadQueryResp;
+
+/*************************************************************
+The BankServer machine uses a database machine as a service to store the bank balance for all its clients.
+On receiving an eWithDrawReq (withdraw requests) from a client, it reads the current balance for the account,
+if there is enough money in the account then it updates the new balance in the database after withdrawal
+and sends a response back to the client.
+*************************************************************/
+machine BankServer
+{
+ var database: Database;
+
+ start state Init {
+ entry Init_Entry;
+ }
+
+ state WaitForWithdrawRequests {
+ on eWithDrawReq do AfterWithDrawReq;
+ }
+
+ fun Init_Entry(initialBalance: map[int, int]) {
+ database = new Database((server = this, initialBalance = initialBalance));
+ goto WaitForWithdrawRequests;
+ }
+
+ fun AfterWithDrawReq(wReq: tWithDrawReq) {
+ var currentBalance: int;
+ var response: tWithDrawResp;
+
+ // read the current account balance from the database
+ currentBalance = ReadBankBalance(database, wReq.accountId);
+ // if there is enough money in account after withdrawal
+ if(currentBalance - wReq.amount >= 10)
+ {
+ UpdateBankBalance(database, wReq.accountId, currentBalance - wReq.amount);
+ response = (status = WITHDRAW_SUCCESS, accountId = wReq.accountId, balance = currentBalance - wReq.amount, rId = wReq.rId);
+ }
+ else // not enough money after withdraw
+ {
+ response = (status = WITHDRAW_ERROR, accountId = wReq.accountId, balance = currentBalance, rId = wReq.rId);
+ }
+
+ // send response to the client
+ send wReq.source, eWithDrawResp, response;
+ }
+
+}
+
+/***************************************************************
+The Database machine acts as a helper service for the Bank server and stores the bank balance for
+each account. There are two API's or functions to interact with the Database:
+ReadBankBalance and UpdateBankBalance.
+****************************************************************/
+machine Database
+{
+ var server: BankServer;
+ var balance: map[int, int];
+ start state Init {
+ entry Init_Entry;
+
+ on eUpdateQuery do AfterUpdateQuery;
+
+ on eReadQuery do AfterReadQuery;
+ }
+
+ fun Init_Entry(input: (server : BankServer, initialBalance: map[int, int])) {
+ server = input.server;
+ balance = input.initialBalance;
+ }
+
+ fun AfterUpdateQuery(query: (accountId: int, balance: int)) {
+ assert query.accountId in balance, "Invalid accountId received in the update query!";
+ balance[query.accountId] = query.balance;
+ }
+
+ fun AfterReadQuery(query: (accountId: int)) {
+ assert query.accountId in balance, "Invalid accountId received in the read query!";
+ send server, eReadQueryResp, (accountId = query.accountId, balance = balance[query.accountId]);
+ }
+
+}
+
+/***************************************************************
+Global functions
+****************************************************************/
+// Function to read the bank balance corresponding to the accountId
+fun ReadBankBalance(database: Database, accountId: int) : int {
+ var currentBalance: int;
+ send database, eReadQuery, (accountId = accountId,);
+ receive {
+ case eReadQueryResp: (resp: (accountId: int, balance: int)) {
+ currentBalance = resp.balance;
+ }
+ }
+ return currentBalance;
+}
+
+// Function to update the account balance for the account Id
+fun UpdateBankBalance(database: Database, accId: int, bal: int)
+{
+ send database, eUpdateQuery, (accountId = accId, balance = bal);
+}
+
+
+Here is the file AbstractBankServer.p:
+
+/*********************************************************
+The AbstractBankServer provides an abstract implementation of the BankServer where it abstract away
+the interaction between the BankServer and Database.
+The AbstractBankServer machine is used to demonstrate how one can replace a complex component in P
+with its abstraction that hides a lot of its internal complexity.
+In this case, instead of storing the balance in a separate database the abstraction store the information
+locally and abstracts away the complexity of bank server interaction with the database.
+For the client, it still exposes the same interface/behavior. Hence, when checking the correctness
+of the client it doesnt matter whether we use BankServer or the AbstractBankServer
+**********************************************************/
+
+machine AbstractBankServer
+{
+ // account balance: map from account-id to balance
+ var balance: map[int, int];
+ start state WaitForWithdrawRequests {
+ entry WaitForWithdrawRequests_Entry;
+
+ on eWithDrawReq do AfterWithDrawReq;
+ }
+
+ fun WaitForWithdrawRequests_Entry(init_balance: map[int, int]) {
+ balance = init_balance;
+ }
+
+ fun AfterWithDrawReq(wReq: tWithDrawReq) {
+ assert wReq.accountId in balance, "Invalid accountId received in the withdraw request!";
+ if(balance[wReq.accountId] - wReq.amount >= 10)
+ {
+ balance[wReq.accountId] = balance[wReq.accountId] - wReq.amount;
+ send wReq.source, eWithDrawResp,
+ (status = WITHDRAW_SUCCESS, accountId = wReq.accountId, balance = balance[wReq.accountId], rId = wReq.rId);
+ }
+ else
+ {
+ send wReq.source, eWithDrawResp,
+ (status = WITHDRAW_ERROR, accountId = wReq.accountId, balance = balance[wReq.accountId], rId = wReq.rId);
+ }
+ }
+
+}
+
+
+Here is the file ClientServerModules.p:
+
+// Client module
+module Client = { Client };
+
+// Bank module
+module Bank = { BankServer, Database };
+
+// Abstract Bank Server module
+module AbstractBank = { AbstractBankServer -> BankServer };
+
+
+
+Here is the folder PSpec:
+
+Here is the file BankBalanceCorrect.p:
+
+/*****************************************************
+This file defines two P specifications
+
+BankBalanceIsAlwaysCorrect (safety property):
+BankBalanceIsAlwaysCorrect checks the global invariant that the account-balance communicated
+to the client by the bank is always correct and the bank never removes more money from the account
+than that withdrawn by the client! Also, if the bank denies a withdraw request then it is only because
+the withdrawal would reduce the account balance to below 10.
+
+GuaranteedWithDrawProgress (liveness property):
+GuaranteedWithDrawProgress checks the liveness (or progress) property that all withdraw requests
+submitted by the client are eventually responded.
+
+Note: stating that "BankBalanceIsAlwaysCorrect checks that if the bank denies a withdraw request
+then the request would reduce the balance to below 10 (< 10)" is equivalent to state that "if there is enough money in the account - at least 10 (>= 10), then the request must not error".
+Hence, the two properties BankBalanceIsAlwaysCorrect and GuaranteedWithDrawProgress together ensure that every withdraw request if allowed will eventually succeed and the bank cannot block correct withdrawal requests.
+*****************************************************/
+
+// event: initialize the monitor with the initial account balances for all clients when the system starts
+event eSpec_BankBalanceIsAlwaysCorrect_Init: map[int, int];
+
+/****************************************************
+BankBalanceIsAlwaysCorrect checks the global invariant that the account balance communicated
+to the client by the bank is always correct and there is no error on the banks side with the
+implementation of the withdraw logic.
+
+For checking this property the spec machine observes the withdraw request (eWithDrawReq) and response (eWithDrawResp).
+- On receiving the eWithDrawReq, it adds the request in the pending-withdraws-map so that on receiving a
+response for this withdraw we can assert that the amount of money deducted from the account is same as
+what was requested by the client.
+
+- On receiving the eWithDrawResp, we look up the corresponding withdraw request and check that: the
+new account balance is correct and if the withdraw failed it is because the withdraw will make the account
+balance go below 10 dollars which is against the bank policies!
+****************************************************/
+spec BankBalanceIsAlwaysCorrect observes eWithDrawReq, eWithDrawResp, eSpec_BankBalanceIsAlwaysCorrect_Init {
+ // keep track of the bank balance for each client: map from accountId to bank balance.
+ var bankBalance: map[int, int];
+ // keep track of the pending withdraw requests that have not been responded yet.
+ // map from reqId -> withdraw request
+ var pendingWithDraws: map[int, tWithDrawReq];
+
+ start state Init {
+ on eSpec_BankBalanceIsAlwaysCorrect_Init goto WaitForWithDrawReqAndResp with Spec_BankBalanceIsAlwaysCorrect_Init;
+ }
+
+ state WaitForWithDrawReqAndResp {
+ on eWithDrawReq do AftereWithDrawReq;
+
+ on eWithDrawResp do AfterWithDrawReq;
+ }
+
+ fun Spec_BankBalanceIsAlwaysCorrect_Init(balance: map[int, int]) {
+ bankBalance = balance;
+ }
+
+ fun AfterWithDrawReq(req: tWithDrawReq) {
+ assert req.accountId in bankBalance,
+ format ("Unknown accountId {0} in the withdraw request. Valid accountIds = {1}", req.accountId, keys(bankBalance));
+ pendingWithDraws[req.rId] = req;
+
+ }
+
+ fun AfterWithDrawResp(resp: tWithDrawResp) {
+ assert resp.accountId in bankBalance,
+ format ("Unknown accountId {0} in the withdraw response!", resp.accountId);
+ assert resp.rId in pendingWithDraws,
+ format ("Unknown rId {0} in the withdraw response!", resp.rId);
+ assert resp.balance >= 10,
+ "Bank balance in all accounts must always be greater than or equal to 10!!";
+
+ if(resp.status == WITHDRAW_SUCCESS)
+ {
+ assert resp.balance == bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount,
+ format ("Bank balance for the account {0} is {1} and not the expected value {2}, Bank is lying!",
+ resp.accountId, resp.balance, bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount);
+ // update the new account balance
+ bankBalance[resp.accountId] = resp.balance;
+ }
+ else
+ {
+ // bank can only reject a request if it will drop the balance below 10
+ assert bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount < 10,
+ format ("Bank must accept the withdraw request for {0}, bank balance is {1}!",
+ pendingWithDraws[resp.rId].amount, bankBalance[resp.accountId]);
+ // if withdraw failed then the account balance must remain the same
+ assert bankBalance[resp.accountId] == resp.balance,
+ format ("Withdraw failed BUT the account balance changed! actual: {0}, bank said: {1}",
+ bankBalance[resp.accountId], resp.balance);
+ }
+
+ }
+
+}
+
+/**************************************************************************
+GuaranteedWithDrawProgress checks the liveness (or progress) property that all withdraw requests
+submitted by the client are eventually responded.
+***************************************************************************/
+spec GuaranteedWithDrawProgress observes eWithDrawReq, eWithDrawResp {
+ // keep track of the pending withdraw requests
+ var pendingWDReqs: set[int];
+
+ start state NopendingRequests {
+ on eWithDrawReq goto PendingReqs with AfterWithDrawReq;
+ }
+
+ hot state PendingReqs {
+ on eWithDrawResp do AfterWithDrawResp;
+
+ on eWithDrawReq goto PendingReqs with AfterWithDrawRespPendingReqs;
+ }
+
+ fun AfterWithDrawReq(req: tWithDrawReq) {
+ pendingWDReqs += (req.rId);
+ }
+
+ fun AfterWithDrawResp(resp: tWithDrawResp) {
+ assert resp.rId in pendingWDReqs, format ("unexpected rId: {0} received, expected one of {1}", resp.rId, pendingWDReqs);
+ pendingWDReqs -= (resp.rId);
+ if(sizeof(pendingWDReqs) == 0) // all requests have been responded
+ goto NopendingRequests;
+ }
+
+ fun AfterWithDrawRespPendingReqs(req: tWithDrawReq) {
+ pendingWDReqs += (req.rId);
+ }
+
+}
+
+
+
+Here is the folder PTst:
+
+Here is the file TestDriver.p:
+
+/*************************************************************
+Machines TestWithSingleClient and TestWithMultipleClients are simple test driver machines
+that configure the system to be checked by the P checker for different scenarios.
+In this case, test the ClientServer system by first randomly initializing the accounts map and
+then checking it with either one Client or with multiple Clients (between 2 and 4).
+*************************************************************/
+
+// Test driver that checks the system with a single Client.
+machine TestWithSingleClient
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // singe client
+ SetupClientServerSystem(1);
+ }
+}
+
+// Test driver that checks the system with multiple Clients.
+machine TestWithMultipleClients
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // multiple clients between (2, 4)
+ SetupClientServerSystem(choose(3) + 2);
+ }
+}
+
+// creates a random map from accountId's to account balance of size `numAccounts`
+fun CreateRandomInitialAccounts(numAccounts: int) : map[int, int]
+{
+ var i: int;
+ var bankBalance: map[int, int];
+ while(i < numAccounts) {
+ bankBalance[i] = choose(100) + 10; // min 10 in the account
+ i = i + 1;
+ }
+ return bankBalance;
+}
+
+/*************************************************************
+Function SetupClientServerSystem takes as input the number of clients to be created and
+configures the ClientServer system by creating the Client and BankServer machines.
+The function also announce the event eSpec_BankBalanceIsAlwaysCorrect_Init to initialize the monitors with initial balance for all accounts.
+*************************************************************/
+// setup the client server system with one bank server and `numClients` clients.
+fun SetupClientServerSystem(numClients: int)
+{
+ var i: int;
+ var server: BankServer;
+ var accountIds: seq[int];
+ var initAccBalance: map[int, int];
+
+ // randomly initialize the account balance for all clients
+ initAccBalance = CreateRandomInitialAccounts(numClients);
+ // create bank server with the init account balance
+ server = new BankServer(initAccBalance);
+
+ // before client starts sending any messages make sure we
+ // initialize the monitors or specifications
+ announce eSpec_BankBalanceIsAlwaysCorrect_Init, initAccBalance;
+
+ accountIds = keys(initAccBalance);
+
+ // create the clients
+ while(i < sizeof(accountIds)) {
+ new Client((serv = server, accountId = accountIds[i], balance = initAccBalance[accountIds[i]]));
+ i = i + 1;
+ }
+}
+
+
+Here is the file Testscript.p:
+
+/* This file contains three different model checking scenarios */
+
+// assert the properties for the single client and single server scenario
+test tcSingleClient [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithSingleClient });
+
+// assert the properties for the two clients and single server scenario
+test tcMultipleClients [main=TestWithMultipleClients]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithMultipleClients });
+
+// assert the properties for the single client and single server scenario but with abstract server
+ test tcAbstractServer [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, AbstractBank, { TestWithSingleClient });
+
+
+
+
+
+
+
+
+Here is another example of a P program:
+
+
+System:
+Consider an espresso coffee machine as a reactive system that must respond correctly to various user inputs. The user interacts with the coffee machine through its control panel.
+The espresso machine consists of two parts: the front-end control panel and the backend coffee maker that actually makes the coffee.
+The control panel presents an interface to the user to perform operations like reset the machine, turn the steamer on and off, request an espresso, and clear the grounds by opening the container.
+The control panel interprets these inputs from the user and sends appropriate commands to the coffee maker.
+
+Correctness Specification:
+By default, the P checker tests whether any event that is received in a state has a handler defined for it, otherwise, it would result in an unhandled event exception.
+If the P checker fails to find a bug, then it implies that the system model can handle any sequence of events generated by the given environment.
+In our coffee machine context, it implies that the coffee machine control panel can appropriately handle any sequence of inputs (button presses) by the user.
+We would also like to check that the coffee machine moves through a desired sequence of states, i.e., WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready.
+
+
+
+The EspressoMachine folder contains the source code for the EspressoMachine project.
+
+The P models (PSrc) for the EspressoMachine example consist of three files:
+1. CoffeeMakerControlPanel.p: Implements the CoffeeMakerControlPanel state machine.
+Basically, the control panel starts in the initial state and kicks off by warming up the coffee maker. After warming is successful, it moves to the ready state where it can either make coffee or start the steamer.
+When asked to make coffee, it first grinds the beans and then brews coffee. In any of these states, if there is an error due to. e.g, no water or no beans, the control panel informs the user of the error and moves to the error state waiting for the user to reset the machine.
+2. CoffeeMaker.p: Implements the CoffeeMaker state machine.
+3. EspressoMachineModules.p: Declares the P module corresponding to EspressoMachine.
+
+
+
+The P Specification (PSpec) for the EspressoMachine project is implemented in Safety.p file.
+
+We define a safety specification, EspressoMachineModesOfOperation that observes the internal state of the EspressoMachine through the events that are announced as the system moves through different states and asserts that it always moves through the desired sequence of states.
+Steady operation: WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready.
+If an error occurs in any of the states above then the EspressoMachine stays in the error state until it is reset and after which it returns to the Warmup state.
+
+
+
+The test scenarios folder for EspressoMachine (PTst) consists of three files: TestDriver.p, TestScript.p, and Users.p.
+The Users.p declares two machines:
+1. SaneUser machine that uses the EspressoMachine with care, pressing the buttons in the right order, and cleaning up the grounds after the coffee is made, and
+2. CrazyUser machine who has never used an espresso machine before, gets too excited, and starts pushing random buttons on the control panel.
+
+
+
+
+Here is the folder PSrc:
+
+Here is the file CoffeeMakerControlPanel.p:
+
+/* Events used by the user to interact with the control panel of the Coffee Machine */
+// event: make espresso button pressed
+event eEspressoButtonPressed;
+// event: steamer button turned off
+event eSteamerButtonOff;
+// event: steamer button turned on
+event eSteamerButtonOn;
+// event: door opened to empty grounds
+event eOpenGroundsDoor;
+// event: door closed after emptying grounds
+event eCloseGroundsDoor;
+// event: reset coffee maker button pressed
+event eResetCoffeeMaker;
+//event: error message from panel to the user
+event eCoffeeMakerError: tCoffeeMakerState;
+//event: coffee machine is ready
+event eCoffeeMakerReady;
+// event: coffee machine user
+event eCoffeeMachineUser: machine;
+
+// enum to represent the state of the coffee maker
+enum tCoffeeMakerState {
+ NotWarmedUp,
+ Ready,
+ NoBeansError,
+ NoWaterError
+}
+
+/*
+CoffeeMakerControlPanel acts as the interface between the CoffeeMaker and User.
+It converts the inputs from the user to appropriate inputs to the CoffeeMaker and sends responses to the user.
+It transitions from one state to another based on the events received from the User and the CoffeeMaker machine.
+In all the states, it appropriately handles different events that can be received, including ignoring or deferring them if they are stale events.
+*/
+machine CoffeeMakerControlPanel
+{
+ var coffeeMaker: EspressoCoffeeMaker;
+ var coffeeMakerState: tCoffeeMakerState;
+ var currentUser: machine;
+
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ coffeeMakerState = NotWarmedUp;
+ coffeeMaker = new EspressoCoffeeMaker(this);
+ WaitForUser();
+ goto WarmUpCoffeeMaker;
+ }
+
+ // block until a user shows up
+ fun WaitForUser() {
+ receive {
+ case eCoffeeMachineUser: (user: machine) {
+ currentUser = user;
+ }
+ }
+ }
+
+ state WarmUpCoffeeMaker {
+ entry WarmUpCoffeeMaker_Entry;
+
+ on eWarmUpCompleted goto CoffeeMakerReady;
+
+ // grounds door is opened or closed will handle it later after the coffee maker has warmed up
+ defer eOpenGroundsDoor, eCloseGroundsDoor;
+ // ignore these inputs from users until the maker has warmed up.
+ ignore eEspressoButtonPressed, eSteamerButtonOff, eSteamerButtonOn, eResetCoffeeMaker;
+ // ignore these errors and responses as they could be from previous state
+ ignore eNoBeansError, eNoWaterError, eGrindBeansCompleted;
+ }
+
+ fun WarmUpCoffeeMaker_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInWarmUpState;
+
+ BeginHeatingCoffeeMaker();
+ }
+
+ state CoffeeMakerReady {
+ entry CoffeeMakerReady_Entry;
+
+ on eOpenGroundsDoor goto CoffeeMakerDoorOpened;
+ on eEspressoButtonPressed goto CoffeeMakerRunGrind;
+ on eSteamerButtonOn goto CoffeeMakerRunSteam;
+
+ // ignore these out of order commands, these must have happened because of an error
+ // from user or sensor
+ ignore eSteamerButtonOff, eCloseGroundsDoor;
+
+ // ignore commands and errors as they are from previous state
+ ignore eWarmUpCompleted, eResetCoffeeMaker, eNoBeansError, eNoWaterError;
+ }
+
+ fun CoffeeMakerReady_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInReadyState;
+
+ coffeeMakerState = Ready;
+ send currentUser, eCoffeeMakerReady;
+ }
+
+ state CoffeeMakerRunGrind {
+ entry CoffeeMakerRunGrind_Entry;
+
+ on eNoBeansError goto EncounteredError with AfterNoBeansError;
+
+ on eNoWaterError goto EncounteredError with AfterNoWaterError;
+
+ on eGrindBeansCompleted goto CoffeeMakerRunEspresso;
+
+ defer eOpenGroundsDoor, eCloseGroundsDoor, eEspressoButtonPressed;
+
+ // Can't make steam while we are making espresso
+ ignore eSteamerButtonOn, eSteamerButtonOff;
+
+ // ignore commands that are old or cannot be handled right now
+ ignore eWarmUpCompleted, eResetCoffeeMaker;
+ }
+
+ fun CoffeeMakerRunGrind_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInBeansGrindingState;
+
+ GrindBeans();
+ }
+
+ fun AfterNoBeansError() {
+ coffeeMakerState = NoBeansError;
+ print "No beans to grind! Please refill beans and reset the machine!";
+ }
+
+ fun AfterNoWaterError() {
+ coffeeMakerState = NoWaterError;
+ print "No Water! Please refill water and reset the machine!";
+ }
+
+ state CoffeeMakerRunEspresso {
+ entry CoffeeMakerRunEspresso_Entry;
+
+ on eEspressoCompleted goto CoffeeMakerReady with AfterEspressoCompleted;
+
+ on eNoWaterError goto EncounteredError with AfterNoWaterError;
+
+ // the user commands will be handled next after finishing this espresso
+ defer eOpenGroundsDoor, eCloseGroundsDoor, eEspressoButtonPressed;
+
+ // Can't make steam while we are making espresso
+ ignore eSteamerButtonOn, eSteamerButtonOff;
+
+ // ignore old commands and cannot reset when making coffee
+ ignore eWarmUpCompleted, eResetCoffeeMaker;
+ }
+
+ fun CoffeeMakerRunEspresso_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInCoffeeBrewingState;
+
+ StartEspresso();
+ }
+
+ fun AfterEspressoCompleted() {
+ send currentUser, eEspressoCompleted;
+ }
+
+ state CoffeeMakerRunSteam {
+ entry StartSteamer;
+
+ on eSteamerButtonOff goto CoffeeMakerReady with StopSteamer;
+
+ on eNoWaterError goto EncounteredError with AfterNoWaterError;
+
+ // user might have cleaned grounds while steaming
+ defer eOpenGroundsDoor, eCloseGroundsDoor;
+
+ // can't make espresso while we are making steam
+ ignore eEspressoButtonPressed, eSteamerButtonOn;
+ }
+
+ state CoffeeMakerDoorOpened {
+ on eCloseGroundsDoor do AfterCloseGroundsDoor;
+
+ // grounds door is open cannot handle these requests just ignore them
+ ignore eEspressoButtonPressed, eSteamerButtonOn, eSteamerButtonOff;
+ }
+
+ fun AfterCloseGroundsDoor() {
+ assert coffeeMakerState != NotWarmedUp;
+ assert coffeeMakerState == Ready;
+ goto CoffeeMakerReady;
+ }
+
+ state EncounteredError {
+ entry EncounteredError_Entry;
+
+ on eResetCoffeeMaker goto WarmUpCoffeeMaker with AfterResetCoffeeMaker;
+
+ // error, ignore these requests until reset.
+ ignore eEspressoButtonPressed, eSteamerButtonOn, eSteamerButtonOff,
+ eOpenGroundsDoor, eCloseGroundsDoor, eWarmUpCompleted, eEspressoCompleted, eGrindBeansCompleted;
+
+ // ignore other simultaneous errors
+ ignore eNoBeansError, eNoWaterError;
+ }
+
+ fun EncounteredError_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eErrorHappened;
+
+ // send the error message to the client
+ send currentUser, eCoffeeMakerError, coffeeMakerState;
+ }
+
+ fun AfterResetCoffeeMaker() {
+ // inform the specification about current state of the coffee maker
+ announce eResetPerformed;
+ }
+
+ fun BeginHeatingCoffeeMaker() {
+ // send an event to maker to start warming
+ send coffeeMaker, eWarmUpReq;
+ }
+
+ fun StartSteamer() {
+ // send an event to maker to start steaming
+ send coffeeMaker, eStartSteamerReq;
+ }
+
+ fun StopSteamer() {
+ // send an event to maker to stop steaming
+ send coffeeMaker, eStopSteamerReq;
+ }
+
+ fun GrindBeans() {
+ // send an event to maker to grind beans
+ send coffeeMaker, eGrindBeansReq;
+ }
+
+ fun StartEspresso() {
+ // send an event to maker to start espresso
+ send coffeeMaker, eStartEspressoReq;
+ }
+}
+
+
+Here is the file CoffeeMaker.p:
+
+/* Requests or operations from the controller to coffee maker */
+
+// event: warmup request when the coffee maker starts or resets
+event eWarmUpReq;
+// event: grind beans request before making coffee
+event eGrindBeansReq;
+// event: start brewing coffee
+event eStartEspressoReq;
+// event start steamer
+event eStartSteamerReq;
+// event: stop steamer
+event eStopSteamerReq;
+
+/* Responses from the coffee maker to the controller */
+// event: completed grinding beans
+event eGrindBeansCompleted;
+// event: completed brewing and pouring coffee
+event eEspressoCompleted;
+// event: warmed up the machine and ready to make coffee
+event eWarmUpCompleted;
+
+/* Error messages from the coffee maker to control panel or controller*/
+// event: no water for coffee, refill water!
+event eNoWaterError;
+// event: no beans for coffee, refill beans!
+event eNoBeansError;
+// event: the heater to warm the machine is broken!
+event eWarmerError;
+
+/*****************************************************
+EspressoCoffeeMaker receives requests from the control panel of the coffee machine and
+based on its state e.g., whether heater is working, or it has beans and water, the maker responds
+back to the controller if the operation succeeded or errored.
+*****************************************************/
+machine EspressoCoffeeMaker
+{
+ // control panel of the coffee machine that sends inputs to the coffee maker
+ var controller: CoffeeMakerControlPanel;
+
+ start state WaitForRequests {
+ entry WaitForRequests_Entry;
+
+ on eWarmUpReq do AftereWithDrawReq;
+
+ on eGrindBeansReq do AfterGrindBeansReq;
+
+ on eStartEspressoReq do AfterStartEspressoReq;
+
+ on eStartSteamerReq do AfterStartSteamerReq;
+
+ on eStopSteamerReq do AfterStopSteamerReq;
+ }
+
+ fun WaitForRequests_Entry(_controller: CoffeeMakerControlPanel) {
+ controller = _controller;
+ }
+
+ fun AfterWarmUpReq() {
+ send controller, eWarmUpCompleted;
+ }
+
+ fun AfterGrindBeansReq() {
+ if (!HasBeans()) {
+ send controller, eNoBeansError;
+ } else {
+ send controller, eGrindBeansCompleted;
+ }
+ }
+
+ fun AfterStartEspressoReq() {
+ if (!HasWater()) {
+ send controller, eNoWaterError;
+ } else {
+ send controller, eEspressoCompleted;
+ }
+ }
+
+ fun AfterStartSteamerReq() {
+ if (!HasWater()) {
+ send controller, eNoWaterError;
+ }
+ }
+
+ fun AfterStopSteamerReq() {
+ /* do nothing, steamer stopped */
+ }
+
+ // nondeterministic functions to trigger different behaviors
+ fun HasBeans() : bool { return $; }
+ fun HasWater() : bool { return $; }
+}
+
+
+Here is the file EspressoMachineModules.p:
+
+module EspressoMachine = { CoffeeMakerControlPanel, EspressoCoffeeMaker };
+
+
+
+Here is the folder PSpec:
+
+Here is the file Safety.p:
+
+/* Events used to inform monitor about the internal state of the CoffeeMaker */
+event eInWarmUpState;
+event eInReadyState;
+event eInBeansGrindingState;
+event eInCoffeeBrewingState;
+event eErrorHappened;
+event eResetPerformed;
+
+/*********************************************
+We would like to ensure that the coffee maker moves through the expected modes of operation.
+We want to make sure that the coffee maker always transitions through the following sequence of states:
+Steady operation:
+ WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready
+With Error:
+If an error occurs in any of the states above, then the Coffee machine stays in the error state until
+it is reset and after which it returns to the Warmup state.
+
+The EspressoMachineModesOfOperation spec machine observes events and ensures that the system moves through the states defined by the monitor.
+Note that if the system allows (has execution as) a sequence of events that are not accepted by the monitor (i.e., the monitor throws an unhandled event exception), then the system does not satisfy the desired specification.
+Hence, this monitor can be thought of accepting only those behaviors of the system that follow the sequence of states modelled by the spec machine.
+For example, if the system moves from Ready to CoffeeMaking state directly without Grinding, then the monitor will raise an ALARM!
+**********************************************/
+spec EspressoMachineModesOfOperation
+observes eInWarmUpState, eInReadyState, eInBeansGrindingState, eInCoffeeBrewingState, eErrorHappened, eResetPerformed
+{
+ start state StartUp {
+ on eInWarmUpState goto WarmUp;
+ }
+
+ state WarmUp {
+ on eErrorHappened goto Error;
+ on eInReadyState goto Ready;
+ }
+
+ state Ready {
+ ignore eInReadyState;
+ on eInBeansGrindingState goto BeanGrinding;
+ on eErrorHappened goto Error;
+ }
+
+ state BeanGrinding {
+ on eInCoffeeBrewingState goto MakingCoffee;
+ on eErrorHappened goto Error;
+ }
+
+ state MakingCoffee {
+ on eInReadyState goto Ready;
+ on eErrorHappened goto Error;
+ }
+
+ state Error {
+ on eResetPerformed goto StartUp;
+ ignore eErrorHappened;
+ }
+}
+
+
+
+Here is the folder PTst:
+
+Here is the file TestDriver.p:
+
+machine TestWithSaneUser
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // create a sane user
+ new SaneUser(new CoffeeMakerControlPanel());
+ }
+}
+
+machine TestWithCrazyUser
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // create a crazy user
+ new CrazyUser((coffeeMaker = new CoffeeMakerControlPanel(), nOps = 5));
+ }
+}
+
+
+Here is the file Testscript.p:
+
+// there are two test cases defined in the EspressoMachine project.
+test tcSaneUserUsingCoffeeMachine [main=TestWithSaneUser]:
+ assert EspressoMachineModesOfOperation in (union { TestWithSaneUser }, EspressoMachine, Users);
+
+test tcCrazyUserUsingCoffeeMachine [main=TestWithCrazyUser]:
+ assert EspressoMachineModesOfOperation in (union { TestWithCrazyUser }, EspressoMachine, Users);
+
+
+Here is the file Users.p:
+
+/*
+A SaneUser who knows how to use the CoffeeMaker
+*/
+machine SaneUser {
+ var coffeeMakerPanel: CoffeeMakerControlPanel;
+ var cups: int;
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry(coffeeMaker: CoffeeMakerControlPanel) {
+ coffeeMakerPanel = coffeeMaker;
+ // inform control panel that I am the user
+ send coffeeMakerPanel, eCoffeeMachineUser, this;
+ // want to make 2 cups of espresso
+ cups = 2;
+
+ goto LetsMakeCoffee;
+ }
+
+ state LetsMakeCoffee {
+ entry LetsMakeCoffee_Entry;
+ }
+
+ fun LetsMakeCoffee_Entry() {
+ while (cups > 0)
+ {
+ // lets wait for coffee maker to be ready
+ WaitForCoffeeMakerToBeReady();
+
+ // press Espresso button
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressEspressoButton);
+
+ // check the status of the machine
+ receive {
+ case eEspressoCompleted: {
+ // lets make the next coffee
+ cups = cups - 1;
+ }
+ case eCoffeeMakerError: (status: tCoffeeMakerState){
+
+ // lets fill the beans or water and reset the machine
+ // and go back to making espresso
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressResetButton);
+ }
+ }
+ }
+
+ // I am a good user so I will clear the coffee grounds before leaving.
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_ClearGrounds);
+
+ // am done, let me exit
+ raise halt;
+ }
+}
+
+enum tCoffeeMakerOperations {
+ CM_PressEspressoButton,
+ CM_PressSteamerButton,
+ CM_PressResetButton,
+ CM_ClearGrounds
+}
+
+/*
+A crazy user who gets excited by looking at a coffee machine and starts stress testing the machine
+by pressing all sorts of random button and opening/closing doors
+*/
+// TODO: We do not support global constants currently, they can be encoded using global functions.
+
+machine CrazyUser {
+ var coffeeMakerPanel: CoffeeMakerControlPanel;
+ var numOperations: int;
+ start state StartPressingButtons {
+ entry StartPressingButtons_Entry;
+
+ // I will ignore all the responses from the coffee maker
+ ignore eCoffeeMakerError, eEspressoCompleted, eCoffeeMakerReady;
+ }
+
+ fun StartPressingButtons_Entry(config: (coffeeMaker: CoffeeMakerControlPanel, nOps: int)) {
+ var pickedOps: tCoffeeMakerOperations;
+
+ numOperations = config.nOps;
+ coffeeMakerPanel = config.coffeeMaker;
+
+ // inform control panel that I am the user
+ send coffeeMakerPanel, eCoffeeMachineUser, this;
+
+ while(numOperations > 0)
+ {
+ pickedOps = PickRandomOperationToPerform();
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, pickedOps);
+ numOperations = numOperations - 1;
+ }
+ }
+
+ // Pick a random enum value (hacky work around)
+ // Currently, the choose operation does not support choose over enum value
+ fun PickRandomOperationToPerform() : tCoffeeMakerOperations {
+ var op_i: int;
+ op_i = choose(3);
+ if(op_i == 0)
+ return CM_PressEspressoButton;
+ else if(op_i == 1)
+ return CM_PressSteamerButton;
+ else if(op_i == 2)
+ return CM_PressResetButton;
+ else
+ return CM_ClearGrounds;
+ }
+}
+
+/* Function to perform an operation on the CoffeeMaker */
+fun PerformOperationOnCoffeeMaker(coffeeMakerCP: CoffeeMakerControlPanel, CM_Ops: tCoffeeMakerOperations)
+{
+ if(CM_Ops == CM_PressEspressoButton) {
+ send coffeeMakerCP, eEspressoButtonPressed;
+ }
+ else if(CM_Ops == CM_PressSteamerButton) {
+ send coffeeMakerCP, eSteamerButtonOn;
+ // wait for some time and then release the button
+ send coffeeMakerCP, eSteamerButtonOff;
+ }
+ else if(CM_Ops == CM_ClearGrounds)
+ {
+ send coffeeMakerCP, eOpenGroundsDoor;
+ // empty ground and close the door
+ send coffeeMakerCP, eCloseGroundsDoor;
+ }
+ else if(CM_Ops == CM_PressResetButton)
+ {
+ send coffeeMakerCP, eResetCoffeeMaker;
+ }
+}
+
+fun WaitForCoffeeMakerToBeReady() {
+ receive {
+ case eCoffeeMakerReady: {}
+ case eCoffeeMakerError: (status: tCoffeeMakerState){ raise halt; }
+ }
+}
+module Users = { SaneUser, CrazyUser };
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/context_files/about_p.txt b/Src/PeasyAI/resources/context_files/about_p.txt
new file mode 100644
index 0000000000..c8d0e30484
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/about_p.txt
@@ -0,0 +1,9 @@
+Distributed systems are notoriously hard to get right (i.e., guaranteeing correctness) as the programmer needs to reason about numerous control paths resulting from the myriad interleaving of events (or messages or failures). Unsurprisingly, programmers can easily introduce subtle errors when designing these systems. Moreover, it is extremely difficult to test distributed systems, as most control paths remain untested, and serious bugs lie dormant for months or even years after deployment.
+
+The P programming framework takes several steps towards addressing these challenges by providing a unified framework for modeling, specifying, implementing, testing, and verifying complex distributed systems.
+
+P provides a high-level state machine based programming language to formally model and specify distributed systems. The syntactic sugar of state machines allows programmers to capture their system design (or protocol logic) as communicating state machines, which is how programmers generally think about their system's design.
+
+P supports specifying and checking both safety as well as liveness specifications (global invariants). Programmers can easily write different scenarios under which they would like to check that the system satisfies the desired correctness specification. The P module system enables programmers to model their system modularly and perform compositional testing to scale the analysis to large distributed systems.
+
+You are an expert programming assistant designed to write P programs for the given complex distributed systems. Avoid overreliance on general programming knowledge and strictly adhere to P's specific requirements. Thoroughly review the P language syntax guide before writing code. Refer the P language syntax guide for the correct syntax while writing.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/context_files/modular-fewshot/p-fewshot-formatted.txt b/Src/PeasyAI/resources/context_files/modular-fewshot/p-fewshot-formatted.txt
new file mode 100644
index 0000000000..39cff5d14f
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular-fewshot/p-fewshot-formatted.txt
@@ -0,0 +1,561 @@
+Here are P language syntax examples that demonstrate proper syntax usage when writing/correcting P programs:
+
+# User-Defined Types
+// Tuple types with named fields
+type tWithDrawReq = (source: Client, accountId: int, amount: int, rId: int);
+type tWithDrawResp = (status: tWithDrawRespStatus, accountId: int, balance: int, rId: int);
+
+
+// Input parameter type
+type DatabaseInput = (server: BankServer, initialBalance: map[int, int]);
+
+
+# Enumerations
+enum tWithDrawRespStatus {
+ WITHDRAW_SUCCESS,
+ WITHDRAW_ERROR
+}
+
+
+# Collection Types
+// Maps (key-value pairs)
+var balance: map[int, int]; // map from account ID to balance
+var pendingWithDraws: map[int, tWithDrawReq]; // map from request ID to request
+
+
+// Sets
+var pendingWDReqs: set[int]; // set of request IDs
+
+
+// Sequences
+var accountIds: seq[int]; // sequence of account IDs
+
+
+# Events
+## Event Declarations
+// Events with payload types
+event eWithDrawReq : tWithDrawReq; // withdraw request event
+event eWithDrawResp: tWithDrawResp; // withdraw response event
+
+
+// Events with inline tuple types
+event eUpdateQuery: (accountId: int, balance: int);
+event eReadQuery: (accountId: int);
+event eReadQueryResp: (accountId: int, balance: int);
+
+
+// Events for specifications
+event eSpec_BankBalanceIsAlwaysCorrect_Init: map[int, int];
+
+
+# State Machines
+## Basic Machine Structure
+machine MachineName
+{
+ // State variables
+ var variableName: type;
+
+ // States
+ start state StateName {
+ // State body
+ }
+
+ state AnotherState {
+ // State body
+ }
+}
+
+
+# Complete Machine Example
+machine Client
+{
+ // Machine variables
+ var server: BankServer;
+ var accountId: int;
+ var nextReqId: int;
+ var numOfWithdrawOps: int;
+ var currentBalance: int;
+
+
+ // Start state with entry action and parameters
+ start state Init {
+ entry (input: (serv: BankServer, accountId: int, balance: int))
+ {
+ server = input.serv;
+ currentBalance = input.balance;
+ accountId = input.accountId;
+ nextReqId = accountId * 100 + 1; // unique request IDs
+ goto WithdrawMoney;
+ }
+ }
+
+
+ // State with entry action and event handlers
+ state WithdrawMoney {
+ entry {
+ if(currentBalance <= 10)
+ goto NoMoneyToWithDraw;
+
+
+ send server, eWithDrawReq,
+ (source = this, accountId = accountId, amount = WithdrawAmount(), rId = nextReqId);
+ nextReqId = nextReqId + 1;
+ }
+
+
+ on eWithDrawResp do (resp: tWithDrawResp) {
+ assert resp.balance >= 10, "Bank balance must be greater than 10!!";
+
+ if(resp.status == WITHDRAW_SUCCESS) {
+ print format("Withdrawal succeeded, new balance = {0}", resp.balance);
+ currentBalance = resp.balance;
+ }
+ else {
+ assert currentBalance == resp.balance, "Balance should not change on failure";
+ print format("Withdrawal failed, balance = {0}", resp.balance);
+ }
+
+
+ if(currentBalance > 10) {
+ goto WithdrawMoney;
+ }
+ }
+ }
+
+
+ state NoMoneyToWithDraw {
+ entry {
+ assert currentBalance == 10, "Should have exactly 10 when no money to withdraw";
+ print format("No money to withdraw, waiting for deposits!");
+ }
+ }
+}
+
+
+# State Operations
+## State Transitions
+// Unconditional transition
+goto TargetState;
+
+
+// Conditional transitions
+if(condition)
+ goto StateA;
+else
+ goto StateB;
+
+
+## Entry Actions
+start state Init {
+ entry (parameters) {
+ // Initialize machine variables
+ // Perform setup actions
+ }
+}
+
+
+state SomeState {
+ entry {
+ // Actions performed when entering state
+ }
+}
+
+
+## Event Handlers
+state WaitingState {
+ // Handle specific event
+ on eWithDrawReq do (wReq: tWithDrawReq) {
+ // Process the event
+ // Access payload via parameter
+ }
+
+ // Handle multiple events
+ on eUpdateQuery do (query: (accountId: int, balance: int)) {
+ balance[query.accountId] = query.balance;
+ }
+
+ on eReadQuery do (query: (accountId: int)) {
+ send server, eReadQueryResp, (accountId = query.accountId, balance = balance[query.accountId]);
+ }
+}
+
+
+# Communication
+## Sending Events
+// Send event with payload
+send targetMachine, eventName, payloadData;
+
+
+// Examples
+send server, eWithDrawReq, (source = this, accountId = accountId, amount = 100, rId = 1);
+send wReq.source, eWithDrawResp, response;
+send database, eUpdateQuery, (accountId = accId, balance = bal);
+
+
+## Receiving Events (Synchronous)
+fun ReadBankBalance(database: Database, accountId: int): int {
+ var currentBalance: int;
+ send database, eReadQuery, (accountId = accountId,);
+ receive {
+ case eReadQueryResp: (resp: (accountId: int, balance: int)) {
+ currentBalance = resp.balance;
+ }
+ }
+ return currentBalance;
+}
+
+
+## Creating New Machines
+// Create machine with parameters
+database = new Database((server = this, initialBalance = initialBalance));
+
+
+// Create machine in function
+server = new BankServer(initAccBalance);
+new Client((serv = server, accountId = accountIds[i], balance = initAccBalance[accountIds[i]]));
+
+
+# Functions
+## Function Declaration and Calls
+// Function with return value
+fun WithdrawAmount(): int {
+ return choose(currentBalance) + 1;
+}
+
+
+// Function with parameters
+fun UpdateBankBalance(database: Database, accId: int, bal: int) {
+ send database, eUpdateQuery, (accountId = accId, balance = bal);
+}
+
+
+// Function returning complex type
+fun CreateRandomInitialAccounts(numAccounts: int): map[int, int] {
+ var i: int;
+ var bankBalance: map[int, int];
+ while(i < numAccounts) {
+ bankBalance[i] = choose(100) + 10;
+ i = i + 1;
+ }
+ return bankBalance;
+}
+
+
+# Control Flow
+## Conditionals
+// If-else statements
+if(balance[wReq.accountId] - wReq.amount > 10) {
+ balance[wReq.accountId] = balance[wReq.accountId] - wReq.amount;
+ send wReq.source, eWithDrawResp, successResponse;
+}
+else {
+ send wReq.source, eWithDrawResp, errorResponse;
+}
+
+
+// Complex conditions
+if(currentBalance - wReq.amount >= 10) {
+ UpdateBankBalance(database, wReq.accountId, currentBalance - wReq.amount);
+ response = (status = WITHDRAW_SUCCESS, accountId = wReq.accountId,
+ balance = currentBalance - wReq.amount, rId = wReq.rId);
+}
+
+
+# Loops
+// While loops
+while(i < numAccounts) {
+ bankBalance[i] = choose(100) + 10;
+ i = i + 1;
+}
+
+
+while(i < sizeof(accountIds)) {
+ new Client((serv = server, accountId = accountIds[i], balance = initAccBalance[accountIds[i]]));
+ i = i + 1;
+}
+
+
+# Built-in Operations
+## Assertions
+// Runtime assertions with messages
+assert wReq.accountId in balance, "Invalid accountId received in the withdraw request!";
+assert resp.balance >= 10, "Bank balance must be greater than 10!!";
+assert currentBalance == resp.balance, "Balance should not change on failure";
+
+
+// Formatted assertion messages
+assert resp.rId in pendingWithDraws,
+ format("Unknown rId {0} in the withdraw response!", resp.rId);
+
+
+## Non-deterministic Choice
+// Choose random value in range [0, n)
+return choose(currentBalance) + 1; // Random value [1, currentBalance+1]
+SetupClientServerSystem(choose(3) + 2); // Random value [2, 4]
+bankBalance[i] = choose(100) + 10; // Random value [10, 109]
+
+
+## Collection Operations
+// Map operations
+balance[accountId] = newValue; // Assignment
+assert accountId in balance; // Membership test
+keys(bankBalance); // Get all keys
+
+
+// Set operations
+pendingWDReqs += (req.rId); // Add to set
+pendingWDReqs -= (resp.rId); // Remove from set
+sizeof(pendingWDReqs); // Set size
+
+
+// Sequence operations
+sizeof(accountIds); // Sequence length
+
+
+## String Formatting and Printing
+// Formatted print statements
+print format("Withdrawal with rId = {0} succeeded, new balance = {1}", resp.rId, resp.balance);
+print format("Still have balance = {0}, lets try to withdraw more", currentBalance);
+
+
+// Formatted assertion messages
+format("Unknown accountId {0} in withdraw request. Valid accountIds = {1}",
+ req.accountId, keys(bankBalance));
+
+
+# Specifications
+## Safety Specifications
+spec BankBalanceIsAlwaysCorrect observes eWithDrawReq, eWithDrawResp, eSpec_BankBalanceIsAlwaysCorrect_Init {
+ var bankBalance: map[int, int];
+ var pendingWithDraws: map[int, tWithDrawReq];
+
+
+ start state Init {
+ on eSpec_BankBalanceIsAlwaysCorrect_Init goto WaitForWithDrawReqAndResp with (balance: map[int, int]) {
+ bankBalance = balance;
+ }
+ }
+
+
+ state WaitForWithDrawReqAndResp {
+ on eWithDrawReq do (req: tWithDrawReq) {
+ assert req.accountId in bankBalance;
+ pendingWithDraws[req.rId] = req;
+ }
+
+
+ on eWithDrawResp do (resp: tWithDrawResp) {
+ assert resp.balance >= 10, "Bank balance must always be >= 10!";
+
+ if(resp.status == WITHDRAW_SUCCESS) {
+ assert resp.balance == bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount;
+ bankBalance[resp.accountId] = resp.balance;
+ }
+ else {
+ assert bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount < 10;
+ assert bankBalance[resp.accountId] == resp.balance;
+ }
+ }
+ }
+}
+
+
+## Liveness Specifications
+spec GuaranteedWithDrawProgress observes eWithDrawReq, eWithDrawResp {
+ var pendingWDReqs: set[int];
+
+
+ start state NopendingRequests {
+ on eWithDrawReq goto PendingReqs with (req: tWithDrawReq) {
+ pendingWDReqs += (req.rId);
+ }
+ }
+
+
+ hot state PendingReqs { // 'hot' indicates liveness-critical state
+ on eWithDrawResp do (resp: tWithDrawResp) {
+ assert resp.rId in pendingWDReqs;
+ pendingWDReqs -= (resp.rId);
+ if(sizeof(pendingWDReqs) == 0)
+ goto NopendingRequests;
+ }
+
+
+ on eWithDrawReq goto PendingReqs with (req: tWithDrawReq) {
+ pendingWDReqs += (req.rId);
+ }
+ }
+}
+
+
+# Modules and Testing
+## Module Definitions
+// Group related machines into modules
+module Client = { Client };
+module Bank = { BankServer, Database };
+
+
+// Module with abstraction (substitution)
+module AbstractBank = { AbstractBankServer -> BankServer };
+
+
+## Test Scenarios
+// Test machines
+machine TestWithSingleClient {
+ start state Init {
+ entry {
+ SetupClientServerSystem(1);
+ }
+ }
+}
+
+
+machine TestWithMultipleClients {
+ start state Init {
+ entry {
+ SetupClientServerSystem(choose(3) + 2);
+ }
+ }
+}
+
+
+## Test Scripts
+// Define test cases with assertions
+test tcSingleClient [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithSingleClient });
+
+
+test tcMultipleClients [main=TestWithMultipleClients]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithMultipleClients });
+
+
+// Test with abstraction
+test tcAbstractServer [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, AbstractBank, { TestWithSingleClient });
+
+
+# Event Handling Control
+## Ignoring Events
+// Use ignore to explicitly drop certain events in specific states without causing unhandled event exceptions:
+pstate WaitForTransactions {
+ on eWriteTransReq do (wTrans : tWriteTransReq) {
+ // Handle write transaction
+ currentWriteTransReq = wTrans;
+ BroadcastToAllParticipants(ePrepareReq, wTrans.trans);
+ StartTimer(timer);
+ goto WaitForPrepareResponses;
+ }
+
+ on eReadTransReq do (rTrans : tReadTransReq) {
+ // Handle read transaction
+ send choose(participants), eReadTransReq, rTrans;
+ }
+
+ // Ignore these events when in this state - they're from previous transactions
+ ignore ePrepareResp, eTimeOut;
+}
+
+# Deferring Events
+// Use defer to postpone handling of events until the machine transitions to a state that can handle them:
+pstate WaitForPrepareResponses {
+ // Defer write requests - process transactions sequentially
+ defer eWriteTransReq;
+
+ on ePrepareResp do (resp : tPrepareResp) {
+ // Process prepare response
+ if (currentWriteTransReq.trans.transId == resp.transId) {
+ if(resp.status == SUCCESS) {
+ countPrepareResponses = countPrepareResponses + 1;
+ if(countPrepareResponses == sizeof(participants)) {
+ DoGlobalCommit();
+ goto WaitForTransactions; // Deferred events will be handled here
+ }
+ }
+ else {
+ DoGlobalAbort(ERROR);
+ goto WaitForTransactions; // Deferred events will be handled here
+ }
+ }
+ }
+
+ on eTimeOut goto WaitForTransactions with { DoGlobalAbort(TIMEOUT); }
+
+ // Still handle read requests immediately
+ on eReadTransReq do (rTrans : tReadTransReq) {
+ send choose(participants), eReadTransReq, rTrans;
+ }
+}
+# Key Differences:
+
+ignore: Events are permanently dropped and lost
+defer: Events are queued and will be processed when the machine transitions to a state that can handle them
+
+# Advanced Features
+## Event Announcements
+// Broadcast events to specifications
+announce eSpec_BankBalanceIsAlwaysCorrect_Init, initAccBalance;
+
+
+## State Transitions with Data
+// Transition with data passing
+on eSpec_BankBalanceIsAlwaysCorrect_Init goto WaitForWithDrawReqAndResp with (balance: map[int, int]) {
+ bankBalance = balance;
+}
+
+
+## Machine References
+// Store references to other machines
+var server: BankServer;
+var database: Database;
+
+
+// Use 'this' to refer to current machine
+send server, eWithDrawReq, (source = this, accountId = accountId, amount = 100, rId = 1);
+
+
+# Common Patterns
+## Request-Response Pattern
+// Sender side
+send targetMachine, eRequest, requestData;
+
+
+// Handler side
+on eRequest do (req: RequestType) {
+ // Process request
+ var response = processRequest(req);
+ send req.source, eResponse, response;
+}
+
+
+// Response handling
+on eResponse do (resp: ResponseType) {
+ // Handle response
+}
+
+
+State Machine Initialization
+start state Init {
+ entry (params: ParamType) {
+ // Initialize variables from parameters
+ // Create helper machines if needed
+ // Transition to main logic state
+ goto MainState;
+ }
+}
+
+
+## Error Handling Pattern
+if(operationSuccessful) {
+ // Success path
+ send client, eSuccessResponse, successData;
+}
+else {
+ // Error path
+ send client, eErrorResponse, errorData;
+}
+
+
+This guide covers the essential P language syntax through practical examples. The language focuses on state machines, event-driven communication, and formal verification of distributed systems properties.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/context_files/modular/p_basics.txt b/Src/PeasyAI/resources/context_files/modular/p_basics.txt
new file mode 100644
index 0000000000..a5c8410443
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_basics.txt
@@ -0,0 +1,27 @@
+Here is the basic P language guide:
+
+A P program consists of a collection of following top-level declarations:
+1. Enums
+2. User Defined Types
+3. Events
+4. State Machines
+5. Specification Monitors
+6. Global Functions
+7. Module System
+
+Here is the list of all words reserved by the P language. These words have a special meaning and purpose, and they cannot be used as identifiers for variables, enums, types, events, machines, function parameters, etc.:
+
+var, type, enum, event, on, do, goto, data, send, announce, receive, case, raise, machine, state, hot, cold, start, spec, module, test, main, fun, observes, entry, exit, with, union, foreach, else, while, return, break, continue, ignore, defer, assert, print, new, sizeof, keys, values, choose, format, if, halt, this, as, to, in, default, Interface, true, false, int, bool, float, string, seq, map, set, any
+
+
+
+1. DO NOT generate interface declarations in P files. P does not use standalone interface declarations.
+2. Variable declarations: Use only "var name: type;" - NO inline initialization with =
+3. Sequence operations: Use "seq += (index, value)" - NOT "seq += (value)"
+4. Set operations: Use "set += (value)" for direct addition
+5. Module definitions: Only reference existing/declared machines in module lists
+
+
+
+
+
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/context_files/modular/p_common_compilation_errors.txt b/Src/PeasyAI/resources/context_files/modular/p_common_compilation_errors.txt
new file mode 100644
index 0000000000..c26618b9a0
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_common_compilation_errors.txt
@@ -0,0 +1,52 @@
+This context file provides guidance for fixing common P language compilation errors. Keep these in mind to avoid compilation issues
+1. Variable Declarations:
+ CORRECT: var i: int; i = 0;
+ WRONG: var i: int = 0;
+
+2. Event Payload Access:
+ CORRECT: Use function parameters to receive payloads
+ state SomeState {
+ entry (payload: PayloadType) { ... }
+ on eEvent do (payload: PayloadType) { ... }
+ }
+ WRONG: var payload = receive(eEvent);
+
+3. State Transitions:
+ CORRECT: goto StateName;
+
+4. Null Handling:
+ CORRECT: if (variable != null) { ... }
+
+5. Single-Field Named Tuple (CRITICAL — most common error):
+ Single-field named tuples REQUIRE a trailing comma.
+ Given: type tRequest = (requestId: int);
+ CORRECT: send target, eRequest, (requestId = 42,); // trailing comma!
+ WRONG: send target, eRequest, (requestId = 42); // COMPILE ERROR — missing comma!
+
+ CORRECT: new Worker((coordinator = this,)); // trailing comma!
+ WRONG: new Worker((coordinator = this)); // COMPILE ERROR — missing comma!
+
+ The error message will say: "no viable alternative at input 'fieldName=value)'"
+ Fix: Add a trailing comma before the closing paren.
+
+COMMON ERRORS AND FIXES:
+
+Error: "no viable alternative at input 'fieldName=value)'" (single-field tuple)
+Fix: Missing trailing comma. Add comma: (field=value) → (field=value,)
+
+Error: "extraneous input 'receive' expecting..."
+Fix: Remove receive() calls, use function parameters instead
+
+Error: Variable declaration issues
+Fix: Separate declaration and assignment
+
+Error: Type mismatch
+Fix: Add explicit type annotations
+
+DEBUGGING INSTRUCTIONS:
+- Maintain original program logic
+- Fix syntax only
+- Check for receive() calls first
+- Ensure all variables declared with types
+- Use parameter-based payload access
+- Keep event-driven state machine structure intact
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/context_files/modular/p_compiler_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_compiler_guide.txt
new file mode 100644
index 0000000000..458c8f1990
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_compiler_guide.txt
@@ -0,0 +1,183 @@
+P Language Compiler Guide: Common Errors and Solutions
+
+1. State Transition Function State Changes
+Error: "Method is used as a transition function, but might change state here"
+Problem: When a transition function (used in 'on' handlers) contains state-changing logic but also has conditional state transitions, it can lead to non-deterministic behavior.
+Solution:
+- Move state-changing logic (like variable assignments) to the entry function of the target state
+- Keep transition functions focused purely on determining the next state
+- Use 'do' handlers instead of 'goto' with conditions if you need complex logic
+Example:
+Incorrect:
+```
+on eResponse goto Working with (response) {
+ if (response.status == SUCCESS) {
+ someVar = true; // State change!
+ } else {
+ goto Ready; // Conditional transition
+ }
+}
+```
+
+Correct:
+```
+on eResponse do (response) {
+ if (response.status == SUCCESS) {
+ goto Working;
+ } else {
+ goto Ready;
+ }
+}
+
+state Working {
+ entry {
+ someVar = true; // State change moved to entry
+ // other initialization
+ }
+}
+```
+
+Best Practices:
+1. Keep transition functions pure - they should only determine the next state
+2. Put state-changing logic in entry/exit functions
+3. Use 'do' handlers when you need complex conditional logic
+4. Ensure state transitions are deterministic
+
+2. Duplicate Declarations
+Error: "'name' duplicates declaration 'name' at file:line:column"
+Problem: In P language, module names, machine names, and other declarations must be unique. This error occurs when the same name is declared multiple times.
+Solution:
+- Ensure each module has a unique name
+- Check for duplicate machine declarations
+- Remove the duplicate declaration in the current file
+Example:
+Incorrect:
+```
+module distributedLockSystem {
+ // First declaration
+}
+
+module distributedLockSystem { // Error: Duplicate module name
+ // Second declaration
+}
+```
+
+Correct:
+```
+module distributedLockSystem {
+ // Single declaration with unique name
+}
+
+module clientModule { // Different name for second module
+ // Another module
+}
+```
+
+Best Practices:
+1. Use descriptive, unique names for modules and machines
+2. Keep a consistent naming convention
+3. Check for name conflicts across all project files
+4. Consider using prefixes for related modules
+
+3. Variable Declaration Scope
+Error: "parse error: extraneous input 'var' expecting {...}"
+Problem: In P language, variable declarations must appear at the start of their scope block, before any other statements.
+Solution:
+- Move all variable declarations to the beginning of the scope block
+- Declare all variables needed in a function/state at the start
+Example:
+Incorrect:
+```
+fun MyFunction() {
+ DoSomething();
+ var x: int; // Error: var declaration after statements
+ x = 5;
+}
+```
+
+Correct:
+```
+fun MyFunction() {
+ var x: int; // Correct: var declaration at start of scope
+ DoSomething();
+ x = 5;
+}
+```
+
+3. Collection Type Operations
+Error: "expected int/float but got seq/set/map"
+Problem: P language has strict type checking for collections (seq, set, map). Common errors occur when:
+- Trying to add elements of wrong type to collections
+- Using incorrect insertion syntax
+- Mixing collection types
+
+Solution:
+For sequences (seq):
+```
+var mySeq: seq[int]; // Declare sequence of integers
+mySeq += (0, 5); // Add element 5 at index 0
+mySeq += (sizeof(mySeq), 10); // Add element 10 at end of sequence
+
+// WRONG - These are not supported:
+mySeq = mySeq + (5,); // Wrong: Cannot concatenate with tuple
+mySeq = append(mySeq, 5); // Wrong: No append function
+```
+
+For sets (set):
+```
+var mySet: set[string]; // Declare set of strings
+mySet += ("element"); // Add element using += with parentheses
+mySet -= ("element"); // Remove element using -=
+```
+
+For maps (map):
+```
+var myMap: map[int, string]; // Declare map with int keys, string values
+myMap += (1, "value"); // Add using += tuple syntax
+myMap[1] = "value"; // Alternative: direct key assignment
+```
+
+Best Practices:
+1. Always declare collection type with proper element type(s)
+2. Use correct syntax for each collection type:
+ - Sequence: += (index, value) for insertion
+ - Set: += (element) with parentheses
+ - Map: Either += (key, value) or key assignment
+3. For sequences, use sizeof() to append to end
+4. Never try to concatenate sequences with +
+5. Always use parentheses around elements for set operations
+
+4. Module Visibility and Imports
+Error: "could not find variable, enum element, or event 'name'"
+Problem: Even though the type/enum/event exists in another file (like Enums_Types_Events.p), it's not visible in the current module because it hasn't been properly imported.
+Solution:
+- Import the module containing the declarations
+- Use the correct module name in the import statement
+- Place imports at the top of the file
+
+Example:
+Incorrect:
+```
+// Client.p
+machine Client {
+ var state: ConsensusState; // Error: ConsensusState not found
+}
+```
+
+Correct:
+```
+// Client.p
+module ClientModule {
+ import Enums_Types_Events; // Import module containing ConsensusState
+
+ machine Client {
+ var state: ConsensusState; // Now ConsensusState is visible
+ }
+}
+```
+
+Best Practices:
+1. Always import modules that contain needed declarations
+2. Place all imports at the top of the file
+3. Use consistent module names across the project
+4. Check import statements when declarations can't be found
diff --git a/Src/PeasyAI/resources/context_files/modular/p_enums_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_enums_guide.txt
new file mode 100644
index 0000000000..e7b3a274ea
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_enums_guide.txt
@@ -0,0 +1,29 @@
+Here is the P enums guide:
+
+1. Enum values in P are considered as global constants and must have unique name.
+2. If an enum is used in a user defined type declaration, the enum identifier cannot be a reserved keyword.
+
+Syntax:: enum enumName { enumElemList }
+
+Here is an example of enum declaration:
+
+// enum representing the response status for the withdraw request
+enum tWithDrawRespStatus { WITHDRAW_SUCCESS, WITHDRAW_ERROR }
+
+// User Defined Type
+type tWithDrawResp = (status: tWithDrawRespStatus, accountId: int, balance: int, rId: int);
+
+
+Here is an example of enum declaration with values:
+
+enum tResponseStatus { ERROR = 500, SUCCESS = 200, TIMEOUT = 400; }
+
+// usage of enums
+var status: tResponseStatus;
+status = ERROR;
+
+// you can coerce an enum to int
+assert (ERROR to int) == 500;
+
+
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_events_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_events_guide.txt
new file mode 100644
index 0000000000..582d41e42f
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_events_guide.txt
@@ -0,0 +1,30 @@
+Here is the P events guide:
+
+1. A P program is a collection of state machines communicating with each other by exchanging events.
+2. An event in P has two parts: an event name and a payload value (optional) that can be sent along with the event.
+3. Event name in P should be unique and different from Enum and Type names.
+4. There should EXIST a user defined type for EACH event payload and that declared type should be used in the event declaration.
+5. IMPORTANT: Declaring events with named tuple payloads is INCORRECT.
+
+When declaring P events with payloads, always follow this EXACT syntax:
+First, declare a user defined type for the payload:
+type payloadTypeName = ((fieldName: typeName)*);
+Then, declare the event using the declared payload type payloadTypeName:
+event eventName: payloadTypeName;
+
+Here is a correct example of events:
+
+// declarations of events with no payloads
+event ePing;
+event ePong;
+
+// declaration of events that have payloads
+type tRequest = (client: machine, requestId: int, key: string);
+// eRequest event with payload of user defined type tRequest
+event eRequest: tRequest;
+
+type tWriteTransResp = (transId: int, status: tTransStatus);
+// eWriteTransResp event with payload of user defined type tWriteTransResp
+event eWriteTransResp : tWriteTransResp;
+
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_machines_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_machines_guide.txt
new file mode 100644
index 0000000000..990cc6d078
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_machines_guide.txt
@@ -0,0 +1,473 @@
+Here is the P state machine guide:
+
+A P program is a collection of concurrently executing state machines that communicate with each other by sending events (or messages) asynchronously.
+
+The underlying model of computation is similar to that of Gul Agha's Actor-model-of-computation. Here is a summary of the important semantic details of P State Machine:
+1. Each P state machine has an unbounded FIFO buffer associated with it.
+2. Sends are asynchronous, i.e., executing a send operation send t,e,v; adds event e with payload value v into the FIFO buffer of the target machine t.
+3. Variables and functions declared within a machine are local, i.e., they are accessible ONLY from within that machine.
+4. Each state in a machine has an entry and an exit function associated with it. The entry function gets executed when the machine enters that state, and similarly, the exit function gets executed when the machine exits that state on an outgoing transition.
+5. After executing the entry function, a machine tries to dequeue an event from its input buffer or blocks if the buffer is empty. Upon dequeueing an event from its input queue, a machine executes the attached event handler which might transition the machine to a different state.
+6. DO NOT REFER to `module` when creating machine code
+7. Variable declarations using var must be placed at the very beginning of their scope (function or machine level) before any executable statements. You cannot declare variables in the middle of a function after other statements have been executed.
+8. Type consistency is strictly enforced - you cannot assign values of one type to variables of another type. Common issues include assigning integer IDs to machine reference variables, mixing machine references with primitive types, and function parameter/return type mismatches.
+
+
+Do NOT leave a machine empty.
+Do NOT explicitly initialize a machine to null.
+Do NOT access functions contained inside of other machines.
+
+
+
+```
+// Machine definitions - no events/types/enums (they come from Enums_Types_Events.p)
+machine TaskManager {
+ var workers: seq[machine]; // receive from constructor or config event
+ var tasks: seq[Task]; // Task type comes from context
+
+ // Start state receives configuration via constructor payload
+ start state Init {
+ entry InitEntry;
+ on eNewTask goto Processing;
+ }
+
+ state Processing {
+ on eTask do HandleTask;
+ defer eNewTask; // queue new tasks while busy
+ ignore eStatusRequest; // drop status pings while processing
+ }
+
+ // Entry function receives the constructor payload
+ fun InitEntry(config: (workers: seq[machine],)) {
+ workers = config.workers;
+ }
+
+ fun HandleTask(task: Task) {
+ // Implementation using contextual types and events
+ }
+}
+```
+
+Created by test driver as: `new TaskManager((workers = workerList,));`
+
+
+
+Here's the formal grammar for P State Machines:
+
+```
+# State Machine in P
+machineDecl : machine name machineBody
+
+# State Machine Body
+machineBody : LBRACE machineEntry* RBRACE;
+machineEntry
+ | varDecl
+ | funDecl
+ | stateDecl
+ ;
+
+# Variable Decl
+varDecl : var iden : type ;
+
+# State Declaration in P
+stateDecl : start? (hot | cold)? state name { stateBody* }
+
+# State Body
+stateBody:
+ | entry funName ; # StateEntryFunNamed
+ | exit funName; # StateExitFunNamed
+
+ ## Transition or Event Handlers in each state
+ | defer eventList ; # StateDeferHandler
+ | ignore eventList ; # StateIgnoreHandler
+ | on eventList do funName ; # OnEventDoHandler
+ | on eventList goto stateName ; # OnEventGotoState
+ | on eventList goto stateName with funName ; # OnEventGotoStateWithNamedHandler
+ ;
+```
+
+
+
+A state machine in P is declared using the 'machine' keyword followed by the machine name and a body enclosed in curly braces.
+
+Syntax:: machine MachineName { machineBody }
+
+Example:
+```
+machine TaskManager {
+ // Machine declarations (variables, states, functions)
+}
+```
+
+The machine body can contain:
+1. Variable declarations
+2. State declarations
+3. Function declarations
+
+
+
+A machine can define a set of local variables that are accessible only from within that machine.
+
+Syntax: `var iden : type ;`
+
+Where `iden` is the name of the variable and `type` is the variable datatype.
+
+Example:
+```
+var server: BankServer;
+var accountId: int;
+var nextReqId: int;
+var currentBalance: int;
+```
+
+
+
+A state can be declared in a machine with a name and a stateBody.
+Syntax:: start? (hot | cold)? state name { stateBody* }
+1. A single state in each machine should be marked as the start state to identify this state as the starting state on machine creation.
+2. A machine should contain at least the start state.
+3. Each state in the machine SHOULD have a UNIQUE name.
+
+State annotations:
+- start: The initial state when a machine is created
+- hot: A state where an unhandled event is treated as an error
+- cold: A state where an unhandled event is silently discarded (default if not specified)
+
+The state body can contain:
+1. Entry function declaration (optional)
+2. Exit function declaration (optional)
+3. Event handlers
+4. Defer statements
+5. Ignore statements
+
+Here is an example of a state machine with multiple states:
+
+machine TaskManager {
+ // Machine's local variables
+ var tasks: seq[Task];
+ var currentState: int;
+
+ // Start state - executed when machine is created
+ start state Idle {
+ entry Idle_Entry;
+
+ on eNewTask do HandleNewTask;
+ on eProcessTask goto Processing;
+ }
+
+ state Processing {
+ entry Processing_Entry;
+ exit Processing_Exit;
+
+ on eTaskComplete goto Idle;
+ on eTaskFailed do HandleFailure;
+
+ // Defer these events until we're back in Idle state
+ defer eNewTask;
+
+ // Ignore these events in this state
+ ignore eStatusRequest;
+ }
+
+ // Entry/exit functions and event handlers
+ fun Idle_Entry() {
+ currentState = 0;
+ }
+
+ fun Processing_Entry() {
+ currentState = 1;
+ }
+
+ fun Processing_Exit() {
+ // Cleanup before leaving Processing state
+ }
+
+ fun HandleNewTask(task: Task) {
+ tasks += (sizeof(tasks), task);
+ }
+
+ fun HandleFailure() {
+ // Handle failure logic
+ }
+}
+
+
+
+
+Functions in P state machines can take parameters, which is especially useful for event handlers that need to access payload data from events. These functions are defined using the standard function declaration syntax.
+
+Syntax:
+```
+fun FunctionName(param1: Type1, param2: Type2, ...): ReturnType {
+ // Function body
+}
+```
+
+Examples:
+
+```
+// Event handler function that takes a task parameter
+fun HandleNewTask(task: Task) {
+ print format("Processing task with id: {0}", task.id);
+ tasks += (sizeof(tasks), task);
+}
+
+// Event handler function that takes a withdrawal request parameter
+fun ProcessWithdrawRequest(req: tWithDrawReq) {
+ assert req.accountId in bankBalance,
+ format("Unknown accountId {0} in the withdraw request. Valid accountIds = {1}",
+ req.accountId, keys(bankBalance));
+
+ // Process the withdrawal
+ if (bankBalance[req.accountId] >= req.amount) {
+ bankBalance[req.accountId] = bankBalance[req.accountId] - req.amount;
+ send req.source, eWithDrawResp, (status = WITHDRAW_SUCCESS, accountId = req.accountId,
+ balance = bankBalance[req.accountId], rId = req.rId);
+ } else {
+ send req.source, eWithDrawResp, (status = WITHDRAW_ERROR, accountId = req.accountId,
+ balance = bankBalance[req.accountId], rId = req.rId);
+ }
+}
+
+// Function that takes multiple parameters with different types
+fun TransferFunds(sourceAcct: int, destAcct: int, amount: int): bool {
+ if (sourceAcct in bankBalance && destAcct in bankBalance && bankBalance[sourceAcct] >= amount) {
+ bankBalance[sourceAcct] = bankBalance[sourceAcct] - amount;
+ bankBalance[destAcct] = bankBalance[destAcct] + amount;
+ return true;
+ }
+ return false;
+}
+```
+
+When these functions are used in event handlers, they receive the payload from the event:
+
+```
+state WaitingForRequests {
+ // The ProcessWithdrawRequest function receives the payload from the eWithDrawReq event
+ on eWithDrawReq do ProcessWithdrawRequest;
+
+ // Similarly with other event handlers
+ on eTransferFundsReq do HandleTransferRequest;
+}
+
+// This function will be called with the payload from the eTransferFundsReq event
+fun HandleTransferRequest(req: tTransferReq) {
+ var result = TransferFunds(req.sourceAcct, req.destAcct, req.amount);
+ send req.source, eTransferResp, (success = result, rId = req.rId);
+}
+```
+
+
+
+Entry and exit functions define behavior that executes when entering or exiting a state.
+
+Entry Function:
+- Executed automatically when a machine enters the state
+- Can optionally take parameters (useful for start states)
+- Defined using 'entry' keyword
+
+Syntax:
+```
+entry FunctionName; // Reference to a separately defined function that can take parameters
+```
+
+Exit Function:
+- Executed automatically when a machine exits the state
+- Defined using 'exit' keyword
+
+Syntax:
+```
+exit FunctionName; // Reference to a separately defined function
+```
+
+Examples:
+```
+// State with entry and exit function references
+state Processing {
+ entry ProcessingEntry;
+ exit ProcessingExit;
+}
+
+// Start state with parameterized entry function
+start state Init {
+ entry InitState;
+}
+
+// The entry function defined separately with parameters
+fun InitState(input: (serv: BankServer, accountId: int, balance: int)) {
+ server = input.serv;
+ currentBalance = input.balance;
+ accountId = input.accountId;
+}
+
+// Simple exit function
+fun ProcessingExit() {
+ // Cleanup code
+ pendingOperations = 0;
+}
+```
+
+Defining entry and exit functions for a state is optional. By default, each function is defined as a no-op function, i.e., `{ // nothing }`.
+
+
+
+Machines can be created dynamically using the 'new' keyword. When creating a new machine, you can pass parameters to its start state's entry function:
+
+Syntax:: new MachineName(parameters);
+
+Example:
+```
+// Create a new database machine and pass initialization parameters
+var database = new Database((server = this, initialBalance = initialBalance));
+```
+
+The created machine starts executing from its start state, passing the provided parameters to the entry function.
+
+
+
+1. An event handler defined for event E in state S describes what statements are executed when a machine dequeues event E in state S.
+2. An event handler is ALWAYS defined WITHIN a state where you expect the corresponding event to be handled.
+3. An event handler references a named function that can take parameters.
+
+Syntax:
+- To execute an event-handler function on receiving an event: on eNameList do funName;
+- To transition to another state: on eNameList goto stateName;
+- To call a function and transition: on eNameList goto stateName with funName;
+
+Examples of event handlers:
+
+state WaitForRelease {
+ // Call function without state change
+ on eRead do SendReadResponse;
+
+ // Change state without function call
+ on eTimeout goto Error;
+
+ // Call function and change state
+ on eReset goto Init with ResetSystem;
+}
+
+
+Event handlers with functions that take parameters:
+```
+state WaitForWithdrawRequests {
+ // ProcessWithdrawRequest will receive the event payload
+ on eWithDrawReq do ProcessWithdrawRequest;
+
+ // HandleErrorResponse will receive the error details
+ on eError goto ErrorState with HandleErrorResponse;
+}
+
+fun ProcessWithdrawRequest(req: tWithDrawReq) {
+ // Access the request parameters
+ var accountId = req.accountId;
+ var amount = req.amount;
+ var requestId = req.rId;
+
+ // Process the request
+ // ...
+}
+
+fun HandleErrorResponse(error: tErrorDetail) {
+ // Log the error before transitioning to ErrorState
+ LogError(error.code, error.message);
+}
+```
+
+Event handlers can also be grouped for multiple events:
+```
+// Multiple events handled the same way
+on eTimeout, eError, eCancel goto CleanupState;
+```
+
+
+
+An event can be deferred in a state, which delays the processing of the event until the machine transitions to a state that doesn't defer it.
+
+Syntax:: defer eventList;
+
+
+state Processing {
+ // Defer new requests until we're done processing
+ defer eNewRequest, eConfigChange;
+
+ // Multiple events can be deferred with a comma-separated list
+ defer eOpenGroundsDoor, eCloseGroundsDoor, eEspressoButtonPressed;
+}
+
+
+Deferred events remain in the machine's input queue but are skipped over until the machine enters a state where the event is not deferred. This is useful for maintaining event ordering while postponing specific events until the machine is ready to handle them.
+
+Important: Whenever a machine encounters a dequeue event, the machine goes over its unbounded FIFO buffer from the front and removes the first event that is not deferred in its current state, keeping the rest of the buffer unchanged.
+
+
+
+An event can be ignored in a state, which drops the event when received in that state.
+
+Syntax:: ignore eventList;
+
+
+state WaitForTimerRequests {
+ // Ignore these events in this state (they will be discarded)
+ ignore eCancelTimer, eDelayedTimeOut;
+
+ // Multiple events can be ignored with a comma-separated list
+ ignore eEspressoButtonPressed, eSteamerButtonOn, eSteamerButtonOff;
+}
+
+
+Ignored events are removed from the machine's input queue when received in that state, without triggering any handler. This is useful when certain events are not relevant in a particular state.
+
+You can think of ignore as a shorthand for a no-op handler for the event.
+
+
+
+Machines communicate by sending events asynchronously to other machines using the send statement.
+
+Syntax:: send target, event, payload;
+
+
+// Send event without payload
+send client, eAcknowledge;
+
+// Send event with simple payload
+send server, eRequest, requestId;
+
+// Send event with complex payload (named tuple)
+send req.source, eWithDrawResp, (status = WITHDRAW_SUCCESS, accountId = req.accountId,
+ balance = bankBalance[req.accountId], rId = req.rId);
+
+// Send to dynamically created machine
+var newWorker = new WorkerMachine();
+send newWorker, eStartWork, (taskId = currentTask, priority = HIGH_PRIORITY);
+
+
+
+The target can be:
+1. A machine reference variable
+2. this (to send to the current machine)
+3. A field from an event payload that contains a machine reference
+
+
+
+
+State machines in P interact with each other primarily through asynchronous event passing. Common patterns include:
+
+1. Client-Server Pattern:
+ - Server machines provide services by handling request events
+ - Client machines send requests and process responses
+
+2. Observer Pattern:
+ - Subject machines send notification events
+ - Observer machines listen for and react to notifications
+
+3. Request-Response Pattern:
+ - Machine A sends a request event to Machine B
+ - Machine B processes the request and sends a response event back to Machine A
+
+4. State Synchronization Pattern:
+ - Machines coordinate by exchanging state information
+ - Used to maintain consistent views of distributed state
+
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_module_system_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_module_system_guide.txt
new file mode 100644
index 0000000000..4d55dcd8d6
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_module_system_guide.txt
@@ -0,0 +1,174 @@
+Here is the P module system guide:
+
+The P module system allows programmers to decompose their complex system into modules to implement and test the system compositionally. It provides mechanisms for organizing related machines and creating larger systems by combining modules.
+
+
+## Module System Overview
+
+In its simplest form, a module in P is a collection of state machines. The P module system allows constructing larger modules by composing or unioning modules together.
+
+When building distributed systems consisting of multiple components, each component can be implemented as a separate module. These modules can then be:
+- Tested independently with abstractions of other components
+- Combined to validate the entire system
+- Used to verify specific correctness properties
+
+For P test cases, a module representing a **closed system** is used as input for validation. A closed system is one where all machines or interfaces that are created are defined or implemented in the unioned modules.
+
+
+
+## Module Grammar
+
+```
+modExpr :
+| ( modExpr ) # AnnonymousModuleExpr
+| { bindExpr (, bindExpr)* } # PrimitiveModuleExpr
+| union modExpr (, modExpr)+ # UnionModuleExpr
+| assert idenList in modExpr # AssertModuleExpr
+| iden # NamedModule
+;
+
+# Bind a machine to an interface
+bindExpr : (iden | iden -> iden) ; # MachineBindExpr
+
+# Create a named module i.e., assign a name to a module
+namedModuleDecl : module iden = modExpr ; # Named module declaration
+```
+
+
+
+## Module Types
+
+P provides several types of modules to support different system organization needs:
+
+### Named Module
+
+A named module declaration simply assigns a name to a module expression.
+
+**Syntax**: `module mName = modExpr;`
+
+Where `mName` is the assigned name for the module and `modExpr` is any valid module expression.
+
+**Example**:
+```kotlin
+module serverModule = { Server, Timer };
+```
+
+This example assigns the name `serverModule` to a primitive module consisting of machines `Server` and `Timer`.
+
+### Primitive Module
+
+A primitive module is a collection of state machines, optionally with bindings to map machine names.
+
+**Syntax**: `{ bindExpr (, bindExpr)* }`
+
+Where `bindExpr` is either:
+- A machine name (`iden`)
+- A mapping `mName -> replaceName` that binds a machine `mName` to replace a machine name `replaceName`
+
+**Example 1**: Simple machine collection
+```kotlin
+// Define modules for client and server components
+module client = { Client };
+module server = { Server, Timer };
+```
+
+**Example 2**: Module with machine bindings
+```kotlin
+// Replace Server with AbstractServer when this module is used
+module serverAbs = { AbstractServer -> Server, Timer };
+```
+
+The binding in `serverAbs` means that whenever the module is used and a `Server` machine would be created (with `new Server(...)`), the `AbstractServer` machine will be created instead. This is particularly useful for replacing concrete implementations with abstractions for testing or verification.
+
+### Union Module
+
+Union modules combine multiple modules to create larger, more complex modules.
+
+**Syntax**: `union modExpr (, modExpr)+`
+
+Where `modExpr` is any P module expression.
+
+**Example**:
+```kotlin
+// Combine client and server modules
+module system = (union client, server);
+
+// Combine client with abstract server
+module systemAbs = (union client, serverAbs);
+```
+
+In the `system` module, `Client` interacts with `Server`, while in the `systemAbs` module, `Client` interacts with `AbstractServer` because of the binding in `serverAbs`.
+
+### Assert Monitors Module
+
+Assert monitors modules attach specification monitors to a module to verify that the module satisfies certain properties.
+
+**Syntax**: `assert idenList in modExpr`
+
+Where `idenList` is a comma-separated list of monitor (spec machine) names, and `modExpr` is the module whose executions are being monitored.
+
+**Example**:
+```kotlin
+assert AtomicitySpec, EventualResponse in TwoPhaseCommit
+```
+
+This module asserts that all executions of the `TwoPhaseCommit` module satisfy the properties specified by the monitors `AtomicitySpec` and `EventualResponse`.
+
+Important: When attaching monitors to a module, the events observed by the monitors must be sent by some machine in the module.
+
+
+
+## Advanced Module Features
+
+P also supports more advanced module constructors:
+- **compose**: For compositional reasoning between modules
+- **hide**: For information hiding between modules
+- **rename**: For renaming machines or events
+
+These are primarily used for more advanced compositional reasoning techniques.
+
+### Example of Real-World Module Usage
+
+In complex distributed systems, you might structure modules as:
+
+```kotlin
+// Basic components
+module clientImpl = { Client, ClientLogger };
+module serverImpl = { Server, Timer, Cache };
+module networkImpl = { NetworkProxy, FailureDetector };
+
+// Create a system with all components
+module fullSystem = (union clientImpl, serverImpl, networkImpl);
+
+// Create a test system with abstract network
+module abstractNetwork = { AbstractNetwork -> NetworkProxy };
+module testSystem = (union clientImpl, serverImpl, abstractNetwork);
+
+// Attach safety and liveness specs
+assert SafetySpec, LivenessSpec in fullSystem
+```
+
+This structure allows testing components independently or together, while also enabling formal verification of system properties.
+
+
+>
+WRONG - Interface mapping syntax:
+module MultiClientSystem = { Client -> Client1, Client -> Client2 };
+
+WRONG - Test declarations in module files:
+test testMain = TestConfig; // WRONG! Test declarations are not allowed in module files
+
+CORRECT - List machines directly:
+module MultiClientSystem = { Client1, Client2, Coordinator, Participant };
+
+Key Rules:
+1. Modules list machine names, not mappings
+2. Use comma-separated machine names only
+3. No -> arrows in module definitions
+4. Only reference machines that are actually defined/declared
+5. Check what machines are available before creating modules
+6. Use only the machine names that appear in your PSrc files
+7. NO test declarations allowed in module files
+
+
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_program_example.txt b/Src/PeasyAI/resources/context_files/modular/p_program_example.txt
new file mode 100644
index 0000000000..401356b021
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_program_example.txt
@@ -0,0 +1,1233 @@
+
+Here is an example of a P program:
+
+
+System:
+Consider a client-server application where clients interact with a bank to withdraw money from their accounts.
+The bank consists of two components:
+1. a bank server that services withdraw requests from the client, and
+2. a backend database that is used to store the account balance information for each client.
+Multiple clients can concurrently send withdraw requests to the bank.
+On receiving a withdraw request, the bank server reads the current bank balance for the client.
+If the withdraw request is allowed, then the server performs the withdrawal, updates the account balance, and responds back to the client with the new account balance.
+
+Correctness Specification:
+The bank must maintain the invariant that each account must have at least 10 dollars as its balance.
+If a withdrawal request takes the account balance below 10, then the withdrawal request must be rejected by the bank.
+The correctness property that we would like to check is that in the presence of concurrent client withdrawal requests, the bank always responds with the correct bank balance for each client, and a withdraw request always succeeds if there is enough balance in the account (i.e., at least 10).
+
+
+
+The ClientServer folder contains the source code for the ClientServer project.
+
+The P models (PSrc) for the ClientServer example consist of four files:
+1. Client.p: Implements the Client state machine that has a set of local variables used to store the local-state of the state machine.
+2. Server.p: Implements the BankServer and the backend Database state machines.
+3. AbstractBankServer.p: Implements the AbstractBankServer state machine that provides a simplified abstraction that unifies the BankServer and Database machines.
+4. ClientServerModules.p: Declares the P modules corresponding to each component in the system.
+
+
+
+The P Specifications (PSpec) for the ClientServer project are implemented in the BankBalanceCorrect.p file.
+
+We define two specifications:
+1. BankBalanceIsAlwaysCorrect (safety property):
+The BankBalanceIsAlwaysCorrect specification checks the global invariant that the account-balance communicated to the client by the bank is always correct and the bank never removes more money from the account than that withdrawn by the client.
+Also, if the bank denies a withdraw request, then it is only because the withdrawal would reduce the account balance to below 10.
+
+2. GuaranteedWithDrawProgress (liveness property):
+The GuaranteedWithDrawProgress specification checks the liveness (or progress) property that all withdraw requests submitted by the client are eventually responded.
+
+The two properties BankBalanceIsAlwaysCorrect and GuaranteedWithDrawProgress together ensure that every withdraw request if allowed will eventually succeed and the bank cannot block correct withdrawal requests.
+
+
+
+The test scenarios folder for ClientServer (PTst) consists of two files: TestDriver.p and TestScript.p.
+
+
+
+
+Here is the folder PSrc:
+
+Here is the file Client.p:
+
+/* User Defined Types */
+
+// payload type associated with the eWithDrawReq, where `source`: client sending the withdraw request,
+// `accountId`: account to withdraw from, `amount`: amount to withdraw, and
+// `rId`: unique request Id associated with each request.
+type tWithDrawReq = (source: Client, accountId: int, amount: int, rId:int);
+
+// payload type associated with the eWithDrawResp, where `status`: response status (below),
+// `accountId`: account withdrawn from, `balance`: bank balance after withdrawal, and
+// `rId`: request id for which this is the response.
+type tWithDrawResp = (status: tWithDrawRespStatus, accountId: int, balance: int, rId: int);
+
+// enum representing the response status for the withdraw request
+enum tWithDrawRespStatus {
+ WITHDRAW_SUCCESS,
+ WITHDRAW_ERROR
+}
+
+// event: withdraw request (from client to bank server)
+event eWithDrawReq : tWithDrawReq;
+// event: withdraw response (from bank server to client)
+event eWithDrawResp: tWithDrawResp;
+
+
+machine Client
+{
+ var server : BankServer;
+ var accountId: int;
+ var nextReqId : int;
+ var numOfWithdrawOps: int;
+ var currentBalance: int;
+
+ /*************************************************************
+ Init state is the start state of the machine where the machine starts executions on being created.
+ The entry function of the Init state initializes the local variables based on the parameters received
+ on creation and jumps to the WithdrawMoney state.
+ *************************************************************/
+ start state Init {
+ entry Init_Entry;
+ }
+
+ /*************************************************************
+ In the WithdrawMoney state, the state machine checks if there is enough money in the account.
+ If the balance is greater than 10, then it issues a random withdraw request to the bank by sending the eWithDrawReq event, otherwise, it jumps to the NoMoneyToWithDraw state.
+ After sending a withdraw request, the machine waits for the eWithDrawResp event.
+ On receiving the eWithDrawResp event, the machine executes the corresponding event handler that confirms if the bank response is as expected and if there is still money in the account
+ and then jumps back to the WithdrawMoney state. Note that each time we (re-)enter a state (through a transition or goto statement), its entry function is executed.
+ *************************************************************/
+ state WithdrawMoney {
+ entry WithdrawMoney_Entry;
+
+ on eWithDrawResp do AfterWithDrawResp;
+ }
+
+ fun AfterWithDrawResp(resp: tWithDrawResp) {
+ // bank always ensures that a client has atleast 10 dollars in the account
+ assert resp.balance >= 10, "Bank balance must be greater than 10!!";
+ if(resp.status == WITHDRAW_SUCCESS) // withdraw succeeded
+ {
+ print format ("Withdrawal with rId = {0} succeeded, new account balance = {1}", resp.rId, resp.balance);
+ currentBalance = resp.balance;
+ }
+ else // withdraw failed
+ {
+ // if withdraw failed then the account balance must remain the same
+ assert currentBalance == resp.balance,
+ format ("Withdraw failed BUT the account balance changed! client thinks: {0}, bank balance: {1}", currentBalance, resp.balance);
+ print format ("Withdrawal with rId = {0} failed, account balance = {1}", resp.rId, resp.balance);
+ }
+
+ if(currentBalance > 10)
+ {
+ print format ("Still have account balance = {0}, lets try and withdraw more", currentBalance);
+ goto WithdrawMoney;
+ }
+ }
+
+ fun Init_Entry(input : (serv : BankServer, accountId: int, balance : int)) {
+ server = input.serv;
+ currentBalance = input.balance;
+ accountId = input.accountId;
+ // hacky: we would like request id's to be unique across all requests from clients
+ nextReqId = accountId*100 + 1; // each client has a unique account id
+ goto WithdrawMoney;
+ }
+
+ fun WithdrawMoney_Entry() {
+ // If current balance is <= 10 then we need more deposits before any more withdrawal
+ if(currentBalance <= 10)
+ goto NoMoneyToWithDraw;
+
+ // send withdraw request to the bank for a random amount between (1 to current balance + 1)
+ send server, eWithDrawReq, (source = this, accountId = accountId, amount = WithdrawAmount(), rId = nextReqId);
+ nextReqId = nextReqId + 1;
+ }
+
+ // function that returns a random integer between (1 to current balance + 1)
+ fun WithdrawAmount() : int {
+ return choose(currentBalance) + 1;
+ }
+
+ state NoMoneyToWithDraw {
+ entry NoMoneyToWithDrawEntry;
+ }
+
+ fun NoMoneyToWithDrawEntry() {
+ // if I am here then the amount of money in my account should be exactly 10
+ assert currentBalance == 10, "Hmm, I still have money that I can withdraw but I have reached NoMoneyToWithDraw state!";
+ print format ("No Money to withdraw, waiting for more deposits!");
+ }
+}
+
+
+Here is the file Server.p:
+
+// payload type associated with eUpdateQuery
+type tUpdateQuery = (accountId: int, balance: int);
+
+// payload type associated with eReadQuery
+type tReadQuery = (accountId: int);
+
+// payload type associated with eReadQueryResp
+type tReadQueryResp = (accountId: int, balance: int);
+
+/** Events used to communicate between the bank server and the backend database **/
+// event: send update the database, i.e. the `balance` associated with the `accountId`
+event eUpdateQuery: tUpdateQuery;
+// event: send a read request for the `accountId`.
+event eReadQuery: tReadQuery;
+// event: send a response (`balance`) corresponding to the read request for an `accountId`
+event eReadQueryResp: tReadQueryResp;
+
+/*************************************************************
+The BankServer machine uses a database machine as a service to store the bank balance for all its clients.
+On receiving an eWithDrawReq (withdraw requests) from a client, it reads the current balance for the account,
+if there is enough money in the account then it updates the new balance in the database after withdrawal
+and sends a response back to the client.
+*************************************************************/
+machine BankServer
+{
+ var database: Database;
+
+ start state Init {
+ entry Init_Entry;
+ }
+
+ state WaitForWithdrawRequests {
+ on eWithDrawReq do AfterWithDrawReq;
+ }
+
+ fun Init_Entry(initialBalance: map[int, int]) {
+ database = new Database((server = this, initialBalance = initialBalance));
+ goto WaitForWithdrawRequests;
+ }
+
+ fun AfterWithDrawReq(wReq: tWithDrawReq) {
+ var currentBalance: int;
+ var response: tWithDrawResp;
+
+ // read the current account balance from the database
+ currentBalance = ReadBankBalance(database, wReq.accountId);
+ // if there is enough money in account after withdrawal
+ if(currentBalance - wReq.amount >= 10)
+ {
+ UpdateBankBalance(database, wReq.accountId, currentBalance - wReq.amount);
+ response = (status = WITHDRAW_SUCCESS, accountId = wReq.accountId, balance = currentBalance - wReq.amount, rId = wReq.rId);
+ }
+ else // not enough money after withdraw
+ {
+ response = (status = WITHDRAW_ERROR, accountId = wReq.accountId, balance = currentBalance, rId = wReq.rId);
+ }
+
+ // send response to the client
+ send wReq.source, eWithDrawResp, response;
+ }
+
+}
+
+/***************************************************************
+The Database machine acts as a helper service for the Bank server and stores the bank balance for
+each account. There are two API's or functions to interact with the Database:
+ReadBankBalance and UpdateBankBalance.
+****************************************************************/
+machine Database
+{
+ var server: BankServer;
+ var balance: map[int, int];
+ start state Init {
+ entry Init_Entry;
+
+ on eUpdateQuery do AfterUpdateQuery;
+
+ on eReadQuery do AfterReadQuery;
+ }
+
+ fun Init_Entry(input: (server : BankServer, initialBalance: map[int, int])) {
+ server = input.server;
+ balance = input.initialBalance;
+ }
+
+ fun AfterUpdateQuery(query: (accountId: int, balance: int)) {
+ assert query.accountId in balance, "Invalid accountId received in the update query!";
+ balance[query.accountId] = query.balance;
+ }
+
+ fun AfterReadQuery(query: (accountId: int)) {
+ assert query.accountId in balance, "Invalid accountId received in the read query!";
+ send server, eReadQueryResp, (accountId = query.accountId, balance = balance[query.accountId]);
+ }
+
+}
+
+/***************************************************************
+Global functions
+****************************************************************/
+// Function to read the bank balance corresponding to the accountId
+fun ReadBankBalance(database: Database, accountId: int) : int {
+ var currentBalance: int;
+ send database, eReadQuery, (accountId = accountId,);
+ receive {
+ case eReadQueryResp: (resp: (accountId: int, balance: int)) {
+ currentBalance = resp.balance;
+ }
+ }
+ return currentBalance;
+}
+
+// Function to update the account balance for the account Id
+fun UpdateBankBalance(database: Database, accId: int, bal: int)
+{
+ send database, eUpdateQuery, (accountId = accId, balance = bal);
+}
+
+
+Here is the file AbstractBankServer.p:
+
+/*********************************************************
+The AbstractBankServer provides an abstract implementation of the BankServer where it abstract away
+the interaction between the BankServer and Database.
+The AbstractBankServer machine is used to demonstrate how one can replace a complex component in P
+with its abstraction that hides a lot of its internal complexity.
+In this case, instead of storing the balance in a separate database the abstraction store the information
+locally and abstracts away the complexity of bank server interaction with the database.
+For the client, it still exposes the same interface/behavior. Hence, when checking the correctness
+of the client it doesnt matter whether we use BankServer or the AbstractBankServer
+**********************************************************/
+
+machine AbstractBankServer
+{
+ // account balance: map from account-id to balance
+ var balance: map[int, int];
+ start state WaitForWithdrawRequests {
+ entry WaitForWithdrawRequests_Entry;
+
+ on eWithDrawReq do AfterWithDrawReq;
+ }
+
+ fun WaitForWithdrawRequests_Entry(init_balance: map[int, int]) {
+ balance = init_balance;
+ }
+
+ fun AfterWithDrawReq(wReq: tWithDrawReq) {
+ assert wReq.accountId in balance, "Invalid accountId received in the withdraw request!";
+ if(balance[wReq.accountId] - wReq.amount >= 10)
+ {
+ balance[wReq.accountId] = balance[wReq.accountId] - wReq.amount;
+ send wReq.source, eWithDrawResp,
+ (status = WITHDRAW_SUCCESS, accountId = wReq.accountId, balance = balance[wReq.accountId], rId = wReq.rId);
+ }
+ else
+ {
+ send wReq.source, eWithDrawResp,
+ (status = WITHDRAW_ERROR, accountId = wReq.accountId, balance = balance[wReq.accountId], rId = wReq.rId);
+ }
+ }
+
+}
+
+
+Here is the file ClientServerModules.p:
+
+// Client module
+module Client = { Client };
+
+// Bank module
+module Bank = { BankServer, Database };
+
+// Abstract Bank Server module
+module AbstractBank = { AbstractBankServer -> BankServer };
+
+
+
+Here is the folder PSpec:
+
+Here is the file BankBalanceCorrect.p:
+
+/*****************************************************
+This file defines two P specifications
+
+BankBalanceIsAlwaysCorrect (safety property):
+BankBalanceIsAlwaysCorrect checks the global invariant that the account-balance communicated
+to the client by the bank is always correct and the bank never removes more money from the account
+than that withdrawn by the client! Also, if the bank denies a withdraw request then it is only because
+the withdrawal would reduce the account balance to below 10.
+
+GuaranteedWithDrawProgress (liveness property):
+GuaranteedWithDrawProgress checks the liveness (or progress) property that all withdraw requests
+submitted by the client are eventually responded.
+
+Note: stating that "BankBalanceIsAlwaysCorrect checks that if the bank denies a withdraw request
+then the request would reduce the balance to below 10 (< 10)" is equivalent to state that "if there is enough money in the account - at least 10 (>= 10), then the request must not error".
+Hence, the two properties BankBalanceIsAlwaysCorrect and GuaranteedWithDrawProgress together ensure that every withdraw request if allowed will eventually succeed and the bank cannot block correct withdrawal requests.
+*****************************************************/
+
+// event: initialize the monitor with the initial account balances for all clients when the system starts
+event eSpec_BankBalanceIsAlwaysCorrect_Init: map[int, int];
+
+/****************************************************
+BankBalanceIsAlwaysCorrect checks the global invariant that the account balance communicated
+to the client by the bank is always correct and there is no error on the banks side with the
+implementation of the withdraw logic.
+
+For checking this property the spec machine observes the withdraw request (eWithDrawReq) and response (eWithDrawResp).
+- On receiving the eWithDrawReq, it adds the request in the pending-withdraws-map so that on receiving a
+response for this withdraw we can assert that the amount of money deducted from the account is same as
+what was requested by the client.
+
+- On receiving the eWithDrawResp, we look up the corresponding withdraw request and check that: the
+new account balance is correct and if the withdraw failed it is because the withdraw will make the account
+balance go below 10 dollars which is against the bank policies!
+****************************************************/
+spec BankBalanceIsAlwaysCorrect observes eWithDrawReq, eWithDrawResp, eSpec_BankBalanceIsAlwaysCorrect_Init {
+ // keep track of the bank balance for each client: map from accountId to bank balance.
+ var bankBalance: map[int, int];
+ // keep track of the pending withdraw requests that have not been responded yet.
+ // map from reqId -> withdraw request
+ var pendingWithDraws: map[int, tWithDrawReq];
+
+ start state Init {
+ on eSpec_BankBalanceIsAlwaysCorrect_Init goto WaitForWithDrawReqAndResp with Spec_BankBalanceIsAlwaysCorrect_Init;
+ }
+
+ state WaitForWithDrawReqAndResp {
+ on eWithDrawReq do AftereWithDrawReq;
+
+ on eWithDrawResp do AfterWithDrawReq;
+ }
+
+ fun Spec_BankBalanceIsAlwaysCorrect_Init(balance: map[int, int]) {
+ bankBalance = balance;
+ }
+
+ fun AfterWithDrawReq(req: tWithDrawReq) {
+ assert req.accountId in bankBalance,
+ format ("Unknown accountId {0} in the withdraw request. Valid accountIds = {1}", req.accountId, keys(bankBalance));
+ pendingWithDraws[req.rId] = req;
+
+ }
+
+ fun AfterWithDrawResp(resp: tWithDrawResp) {
+ assert resp.accountId in bankBalance,
+ format ("Unknown accountId {0} in the withdraw response!", resp.accountId);
+ assert resp.rId in pendingWithDraws,
+ format ("Unknown rId {0} in the withdraw response!", resp.rId);
+ assert resp.balance >= 10,
+ "Bank balance in all accounts must always be greater than or equal to 10!!";
+
+ if(resp.status == WITHDRAW_SUCCESS)
+ {
+ assert resp.balance == bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount,
+ format ("Bank balance for the account {0} is {1} and not the expected value {2}, Bank is lying!",
+ resp.accountId, resp.balance, bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount);
+ // update the new account balance
+ bankBalance[resp.accountId] = resp.balance;
+ }
+ else
+ {
+ // bank can only reject a request if it will drop the balance below 10
+ assert bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount < 10,
+ format ("Bank must accept the withdraw request for {0}, bank balance is {1}!",
+ pendingWithDraws[resp.rId].amount, bankBalance[resp.accountId]);
+ // if withdraw failed then the account balance must remain the same
+ assert bankBalance[resp.accountId] == resp.balance,
+ format ("Withdraw failed BUT the account balance changed! actual: {0}, bank said: {1}",
+ bankBalance[resp.accountId], resp.balance);
+ }
+
+ }
+
+}
+
+/**************************************************************************
+GuaranteedWithDrawProgress checks the liveness (or progress) property that all withdraw requests
+submitted by the client are eventually responded.
+***************************************************************************/
+spec GuaranteedWithDrawProgress observes eWithDrawReq, eWithDrawResp {
+ // keep track of the pending withdraw requests
+ var pendingWDReqs: set[int];
+
+ start state NopendingRequests {
+ on eWithDrawReq goto PendingReqs with AfterWithDrawReq;
+ }
+
+ hot state PendingReqs {
+ on eWithDrawResp do AfterWithDrawResp;
+
+ on eWithDrawReq goto PendingReqs with AfterWithDrawRespPendingReqs;
+ }
+
+ fun AfterWithDrawReq(req: tWithDrawReq) {
+ pendingWDReqs += (req.rId);
+ }
+
+ fun AfterWithDrawResp(resp: tWithDrawResp) {
+ assert resp.rId in pendingWDReqs, format ("unexpected rId: {0} received, expected one of {1}", resp.rId, pendingWDReqs);
+ pendingWDReqs -= (resp.rId);
+ if(sizeof(pendingWDReqs) == 0) // all requests have been responded
+ goto NopendingRequests;
+ }
+
+ fun AfterWithDrawRespPendingReqs(req: tWithDrawReq) {
+ pendingWDReqs += (req.rId);
+ }
+
+}
+
+
+
+Here is the folder PTst:
+
+Here is the file TestDriver.p:
+
+/*************************************************************
+Machines TestWithSingleClient and TestWithMultipleClients are simple test driver machines
+that configure the system to be checked by the P checker for different scenarios.
+In this case, test the ClientServer system by first randomly initializing the accounts map and
+then checking it with either one Client or with multiple Clients (between 2 and 4).
+*************************************************************/
+
+// Test driver that checks the system with a single Client.
+machine TestWithSingleClient
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // singe client
+ SetupClientServerSystem(1);
+ }
+}
+
+// Test driver that checks the system with multiple Clients.
+machine TestWithMultipleClients
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // multiple clients between (2, 4)
+ SetupClientServerSystem(choose(3) + 2);
+ }
+}
+
+// creates a random map from accountId's to account balance of size `numAccounts`
+fun CreateRandomInitialAccounts(numAccounts: int) : map[int, int]
+{
+ var i: int;
+ var bankBalance: map[int, int];
+ while(i < numAccounts) {
+ bankBalance[i] = choose(100) + 10; // min 10 in the account
+ i = i + 1;
+ }
+ return bankBalance;
+}
+
+/*************************************************************
+Function SetupClientServerSystem takes as input the number of clients to be created and
+configures the ClientServer system by creating the Client and BankServer machines.
+The function also announce the event eSpec_BankBalanceIsAlwaysCorrect_Init to initialize the monitors with initial balance for all accounts.
+*************************************************************/
+// setup the client server system with one bank server and `numClients` clients.
+fun SetupClientServerSystem(numClients: int)
+{
+ var i: int;
+ var server: BankServer;
+ var accountIds: seq[int];
+ var initAccBalance: map[int, int];
+
+ // randomly initialize the account balance for all clients
+ initAccBalance = CreateRandomInitialAccounts(numClients);
+ // create bank server with the init account balance
+ server = new BankServer(initAccBalance);
+
+ // before client starts sending any messages make sure we
+ // initialize the monitors or specifications
+ announce eSpec_BankBalanceIsAlwaysCorrect_Init, initAccBalance;
+
+ accountIds = keys(initAccBalance);
+
+ // create the clients
+ while(i < sizeof(accountIds)) {
+ new Client((serv = server, accountId = accountIds[i], balance = initAccBalance[accountIds[i]]));
+ i = i + 1;
+ }
+}
+
+
+Here is the file Testscript.p:
+
+/* This file contains three different model checking scenarios */
+
+// assert the properties for the single client and single server scenario
+test tcSingleClient [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithSingleClient });
+
+// assert the properties for the two clients and single server scenario
+test tcMultipleClients [main=TestWithMultipleClients]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithMultipleClients });
+
+// assert the properties for the single client and single server scenario but with abstract server
+ test tcAbstractServer [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, AbstractBank, { TestWithSingleClient });
+
+
+
+
+
+
+
+
+Here is another example of a P program:
+
+
+System:
+Consider an espresso coffee machine as a reactive system that must respond correctly to various user inputs. The user interacts with the coffee machine through its control panel.
+The espresso machine consists of two parts: the front-end control panel and the backend coffee maker that actually makes the coffee.
+The control panel presents an interface to the user to perform operations like reset the machine, turn the steamer on and off, request an espresso, and clear the grounds by opening the container.
+The control panel interprets these inputs from the user and sends appropriate commands to the coffee maker.
+
+Correctness Specification:
+By default, the P checker tests whether any event that is received in a state has a handler defined for it, otherwise, it would result in an unhandled event exception.
+If the P checker fails to find a bug, then it implies that the system model can handle any sequence of events generated by the given environment.
+In our coffee machine context, it implies that the coffee machine control panel can appropriately handle any sequence of inputs (button presses) by the user.
+We would also like to check that the coffee machine moves through a desired sequence of states, i.e., WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready.
+
+
+
+The EspressoMachine folder contains the source code for the EspressoMachine project.
+
+The P models (PSrc) for the EspressoMachine example consist of three files:
+1. CoffeeMakerControlPanel.p: Implements the CoffeeMakerControlPanel state machine.
+Basically, the control panel starts in the initial state and kicks off by warming up the coffee maker. After warming is successful, it moves to the ready state where it can either make coffee or start the steamer.
+When asked to make coffee, it first grinds the beans and then brews coffee. In any of these states, if there is an error due to. e.g, no water or no beans, the control panel informs the user of the error and moves to the error state waiting for the user to reset the machine.
+2. CoffeeMaker.p: Implements the CoffeeMaker state machine.
+3. EspressoMachineModules.p: Declares the P module corresponding to EspressoMachine.
+
+
+
+The P Specification (PSpec) for the EspressoMachine project is implemented in Safety.p file.
+
+We define a safety specification, EspressoMachineModesOfOperation that observes the internal state of the EspressoMachine through the events that are announced as the system moves through different states and asserts that it always moves through the desired sequence of states.
+Steady operation: WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready.
+If an error occurs in any of the states above then the EspressoMachine stays in the error state until it is reset and after which it returns to the Warmup state.
+
+
+
+The test scenarios folder for EspressoMachine (PTst) consists of three files: TestDriver.p, TestScript.p, and Users.p.
+The Users.p declares two machines:
+1. SaneUser machine that uses the EspressoMachine with care, pressing the buttons in the right order, and cleaning up the grounds after the coffee is made, and
+2. CrazyUser machine who has never used an espresso machine before, gets too excited, and starts pushing random buttons on the control panel.
+
+
+
+
+Here is the folder PSrc:
+
+Here is the file CoffeeMakerControlPanel.p:
+
+/* Events used by the user to interact with the control panel of the Coffee Machine */
+// event: make espresso button pressed
+event eEspressoButtonPressed;
+// event: steamer button turned off
+event eSteamerButtonOff;
+// event: steamer button turned on
+event eSteamerButtonOn;
+// event: door opened to empty grounds
+event eOpenGroundsDoor;
+// event: door closed after emptying grounds
+event eCloseGroundsDoor;
+// event: reset coffee maker button pressed
+event eResetCoffeeMaker;
+//event: error message from panel to the user
+event eCoffeeMakerError: tCoffeeMakerState;
+//event: coffee machine is ready
+event eCoffeeMakerReady;
+// event: coffee machine user
+event eCoffeeMachineUser: machine;
+
+// enum to represent the state of the coffee maker
+enum tCoffeeMakerState {
+ NotWarmedUp,
+ Ready,
+ NoBeansError,
+ NoWaterError
+}
+
+/*
+CoffeeMakerControlPanel acts as the interface between the CoffeeMaker and User.
+It converts the inputs from the user to appropriate inputs to the CoffeeMaker and sends responses to the user.
+It transitions from one state to another based on the events received from the User and the CoffeeMaker machine.
+In all the states, it appropriately handles different events that can be received, including ignoring or deferring them if they are stale events.
+*/
+machine CoffeeMakerControlPanel
+{
+ var coffeeMaker: EspressoCoffeeMaker;
+ var coffeeMakerState: tCoffeeMakerState;
+ var currentUser: machine;
+
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ coffeeMakerState = NotWarmedUp;
+ coffeeMaker = new EspressoCoffeeMaker(this);
+ WaitForUser();
+ goto WarmUpCoffeeMaker;
+ }
+
+ // block until a user shows up
+ fun WaitForUser() {
+ receive {
+ case eCoffeeMachineUser: (user: machine) {
+ currentUser = user;
+ }
+ }
+ }
+
+ state WarmUpCoffeeMaker {
+ entry WarmUpCoffeeMaker_Entry;
+
+ on eWarmUpCompleted goto CoffeeMakerReady;
+
+ // grounds door is opened or closed will handle it later after the coffee maker has warmed up
+ defer eOpenGroundsDoor, eCloseGroundsDoor;
+ // ignore these inputs from users until the maker has warmed up.
+ ignore eEspressoButtonPressed, eSteamerButtonOff, eSteamerButtonOn, eResetCoffeeMaker;
+ // ignore these errors and responses as they could be from previous state
+ ignore eNoBeansError, eNoWaterError, eGrindBeansCompleted;
+ }
+
+ fun WarmUpCoffeeMaker_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInWarmUpState;
+
+ BeginHeatingCoffeeMaker();
+ }
+
+ state CoffeeMakerReady {
+ entry CoffeeMakerReady_Entry;
+
+ on eOpenGroundsDoor goto CoffeeMakerDoorOpened;
+ on eEspressoButtonPressed goto CoffeeMakerRunGrind;
+ on eSteamerButtonOn goto CoffeeMakerRunSteam;
+
+ // ignore these out of order commands, these must have happened because of an error
+ // from user or sensor
+ ignore eSteamerButtonOff, eCloseGroundsDoor;
+
+ // ignore commands and errors as they are from previous state
+ ignore eWarmUpCompleted, eResetCoffeeMaker, eNoBeansError, eNoWaterError;
+ }
+
+ fun CoffeeMakerReady_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInReadyState;
+
+ coffeeMakerState = Ready;
+ send currentUser, eCoffeeMakerReady;
+ }
+
+ state CoffeeMakerRunGrind {
+ entry CoffeeMakerRunGrind_Entry;
+
+ on eNoBeansError goto EncounteredError with AfterNoBeansError;
+
+ on eNoWaterError goto EncounteredError with AfterNoWaterError;
+
+ on eGrindBeansCompleted goto CoffeeMakerRunEspresso;
+
+ defer eOpenGroundsDoor, eCloseGroundsDoor, eEspressoButtonPressed;
+
+ // Can't make steam while we are making espresso
+ ignore eSteamerButtonOn, eSteamerButtonOff;
+
+ // ignore commands that are old or cannot be handled right now
+ ignore eWarmUpCompleted, eResetCoffeeMaker;
+ }
+
+ fun CoffeeMakerRunGrind_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInBeansGrindingState;
+
+ GrindBeans();
+ }
+
+ fun AfterNoBeansError() {
+ coffeeMakerState = NoBeansError;
+ print "No beans to grind! Please refill beans and reset the machine!";
+ }
+
+ fun AfterNoWaterError() {
+ coffeeMakerState = NoWaterError;
+ print "No Water! Please refill water and reset the machine!";
+ }
+
+ state CoffeeMakerRunEspresso {
+ entry CoffeeMakerRunEspresso_Entry;
+
+ on eEspressoCompleted goto CoffeeMakerReady with AfterEspressoCompleted;
+
+ on eNoWaterError goto EncounteredError with AfterNoWaterError;
+
+ // the user commands will be handled next after finishing this espresso
+ defer eOpenGroundsDoor, eCloseGroundsDoor, eEspressoButtonPressed;
+
+ // Can't make steam while we are making espresso
+ ignore eSteamerButtonOn, eSteamerButtonOff;
+
+ // ignore old commands and cannot reset when making coffee
+ ignore eWarmUpCompleted, eResetCoffeeMaker;
+ }
+
+ fun CoffeeMakerRunEspresso_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eInCoffeeBrewingState;
+
+ StartEspresso();
+ }
+
+ fun AfterEspressoCompleted() {
+ send currentUser, eEspressoCompleted;
+ }
+
+ state CoffeeMakerRunSteam {
+ entry StartSteamer;
+
+ on eSteamerButtonOff goto CoffeeMakerReady with StopSteamer;
+
+ on eNoWaterError goto EncounteredError with AfterNoWaterError;
+
+ // user might have cleaned grounds while steaming
+ defer eOpenGroundsDoor, eCloseGroundsDoor;
+
+ // can't make espresso while we are making steam
+ ignore eEspressoButtonPressed, eSteamerButtonOn;
+ }
+
+ state CoffeeMakerDoorOpened {
+ on eCloseGroundsDoor do AfterCloseGroundsDoor;
+
+ // grounds door is open cannot handle these requests just ignore them
+ ignore eEspressoButtonPressed, eSteamerButtonOn, eSteamerButtonOff;
+ }
+
+ fun AfterCloseGroundsDoor() {
+ assert coffeeMakerState != NotWarmedUp;
+ assert coffeeMakerState == Ready;
+ goto CoffeeMakerReady;
+ }
+
+ state EncounteredError {
+ entry EncounteredError_Entry;
+
+ on eResetCoffeeMaker goto WarmUpCoffeeMaker with AfterResetCoffeeMaker;
+
+ // error, ignore these requests until reset.
+ ignore eEspressoButtonPressed, eSteamerButtonOn, eSteamerButtonOff,
+ eOpenGroundsDoor, eCloseGroundsDoor, eWarmUpCompleted, eEspressoCompleted, eGrindBeansCompleted;
+
+ // ignore other simultaneous errors
+ ignore eNoBeansError, eNoWaterError;
+ }
+
+ fun EncounteredError_Entry() {
+ // inform the specification about current state of the coffee maker
+ announce eErrorHappened;
+
+ // send the error message to the client
+ send currentUser, eCoffeeMakerError, coffeeMakerState;
+ }
+
+ fun AfterResetCoffeeMaker() {
+ // inform the specification about current state of the coffee maker
+ announce eResetPerformed;
+ }
+
+ fun BeginHeatingCoffeeMaker() {
+ // send an event to maker to start warming
+ send coffeeMaker, eWarmUpReq;
+ }
+
+ fun StartSteamer() {
+ // send an event to maker to start steaming
+ send coffeeMaker, eStartSteamerReq;
+ }
+
+ fun StopSteamer() {
+ // send an event to maker to stop steaming
+ send coffeeMaker, eStopSteamerReq;
+ }
+
+ fun GrindBeans() {
+ // send an event to maker to grind beans
+ send coffeeMaker, eGrindBeansReq;
+ }
+
+ fun StartEspresso() {
+ // send an event to maker to start espresso
+ send coffeeMaker, eStartEspressoReq;
+ }
+}
+
+
+Here is the file CoffeeMaker.p:
+
+/* Requests or operations from the controller to coffee maker */
+
+// event: warmup request when the coffee maker starts or resets
+event eWarmUpReq;
+// event: grind beans request before making coffee
+event eGrindBeansReq;
+// event: start brewing coffee
+event eStartEspressoReq;
+// event start steamer
+event eStartSteamerReq;
+// event: stop steamer
+event eStopSteamerReq;
+
+/* Responses from the coffee maker to the controller */
+// event: completed grinding beans
+event eGrindBeansCompleted;
+// event: completed brewing and pouring coffee
+event eEspressoCompleted;
+// event: warmed up the machine and ready to make coffee
+event eWarmUpCompleted;
+
+/* Error messages from the coffee maker to control panel or controller*/
+// event: no water for coffee, refill water!
+event eNoWaterError;
+// event: no beans for coffee, refill beans!
+event eNoBeansError;
+// event: the heater to warm the machine is broken!
+event eWarmerError;
+
+/*****************************************************
+EspressoCoffeeMaker receives requests from the control panel of the coffee machine and
+based on its state e.g., whether heater is working, or it has beans and water, the maker responds
+back to the controller if the operation succeeded or errored.
+*****************************************************/
+machine EspressoCoffeeMaker
+{
+ // control panel of the coffee machine that sends inputs to the coffee maker
+ var controller: CoffeeMakerControlPanel;
+
+ start state WaitForRequests {
+ entry WaitForRequests_Entry;
+
+ on eWarmUpReq do AftereWithDrawReq;
+
+ on eGrindBeansReq do AfterGrindBeansReq;
+
+ on eStartEspressoReq do AfterStartEspressoReq;
+
+ on eStartSteamerReq do AfterStartSteamerReq;
+
+ on eStopSteamerReq do AfterStopSteamerReq;
+ }
+
+ fun WaitForRequests_Entry(_controller: CoffeeMakerControlPanel) {
+ controller = _controller;
+ }
+
+ fun AfterWarmUpReq() {
+ send controller, eWarmUpCompleted;
+ }
+
+ fun AfterGrindBeansReq() {
+ if (!HasBeans()) {
+ send controller, eNoBeansError;
+ } else {
+ send controller, eGrindBeansCompleted;
+ }
+ }
+
+ fun AfterStartEspressoReq() {
+ if (!HasWater()) {
+ send controller, eNoWaterError;
+ } else {
+ send controller, eEspressoCompleted;
+ }
+ }
+
+ fun AfterStartSteamerReq() {
+ if (!HasWater()) {
+ send controller, eNoWaterError;
+ }
+ }
+
+ fun AfterStopSteamerReq() {
+ /* do nothing, steamer stopped */
+ }
+
+ // nondeterministic functions to trigger different behaviors
+ fun HasBeans() : bool { return $; }
+ fun HasWater() : bool { return $; }
+}
+
+
+Here is the file EspressoMachineModules.p:
+
+module EspressoMachine = { CoffeeMakerControlPanel, EspressoCoffeeMaker };
+
+
+
+Here is the folder PSpec:
+
+Here is the file Safety.p:
+
+/* Events used to inform monitor about the internal state of the CoffeeMaker */
+event eInWarmUpState;
+event eInReadyState;
+event eInBeansGrindingState;
+event eInCoffeeBrewingState;
+event eErrorHappened;
+event eResetPerformed;
+
+/*********************************************
+We would like to ensure that the coffee maker moves through the expected modes of operation.
+We want to make sure that the coffee maker always transitions through the following sequence of states:
+Steady operation:
+ WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready
+With Error:
+If an error occurs in any of the states above, then the Coffee machine stays in the error state until
+it is reset and after which it returns to the Warmup state.
+
+The EspressoMachineModesOfOperation spec machine observes events and ensures that the system moves through the states defined by the monitor.
+Note that if the system allows (has execution as) a sequence of events that are not accepted by the monitor (i.e., the monitor throws an unhandled event exception), then the system does not satisfy the desired specification.
+Hence, this monitor can be thought of accepting only those behaviors of the system that follow the sequence of states modelled by the spec machine.
+For example, if the system moves from Ready to CoffeeMaking state directly without Grinding, then the monitor will raise an ALARM!
+**********************************************/
+spec EspressoMachineModesOfOperation
+observes eInWarmUpState, eInReadyState, eInBeansGrindingState, eInCoffeeBrewingState, eErrorHappened, eResetPerformed
+{
+ start state StartUp {
+ on eInWarmUpState goto WarmUp;
+ }
+
+ state WarmUp {
+ on eErrorHappened goto Error;
+ on eInReadyState goto Ready;
+ }
+
+ state Ready {
+ ignore eInReadyState;
+ on eInBeansGrindingState goto BeanGrinding;
+ on eErrorHappened goto Error;
+ }
+
+ state BeanGrinding {
+ on eInCoffeeBrewingState goto MakingCoffee;
+ on eErrorHappened goto Error;
+ }
+
+ state MakingCoffee {
+ on eInReadyState goto Ready;
+ on eErrorHappened goto Error;
+ }
+
+ state Error {
+ on eResetPerformed goto StartUp;
+ ignore eErrorHappened;
+ }
+}
+
+
+
+Here is the folder PTst:
+
+Here is the file TestDriver.p:
+
+machine TestWithSaneUser
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // create a sane user
+ new SaneUser(new CoffeeMakerControlPanel());
+ }
+}
+
+machine TestWithCrazyUser
+{
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry() {
+ // create a crazy user
+ new CrazyUser((coffeeMaker = new CoffeeMakerControlPanel(), nOps = 5));
+ }
+}
+
+
+Here is the file Testscript.p:
+
+// there are two test cases defined in the EspressoMachine project.
+test tcSaneUserUsingCoffeeMachine [main=TestWithSaneUser]:
+ assert EspressoMachineModesOfOperation in (union { TestWithSaneUser }, EspressoMachine, Users);
+
+test tcCrazyUserUsingCoffeeMachine [main=TestWithCrazyUser]:
+ assert EspressoMachineModesOfOperation in (union { TestWithCrazyUser }, EspressoMachine, Users);
+
+
+Here is the file Users.p:
+
+/*
+A SaneUser who knows how to use the CoffeeMaker
+*/
+machine SaneUser {
+ var coffeeMakerPanel: CoffeeMakerControlPanel;
+ var cups: int;
+ start state Init {
+ entry Init_Entry;
+ }
+
+ fun Init_Entry(coffeeMaker: CoffeeMakerControlPanel) {
+ coffeeMakerPanel = coffeeMaker;
+ // inform control panel that I am the user
+ send coffeeMakerPanel, eCoffeeMachineUser, this;
+ // want to make 2 cups of espresso
+ cups = 2;
+
+ goto LetsMakeCoffee;
+ }
+
+ state LetsMakeCoffee {
+ entry LetsMakeCoffee_Entry;
+ }
+
+ fun LetsMakeCoffee_Entry() {
+ while (cups > 0)
+ {
+ // lets wait for coffee maker to be ready
+ WaitForCoffeeMakerToBeReady();
+
+ // press Espresso button
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressEspressoButton);
+
+ // check the status of the machine
+ receive {
+ case eEspressoCompleted: {
+ // lets make the next coffee
+ cups = cups - 1;
+ }
+ case eCoffeeMakerError: (status: tCoffeeMakerState){
+
+ // lets fill the beans or water and reset the machine
+ // and go back to making espresso
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressResetButton);
+ }
+ }
+ }
+
+ // I am a good user so I will clear the coffee grounds before leaving.
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_ClearGrounds);
+
+ // am done, let me exit
+ raise halt;
+ }
+}
+
+enum tCoffeeMakerOperations {
+ CM_PressEspressoButton,
+ CM_PressSteamerButton,
+ CM_PressResetButton,
+ CM_ClearGrounds
+}
+
+/*
+A crazy user who gets excited by looking at a coffee machine and starts stress testing the machine
+by pressing all sorts of random button and opening/closing doors
+*/
+// TODO: We do not support global constants currently, they can be encoded using global functions.
+
+machine CrazyUser {
+ var coffeeMakerPanel: CoffeeMakerControlPanel;
+ var numOperations: int;
+ start state StartPressingButtons {
+ entry StartPressingButtons_Entry;
+
+ // I will ignore all the responses from the coffee maker
+ ignore eCoffeeMakerError, eEspressoCompleted, eCoffeeMakerReady;
+ }
+
+ fun StartPressingButtons_Entry(config: (coffeeMaker: CoffeeMakerControlPanel, nOps: int)) {
+ var pickedOps: tCoffeeMakerOperations;
+
+ numOperations = config.nOps;
+ coffeeMakerPanel = config.coffeeMaker;
+
+ // inform control panel that I am the user
+ send coffeeMakerPanel, eCoffeeMachineUser, this;
+
+ while(numOperations > 0)
+ {
+ pickedOps = PickRandomOperationToPerform();
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, pickedOps);
+ numOperations = numOperations - 1;
+ }
+ }
+
+ // Pick a random enum value (hacky work around)
+ // Currently, the choose operation does not support choose over enum value
+ fun PickRandomOperationToPerform() : tCoffeeMakerOperations {
+ var op_i: int;
+ op_i = choose(3);
+ if(op_i == 0)
+ return CM_PressEspressoButton;
+ else if(op_i == 1)
+ return CM_PressSteamerButton;
+ else if(op_i == 2)
+ return CM_PressResetButton;
+ else
+ return CM_ClearGrounds;
+ }
+}
+
+/* Function to perform an operation on the CoffeeMaker */
+fun PerformOperationOnCoffeeMaker(coffeeMakerCP: CoffeeMakerControlPanel, CM_Ops: tCoffeeMakerOperations)
+{
+ if(CM_Ops == CM_PressEspressoButton) {
+ send coffeeMakerCP, eEspressoButtonPressed;
+ }
+ else if(CM_Ops == CM_PressSteamerButton) {
+ send coffeeMakerCP, eSteamerButtonOn;
+ // wait for some time and then release the button
+ send coffeeMakerCP, eSteamerButtonOff;
+ }
+ else if(CM_Ops == CM_ClearGrounds)
+ {
+ send coffeeMakerCP, eOpenGroundsDoor;
+ // empty ground and close the door
+ send coffeeMakerCP, eCloseGroundsDoor;
+ }
+ else if(CM_Ops == CM_PressResetButton)
+ {
+ send coffeeMakerCP, eResetCoffeeMaker;
+ }
+}
+
+fun WaitForCoffeeMakerToBeReady() {
+ receive {
+ case eCoffeeMakerReady: {}
+ case eCoffeeMakerError: (status: tCoffeeMakerState){ raise halt; }
+ }
+}
+module Users = { SaneUser, CrazyUser };
+
+
+
+
+
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/context_files/modular/p_program_structure_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_program_structure_guide.txt
new file mode 100644
index 0000000000..fb11877cb5
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_program_structure_guide.txt
@@ -0,0 +1,79 @@
+# P Project Structure Generation Instructions
+
+## MANDATORY PROJECT STRUCTURE
+
+You MUST generate a P project with EXACTLY this folder and file structure. Do NOT deviate from this structure:
+
+## FOLDER CONTENTS SPECIFICATION
+
+### PSrc Folder - Implementation Files
+**Purpose**: Contains all state machines representing the implementation (model) of the system or protocol to be verified or tested.
+
+**Required Files**:
+1. **Individual Machine Files**: One `.p` file per major machine/component
+ - `.p` (e.g., `Coordinator.p`, `Participant.p`, `Client.p`)
+ - NO specs, NO tests, NO modules in these files
+
+2. **Modules.p**: Contains module definitions that group related machines
+
+**Note**: Enums_Types_Events.p is generated separately and provided as context - DO NOT generate this file.
+
+### PSpec Folder - Specification Files
+**Purpose**: contains all the specifications representing the correctness properties that the system must satisfy.
+
+**Required Files**:
+- `.p` files (e.g., `SafetySpec.p`, `LivenessSpec.p`)
+- Each file contains monitor specifications using `spec` keyword
+
+
+### PTst Folder - Test Files
+**Purpose**: Contains all the environment or test harness state machines that model the non-deterministic scenarios under which we want to check that the system model in PSrc satisfies the specifications in PSpec. P allows writing different model checking scenarios as test-cases.
+
+
+**Required Files**:
+
+1. **TestDriver.p**: TestDrivers are collections of state machines that implement the test harnesses (or environment state machines) for different test scenarios.
+
+2. **TestScript.p**: TestScripts are collections of test cases that are automatically run by the P checker. P allows programmers to write different test cases.
+
+
+## GENERATION RULES
+
+### MANDATORY REQUIREMENTS:
+1. **Create EXACTLY these 3 folders**: PSrc, PSpec, PTst
+2. **PSrc must contain**: Individual machine files + Modules.p (EventTypesAndEnums.p is provided separately)
+3. **PSpec must contain**: Specification monitor files
+4. **PTst must contain**: TestDriver.p + TestScript.p
+5. **DO NOT generate EventTypesAndEnums.p** - this is provided as context from separate generator
+
+
+## DESIGN DOCUMENT ANALYSIS
+
+When given a design document:
+
+1. **Identify Components**: Extract all system components → Create individual machine files in PSrc
+2. **Identify Properties**: Extract safety/liveness requirements → Create spec files in PSpec
+3. **Identify Test Scenarios**: Extract testing requirements → Create test cases in PTst
+4. **Create Modules**: Group related machines → Add to Modules.p
+
+**Note**: Events, types, and enums are provided via EventTypesAndEnums.p context - do not extract or generate these.
+
+## EXAMPLE PROJECT STRUCTURE
+
+```
+TwoPhaseCommit/
+├── PSrc/
+│ ├── Coordinator.p # Coordinator machine only
+│ ├── Participant.p # Participant machine only
+│ ├── Timer.p # Timer machine only
+│ └── Modules.p # Module definitions
+├── PSpec/
+│ ├── AtomicitySpec.p # Atomicity safety monitor
+│ └── ProgressSpec.p # Progress liveness monitor
+└── PTst/
+ ├── TestDriver.p # Test environment machines
+ └── TestScript.p # Test case definitions
+
+Note: EventTypesAndEnums.p is provided separately as context
+```
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_spec_monitors_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_spec_monitors_guide.txt
new file mode 100644
index 0000000000..c1ef04dd81
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_spec_monitors_guide.txt
@@ -0,0 +1,86 @@
+Here is the P specification monitors guide:
+
+P specification monitors or spec machines are used to write the safety and liveness specifications the system must satisfy for correctness.
+
+Syntactically, machines and spec machines in P are very similar in terms of the state machine structure. But, they have some key differences:
+1. spec machines in P are observer machines (imagine runtime monitors); they observe a set of events in the execution of the system and, based on these observed events (may keep track of local states), assert the desired global safety and liveness specifications.
+2. Since spec machines are observer machines, they cannot have any side effects on the system behavior and hence, spec machines cannot perform send, receive, new, and annouce.
+3. spec machines are global machines; in other words, there is only a single instance of each monitor created at the start of the execution of the system.
+4. Since dynamic creation of monitors is not supported, spec machines cannot use this expression described in tags.
+5. spec machines are synchronously composed with the system that is monitored. The way this is achieved is: each time there is a send or announce of an event during the execution of a system, all the monitors or specifications that are observing that event are executed synchronously at that point.
+6. Another way to imagine this is: just before send or annouce of an event, we deliver this event to all the monitors that are observing the event and synchronously execute the monitors at that point.
+7. spec machines can have hot and cold annotations on their states to model liveness specifications.
+8. $, $$, this, new, send, announce, receive, and pop are NOT allowed in monitors.
+9. Entry functions in spec machines CANNOT take any parameter.
+10. Never generate empty functions or functions with only comments. Every function declaration must include complete, working implementation with actual executable code. If you create or reference a function, it must be fully implemented, not just a placeholder or TODO comment.
+
+Syntax: spec iden observes eventsList statemachineBody;
+iden is the name of the spec machine,
+eventsList is the comma separated list of events observed by the spec machine, and
+statemachineBody is the implementation of the specification and its grammar is similar to the grammar in the tags.
+
+
+Here is a specification that checks a very simple global invariant that all eRequest events that are being sent by clients in the system have a globally monotonically increasing rId:
+
+/*******************************************************************
+ReqIdsAreMonotonicallyIncreasing observes the eRequest event and
+checks that the payload (Id) associated with the requests sent
+by all concurrent clients in the system is always globally
+monotonically increasing by 1
+*******************************************************************/
+spec ReqIdsAreMonotonicallyIncreasing observes eRequest {
+ // keep track of the Id in the previous request
+ var previousId : int;
+ start state Init {
+ on eRequest do AfterRequest;
+ }
+
+ fun CheckIfReqIdsAreMonotonicallyIncreasing(req: tRequest) {
+ assert req.rId > previousId, format ("Request Ids not monotonically increasing, got {0}, previously seen Id was {1}", req.rId, previousId);
+ previousId = req.rId;
+ }
+}
+
+
+
+
+1. hot annotation can be used on states to mark them as intermediate or error states.
+2. The key idea is that the system satisfies a liveness specification if, at the end of the execution, the monitor is not in a hot state.
+3. Properties like 'eventually something holds' or 'every event X is eventually followed by Y' or 'eventually the system enters a convergence state' can be specified by marking the intermediate state as hot states and the checker checks that all the executions of the system eventually end in a non-hot state.
+4. If there exists an execution that fails to come out of a hot state eventually, then it is flagged as a potential liveness violation.
+
+Here is a specification that checks the global liveness property that every event eRequest is eventually followed by a corresponding successful eResponse event:
+
+/**************************************************************************
+GuaranteedProgress observes the eRequest and eResponse events;
+it asserts that every request is always responded by a successful response.
+***************************************************************************/
+spec GuaranteedProgress observes eRequest, eResponse {
+ // keep track of the pending requests
+ var pendingReqs: set[int];
+ start state NopendingRequests {
+ on eRequest goto PendingReqs with AddPendingRequest;
+ }
+
+ hot state PendingReqs {
+ on eResponse do AfterResponse;
+ on eRequest goto PendingReqs with AddPendingRequest;
+ }
+
+ fun AddPendingRequest(req: tRequest) {
+ pendingReqs += (req.rId);
+ }
+
+ fun AfterResponse(resp: tResponse) {
+ assert resp.rId in pendingReqs, format ("unexpected rId: {0} received, expected one of {1}", resp.rId, pendingReqs);
+ if(resp.status == SUCCESS)
+ {
+ pendingReqs -= (resp.rId);
+ if(sizeof(pendingReqs) == 0) // requests already responded
+ goto NopendingRequests;
+ }
+ }
+}
+
+
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_statements_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_statements_guide.txt
new file mode 100644
index 0000000000..1e1df4030e
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_statements_guide.txt
@@ -0,0 +1,436 @@
+Here is the P statements and expressions guide:
+
+P provides various statements and expressions for state machines to communicate, control flow, and perform operations. This guide covers the key statements and expressions in P language.
+
+
+P Statements Grammar:
+```
+statement : { statement* } # CompoundStmt
+| assert expr (, expr)? ; # AssertStmt
+| print expr ; # PrintStmt
+| foreach (iden in expr) statement # ForeachStmt
+| while ( expr ) statement # WhileStmt
+| if ( expr ) statement (else statement)? # IfThenElseStmt
+| break ; # BreakStmt
+| continue ; # ContinueStmt
+| return expr? ; # ReturnStmt
+| lvalue = rvalue ; # AssignStmt
+| lvalue += ( expr, rvalue ) ; # InsertStmt
+| lvalue += ( rvalue ) ; # AddStmt
+| lvalue -= rvalue ; # RemoveStmt
+| new iden (rvalue?) ; # CtorStmt
+| iden ( rvalueList? ) ; # FunCallStmt
+| raise expr (, rvalue)? ; # RaiseStmt
+| send expr, expr (, rvalue)? ; # SendStmt
+| announce expr (, rvalue)? ; # AnnounceStmt
+| goto iden (, rvalue)? ; # GotoStmt
+| receive { recvCase+ } # ReceiveStmt
+;
+```
+
+P Expressions Grammar:
+```
+expr :
+| (expr) # ParenExpr
+| primitiveExpr # PrimitiveExpr
+| formatedString # FormatStringExpr
+| (tupleBody) # TupleExpr
+| (namedTupleBody) # NamedTupleExpr
+| expr.int # TupleAccessExpr
+| expr.iden # NamedTupleAccessExpr
+| expr[expr] # AccessExpr
+| keys(expr) # KeysExpr
+| values(expr) # ValuesExpr
+| sizeof(expr) # SizeofExpr
+| expr in expr # ContainsExpr
+| default(type) # DefaultExpr
+| new iden ( rvalueList? ) # NewExpr
+| iden ( rvalueList? ) # FunCallExpr
+| (- | !) expr # UnaryExpr
+| expr (* | / | + | -) expr # ArithBinExpr
+| expr (== | !=) expr # EqualityBinExpr
+| expr (&& | ||) expr # LogicalBinExpr
+| expr (< | > | >= | <= ) expr # CompBinExpr
+| expr as type # CastExpr
+| expr to type # CoerceExpr
+| choose ( expr? ) # ChooseExpr
+;
+```
+
+
+Here is an example of assert statement that asserts that the requestId is always greater than 1 and is in the set of all requests:
+
+assert (requestId > 1) && (requestId in allRequestsSet);
+
+
+Here is an example of assert statement with error message:
+
+assert x >= 0, "Expected x to be always positive";
+
+
+Here is an example of assert with formatted error message:
+assert (requestId in allRequestsSet),
+format ("requestId {0} is not in the requests set = {1}", requestId, allRequestsSet);
+
+
+
+
+Print statements can be used for writing or printing log messages into the error traces (especially for debugging purposes).
+Syntax:: print expr;
+The print statement must have an expression of type string.
+
+Here is an example of print statement that prints "Hello World!" in the execution trace log:
+
+print "Hello World!";
+
+
+Here is an example of print statement that prints formatted string message "Hello World to You!!" in the execution trace log:
+
+x = "You";
+print format("Hello World to {0}!!", x);
+
+
+
+
+While statement in P is just like while loops in other popular programming languages like C, C# or Java.
+Syntax:: while (expr) statement
+expr is the conditional boolean expression and statement could be any P statement.
+
+Here is an example of while loop:
+
+i = 0;
+while (i < 10)
+{
+ i = i + 1;
+}
+
+
+Here is an example of while loop iterating over collection:
+
+i = 0;
+while (i < sizeof(s))
+{
+ print s[i];
+ i = i + 1;
+}
+
+
+
+
+IfThenElse statement in P is just like conditional if statements in other popular programming languages like C, C# or Java.
+Syntax:: if(expr) statement (else statement)?
+expr is the conditional boolean expression and statement could be any P statement. The else block is optional.
+
+
+if(x > 10) {
+ x = x + 20;
+}
+
+
+
+if(x > 10)
+{
+ x = 0;
+}
+else
+{
+ x = x + 1;
+}
+
+
+
+
+break and continue statements in P are just like in other popular programming languages like C, C# or Java to break out of the while loop or to continue to the next iteration of the loop respectively.
+
+while(true) {
+ if(x == 10)
+ break;
+ x = x + 1;
+}
+
+
+
+while(true) {
+ if(x == 10) // skip the loop when x is 10
+ continue;
+}
+
+
+
+
+1. return statement in P can be used to return (or return a value) from any function.
+2. return statement is written in the function body.
+3. If a function has a returnType, the function should necessarily contain a return statement in the function body.
+
+Here is an example of return statement:
+
+fun IncrementX() {
+ if(x > MAX_INT)
+ return;
+ x = x + 1;
+}
+
+
+Here is an example of return value statement:
+
+fun Max(x: int, y: int) : int{
+if(x > y)
+ return x;
+else
+ return y;
+}
+
+
+
+
+P has value semantics or copy-by-value semantics and does not support any notion of references.
+
+Syntax:: leftvalue = rightvalue;
+1. Note that because of value semantics, assignment in P copies the value of the rightvalue into leftvalue.
+2. leftvalue could be any variable, a tuple field access, or an element in a collection.
+3. rightvalue could be any expression that evaluates to the same type as lvalue.
+4. In P language syntax, variable assignment CANNOT be combined with variable declaration.
+5. Compound assignment operators (+=, -=) are NOT supported for primitive type variables.
+
+Here is an example of assignment:
+
+var a: seq[string];
+var b: seq[string];
+b += (0, "b");
+a = b; // copy value
+a += (1, "a");
+print a; // will print ["b", "a"]
+print b; // will print ["b"]
+
+
+Here is another example of assignment:
+
+a = 10;
+s[i] = 20;
+tup1.a = "x";
+tup2.0 = 10;
+t = foo();
+
+
+Here is an INCORRECT example of assignment:
+
+var i: int;
+var requestId = 5; // Direct assignment without variable declaration is incorrect
+var availableServer: machine = ChooseAvailableServer(); // Incorrect to combine declaration and assignment
+i += 1; // operator += not supported for int
+
+
+
+
+New statement is used to create an instance of a machine in P.
+Syntax:: new iden (rvalue?);
+
+Here is an example that uses new to create a dynamic instance of a Client machine
+new Client();
+
+
+
+
+1. The statement raise e, v; terminates the evaluation of the function raising an event e with payload v.
+2. The control of the state machine jumps to end of the function and the state machine immediately handles the raised event.
+
+Syntax:: raise expr (, rvalue)?;
+rvalue should be same as the payloadType of expr and should STRICTLY be a NAMED TUPLE as detailed in tags.
+
+Here is an example of raise event:
+
+fun handleRequest(req: tRequest)
+{
+ // ohh, this is a Add request and I have a event handler for it
+ if(req.type == "Add")
+ raise eAddOperation; // terminates function
+
+ assert req.type != "Add"; // valid
+}
+
+state HandleRequest {
+ on eAddOperation do AddOperation;
+}
+
+
+
+
+1. Send statement is one of the most important statements in P as it is used to send messages to other state machines.
+2. Send takes as argument a triple send t, e, v, where t is a reference to the target state machine, e is the event sent and v is the associated payload.
+3. Sends in P are asynchronous and non-blocking. Statement send t, e, v enqueues the event e with payload v into the target machine t's message buffer.
+4. Within EACH machine where you write the send statement, declare variable for the target machine t with the same type as the target machine. If the type for target machine does not exist, declare t as a machine.
+5. t should ALWAYS be a machine and v should ALWAYS be of the same type as the payload type defined for the event e. Do NOT use nested named tuples as the payload v when the payload type is a single field named tuple.
+
+Syntax:: send lvalue, expr (, rvalue)?;
+lvalue should STRICTLY be of the same type as the target machine t.
+rvalue should STRICTLY be of the same type as the payload type defined for the event. Do NOT use nested named tuples as the payload when the payload type is a single field named tuple.
+
+Here is an example of send event with no payload:
+
+machine BankServer {
+ // inside machine
+}
+
+machine Database {
+ // BankServer is a state machine
+ var server: BankServer;
+
+ fun AfterReadQuery(query: (accountId: int)) {
+ assert query.accountId in balance, "Invalid accountId received in the read query!";
+ send server, eReadQueryResp;
+ }
+}
+
+
+Here is an example of send event with payload:
+
+var server: BankServer;
+send server, eReadQueryResp, (accountId = query.accountId, balance = balance[query.accountId]);
+
+
+Here is an INCORRECT example of send event with payload:
+
+// Missing names in the tuple
+send server, eReadQueryResp, (query.accountId, balance[query.accountId]);
+
+
+
+
+1. Announce is used to publish messages to specification monitors in P.
+2. Each monitor observes a set of events and whenever a machine sends an event that is in the observes set of a monitor then it is synchronously delivered to the monitor. Announce can be used to publish an event to all the monitors that are observing that event.
+3. Announce only delivers events to specification monitors (not state machines) and hence has no side effect on the system behavior.
+Syntax:: annouce eventName (, rvalue)?;
+rvalue should STRICTLY be of the same type as the payload type defined for the event. Do NOT use nested named tuples as the payload when the payload type is a single field named tuple.
+
+Here is an example of announce event with payload:
+
+// Consider a specification monitor that continuously observes eStateUpdate event to keep track of the system state and then asserts a required property when the system converges.
+spec CheckConvergedState observes eStateUpdate, eSystemConverged {
+ // something inside spec
+}
+
+//announce statement can be used to inform the monitor when the system has converged and that we should assert the global specification.
+announce eSystemConverged, payload;
+
+
+
+
+1. Goto statement can be used to jump to a particular state.
+2. On executing a goto, the state machine exits the current state (terminating the execution of the current function) and enters the target state.
+3. Goto statement should ALWAYS be written WITHIN a function body.
+4. The optional payload accompanying the goto statement becomes the input parameter to the function at entry of the target state.
+Syntax:: goto stateName (, rvalue)?;
+
+Here is an example of goto:
+
+start state Init {
+ on eProcessRequest goto SendPingsToAllNodes;
+}
+
+state SendPingsToAllNodes {
+ // do something
+}
+
+state {
+ on eFailure, eCancelled goto Done;
+}
+
+state Done {
+ // do something
+}
+
+
+Here is an example of goto with payload:
+
+state ServiceRequests {
+ fun processRequest(req: tRequest) {
+ // process request with some logic
+ lastReqId = req.Id;
+ goto WaitForRequests, lastReqId;
+ }
+}
+
+state WaitForRequests {
+ entry AfterRequest_Entry;
+}
+
+fun AfterRequest_Entry(lastReqId: int) {
+ // do something
+}
+
+
+
+
+1. Receive statements in P are used to perform blocking await/receive for a set of events inside a function.
+2. Each receive statement can block or wait on a set of events, all other events are automatically deferred by the state machine.
+3. On receiving an event that the receive is blocking on (case blocks), the state machine unblocks, executes the corresponding case-handler and resumes executing the next statement after receive.
+
+Syntax::
+receive { recvCase+ }
+/* case block inside a receive statement */
+recvCase : case eventList : anonFunction
+
+Here is an example of receive awaiting a single event:
+
+fun AcquireLock(lock: machine)
+{
+ send lock, eAcquireLock;
+ receive {
+ case eLockGranted: (result: tResponse) { /* case handler */ }
+ }
+ print "Lock Acquired!"
+ // Note that when executing the AcquireLock function the state machine blocks at the receive statement, it automatically defers all the events except the eLockGranted event.
+ // On receiving the eLockGranted, the case-handler is executed and then the print statement.
+}
+
+
+Here is an example of receive awaiting multiple events:
+
+fun WaitForTime(timer: Timer, time: int)
+{
+ var success: bool;
+ send timer, eStartTimer, time;
+ receive {
+ case eTimeOut: { success = true; }
+ case eStartTimerFailed: { success = false; }
+ }
+ if (success) print "Successfully waited!"
+ // Note that when executing the WaitForTime function the state machine blocks at the receive statement, it automatically defers all the events except the eTimeOut and eStartTimerFailed events.
+}
+
+
+
+
+1. Switch case statements are NOT supported in P. Instead, use IfThenElse statements as described in tags.
+2. Usage of 'with' keyword is VALID ONLY in the syntax of event handlers as described in tags.
+3. 'const' keyword is NOT supported in P. Constants in P are defined as described in tags.
+4. 'is' operator is NOT supported in the P language.
+5. 'self' keyword is NOT supported in the P language.
+6. values() or indexOf() functions are NOT supported in P. Do not use library functions from other programming languages.
+7. to string is NOT supported in P.
+
+Here is a non-exhaustive list of the P syntax rules that you should strictly adhere to when writing P code:
+1. Variable declaration and assignment must be done in separate statements.
+2. All variables in a function body must be declared at the top before any other statements.
+3. The `foreach` loop must declare the iteration variable at the top of the function body.
+4. Do not use 'self' or 'this' for accessing variables inside a machine.
+5. Named function at entry CANNOT take more than 1 parameter, as described in tags.
+6. Exit functions cannot have parameters.
+7. The target machine in a `send` statement must be a variable of the same type as the target machine.
+8. The logical not operator `!` must be used with parentheses: `!(expr in expr)`.
+9. '!in' and 'not in' are not supported for collection membership checks.
+10. Default values for types are obtained using 'default(type)' syntax.
+11. Collections are initialized to empty by default and should not be reassigned to default values.
+12. Initializing non-empty collections requires specific syntax (e.g., `seq += (index, value)`, `map[key] = value`).
+13. The `ignore` statement must list the event names: `ignore eventList;`.
+14. Formatted strings use a specific syntax: `format("formatString {0} {1}", arg1, arg2)`.
+15. Creating a single field named tuple requires a trailing comma after the value assignment, as described in tags.
+16. User defined types are assigned values using named tuples as detailed in tags.
+17. Do NOT access functions contained inside of other machines.
+18. Entry functions in spec machines CANNOT take any parameter.
+19. $, $$, this, new, send, announce, receive, and pop are not allowed in monitor.
+20. All events referenced in the code must be declared and make sure duplicate enum declarations are not present across multiple files.
+21. Ensure all type definitions are available before they are used.
+22. Inside a function, all variable declarations must come at the very beginning of a function, before any executable statements.
+
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_test_cases_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_test_cases_guide.txt
new file mode 100644
index 0000000000..1c54e77a73
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_test_cases_guide.txt
@@ -0,0 +1,134 @@
+Here is the P test cases guide:
+
+P test cases are used to define different finite scenarios under which the correctness of a system or module can be verified. Test cases allow developers to systematically check that their systems behave correctly under various conditions.
+
+
+## Test Case Overview
+
+Test cases in P define the specific scenarios under which a module or system should be validated. Each test case typically consists of:
+- A system module being tested
+- An environment module (test harness/driver) that generates inputs
+- Optional specification monitors that verify properties - If so,use existing specification names in assert clauses
+
+The P checker automatically checks these test cases by exploring all possible executions of the system to ensure it behaves correctly under all scenarios defined by the test case.
+
+
+
+## Test Case Grammar
+
+```
+testcase
+| test iden [main=iden] : modExpr ; # TestDecl
+;
+```
+
+Where:
+- `iden` (first occurrence) is the test case name
+- `iden` (second occurrence, optional) is the name of the main machine
+- `modExpr` is the module under test, specified using P module expressions
+
+
+
+- **TestDriver file**: Contains state machines that implement test harnesses/environment machines
+- **TestScripts file**: Contains test case declarations that reference the test drivers
+
+## Test Case Declaration
+
+**Syntax**: `test tName [main=mName] : module_under_test ;`
+
+Where:
+- `tName` is the name of the test case
+- `mName` (optional) is the name of the **main** machine where the execution of the system starts
+- `module_under_test` is the module to be tested, which can be any valid module expression
+
+When a test case is executed, the P checker creates the specified main machine to start the system's execution. If no main machine is specified, the checker determines a suitable start machine automatically.
+
+
+
+## Properties Checked by the P Checker
+
+For each test case, the P checker automatically verifies several properties across all possible execution paths:
+
+1. **No Unhandled Events**: Ensures that all events sent to machines are properly handled
+2. **Assertion Validity**: Confirms all local assertions in the program hold
+3. **Deadlock Freedom**: Verifies that the system never reaches a deadlocked state
+4. **Specification Compliance**: Validates that all specification monitors attached to the module are satisfied:
+ - Safety properties are never violated
+ - Liveness properties are eventually satisfied (the system does not remain indefinitely in a "hot" state)
+
+
+
+## Test Case Examples
+
+### Basic Test Case with a Main Machine
+```
+// Test with a single client and a server
+test tcSingleClient [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithSingleClient });
+```
+
+This test case:
+- Is named `tcSingleClient`
+- Starts execution from the `TestWithSingleClient` machine
+- Tests a module consisting of `Client` and `Bank` modules plus a test driver
+- Verifies properties specified by the `BankBalanceIsAlwaysCorrect` and `GuaranteedWithDrawProgress` monitors
+
+### Multiple Test Cases for Different Scenarios
+```
+// Test with a single client and a server
+test tcSingleClient [main=TestDriverSingle]:
+ (union clientSystem, { TestDriverSingle });
+
+// Test with multiple clients and a server
+test tcMultipleClients [main=TestDriverMultiple]:
+ (union clientSystem, { TestDriverMultiple });
+
+// Test with clients, server and an abstract network model
+test tcWithNetwork [main=TestDriverNetwork]:
+ assert NetworkSpec in (union clientSystem, abstractNetwork, { TestDriverNetwork });
+```
+
+These test cases validate the system under different scenarios, from simple single-client tests to more complex configurations with multiple clients or abstract network models.
+
+
+
+## Best Practices for Test Cases
+
+1. **Start Simple**: Begin with simple test cases and gradually increase complexity
+2. **Isolate Components**: Test individual components before testing the entire system
+3. **Use Abstractions**: Replace complex components with simpler abstractions to focus testing
+4. **Test Edge Cases**: Create test cases specifically designed to cover edge cases and error scenarios
+5. **Monitor Key Properties**: Attach appropriate specification monitors to verify important system properties
+6. **Incremental Testing**: Create a suite of test cases that incrementally test more features or components
+7. **NEVER declare the same event in both test scripts and test drivers** - this causes compilation errors!
+
+Example of an incremental testing approach:
+```
+// Define component modules
+module clientModule = { Client };
+module serverModule = { Server };
+module networkModule = { Network };
+
+// Define test scenarios with increasing complexity
+test tcClientOnly [main=ClientTest]:
+ assert ClientSpec in (union clientModule, { ClientTest });
+
+test tcClientServer [main=ClientServerTest]:
+ assert ClientServerSpec in
+ (union clientModule, serverModule, { ClientServerTest });
+
+test tcFullSystem [main=FullSystemTest]:
+ assert SystemSpec in
+ (union clientModule, serverModule, networkModule, { FullSystemTest });
+```
+
+8. When writing P test cases, the main machine must exist in the test module being executed. If using a test driver machine as the main machine, it must be included in the module expression using union syntax.
+
+Example Pattern
+```
+test testName [main=TestDriverMachine] :
+ assert specifications in (union SystemModule, {TestDriverMachine});
+```
+
+
diff --git a/Src/PeasyAI/resources/context_files/modular/p_types_guide.txt b/Src/PeasyAI/resources/context_files/modular/p_types_guide.txt
new file mode 100644
index 0000000000..7c5640356b
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/modular/p_types_guide.txt
@@ -0,0 +1,605 @@
+Here is the P types guide:
+
+P Supports the following data types:
+1. Primitive: int, bool, float, string, machine, and event.
+2. Record: tuple and named tuple.
+3. Collection: map, seq, and set.
+4. User Defined: These are user defined types that are constructed using any of the P types listed above.
+5. Universal Supertypes: any and data.
+
+Here are the syntactical details of each of these data types:
+
+
+Here is an example of how primitive data types are declared:
+
+// some function body in the P program
+{
+ var i: int;
+ var j: float;
+ var k: string;
+ var l: bool;
+
+ i = 10;
+ j = 10.0;
+ k = "Failure!!";
+ l = (i == (j to int));
+}
+
+
+
+
+1. The fields of a tuple can be accessed by using the . operation followed by the field index.
+2. Named tuples are similar to tuples with each field having an associated name.
+3. The fields of a named tuple can be accessed by using the . operation followed by the field name.
+
+Here is an example of a tuple:
+
+// tuple with three fields
+var tupleEx: (int, bool, int);
+
+// constructing a value of tuple type.
+tupleEx = (20, false, 21);
+
+// accessing the first and third element of the tupleEx
+tupleEx.0 = tupleEx.0 + tupleEx.2;
+
+
+Here is an example of a named tuple:
+
+// named tuple with three fields
+var namedTupleEx: (x1: int, x2: bool, x3: int);
+
+// constructing a value of named tuple type.
+namedTupleEx = (x1 = 20, x2 = false, x3 = 21);
+
+// accessing the first and third element of the namedTupleEx
+namedTupleEx.x1 = namedTupleEx.x1 + namedTupleEx.x3;
+
+
+
+A tuple value can be created using the following expressions:
+Syntax:: (rvalue,) for a single field tuple value, or (rvalue (, rvalue)+) for tuple with multiple fields.
+
+Here is an example of creating tuple value:
+
+// tuple value of type (int,)
+(10,)
+
+// tuple value of type (string, (string, string))
+("Hello", ("World", "!"))
+
+// assume x: int and y: string
+// tuple value of type (int, string)
+(x, y)
+
+
+
+
+A named tuple value can be created using the following syntax:
+Syntax:: (iden = rvalue,) for a single field named tuple value, or (iden = rvalue (, iden = rvalue)+) for a named tuple with multiple fields.
+A trailing comma is required after the value assignment in a single field named tuple.
+
+Here is an example of creating a single field named tuple value:
+
+// named tuple value of type (reqId: int, )
+(reqId = 10,)
+
+
+Here is an incorrect example of creating a single field named tuple value:
+
+(reqId = 10) // Missing comma
+
+
+Here is an example of creating a named tuple with multiple fields:
+
+// assume x: int and y: string
+// named tuple value of type (val:int, str:string)
+(val = x, str = y)
+
+// named tuple value of type (h: string, (w: string, a: string))
+(h = "Hello", (w = "World", a = "!"))
+
+
+Here is an incorrect example of creating a named tuple with multiple fields:
+
+// assume x: int and y: string
+(x, y)
+
+
+
+
+
+P supports three collection types:
+1. map[K, V] represents a map type with keys of type K and values of type V.
+2. seq[T] represents a sequence type with elements of type T.
+3. set[T] represents a set type with elements of type T.
+
+
+Here is the syntax to index into collections to access its elements:
+Syntax:: expr_c[expr_i]
+1. If expr_c is a value of sequence type, then expr_i must be an integer expression and expr_c[expr_i] represents the element at index expr_i.
+2. If expr_c is a value of set type, then expr_i must be an integer expression and expr_c[expr_i] represents the element at index expr_i but note that for a set there is no guarantee for the order in which elements are stored in the set.
+3. If expr_c is a value of map type, then expr_i represents the key to look up and expr_c[expr_i] represents the value for the key expr_i.
+
+
+
+foreach statement can be used to iterate over a collection in P.
+Syntax:: foreach (iden in expr)
+Declare iden at the top of the function body in EACH function. Type of iden must be same as the elements type in the collection.
+
+1. iden is the name of the variable that stores the element from the collection during iterations.
+2. expr represents the collection over which we want to iterate.
+3. One can mutate the collection represented by expr when iterating over it as the foreach statement enumerates over a clone of the collection.
+
+Here is an example of foreach over a sequence:
+
+var sq : seq[string];
+var str : string; // str is declared
+
+foreach(str in sq) {
+ print str;
+}
+
+
+Here is an INCORRECT example of foreach over a sequence:
+
+var sq : seq[string];
+// Missing declaration of str
+foreach(str in sq) {
+ print str;
+}
+
+
+Here is an example of foreach over a set:
+
+var ints: set[int];
+var iter, sum: int;
+// iterate over a set of integers
+foreach(iter in ints)
+{
+ sum = sum + iter;
+}
+
+
+Here is an example of foreach over a map:
+
+var intsM: map[int, int];
+var key: int;
+foreach(key in keys(intsM))
+{
+ intsM[key] = intsM[key] + delta;
+}
+
+
+Here is an example of mutating a collection with foreach:
+
+var ints: set[int];
+var i: int;
+foreach(i in ints)
+{
+ ints -= (i);
+}
+assert sizeof(ints) == 0;
+
+
+
+
+Here are the details on how to add an element into a sequence:
+
+1. For a sequence sq, the value of index i should be between 0 <= i <= sizeof(sq).
+2. Index i = 0 inserts x at the start of sq and i = sizeof(sq) appends x at the end of sq.
+3. To insert an element in an empty sequence, reference the details given in tags.
+Syntax:: lvalue += (expr, rvalue);
+expr is the index of the sequence and rvalue is the value to be inserted. Round brackets are important.
+
+Here is an example of inserting an element into a sequence:
+
+type Task = (description: string, assignedTo: string, dueDate: int);
+
+machine TaskManager {
+ var allTasks: seq[Task];
+
+ start state Idle {
+ entry Idle_Entry;
+ }
+
+ fun Idle_Entry() {
+ // Initialize the sequence to be empty
+ allTasks = default(seq[Task]);
+ }
+
+ // Function to add a new task to the sequence
+ fun AddTask(task: Task) {
+ // Add task to end of sequence using proper index syntax
+ allTasks += (sizeof(allTasks), task); // Add at index sizeof(allTasks)
+ }
+
+ // Examples of other sequence operations:
+ fun InsertTaskAtStart(task: Task) {
+ allTasks += (0, task); // Add at index 0
+ }
+
+ fun InsertTaskAtPosition(task: Task, position: int) {
+ // position must be 0 <= position <= sizeof(allTasks)
+ allTasks += (position, task); // Add at specific index
+ }
+}
+
+
+Here is an incorrect example of inserting an element in a sequence:
+
+var sq : seq[T];
+var x: T;
+
+// WRONG - Missing index:
+sq += (x); // Wrong: Must specify index
+
+// WRONG - Cannot concatenate sequences:
+sq = sq + (x,); // Wrong: Cannot use + operator
+sq = append(sq, x); // Wrong: No append function exists
+sq = sq + default(seq[T]) + (x,); // Wrong: Cannot concatenate sequences
+
+
+
+
+Here is the syntax to add or insert an element in a map:
+Syntax: lvalue += (expr, rvalue);
+lvalue is a value of map in P. expr is the key to be inserted and rvalue is its corresponding value.
+round brackets surrounding rvalue are important.
+
+Here is an example of inserting an element into a map:
+
+var mp : map[K,V];
+var x: K, y: V;
+
+// adds (x, y) into the map
+mp += (x, y);
+
+// adds (x, y) into the map, if key x already exists then updates its value to y.
+mp[x] = y;
+
+
+
+
+Here is the syntax to add or insert an element in a set:
+Syntax:: lvalue += (rvalue);
+lvalue is a value of set in P. round brackets surrounding rvalue are important.
+
+Here is an example of inserting an element into a set:
+
+var st : set[T];
+var x: T;
+
+// adds x into the set st
+// x is surrounded by round brackets
+st += (x);
+
+
+Here is an incorrect example of inserting an element into a set:
+
+var st : set[T];
+var x: T;
+
+// Missing round brackets surrounding x
+st += x;
+
+
+
+
+
+1. The default feature in P can be used to obtain the default value of a collection.
+2. P variables on declaration are automatically initialized to their default values.
+
+Syntax:: default(type)
+type is any P type and default(type) represents the default value for the type
+
+Here is an example of initializing an empty sequence:
+
+var sq : seq[int];
+// by default a seq type is initialized to an empty seq
+assert sizeof(sq) == 0;
+
+// set the variable to an empty seq
+sq = default(seq[int]);
+
+
+Here is an example of initializing an empty map:
+
+var myMap: map[int, int];
+
+// by default a map type is initialized to an empty map
+assert sizeof(myMap) == 0;
+
+// set the variable to an empty map
+myMap = default(map[int, int]);
+
+
+
+var s : set[int];
+// by default a set type is initialized to an empty set
+assert sizeof(s) == 0;
+
+// set the variable to an empty set
+s = default(set[int]);
+
+
+
+
+In P language, a collection when declared is automatically initialized by default to empty collection. Do NOT set it to empty again.
+
+
+In P, a sequence when declared is initialized by default to empty sequence. Do NOT set the sequence to empty again.
+
+Here is an example of initializing a non-empty sequence:
+
+var mySeq: seq[int];
+var i: int;
+var numOfIterations: int;
+
+i = 0;
+numOfIterations = 5;
+
+// initialize mySeq
+while(index < numOfIterations) {
+ mySeq += (sizeof(mySeq), i); // Append i to the end of mySeq
+ i = i + 1;
+}
+
+// At this point, mySeq contains [0, 1, 2, 3, 4]
+
+
+
+
+In P, a map when declared is initialized by default to empty map. Do NOT set the map to empty again.
+
+Here is an example of how to initialize a non-empty map:
+
+var bankBalance: map[int, int];
+var i: int;
+while(i < 6) {
+ bankBalance[i] = choose(100) + 10;
+ i = i + 1;
+}
+
+
+
+
+In P, a set when declared is initialized by default to empty set. Do NOT set the set to empty again.
+
+Here is an example of how to initialize a non-empty set:
+
+var participants: set[Participant];
+var i : int;
+var num: int;
+
+num = 7;
+while (i < num) {
+ participants += (new Participant());
+ i = i + 1;
+}
+
+
+
+
+Remove statement is used to remove an element from a collection.
+Syntax:: lvalue -= rvalue;
+
+
+For a sequence sq, the value of index i above should be between 0 <= i <= sizeof(sq) - 1.
+
+Here is an example of removing an element at an index from a sequence:
+
+var sq : seq[T];
+var i : int;
+
+// i is the index in sq, NOT an element of sq
+sq -= (i);
+
+
+Here is an incorrect example of removing an element at an index from a sequence:
+
+var sq : seq[T];
+var i : int;
+
+// Missing round brackets surrounding i
+sq -= i;
+
+
+
+
+Here is an example of removing an element from a map:
+
+var mp : map[K,V];
+var x: K;
+
+// Removes the element (x, _) from the map i.e., removes the element with key x from mp
+mp -= (x);
+
+
+
+
+Here is an example of removing an element from a set:
+
+var st : set[T];
+var x: T;
+
+// removes x from the set st
+st -= (x);
+
+
+Here is an INCORRECT example of removing an element from a set:
+
+var st : set[T];
+var x: T;
+
+// Missing round brackets surrounding x
+st -= x;
+
+
+
+
+
+P supports four operations on collection types:
+1. sizeof
+2. keys
+3. values
+4. in (to check containment)
+
+
+Syntax:: sizeof(expr)
+expr is a value of type set, seq or map, returns an integer value representing the size or length of the collection.
+
+Here is an example of sizeof:
+
+var sq: seq[int];
+while (i < sizeof(sq)) {
+ i = i + 1;
+}
+
+
+
+
+keys function is used to get access to a sequence of all the keys in map and then operate over it.
+Syntax:: keys(expr)
+1. expr must be a map value
+2. If expr: map[K, V], then keys(expr) returns a sequence (of type seq[K]) of all keys in the map.
+
+Here is an example of keys function:
+
+var iter: int;
+foreach(iter in keys(mapI))
+{
+ assert iter in mapI, "Key should be in the map";
+ mapI[iter] = 0;
+}
+
+
+
+
+values function is used to get access to a sequence of all the values in map and then operate over it.
+Syntax:: values(expr)
+1. expr must be a map value
+2. If expr: map[K, V], then values(expr) returns a sequence (of type seq[V]) of all values in the map.
+
+Here is an example of values function:
+
+var iter: int;
+var rooms: map[int, tRoomInfo];
+foreach(iter in values(rooms))
+{
+ assert iter == 0, "All values must be zero!";
+}
+
+
+
+
+1. P provides the in operation to check if an element (or key in the case of a map) belongs to a collection.
+2. The in expression evaluates to true if the collection contains the element and false otherwise.
+3. !in is NOT supported in P.
+4. 'not in' is NOT supported in P.
+
+Syntax:: expr_e in expr_c
+expr_e is the element (or key in the case of map) and expr_c is the collection value.
+
+Here is an example of checking whether an element is contained in a collection:
+
+var sq: seq[tRequest];
+var mp: map[int, tRequest];
+var rr: tRequest;
+var i: int;
+if(rr in sq && rr in values(mp) && i in mp) {
+ // do something
+}
+if (!(rr in sq)) {
+ // do something else
+}
+
+
+
+
+
+
+
+1. P supports assigning names to types i.e., creating typedef.
+2. NOTE that these typedefs are simply assigning names to P types and does not effect the sub-typing relation.
+3. User defined types are assigned values using named tuples as detailed in tags.
+
+Syntax:: type typeName = typedef;
+typeName should not be named as any of the reserved keywords listed in the tags.
+
+Here is an example of declaring a user defined type:
+
+// defining a type tLookUpRequest
+type tLookUpRequest = (client: machine, requestId: int, key: string);
+
+// defining a type tLookUpRequestX
+type tLookUpRequestX = (client: machine, requestId: int, key: string);
+
+// Note that the types tLookUpRequest and tLookUpRequestX are same, the compiler does not distinguish between the two types.
+
+
+Here is another example of declaring a user defined type:
+
+enum tTransStatus { SUCCESS, ERROR, TIMEOUT }
+
+// defining a type tWriteTransResp
+type tWriteTransResp = (transId: int, status: tTransStatus);
+
+
+
+Here is an example of assigning value to a user defined type:
+
+var resp: tWriteTransResp;
+// named tuple
+resp = (transId = trans.transId, status = TIMEOUT);
+
+
+
+
+1. any type in P is the supertype of all types. Also, note that in P, seq[any] is a super type of seq[int] and similarly for other collection types.
+2. data type in P is the supertype of all types in P that do not have a machine type embedded in it.
+3. For example, data is a supertype of (key: string, value: int) but not (key: string, client: machine).
+
+
+
+1. The default feature in P can be used to obtain the default value of any P type.
+2. P variables on declaration are automatically initialized to their default values.
+
+Syntax:: default(type)
+type is any P type and default(type) represents the default value for the type
+
+Here is an example of default value:
+
+type tRequest = (client: machine, requestId: int);
+
+// somewhere inside a function
+x = default(tRequest);
+
+assert x.client == default(machine);
+
+
+Here's a table of P Types and their corresponding default values:
+
+
+ P Types
+ Default Value
+
+
+ int
+ 0
+
+
+ float
+ 0.0
+
+
+ bool
+ false
+
+
+ string
+ ""
+
+
+
+
diff --git a/Src/PeasyAI/resources/context_files/p_documentation_reference.txt b/Src/PeasyAI/resources/context_files/p_documentation_reference.txt
new file mode 100644
index 0000000000..c12fb1b0a1
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/p_documentation_reference.txt
@@ -0,0 +1,275 @@
+P Language Documentation Reference
+===================================
+
+This file consolidates key patterns, idioms, and reference material from the
+official P documentation for use by the RAG system. It supplements the modular
+guide files (p_machines_guide.txt, p_types_guide.txt, etc.) with tutorial-level
+patterns and protocol idioms.
+
+================================================================================
+SECTION 1: Common P Programming Patterns
+================================================================================
+
+
+Client-Server Pattern:
+- Server creates a Database or backend machine in its start state.
+- Clients are created by the test driver and receive a reference to the server.
+- Clients send requests with a source=this field so the server can respond.
+- Server processes request and sends response back to req.source.
+
+Example flow:
+ TestDriver creates BankServer -> BankServer creates Database
+ TestDriver creates Client(serv=server, accountId=id, balance=bal)
+ Client sends eWithDrawReq(source=this, accountId=..., amount=..., rId=...)
+ BankServer handles eWithDrawReq, queries Database, sends eWithDrawResp back
+
+
+
+Request-Response with Blocking Receive:
+Global helper functions can encapsulate send+receive pairs:
+
+fun ReadBankBalance(database: Database, accountId: int) : int {
+ var currentBalance: int;
+ send database, eReadQuery, (accountId = accountId,);
+ receive {
+ case eReadQueryResp: (resp: (accountId: int, balance: int)) {
+ currentBalance = resp.balance;
+ }
+ }
+ return currentBalance;
+}
+
+
+
+Nondeterministic Testing:
+- Use choose(N) to generate random integers [0, N)
+- Use $ for nondeterministic boolean choice
+- PChecker explores all possible schedules
+- Use choose() in test drivers to vary system configurations
+
+Example:
+ fun HasBeans() : bool { return $; }
+ fun RandomAmount() : int { return choose(100) + 1; }
+ SetupSystem(choose(3) + 2); // 2 to 4 clients
+
+
+
+Monitor Initialization via Announce:
+Spec monitors may need initial state (e.g., initial balances).
+Use a dedicated event + announce in the test driver BEFORE creating clients.
+
+ event eSpec_Init: map[int, int];
+
+ // In test driver:
+ announce eSpec_Init, initBalances;
+ // Then create clients that start sending requests
+
+ // In spec:
+ spec MySafety observes eSpec_Init, eRequest, eResponse {
+ var state: map[int, int];
+ start state Init {
+ on eSpec_Init goto Active with InitState;
+ }
+ fun InitState(init: map[int, int]) { state = init; }
+ // ...
+ }
+
+
+
+Failure Injection Pattern:
+- FailureInjector machine nondeterministically sends halt events to machines
+- Used to test system resilience under node failures
+- Typically created by the test driver
+
+Example:
+ machine FailureInjector {
+ var nodes: seq[machine];
+ start state InjectFailures {
+ entry {
+ // nondeterministically fail some nodes
+ send nodes[choose(sizeof(nodes))], halt;
+ }
+ }
+ }
+
+
+
+Timer Pattern:
+Timer machines simulate timeouts in the system.
+Create a Timer, send eStartTimer, and handle eTimeOut or eCancelTimerSuccess.
+
+ machine Timer {
+ var target: machine;
+ start state WaitForTimerRequests {
+ entry (creator: machine) { target = creator; }
+ on eStartTimer goto TimerStarted;
+ ignore eCancelTimer, eDelayedTimeOut;
+ }
+ state TimerStarted {
+ defer eStartTimer;
+ on eCancelTimer goto WaitForTimerRequests with CancelTimer;
+ on eDelayedTimeOut goto WaitForTimerRequests with TimeoutHandler;
+ entry StartTimerEntry;
+ }
+ // ...
+ }
+
+
+
+Broadcast to All Machines:
+Iterate over a collection of machine references and send to each:
+
+ var participants: seq[machine];
+ var i: int;
+ i = 0;
+ while (i < sizeof(participants)) {
+ send participants[i], ePrepare, (coordinator = this, transId = tid);
+ i = i + 1;
+ }
+
+
+
+Quorum / Majority Pattern:
+Track votes in a set, check if majority reached:
+
+ var votes: set[machine];
+ votes += (sender);
+ if (sizeof(votes) > sizeof(allNodes) / 2) {
+ // quorum reached
+ goto Committed;
+ }
+
+
+================================================================================
+SECTION 2: P Project Organization
+================================================================================
+
+
+A P project has three main folders:
+
+PSrc/ - Source machines, types, events, enums, modules
+ - Enums_Types_Events.p (or split per-component)
+ - MachineName.p (one file per machine or logical group)
+ - Modules.p (module definitions)
+
+PSpec/ - Safety and liveness specifications
+ - Safety.p (spec machines)
+
+PTst/ - Test drivers and test scripts
+ - TestDriver.p (test driver machines with setup logic)
+ - TestScript.p (test case declarations referencing drivers)
+
+Project file: ProjectName.pproj
+
+
+
+Naming Conventions:
+- Types: tCamelCase (tRequest, tWithDrawResp)
+- Events: eCamelCase (eRequest, eWithDrawResp, eTimeout)
+- Machines: PascalCase (BankServer, Client, Coordinator)
+- Enums: tCamelCase for type name, UPPER_SNAKE for values
+ enum tStatus { SUCCESS, FAILURE, TIMEOUT }
+- Specs: PascalCase (BankBalanceIsAlwaysCorrect, GuaranteedProgress)
+- States: PascalCase (Init, WaitForRequests, Processing)
+- Functions: PascalCase (HandleRequest, AfterResponse)
+- Modules: camelCase (clientModule, serverModule)
+
+
+================================================================================
+SECTION 3: Common Compilation Pitfalls
+================================================================================
+
+
+1. Variable declaration with initialization:
+ WRONG: var x: int = 5;
+ RIGHT: var x: int; x = 5;
+
+2. Sequence append:
+ WRONG: mySeq += (value);
+ RIGHT: mySeq += (sizeof(mySeq), value);
+
+3. Single-field named tuple:
+ WRONG: (field = value)
+ RIGHT: (field = value,) // trailing comma required
+
+4. Negated collection membership:
+ WRONG: !x in mySet or x !in mySet or x not in mySet
+ RIGHT: !(x in mySet)
+
+5. Inline event payload types:
+ WRONG: event eMsg: (x: int, y: string);
+ RIGHT: type tMsg = (x: int, y: string); event eMsg: tMsg;
+
+6. Entry function parameters in specs:
+ WRONG: spec S observes e { start state Init { entry (p: int) { ... } } }
+ RIGHT: spec S observes e { start state Init { entry { ... } } }
+
+7. Duplicate declarations across files:
+ Events, types, enums must be declared exactly once across all files.
+
+8. Variable declarations after statements:
+ WRONG: fun F() { doSomething(); var x: int; }
+ RIGHT: fun F() { var x: int; doSomething(); }
+
+9. Foreach iteration variable:
+ WRONG: foreach(x in collection) { ... } // x not declared
+ RIGHT: var x: int; foreach(x in collection) { ... } // x declared at top
+
+10. Integer increment:
+ WRONG: i += 1;
+ RIGHT: i = i + 1; // compound assignment only for collections
+
+11. Switch/case:
+ WRONG: switch(x) { case 1: ... }
+ RIGHT: if (x == 1) { ... } else if (x == 2) { ... }
+
+12. Self reference:
+ WRONG: self or this.field
+ RIGHT: this (only for passing machine reference, NOT for field access)
+
+13. Accessing other machine's functions:
+ WRONG: otherMachine.someFunction()
+ RIGHT: send otherMachine, eRequestEvent, payload; // communicate via events
+
+
+================================================================================
+SECTION 4: Protocol-Specific Patterns
+================================================================================
+
+
+Two-Phase Commit:
+- Coordinator sends ePrepare to all Participants
+- Participants respond with eVoteYes or eVoteNo
+- If all vote yes: Coordinator sends eGlobalCommit
+- If any vote no: Coordinator sends eGlobalAbort
+- Safety: atomicity (all commit or all abort)
+- Test driver creates Coordinator and Participants
+
+
+
+Paxos Consensus:
+- Proposer sends ePrepare(ballot) to Acceptors
+- Acceptors respond with ePromise or eReject
+- If majority promise: Proposer sends eAccept(ballot, value)
+- Acceptors respond with eAccepted
+- If majority accept: value is chosen, notify Learners
+- Safety: only one value can be chosen
+
+
+
+Raft Consensus:
+- Leader election via randomized timeouts
+- Leader sends eAppendEntries heartbeats
+- Followers respond with eAppendEntriesResponse
+- Leader replicates log entries to followers
+- Committed when majority have replicated
+- Safety: one leader per term, log matching
+
+
+
+Leader Election (Ring):
+- Nodes arranged in a ring, each knows its successor
+- Each node sends its ID around the ring
+- Forward higher IDs, drop lower IDs
+- Node that receives its own ID becomes leader
+
diff --git a/Src/PeasyAI/resources/context_files/p_nuances.txt b/Src/PeasyAI/resources/context_files/p_nuances.txt
new file mode 100644
index 0000000000..f2ec9cdc72
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/p_nuances.txt
@@ -0,0 +1,30 @@
+
+1. Switch case statements are NOT supported in P. Instead, use IfThenElse statements as described in tags.
+2. Usage of 'with' keyword is VALID ONLY in the syntax of event handlers as described in tags.
+3. 'const' keyword is NOT supported in P. Constants in P are defined as described in tags.
+4. 'is' operator is NOT supported in the P language.
+5. 'self' keyword is NOT supported in the P language.
+6. values() or indexOf() functions are NOT supported in P. Do not use library functions from other programming languages.
+7. to string is NOT supported in P.
+
+Here is a non-exhaustive list of the P syntax rules that you should strictly adhere to when writing P code:
+1. Variable declaration and assignment must be done in separate statements.
+2. All variables in a function body must be declared at the top before any other statements.
+3. The `foreach` loop must declare the iteration variable at the top of the function body.
+4. Do not use 'self' or 'this' for accessing variables inside a machine.
+5. Named function at entry CANNOT take more than 1 parameter, as described in tags.
+6. Exit functions cannot have parameters.
+7. The target machine in a `send` statement must be a variable of the same type as the target machine.
+8. The logical not operator `!` must be used with parentheses: `!(expr in expr)`.
+9. '!in' and 'not in' are not supported for collection membership checks.
+10. Default values for types are obtained using 'default(type)' syntax.
+11. Collections are initialized to empty by default and should not be reassigned to default values.
+12. Initializing non-empty collections requires specific syntax (e.g., `seq += (index, value)`, `map[key] = value`).
+13. The `ignore` statement must list the event names: `ignore eventList;`.
+14. Formatted strings use a specific syntax: `format("formatString {0} {1}", arg1, arg2)`.
+15. Creating a single field named tuple requires a trailing comma after the value assignment, as described in tags.
+16. User defined types are assigned values using named tuples as detailed in tags.
+17. Do NOT access functions contained inside of other machines.
+18. Entry functions in spec machines CANNOT take any parameter.
+19. $, $$, this, new, send, announce, receive, and pop are not allowed in monitor.
+
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/context_files/singleshot_prompt.txt b/Src/PeasyAI/resources/context_files/singleshot_prompt.txt
new file mode 100644
index 0000000000..b7ae6fdddf
--- /dev/null
+++ b/Src/PeasyAI/resources/context_files/singleshot_prompt.txt
@@ -0,0 +1,92 @@
+You are an expert P language programmer. Based on the provided design document, generate complete P code with all necessary components. Structure your response using XML-style tags to clearly separate different components.
+
+Follow this structure:
+
+
+Generate the directory structure for the P project, with one directory per line
+Include PSrc/, PSpec/, and PTst/ directories
+
+
+
+
+Create a .pproj file in the project root directory with proper XML configuration:
+- ProjectName: {project_name}
+- InputFiles: References to PSrc, PSpec, and PTst directories
+- OutputDir: Directory for generated code (PGenerated)
+
+
+
+Generate all enums and type definitions required by the system
+Follow P language syntax for enum and type declarations
+
+
+
+Define all events needed for communication between machines
+Include event parameters and their types
+
+
+
+Define all state machines needed by the system
+Include:
+- Machine name and parameters
+- State declarations
+- Event handlers
+- State transitions
+- Entry/exit actions
+Separate multiple machines with ... tags
+
+
+
+Define specification monitors to verify system properties
+Include safety and liveness properties
+Separate multiple monitors with ... tags
+
+
+
+Define the module system structure
+Include:
+- Module declarations
+- Interface definitions
+- Module dependencies
+
+
+
+Generate specification files for verification
+Include:
+- Safety properties
+- Liveness properties
+- Invariants
+Separate files with ... tags
+
+
+
+Generate test files to verify system behavior
+Include:
+- Test scenarios
+- Test configurations
+- Expected outcomes
+Separate files with ... tags
+
+
+
+List all source files and their contents
+Format: filename: content_type
+Example:
+Client.p: client_machine
+Server.p: server_machine
+
+
+Requirements:
+1. All code must follow P language syntax
+2. Use proper indentation for readability
+3. Include comments explaining complex logic
+4. Ensure all components are properly connected
+5. Follow P language best practices
+6. Include error handling where appropriate
+7. Ensure proper event handling between machines
+8. Verify all type declarations match usage
+
+Here is the design document to implement:
+{design_doc}
+
+Generate the complete P code implementation following the above structure.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_code_description.txt b/Src/PeasyAI/resources/instructions/generate_code_description.txt
new file mode 100644
index 0000000000..b9af8469ec
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_code_description.txt
@@ -0,0 +1,5 @@
+Please describe the given P code.
+Here are the instructions on how to write the description:
+1. In Overview section, provide a brief high level overview of what the code is trying to document
+2. In Breakdown section, breakdown the key logic of the code into multiple blocks. For each block, include the code snippet followed by an elaborate explanation of the snippet.
+3. Give short descriptions for event and type declarations. Elaborate more on the event handlers and function logic within the state machines
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_design_doc.txt b/Src/PeasyAI/resources/instructions/generate_design_doc.txt
new file mode 100644
index 0000000000..b30193a41a
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_design_doc.txt
@@ -0,0 +1,8 @@
+Please generate a design document in markdown format for the provided P code.
+Here are the instructions on the content that must be included in each section of the design document:
+1. Title: Use a top-level markdown heading (`# Title`) with the title of the system modeled in the given P code
+2. Introduction (`## Introduction`): Brief overview of the system described in the P code, followed by a numbered list of assumptions
+3. Components (`## Components`): Split into `### Source Components` and `### Test Components`. For each component or actor, include a brief description of what the component represents, its states, local state (variable names with plain English descriptions, no types), initialization (described in plain English — what information the machine needs to start and how it receives it, without code or type annotations), and a bulleted list of behaviors
+4. Interactions (`## Interactions`): For each event being passed between components/machines, include a numbered entry with Source, Target, Payload (described in plain English, no code or type annotations), Description, and Effects
+5. Specifications (`## Specifications`): For each specification, include the property name, whether it is a safety or liveness property, and a precise English statement that naturally references the relevant events by name
+6. Test Scenarios (`## Test Scenarios`): Include a numbered list of concrete test scenarios with specific machine counts and expected outcomes
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_enums_types_events.txt b/Src/PeasyAI/resources/instructions/generate_enums_types_events.txt
new file mode 100644
index 0000000000..2a72fdd529
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_enums_types_events.txt
@@ -0,0 +1,45 @@
+Write the enums, types, and events required for each of the state machines. If multiple state machines need the same enums, types, and events, write them only once. Do not write duplicate declarations.
+
+## CRITICAL SYNTAX RULES:
+
+1. **Type definitions do NOT use trailing commas**, even for single-field named tuples:
+ ```
+ type tLearnPayload = (learnedValue: int); // CORRECT
+ type tAcceptorConfig = (learners: seq[machine]); // CORRECT
+ type tProposePayload = (proposer: machine, num: int); // CORRECT
+ ```
+ Note: Trailing commas are only used in VALUE expressions like `(field = value,)`, never in type definitions.
+
+2. **Use descriptive, unambiguous field names.** Each type's field names will be used throughout the codebase in payload construction and field access. Choose names that are specific to the type:
+ - GOOD: `(proposalNumber: int, acceptedValue: int)` — clear what each field means
+ - BAD: `(value: int)` — ambiguous, easily confused across types
+
+3. **Config types** for machine initialization should match the machine's dependencies exactly. Name them `tConfig`:
+ ```
+ type tProposerConfig = (acceptors: seq[machine], learners: seq[machine], proposerId: int);
+ type tAcceptorConfig = (learners: seq[machine]);
+ type tLearnerConfig = (majoritySize: int);
+ ```
+
+4. **Do NOT declare config init events** (like eProposerInit, eAcceptorInit) unless the design explicitly uses the init-event pattern. Most machines use the constructor payload pattern where config is passed via `new Machine(config)`.
+
+5. **Named tuple construction must match the type definition exactly.** Every ``type`` you define here will be used in ``new Machine(...)`` and ``send ..., eEvent, ...`` call sites throughout the project. The field names you choose become the API contract:
+ ```
+ // Given this type definition:
+ type tNodeConfig = (failureDetector: machine);
+ // ALL construction sites MUST use the exact field name:
+ new Node((failureDetector = fd,)); // CORRECT
+ new Node((fd,)); // WRONG — anonymous tuple
+ new Node(fd); // WRONG — bare value
+ ```
+ Choose field names carefully — they cannot be changed later without updating every call site.
+
+6. **Event payload types define the send-site contract.** When you declare ``event eFoo: tBarPayload;``, every ``send target, eFoo, ...`` in the project must construct a ``tBarPayload`` with named fields:
+ ```
+ // Given: event eNodeSuspected: tNodeSuspectedPayload;
+ // type tNodeSuspectedPayload = (suspectedNode: machine);
+ send client, eNodeSuspected, (suspectedNode = node,); // CORRECT
+ send client, eNodeSuspected, (node); // WRONG
+ ```
+
+Return only the generated P code without any explanation attached. Return the P code enclosed in .
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_filenames.txt b/Src/PeasyAI/resources/instructions/generate_filenames.txt
new file mode 100644
index 0000000000..70564c2eec
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_filenames.txt
@@ -0,0 +1,4 @@
+List the names of only those P files that are required in PSrc, PSpec, and PTst folders of the P project.
+Here is an example of the format I want:
+PSrc: fileA.p, fileB.p ...
+This is very important and critical - Only return the folder-wise comma-seperated list without any additional text.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_files_to_fix_for_pchecker.txt b/Src/PeasyAI/resources/instructions/generate_files_to_fix_for_pchecker.txt
new file mode 100644
index 0000000000..98aef47d1b
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_files_to_fix_for_pchecker.txt
@@ -0,0 +1,17 @@
+### Error Trace
+When running {tool} on a P project, the following error trace was generated. {additional_error_info}
+
+{error_trace}
+
+### Relevant Code
+Below is the P project code where each .p file is provided within XML tags with its filename.
+
+{p_code}
+
+### ANALYSIS REQUEST
+
+Based on the code and error trace above, generate the following:
+1. A clear explanation of what's causing the error. Enclose the explanation in "error_understanding" xml tag
+2. Please provide a brief, natural language description of only the best approach to fixing the error. Enclose this in "fix_description" xml tag
+3. Based on the recommended solution above, list only the files that need to be modified (separated by commas). Enclose this in "files_to_fix" xml tags
+4. Do NOT modify, add, or remove test cases.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_fixed_file_for_pchecker.txt b/Src/PeasyAI/resources/instructions/generate_fixed_file_for_pchecker.txt
new file mode 100644
index 0000000000..6d69e01b12
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_fixed_file_for_pchecker.txt
@@ -0,0 +1,17 @@
+Based on your previous analysis and fix recommendations, generate the fixed P code only for files that require changes.
+For each file that needs fixing:
+1. Apply the recommended fixes
+2. Identify and apply any necessary consequential changes required to maintain the functionality
+3. Ensure the complete fixed code is valid
+
+Here is the original p code of all p files in the project. The code for each file is enclosed in its filename xml tags:
+{p_code}
+
+Your recommended fixes:
+{fix_description}
+
+Please
+1. Fix the error in the necessary file
+2. Identify ALL other files that would need modifications as a result of this change
+3. Provide complete updated versions of each affected file
+4. Enclose each modified file in XML tags using the filename as the tag name. Don't generate any additional response or text.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_machine.txt b/Src/PeasyAI/resources/instructions/generate_machine.txt
new file mode 100644
index 0000000000..73c3bb0c80
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_machine.txt
@@ -0,0 +1,61 @@
+Using the provided P state machine structure as a starting point, implement all function bodies for the machine {machineName}. DO NOT change any of the structure, including variable declarations, state declarations, event handlers, or function signatures. Focus ONLY on implementing the body of each function.
+
+IMPLEMENTATION INSTRUCTIONS:
+
+For each function:
+1. Implement the body according to the intended behavior
+2. Use proper P statements and expressions (conditionals, loops, send statements, etc.)
+3. Ensure the implementation aligns with the "Components" and "Interactions" sections of the design document
+
+## Key Rules:
+1. Never use `=` in variable declarations
+2. Declare: `var name: type;`
+3. Initialize later: `name = value;` (in functions/entry blocks)
+4. P provides defaults - explicit initialization often unnecessary
+5. EVERY event that can be received by this machine in a given state MUST be handled in that state. If an event is not relevant in a state, add `ignore eEventName;` to that state. If it should be processed later, add `defer eEventName;`. PChecker will flag unhandled events as bugs.
+6. Think about which events could arrive late or out-of-order. For example, if a machine transitions from PhaseA to PhaseB, events from PhaseA may still be in the queue — add `ignore` for those stale events in PhaseB.
+
+## Initialization:
+If this machine has a start-state entry function with a parameter, that parameter carries the machine's constructor config (e.g., references to other machines, IDs, sizes). The implementation MUST:
+- Store every field from the config payload into the corresponding local variable.
+- Compute derived values (e.g., majoritySize = sizeof(acceptors) / 2 + 1).
+- If using the init-event pattern instead, the configuration handler function must similarly store all fields and then `goto` the first operational state.
+
+## CRITICAL — Tuple Construction:
+Named-tuple types use ``(field = value, ...)`` syntax. Always include ALL fields.
+ Given: type tConfig = (coordinator: machine, count: int);
+ CORRECT: new Machine((coordinator = coord, count = 3));
+ WRONG: new Machine((coord, 3)); // anonymous tuple — type mismatch!
+ CORRECT single-field: send target, eMsg, (reqId = 42,); // trailing comma required!
+ WRONG single-field: send target, eMsg, (reqId = 42); // missing trailing comma → PARSE ERROR
+
+## CRITICAL — Use Exact Type and Field Names from Enums_Types_Events.p:
+When the Enums_Types_Events.p file is provided as context, you MUST use the EXACT type names and field names defined there. Do NOT invent alternative names.
+ Given: type tPromisePayload = (proposer: machine, highestProposalSeen: int, acceptedValue: int);
+ CORRECT: fun HandlePromise(msg: tPromisePayload) {{ ... msg.highestProposalSeen ... msg.acceptedValue ... }}
+ WRONG: fun HandlePromise(msg: tPromise) {{ ... msg.propNum ... msg.value ... }}
+The function parameter type annotation MUST match the event's payload type exactly as declared.
+
+## File-scope helper functions:
+Some machines provide convenience helper functions declared OUTSIDE the machine body (at file scope). For example, a Timer machine may provide:
+```
+fun CreateTimer(client: machine) : Timer {{
+ return new Timer(client);
+}}
+fun StartTimer(timer: Timer) {{
+ send timer, eStartTimer;
+}}
+```
+If the design document specifies helper functions for this machine, include them after the machine's closing brace.
+
+## Checklist before returning code:
+- Every state has handlers, defer, or ignore for EVERY event this machine could receive.
+- The start state entry correctly unpacks the constructor payload (if parameterized).
+- All `send` targets are machine-typed variables (never null/default).
+- No events, types, or enums are declared in this file.
+- Single-field named tuple VALUES always have a trailing comma: ``(field = value,)``. But type annotations do NOT: ``fun Foo(x: tMyType)`` or ``fun Foo(x: (field: int))``.
+- All type names in function signatures match EXACTLY what is declared in Enums_Types_Events.p.
+- All field names in payload access (e.g., payload.fieldName) and construction (e.g., (fieldName = value,)) match EXACTLY what is declared in the type definition.
+- If the design doc specifies file-scope helper functions, they are included after the machine body.
+
+Verify that the generated code strictly follows all the P syntax rules before considering it final. Return only the generated P code without any explanation attached. Return the P code enclosed in XML tags where the tag name is the filename.
diff --git a/Src/PeasyAI/resources/instructions/generate_machine_structure.txt b/Src/PeasyAI/resources/instructions/generate_machine_structure.txt
new file mode 100644
index 0000000000..063691072a
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_machine_structure.txt
@@ -0,0 +1,110 @@
+Distributed systems are notoriously hard to get right (i.e., guaranteeing correctness) as the programmer needs to reason about numerous control paths resulting from the myriad interleaving of events (or messages or failures). Unsurprisingly, programmers can easily introduce subtle errors when designing these systems. Moreover, it is extremely difficult to test distributed systems, as most control paths remain untested, and serious bugs lie dormant for months or even years after deployment.
+
+The P programming framework takes several steps towards addressing these challenges by providing a unified framework for modeling, specifying, implementing, testing, and verifying complex distributed systems.
+
+P provides a high-level state machine based programming language to formally model and specify distributed systems. The syntactic sugar of state machines allows programmers to capture their system design (or protocol logic) as communicating state machines, which is how programmers generally think about their system's design.
+
+P supports specifying and checking both safety as well as liveness specifications (global invariants). Programmers can easily write different scenarios under which they would like to check that the system satisfies the desired correctness specification. The P module system enables programmers to model their system modularly and perform compositional testing to scale the analysis to large distributed systems.
+
+You are an expert programming assistant designed to write P programs for the given complex distributed systems. Avoid overreliance on general programming knowledge and strictly adhere to P's specific requirements. Thoroughly review the P language syntax guide before writing code. Refer the provided context files for the correct syntax while writing.
+
+
+Write the structure of P code for the state machine {machineName}. Include all variable declarations, state declarations, event handlers, and function declarations, BUT leave all function bodies empty (with just placeholder comments). Ensure that the "Components" and "Interactions" sections of the design document are reflected in the structure. The structure should include:
+
+1. Machine declaration with local variable declarations
+2. All states with their annotations (start, hot, cold)
+3. Entry and exit function references within states
+4. Event handlers (on-do and on-goto statements)
+5. Defer and ignore statements
+6. Function declarations with proper parameters and return types, but EMPTY bodies, do add COMMENTS for future stages of code generation.
+
+## CRITICAL: Machine Configuration / Initialization
+
+If this machine depends on references to other machines (e.g., a list of acceptors, a server reference, learner machines) or any configuration, it MUST receive them through one of these two patterns:
+
+### Pattern A — Constructor payload (preferred when all deps are known at creation time)
+
+The start state entry function receives configuration directly from the `new` call:
+
+```
+machine ExampleServer {{
+ var workers: seq[machine];
+ var serverId: int;
+
+ start state Init {{
+ entry InitEntry; // receives config from new ExampleServer(config)
+ on eRequest goto Serving;
+ }}
+
+ fun InitEntry(config: (workers: seq[machine], id: int)) {{
+ workers = config.workers;
+ serverId = config.id;
+ }}
+ ...
+}}
+```
+Test driver creates it as: `new ExampleServer((workers = workerList, id = 1));`
+
+### Pattern B — Initialization event (when config arrives after creation)
+
+The machine blocks in its start state waiting for a configuration event, then transitions to its core logic:
+
+```
+machine ExampleWorker {{
+ var coordinator: machine;
+
+ start state WaitForConfig {{
+ on eWorkerConfig goto Ready with Configure;
+ defer eTask; // defer work events until configured
+ }}
+
+ state Ready {{
+ on eTask do HandleTask;
+ }}
+
+ fun Configure(config: tWorkerConfig) {{
+ coordinator = config.coordinator;
+ }}
+ ...
+}}
+```
+Test driver creates and configures it as:
+```
+worker1 = new ExampleWorker();
+send worker1, eWorkerConfig, (coordinator = coord,);
+```
+
+### Rules
+- Pick ONE pattern per machine and be consistent.
+- Do NOT use ad-hoc event names like eStart or eInit that carry no typed payload. If using Pattern B, define a proper typed config event with a named-tuple payload in Enums_Types_Events.p.
+- If a machine has no external dependencies, its entry function can take no parameters.
+
+## Event Handling Completeness
+
+For EVERY state, think about which events could arrive in that state (including events from prior states still in the queue) and add:
+- `on eX do Handler;` — if the event should be processed
+- `defer eX;` — if the event should be queued for a later state
+- `ignore eX;` — if the event should be silently dropped
+
+PChecker will report an unhandled-event bug for any event that arrives in a state without a handler, defer, or ignore.
+
+## CRITICAL — Use Exact Type Names from Enums_Types_Events.p:
+When the Enums_Types_Events.p file is provided as context, you MUST use the EXACT type names defined there for function parameter annotations and event handler signatures. Do NOT invent alternative names.
+ Given: event ePromise: tPromisePayload;
+ CORRECT: on ePromise do HandlePromise; ... fun HandlePromise(msg: tPromisePayload) {{ ... }}
+ WRONG: on ePromise do HandlePromise; ... fun HandlePromise(msg: tPromise) {{ ... }}
+
+## File-scope helper functions:
+If the design document specifies helper functions for this machine (e.g., CreateTimer, StartTimer), include their declarations (with empty bodies) AFTER the machine's closing brace. These are convenience wrappers declared at file scope.
+
+The machine file should NOT contain:
+
+1. Specification monitors (spec keyword)
+2. Test modules (module keyword with assert)
+3. Test cases (test keyword)
+4. Test setup functions
+5. Monitor specifications with observes keyword
+6. Any code that uses: spec, test, assert, observes keywords
+7. DO NOT declare events, types, or enums in the machine file - these will be provided separately as context.
+
+Return only the generated P structure code without any explanation attached. Return the P code enclosed in XML tags where the tag name is "structure".
diff --git a/Src/PeasyAI/resources/instructions/generate_modules_file.txt b/Src/PeasyAI/resources/instructions/generate_modules_file.txt
new file mode 100644
index 0000000000..cf052de92b
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_modules_file.txt
@@ -0,0 +1 @@
+Write P code for the {filename} file. Verify that the generated code strictly follows all the P syntax rules before considering it final. Return only the generated P code without any explanation attached. Return the P code enclosed in XML tags where the tag name is the filename.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_project_structure.txt b/Src/PeasyAI/resources/instructions/generate_project_structure.txt
new file mode 100644
index 0000000000..9e15050cdc
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_project_structure.txt
@@ -0,0 +1,13 @@
+Create the project directory structure for the P project named {project_name}.
+
+1. Create the three main directories:
+ - PSrc: Source code for P state machines
+ - PSpec: Specification monitors
+ - PTst: Test cases
+
+2. Create a .pproj file in the project root directory with proper XML configuration:
+ - ProjectName: {project_name}
+ - InputFiles: References to PSrc, PSpec, and PTst directories
+ - OutputDir: Directory for generated code (PGenerated)
+
+Create all necessary directories and the .pproj file according to P language project standards.
diff --git a/Src/PeasyAI/resources/instructions/generate_sop_spec.txt b/Src/PeasyAI/resources/instructions/generate_sop_spec.txt
new file mode 100644
index 0000000000..2a9ce5407d
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_sop_spec.txt
@@ -0,0 +1,19 @@
+Carefully read the following standard operating procedure (SOP) and ensure you understand the correct order of actions performed to complete the task.
+
+
+{sop_content}
+
+
+Now, review the following list of the actions. Pay attention to the input and output of each action, as they are essential for determining the next action to take
+
+{actions_content}
+
+
+
+Now, carefully considering the instructions in standard operating procedure and action types, generate only one P specification that ensures that the actions are performed in the correct order.
+- Start by generating the necessary types for payloads and return types of actions
+- Generate events for the actions listed in the tags
+- Add necessary assertions to validate all the decision points in this SOP
+- Add assertions to catch if the system performs any action that deviates from the order given in this SOP
+
+Generate only P specification code and return the P code enclosed in XML tags where the tag name is the filename. P tests and P source implementation are not needed. Verify that the generated code strictly follows all the P syntax rules before considering it final.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/generate_spec_files.txt b/Src/PeasyAI/resources/instructions/generate_spec_files.txt
new file mode 100644
index 0000000000..679a6b0edc
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_spec_files.txt
@@ -0,0 +1,42 @@
+Write P code for the {filename} file. Ensure that the specifications described in the "Specifications" section of the design document are implemented.
+
+CRITICAL RULES FOR SPEC MONITORS:
+1. Spec monitors are declared with `spec SpecName observes event1, event2 {{ ... }}`.
+2. Spec monitors CANNOT use: `this`, `new`, `send`, `announce`, `receive`, `$`, `$$`, `pop`. These are ILLEGAL in monitors and will cause compilation errors.
+3. Monitors can ONLY observe events and maintain internal state (variables, maps, sets). They assert properties based on observed event payloads.
+4. If you need to track per-machine state (e.g., per-consumer offsets), use a `map[machine, ...]` keyed by the machine reference from the event payload — NOT `this`.
+5. Each safety property from the "Specifications" section should be a separate `spec` declaration.
+6. Every spec monitor MUST contain at least one `assert` statement. A spec that observes events but never asserts anything is useless — PChecker will not detect any bugs. If the property is "X must never happen", assert the negation: `assert !badCondition, "X happened";`.
+7. NEVER generate empty functions or functions with only comments. If an event handler doesn't need logic, simply don't observe that event.
+8. Only observe events that are actually defined in the Enums_Types_Events.p file. Do NOT observe events that don't exist.
+
+## Example of a CORRECT spec monitor:
+
+```
+spec MutualExclusion observes eLockGranted, eLockReleased {{
+ var lockHolders: map[int, machine];
+
+ start state Monitoring {{
+ on eLockGranted do (payload: tLockGranted) {{
+ assert !(payload.resourceId in lockHolders),
+ format("Resource {{0}} already held", payload.resourceId);
+ lockHolders[payload.resourceId] = payload.client;
+ }}
+ on eLockReleased do (payload: tLockReleased) {{
+ if (payload.resourceId in lockHolders) {{
+ lockHolders -= (payload.resourceId);
+ }}
+ }}
+ }}
+}}
+```
+
+Note: The spec uses event payloads to track state, asserts on every relevant transition, and never uses `this`/`send`/`new`.
+
+## CRITICAL — Use Exact Type and Field Names from Enums_Types_Events.p:
+When the Enums_Types_Events.p file is provided as context, you MUST use the EXACT type names and field names defined there. Do NOT invent alternative names.
+ Given: event eAccepted: tAcceptedPayload; and type tAcceptedPayload = (proposalNumber: int, acceptedValue: int);
+ CORRECT: on eAccepted do (payload: tAcceptedPayload) {{ ... payload.acceptedValue ... }}
+ WRONG: on eAccepted do (payload: tAccepted) {{ ... payload.value ... }}
+
+Verify that the generated code strictly follows all the P syntax rules before considering it final. Return only the generated P code without any explanation attached. Return the P code enclosed in XML tags where the tag name is the filename.
diff --git a/Src/PeasyAI/resources/instructions/generate_test_files.txt b/Src/PeasyAI/resources/instructions/generate_test_files.txt
new file mode 100644
index 0000000000..29773d2e76
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/generate_test_files.txt
@@ -0,0 +1,167 @@
+Write P code for the {filename} file.
+
+CRITICAL REQUIREMENTS:
+
+1. For EACH scenario in the "Test Scenarios" section, create a dedicated scenario machine (e.g., Scenario1_XXX) with a start state that instantiates the system.
+2. For EACH scenario machine, you MUST write a `test` declaration. Without test declarations, PChecker cannot discover or run any tests. The format is:
+ test tcScenarioName [main=ScenarioMachine]:
+ assert SpecName1, SpecName2 in
+ {{ MachineA, MachineB, ..., ScenarioMachine }};
+ Use the spec/monitor names from the PSpec files in the assert clause. List ALL machines that participate in the scenario inside the braces.
+3. The test driver file MUST contain BOTH the scenario machines AND the test declarations. Do NOT put them in separate files.
+4. Do NOT re-declare or re-define machines that already exist in PSrc. Only define NEW scenario-driver machines here.
+5. You MUST only use events that are already defined in Enums_Types_Events.p. Do NOT invent new events.
+6. Scenario machines must be SIMPLE launchers. They create all system components in the correct order and then stop. They should NOT handle any protocol events (no `on eXxx` handlers).
+
+## CRITICAL: Machine Wiring — Read the Machine Entry Signatures
+
+Look at every machine file provided as context. Each machine's start state has an entry function. That entry function's parameter defines EXACTLY what the machine expects when created. You MUST match it.
+
+There are two initialization patterns used by the machines:
+
+### Pattern A: Constructor payload — machine expects config via `new Machine(payload)`
+If a machine's start-state entry function has a parameter, you MUST pass a matching named-tuple payload when creating it with `new`.
+
+Example — if Proposer.p has:
+```
+start state Init {{
+ entry InitEntry;
+ ...
+}}
+fun InitEntry(config: (acceptors: seq[machine], learners: seq[machine], id: int)) {{
+ ...
+}}
+```
+Then the test driver MUST create it as:
+```
+proposer = new Proposer((acceptors = acceptorSeq, learners = learnerSeq, id = 1));
+```
+
+### Pattern B: Initialization event — machine blocks on a config event
+If a machine's start state waits for an event (e.g., `on eAcceptorConfig goto Ready with Configure;`), you MUST send that event after creating the machine.
+
+Example — if Acceptor.p has:
+```
+start state WaitForConfig {{
+ on eAcceptorConfig goto Ready with Configure;
+ defer ePropose, eAcceptRequest;
+}}
+fun Configure(config: tAcceptorConfig) {{
+ ...
+}}
+```
+Then the test driver MUST do:
+```
+acceptor1 = new Acceptor();
+send acceptor1, eAcceptorConfig, (learners = learnerSeq,);
+```
+
+### Wiring Rules
+- Create machines in DEPENDENCY ORDER: machines with no dependencies first (e.g., Learner), then machines that reference them (e.g., Acceptor needs learners), then machines that reference those (e.g., Proposer needs acceptors).
+- Build `seq[machine]` or `set[machine]` collections BEFORE passing them to dependent machines.
+- NEVER leave machine-typed variables uninitialized (default/null). Every machine reference must point to a created machine.
+- NEVER pass `this` (the scenario machine) as a protocol participant reference.
+- For each `new` call, verify the payload tuple field names and types match the machine's entry function parameter EXACTLY. Single-field tuples need a trailing comma: ``(field = value,)``.
+
+## Complete Example
+
+Given machines with these signatures:
+- Learner: `fun InitEntry()` — no config needed
+- Acceptor: `fun InitEntry(config: (learners: seq[machine]))` — needs learners
+- Proposer: `fun InitEntry(config: (acceptors: seq[machine], learners: seq[machine], id: int))` — needs acceptors + learners
+- Client: `fun InitEntry(config: (proposer: machine, value: int))` — needs proposer
+
+The scenario machine must be:
+```
+machine Scenario1_BasicPaxos {{
+ start state Init {{
+ entry {{
+ var learner1: machine;
+ var acceptors: seq[machine];
+ var learners: seq[machine];
+ var proposer: machine;
+ var client: machine;
+
+ // 1. Create machines with no dependencies
+ learner1 = new Learner();
+ learners += (0, learner1);
+
+ // 2. Create machines that depend on step 1
+ acceptors += (0, new Acceptor((learners = learners,)));
+ acceptors += (1, new Acceptor((learners = learners,)));
+ acceptors += (2, new Acceptor((learners = learners,)));
+
+ // 3. Create machines that depend on steps 1-2
+ proposer = new Proposer((acceptors = acceptors, learners = learners, id = 1));
+
+ // 4. Create clients that depend on proposer
+ client = new Client((proposer = proposer, value = 42));
+ }}
+ }}
+}}
+
+test tcBasicPaxos [main=Scenario1_BasicPaxos]:
+ assert OnlyOneValueChosen in
+ {{ Proposer, Acceptor, Learner, Client, Scenario1_BasicPaxos }};
+```
+
+## CRITICAL — Resolving Circular Dependencies in Machine Wiring
+
+Sometimes there is a circular dependency: Machine A's constructor needs a reference to Machine B, and Machine B's constructor needs a collection of Machine A instances. You MUST resolve this correctly.
+
+Strategy: break the cycle by using an initialization event for one side.
+
+Example — FailureDetector needs a `seq[Node]`, but each Node needs the FD reference:
+```
+// Step 1: Create nodes WITHOUT the FD reference (they'll get it via an init event)
+node1 = new Node();
+node2 = new Node();
+nodes += (0, node1);
+nodes += (1, node2);
+
+// Step 2: Create the FD with the node list
+fd = new FailureDetector((nodes = nodes,));
+
+// Step 3: Send the FD reference to each node via an init event
+send node1, eNodeInit, (failureDetector = fd,);
+send node2, eNodeInit, (failureDetector = fd,);
+```
+
+Alternative: if the Node machine accepts its config via constructor payload and has no circular dependency, simply create machines in the right order:
+```
+// If FD doesn't need node references at construction time:
+fd = new FailureDetector();
+node1 = new Node((failureDetector = fd,));
+node2 = new Node((failureDetector = fd,));
+```
+
+The key rule: every machine reference passed in a constructor or event payload must point to an already-created machine that will correctly handle the events sent to it.
+
+## CRITICAL — Named Tuple Construction
+
+When a machine's entry function expects a named-tuple type like ``tConfig = (field1: T1, field2: T2)``, you MUST construct it with named fields:
+```
+CORRECT: new Machine((field1 = val1, field2 = val2));
+WRONG: new Machine((val1, val2)); // anonymous tuple — type mismatch!
+WRONG: new Machine(val1); // bare value — type mismatch!
+```
+For single-field types, include a trailing comma: ``new Machine((field = value,));``
+
+Similarly for ``send``: if ``event eFoo: tPayload;`` and ``type tPayload = (bar: int);``, then:
+```
+CORRECT: send target, eFoo, (bar = 42,);
+WRONG: send target, eFoo, (42); // anonymous — type mismatch!
+WRONG: send target, eFoo, (this); // bare value — type mismatch!
+```
+
+## Checklist before returning code:
+- Every scenario machine is matched by a `test` declaration.
+- Every `test` declaration includes `assert SpecName in` referencing the spec monitors from PSpec. Without `assert`, PChecker won't verify any properties.
+- All machines used in the scenario are listed inside the braces `{{ ... }}`.
+- No events, types, or machines from PSrc are redeclared.
+- Machine creation order respects dependencies.
+- Single-field named tuples have trailing commas: `(field = value,)`.
+- Every machine reference in a constructor payload points to a real, already-created machine that handles the expected events.
+- Every `new Machine(...)` and `send ..., eEvent, ...` uses named tuple syntax matching the type definition.
+
+Verify that the generated code strictly follows all the P syntax rules before considering it final. Return only the generated P code without any explanation attached. Return the P code enclosed in XML tags where the tag name is the filename.
diff --git a/Src/PeasyAI/resources/instructions/initial_instructions.txt b/Src/PeasyAI/resources/instructions/initial_instructions.txt
new file mode 100644
index 0000000000..c7b6f4c935
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/initial_instructions.txt
@@ -0,0 +1,4 @@
+Here is the system description:
+{userText}
+
+For the given system, list the names of all the P state machines required. Only return the comma-seperated list without any additional text.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/p_code_sanity_check.txt b/Src/PeasyAI/resources/instructions/p_code_sanity_check.txt
new file mode 100644
index 0000000000..ea9a0bb001
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/p_code_sanity_check.txt
@@ -0,0 +1,229 @@
+You are tasked with checking P language code files for compliance with critical compilation rules and making necessary corrections. Follow these steps carefully:
+
+1. EVENT DECLARATIONS CHECK
+- Analyze Enums_Types_Events.p to understand existing declarations
+- Cross-reference with machine names to understand the system structure
+- Scan the target file for event declarations and usage
+- Module definitions: Only reference existing/declared machines in module lists
+- DO NOT DEFINE ANY NEW MACHINES OR SPECS OR TESTS IN modules
+- Verify that every event used is either:
+ * Declared in Enums_Types_Events.p, or in any other available files in the context or
+ * Properly declared within the current file
+- Check for duplicate declarations against Enums_Types_Events.p
+- If an event is used but not declared anywhere, add its declaration to the current file
+
+2. TYPE DEFINITIONS CHECK
+- Analyze Enums_Types_Events.p for existing type definitions
+- Create a dependency graph of type usage in the current file
+- For each type reference:
+ * Check if it's defined in Enums_Types_Events.p
+ * If not, verify it's defined in the current file before usage
+ * For types defined in the current file:
+ - Ensure the types appear before their first usage
+ - Check for conflicts with types in Enums_Types_Events.p
+ - Move definitions earlier if needed
+
+3. VARIABLE DECLARATIONS AND SCOPE CHECK
+- Common Error Pattern 1 - Variables Declared Mid-Block:
+ ```p
+ // INCORRECT
+ machine Client {
+ start state Init {
+ entry {
+ DoSomething();
+ var temp: int; // Declaration after code
+ temp = 5;
+ }
+ }
+ }
+
+ // CORRECT
+ machine Client {
+ start state Init {
+ entry {
+ var temp: int; // Declaration at start
+ DoSomething();
+ temp = 5;
+ }
+ }
+ }
+ ```
+
+- Common Error Pattern 2 - Variables in Wrong Scope:
+ ```p
+ // INCORRECT
+ machine Server {
+ fun ProcessRequest() {
+ while (HasMore()) {
+ var request: Request; // Wrong scope
+ HandleRequest(request);
+ }
+ }
+ }
+
+ // CORRECT
+ machine Server {
+ fun ProcessRequest() {
+ var request: Request; // Correct scope
+ while (HasMore()) {
+ HandleRequest(request);
+ }
+ }
+ }
+ ```
+
+- Declare First, Initialize Later Pattern:
+ ```p
+ // CORRECT P Syntax
+ // ✅ Declaration only
+ var participantsArray: seq[machine];
+ var counter: int;
+ var isReady: bool;
+
+ // ✅ Initialize separately
+ fun InitializeVariables() {
+ participantsArray = default(seq[machine]);
+ counter = 0;
+ isReady = false;
+ }
+ ```
+
+- P Auto-Defaults (Often No Initialization Needed):
+ ```p
+ var counter: int; // Automatically 0
+ var sequence: seq[int]; // Automatically empty
+ var mapping: map[int, string]; // Automatically empty
+ ```
+
+- CRITICAL: P does NOT support these operations:
+ * NO append() function exists - use seq = seq + (element,)
+ * NO inline initialization like var seq: seq[int] = {};
+
+
+- Type compatibility checks:
+ ```p
+ // Sequence Operations in P:
+ var seq: seq[T];
+ var x: T, i: int;
+
+ // CORRECT - Add element x at index i
+ seq += (i, x);
+
+ // CORRECT - Add element to end of sequence
+ seq += (sizeof(seq), x);
+
+ // WRONG - These are not supported:
+ seq = seq + (x,); // Wrong: Cannot concatenate with tuple
+ seq = append(seq, x); // Wrong: No append function
+ seq = seq + default(seq[T]) + (x,); // Wrong: Cannot concatenate sequences
+ ```
+
+5. VARIABLE INITIALIZATION CHECK
+- Initialize collections properly:
+ * Sets: `var mySet: set[int]; mySet = {};`
+ * Sequences: `var mySeq: seq[int]; mySeq = default(seq[int]);`
+ * Maps: `var myMap: map[int, string]; myMap = default(map[int, string]);`
+- Initialize at appropriate scope level, not mixed with other statements
+
+6. TYPE COMPATIBILITY CHECK
+- Check operations between different types:
+ * Set operations must be between same set types
+ * Cannot directly convert between set and sequence
+ * Sequence element types must match exactly
+ * When using set elements in sequence, convert properly:
+ - Use helper functions like SetToSeq for conversion
+ - Ensure return types match expected types
+ * Example:
+ ```p
+ fun SetToSeq(s: set[int]) : seq[int] {
+ var result: seq[int];
+ foreach (elem in s) {
+ result = result + (elem,);
+ }
+ return result;
+ }
+ ```
+
+
+
+8. DUPLICATE DECLARATION CHECK
+- Scan across ALL files for duplicate declarations
+- Check for duplicate spec/monitor names across PSpec files
+- Common error: Same spec declared in multiple files
+- Example fix:
+ ```p
+ // If DeadlockFreedom exists in MutualExclusionSpec.p
+ // Remove it from DeadlockFreedomSpec.p or rename it
+ ```
+
+7. VARIABLE SCOPE CHECK
+- Declare all variables at the start of their scope
+- Move any var declarations found mid-function to the top
+- Ensure variables used in foreach loops are properly declared
+- CRITICAL: P does NOT allow inline initialization in declarations
+- Example:
+ ```p
+ fun ProcessNextInQueue(resourceId: int) {
+ // ALL variable declarations first - NO inline initialization
+ var nextClient: machine;
+ var nextClientId: int;
+ var i: int;
+ var newQueue: seq[machine];
+ var lockRequest: tLockRequest;
+
+ // Then initialization separately
+ newQueue = default(seq[machine]);
+ i = 0;
+
+ // Rest of function logic...
+ }
+ ```
+
+- Fix parser errors like "mismatched input '=' expecting ';'":
+ ```p
+ // WRONG - Causes parser error
+ var lockQueue: seq[tLockRequest] = default(seq[tLockRequest]);
+
+ // CORRECT - Separate declaration and initialization
+ var lockQueue: seq[tLockRequest];
+ lockQueue = default(seq[tLockRequest]);
+ ```
+
+CORRECTION PROCESS:
+1. First scan the entire file to identify all issues
+2. Create a list of required changes
+3. Make changes in this order:
+ - Type definitions and event declarations
+ - Variable declarations and initializations
+ - Collection operations syntax
+ - Type compatibility issues
+4. For each change:
+ - Ensure the syntax matches P language requirements exactly
+ - Verify type compatibility
+ - Check variable scoping and initialization
+ - Use correct collection syntax: seq + (elem,), set + (elem), map + (key, value)
+5. After changes:
+ - Verify all variables are declared at scope start
+ - Check all collection operations use proper P syntax
+ - Ensure type compatibility across operations
+ - Validate helper functions for type conversions
+6. Maintain original code formatting and comments
+
+IMPORTANT NOTES:
+- Preserve all existing functionality while making syntax corrections
+- Keep all comments and documentation intact
+- Maintain consistent indentation and code style
+- If multiple solutions are possible, choose the one that requires minimal code changes
+- Use verified P language syntax for all operations
+
+OUTPUT FORMAT:
+These Rules are CRITICAL. YOU MUST FIX THE CODE IF SOMETHING IS WRONG.
+
+MANDATORY REQUIREMENTS:
+- Always return corrected P code, never return unchanged code if issues exist
+- Fix ALL variable declaration positioning issues
+- Fix ALL collection operation syntax issues
+- Fix ALL type compatibility issues
+- Fix ALL event/type declaration issues
+
+Return only the corrected P code without any explanation. Return the P code enclosed in XML tags where the tag name is the filename.
diff --git a/Src/PeasyAI/resources/instructions/review_code_documentation.txt b/Src/PeasyAI/resources/instructions/review_code_documentation.txt
new file mode 100644
index 0000000000..5a9c318b43
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/review_code_documentation.txt
@@ -0,0 +1,65 @@
+You are adding documentation comments to a generated P program file.
+
+You are given:
+1. The generated P code (already syntactically correct).
+2. The design document that describes the system being modeled.
+3. Other project files for cross-reference context.
+
+Your job is to add insightful `//` comments that help a developer understand and maintain the code long-term. Do NOT just copy text from the design document — the developer can read the design doc themselves. Instead, explain the *why* behind the code: what invariant is being maintained, what protocol step is being executed, what the tricky parts are.
+
+## COMMENT GUIDELINES
+
+### File Header
+Add a 2-4 line header at the top of the file:
+- Name the system and file's role (e.g., "Coordinator for the Two Phase Commit protocol")
+- One sentence summarizing the key responsibility or invariant this file maintains
+
+### Machine / Spec Declarations
+Above each `machine` or `spec` declaration, add a brief block comment (3-6 lines) explaining:
+- What role this component plays in the protocol
+- What key invariant or safety property it maintains or contributes to
+- Any non-obvious design decisions (e.g., "serializes concurrent requests to avoid conflicting prepares")
+
+### State Declarations
+Above each `state` declaration, add 1-2 lines explaining:
+- What phase of the protocol this state represents
+- What the machine is waiting for or doing in this state
+- Any deferred/ignored events and WHY they are deferred/ignored (not just that they are)
+
+### Variable Declarations
+Add inline comments on `var` declarations explaining:
+- What the variable tracks and WHY it's needed (not just restating the type)
+- For collections: what the keys/values represent in protocol terms
+
+### Event Handlers (`on ... do`)
+Above each event handler, add 1-2 lines explaining:
+- What protocol step this handler implements
+- What the expected outcome is (e.g., "accumulates votes; triggers commit/abort decision when all received")
+- Any non-obvious logic (e.g., "uses choose() to model non-deterministic participant failure")
+
+### Send Statements
+For important `send` statements (especially those that drive protocol transitions), add a brief inline or above-line comment explaining:
+- What protocol action this message represents
+- Why it's sent at this point
+
+### Assertions
+Above `assert` statements, explain:
+- What safety property is being checked
+- Under what conditions it could be violated
+
+### DO NOT
+- Do NOT add comments that just restate the code (e.g., `// send prepare request` above `send p, ePrepareReq`)
+- Do NOT add comments on every single line — focus on non-obvious logic
+- Do NOT change any code — only add `//` comments
+- Do NOT add comments inside type/event declaration files (Enums_Types_Events.p) beyond the file header — the type names and field names are self-documenting
+- Do NOT remove any existing comments
+
+## RESPONSE FORMAT
+
+Return the complete file with comments added, wrapped in the following format:
+
+
+... the full P code with comments added ...
+
+
+Return ONLY the documented code. Do not include analysis or explanation outside the tags.
diff --git a/Src/PeasyAI/resources/instructions/review_spec_correctness.txt b/Src/PeasyAI/resources/instructions/review_spec_correctness.txt
new file mode 100644
index 0000000000..678508df1a
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/review_spec_correctness.txt
@@ -0,0 +1,107 @@
+You are reviewing a P specification monitor file for correctness against the design document's safety properties.
+
+You are given:
+1. The generated spec monitor code.
+2. All machine source files (showing events each machine sends/handles).
+3. The Enums_Types_Events.p file (showing all events and their payload types).
+4. The design document (showing the intended safety properties).
+
+Your job is to check the following and return a FIXED version of the spec file:
+
+## CHECK 1: Observes Clause Completeness
+The spec must observe ALL events it needs to verify the safety property.
+
+Think step-by-step:
+- Read the safety property from the design document.
+- Identify which events carry information relevant to that property.
+- Verify the `observes` clause includes all of them.
+
+Example — "If a node crashes, it must eventually be suspected":
+- Needs `eCrash` (to know which node crashed)
+- Needs `eNodeSuspected` (to know which node was suspected)
+- Needs `eHeartbeat` (to know a crashed node stopped sending heartbeats)
+
+BAD: `spec ReliableDetection observes eNodeSuspected { ... }`
+GOOD: `spec ReliableDetection observes eCrash, eHeartbeat, eNodeSuspected { ... }`
+
+## CHECK 2: Correct Event-to-Machine Tracking
+The spec must correctly track which machine an event refers to. Events carry payload with machine references — the spec must use the payload to identify the specific machine.
+
+BAD (tracks nothing specific):
+```
+on eCrash do {
+ crashCount = crashCount + 1;
+}
+```
+
+GOOD (tracks which machine crashed):
+```
+on eCrash goto CrashHandling with HandleCrash;
+```
+But wait — `eCrash` has no payload in many designs. If `eCrash` is sent to a specific node, the spec sees the event but doesn't know WHICH node received it. In that case, the spec needs a different strategy.
+
+KEY INSIGHT: Spec monitors observe events globally. `on eCrash do ...` fires for EVERY `eCrash` event in the system. If `eCrash` has no payload, the spec cannot know which machine was the target. The spec must use other observable events (like the absence of heartbeats from a specific node) to infer crashes.
+
+If an event has a payload (e.g., `event eNodeSuspected: tNodeSuspectedPayload;` with `type tNodeSuspectedPayload = (suspectedNode: machine);`), the handler MUST accept the payload parameter and use it:
+
+BAD:
+```
+on eNodeSuspected do {
+ suspectedCount = suspectedCount + 1;
+}
+```
+
+GOOD:
+```
+on eNodeSuspected do (payload: tNodeSuspectedPayload) {
+ suspectedNodes += (payload.suspectedNode);
+}
+```
+
+## CHECK 3: Assertion Logic Matches the Safety Property
+The assertions must actually verify what the design document says. Read the property carefully and check that the assert conditions are correct.
+
+Example property: "If a node crashes, it must eventually be suspected"
+- This means: after eCrash for node X, eventually eNodeSuspected for node X should fire.
+- The spec should track crashed nodes and suspected nodes, and assert consistency.
+
+BAD assertion (checks something unrelated):
+```
+assert !(node in heartbeatAfterSuspicion),
+ "Node sent heartbeat after suspicion";
+```
+
+GOOD assertion (checks the actual property):
+```
+// Track that a crashed node was indeed suspected
+on eNodeSuspected do (payload: tNodeSuspectedPayload) {
+ if (payload.suspectedNode in crashedNodes) {
+ detectedCrashes += (payload.suspectedNode);
+ }
+}
+```
+
+## CHECK 4: No Forbidden Keywords
+Spec monitors CANNOT use: `this`, `new`, `send`, `announce`, `receive`, `$`, `$$`, `pop`.
+These are illegal in monitors and will cause compilation errors.
+
+## CHECK 5: Payload Type Names Match Exactly
+All event handler parameter types must match the types declared in Enums_Types_Events.p exactly.
+
+## RESPONSE FORMAT
+
+Return your response in this exact format:
+
+
+Brief description of what was wrong and how you fixed it.
+List each issue found as a bullet point.
+
+
+
+
+... fixed spec code ...
+
+
+
+Use the ACTUAL filename of the spec file (e.g., Safety.p, Spec.p, Specification.p).
+Only include files that changed. If the spec is already correct, return it unchanged.
diff --git a/Src/PeasyAI/resources/instructions/review_test_wiring.txt b/Src/PeasyAI/resources/instructions/review_test_wiring.txt
new file mode 100644
index 0000000000..d622b63944
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/review_test_wiring.txt
@@ -0,0 +1,106 @@
+You are reviewing a P test driver file for correctness of machine wiring and initialization.
+
+You are given:
+1. The generated test driver code.
+2. All machine source files (showing each machine's constructor signature).
+3. The Enums_Types_Events.p file (showing all types and events).
+
+Your job is to check ONLY the following and return a FIXED version of the test driver:
+
+## CHECK 1: Dependency Order
+Every `new Machine(config)` call must happen AFTER all machines referenced in `config` have been created.
+
+BAD:
+```
+fd = new FailureDetector((nodes = nodeSet,)); // nodeSet is empty here!
+node1 = new Node((failureDetector = fd,));
+nodeSet += (node1);
+```
+
+GOOD:
+```
+node1 = new Node((failureDetector = fd,));
+nodeSet += (node1);
+fd = new FailureDetector((nodes = nodeSet,)); // nodeSet has node1
+```
+
+## CHECK 2: Circular Dependencies Must Be Resolved
+If Machine A's constructor needs Machine B, and Machine B's constructor needs Machine A, one side MUST use the init-event pattern to break the cycle.
+
+BAD (circular — impossible to satisfy both constructors):
+```
+fd = new FailureDetector((nodes = nodeSet,)); // needs nodes
+node1 = new Node((failureDetector = fd,)); // needs fd
+// But fd was created with empty nodeSet!
+```
+
+GOOD (break cycle with init-event):
+```
+// Create FD first with empty nodes (it will get them via registration)
+fd = new FailureDetector();
+// Create nodes with FD reference
+node1 = new Node((failureDetector = fd,));
+nodeSet += (node1);
+// Send nodes to FD via init event
+send fd, eFDInit, (nodes = nodeSet,);
+```
+
+ALTERNATIVE GOOD (if the machine supports it — create nodes first, then FD):
+```
+// If Node can be created without FD and configured later:
+node1 = new Node();
+nodeSet += (node1);
+fd = new FailureDetector((nodes = nodeSet,));
+send node1, eNodeInit, (failureDetector = fd,);
+```
+
+When resolving a circular dependency:
+- Look at which machine's constructor is SIMPLER (fewer fields, or has fields that are easier to provide later).
+- Break the cycle on that side by removing the problematic field from its constructor config type and adding an init event instead.
+- You MUST also update the machine's code to handle the init event if it doesn't already.
+- If the machine already handles an init event pattern (start state waits for a config event), use that.
+
+## CHECK 3: No Empty Collections Passed as Config
+If a machine's constructor expects `set[machine]` or `seq[machine]`, the collection MUST contain the actual machines, not be empty or default.
+
+BAD:
+```
+fdConfig = (nodes = default(set[machine]),);
+fd = new FailureDetector(fdConfig);
+```
+
+GOOD:
+```
+nodeSet += (node1);
+nodeSet += (node2);
+fd = new FailureDetector((nodes = nodeSet,));
+```
+
+## CHECK 4: Named Tuple Construction
+Every `new Machine(...)` and `send ..., eEvent, ...` must use named tuple syntax matching the type definition exactly.
+
+## RESPONSE FORMAT
+
+If the test driver is correct, return it unchanged.
+
+If there are issues, return the FIXED test driver code. If fixing requires changes to machine files (e.g., adding an init event handler), also return those changed files.
+
+Return your response in this exact format:
+
+
+Brief description of what was wrong and how you fixed it.
+
+
+
+
+... fixed test driver code ...
+
+
+... only if you needed to add new init events ...
+
+
+... only if you needed to add init event handlers ...
+
+
+
+Only include files that changed. If nothing changed, include only the original TestDriver.p.
diff --git a/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_collections.txt b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_collections.txt
new file mode 100644
index 0000000000..5d546206d9
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_collections.txt
@@ -0,0 +1,41 @@
+You are tasked with checking P language code files for COLLECTION OPERATIONS compliance. Follow these steps:
+
+COLLECTION OPERATIONS CHECK:
+- CRITICAL: P does NOT support these operations:
+ * NO append() function exists - use seq = seq + (element,)
+ * NO inline initialization like var seq: seq[int] = {};
+
+- Sequence Operations in P:
+ ```p
+ var seq: seq[T];
+ var x: T, i: int;
+
+ // CORRECT - Add element x at index i
+ seq += (i, x);
+
+ // CORRECT - Add element to end of sequence
+ seq += (sizeof(seq), x);
+
+ // WRONG - These are not supported:
+ seq = seq + (x,); // Wrong: Cannot concatenate with tuple
+ seq = append(seq, x); // Wrong: No append function
+ seq = seq + default(seq[T]) + (x,); // Wrong: Cannot concatenate sequences
+ ```
+
+CORRECT COLLECTION SYNTAX:
+- Sequence operations: `seq + (elem,)`, `seq += (index, elem)`
+- Set operations: `set + (elem)`, `set - (elem)`
+- Map operations: `map + (key, value)`, `map - (key)`
+
+CORRECTION PROCESS:
+1. Scan the entire file to identify collection operation issues
+2. Create a list of required changes for collections only
+3. Make changes ensuring:
+ - Use correct P syntax for all collection operations
+ - Replace unsupported functions with supported syntax
+ - Maintain original functionality
+4. Verify all collection operations compile correctly
+
+OUTPUT FORMAT:
+Return only the corrected P code with collection operation fixes applied.
+Return the P code enclosed in XML tags where the tag name is the filename.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_duplicate_declaration.txt b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_duplicate_declaration.txt
new file mode 100644
index 0000000000..51c5a1f6ae
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_duplicate_declaration.txt
@@ -0,0 +1,26 @@
+You are tasked with checking P language code files for DUPLICATE DECLARATIONS compliance. Follow these steps:
+
+DUPLICATE DECLARATION CHECK:
+- Scan across ALL files for duplicate declarations
+- Check for duplicate spec/monitor names across PSpec files
+- Common error: Same spec declared in multiple files
+- Check for duplicate:
+ * Event declarations
+ * Type definitions
+ * Machine names
+ * Function names
+ * Spec/Monitor names
+
+CORRECTION PROCESS:
+1. Scan the entire file and compare with Enums_Types_Events.p
+2. Identify all duplicate declarations
+3. Create a list of required changes for duplicates only
+4. Make changes by either:
+ - Removing duplicate declarations
+ - Renaming conflicting declarations
+ - Moving declarations to appropriate files
+5. Ensure no functionality is lost during duplicate removal
+
+OUTPUT FORMAT:
+Return only the corrected P code with duplicate declaration fixes applied.
+Return the P code enclosed in XML tags where the tag name is the filename.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_event_declaration b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_event_declaration
new file mode 100644
index 0000000000..ca745f5c66
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_event_declaration
@@ -0,0 +1,26 @@
+You are tasked with checking P language code files for EVENT DECLARATIONS compliance. Follow these steps:
+
+EVENT DECLARATIONS CHECK:
+- Analyze Enums_Types_Events.p to understand existing declarations
+- Cross-reference with machine names to understand the system structure
+- Scan the target file for event declarations and usage
+- Module definitions: Only reference existing/declared machines in module lists
+- DO NOT DEFINE ANY NEW MACHINES OR SPECS OR TESTS IN modules
+- Verify that every event used is either:
+ * Declared in Enums_Types_Events.p, or in any other available files in the context or
+ * Properly declared within the current file
+- Check for duplicate declarations against Enums_Types_Events.p
+- If an event is used but not declared anywhere, add its declaration to the current file
+
+CORRECTION PROCESS:
+1. Scan the entire file to identify event declaration issues
+2. Create a list of required changes for events only
+3. Make changes ensuring:
+ - All events are properly declared before use
+ - No duplicate declarations exist
+ - Module definitions only reference existing machines
+4. Verify all event references have corresponding declarations
+
+OUTPUT FORMAT:
+Return only the corrected P code with event declaration fixes applied.
+Return the P code enclosed in XML tags where the tag name is the filename.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_initialization.txt b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_initialization.txt
new file mode 100644
index 0000000000..901e336ea5
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_initialization.txt
@@ -0,0 +1,48 @@
+You are tasked with checking P language code files for VARIABLE INITIALIZATION compliance. Follow these steps:
+
+VARIABLE INITIALIZATION CHECK:
+- Declare First, Initialize Later Pattern:
+ ```p
+ // CORRECT P Syntax
+ // ✅ Declaration only
+ var participantsArray: seq[machine];
+ var counter: int;
+ var isReady: bool;
+
+ // ✅ Initialize separately
+ fun InitializeVariables() {
+ participantsArray = default(seq[machine]);
+ counter = 0;
+ isReady = false;
+ }
+ ```
+
+- P Auto-Defaults (Often No Initialization Needed):
+ ```p
+ var counter: int; // Automatically 0
+ var sequence: seq[int]; // Automatically empty
+ var mapping: map[int, string]; // Automatically empty
+ ```
+
+- Initialize collections properly:
+ * Sets: `var mySet: set[int]; mySet = {};`
+ * Sequences: `var mySeq: seq[int]; mySeq = default(seq[int]);`
+ * Maps: `var myMap: map[int, string]; myMap = default(map[int, string]);`
+- Initialize at appropriate scope level, not mixed with other statements
+
+CRITICAL - P does NOT support these operations:
+* NO append() function exists
+* NO inline initialization like var seq: seq[int] = {};
+
+CORRECTION PROCESS:
+1. Scan the entire file to identify initialization issues
+2. Create a list of required changes for initialization only
+3. Make changes ensuring:
+ - Proper collection initialization syntax
+ - Separate declaration from initialization
+ - Use of default() for complex types
+4. Remove any unsupported initialization patterns
+
+OUTPUT FORMAT:
+Return only the corrected P code with initialization fixes applied.
+Return the P code enclosed in XML tags where the tag name is the filename.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_type_def.txt b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_type_def.txt
new file mode 100644
index 0000000000..c6a6d6f73b
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_type_def.txt
@@ -0,0 +1,44 @@
+You are tasked with checking P language code files for TYPE DEFINITIONS compliance. Follow these steps:
+
+TYPE DEFINITIONS CHECK:
+- Analyze Enums_Types_Events.p for existing type definitions
+- Create a dependency graph of type usage in the current file
+- For each type reference:
+ * Check if it's defined in Enums_Types_Events.p
+ * If not, verify it's defined in the current file before usage
+ * For types defined in the current file:
+ - Ensure the types appear before their first usage
+ - Check for conflicts with types in Enums_Types_Events.p
+ - Move definitions earlier if needed
+
+TYPE COMPATIBILITY CHECK:
+- Check operations between different types:
+ * Set operations must be between same set types
+ * Cannot directly convert between set and sequence
+ * Sequence element types must match exactly
+ * When using set elements in sequence, convert properly:
+ - Use helper functions like SetToSeq for conversion
+ - Ensure return types match expected types
+ * Example:
+ ```p
+ fun SetToSeq(s: set[int]) : seq[int] {
+ var result: seq[int];
+ foreach (elem in s) {
+ result = result + (elem,);
+ }
+ return result;
+ }
+ ```
+
+CORRECTION PROCESS:
+1. Scan the entire file to identify type definition and compatibility issues
+2. Create a list of required changes for types only
+3. Make changes ensuring:
+ - Type definitions appear before usage
+ - No type conflicts exist
+ - Type compatibility is maintained across operations
+4. Add helper functions for type conversions if needed
+
+OUTPUT FORMAT:
+Return only the corrected P code with type definition fixes applied.
+Return the P code enclosed in XML tags where the tag name is the filename.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_var_declaration_scope.txt b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_var_declaration_scope.txt
new file mode 100644
index 0000000000..6e209cc9c5
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/sanity_checks/sanity_check_var_declaration_scope.txt
@@ -0,0 +1,97 @@
+You are tasked with checking P language code files for VARIABLE DECLARATIONS AND SCOPE compliance. Follow these steps:
+
+VARIABLE DECLARATIONS AND SCOPE CHECK:
+- Common Error Pattern 1 - Variables Declared Mid-Block:
+ ```p
+ // INCORRECT
+ machine Client {
+ start state Init {
+ entry {
+ DoSomething();
+ var temp: int; // Declaration after code
+ temp = 5;
+ }
+ }
+ }
+
+ // CORRECT
+ machine Client {
+ start state Init {
+ entry {
+ var temp: int; // Declaration at start
+ DoSomething();
+ temp = 5;
+ }
+ }
+ }
+ ```
+
+- Common Error Pattern 2 - Variables in Wrong Scope:
+ ```p
+ // INCORRECT
+ machine Server {
+ fun ProcessRequest() {
+ while (HasMore()) {
+ var request: Request; // Wrong scope
+ HandleRequest(request);
+ }
+ }
+ }
+
+ // CORRECT
+ machine Server {
+ fun ProcessRequest() {
+ var request: Request; // Correct scope
+ while (HasMore()) {
+ HandleRequest(request);
+ }
+ }
+ }
+ ```
+
+VARIABLE SCOPE CHECK:
+- Declare all variables at the start of their scope
+- Move any var declarations found mid-function to the top
+- Ensure variables used in foreach loops are properly declared
+- CRITICAL: P does NOT allow inline initialization in declarations
+
+Example:
+```p
+fun ProcessNextInQueue(resourceId: int) {
+ // ALL variable declarations first - NO inline initialization
+ var nextClient: machine;
+ var nextClientId: int;
+ var i: int;
+ var newQueue: seq[machine];
+ var lockRequest: tLockRequest;
+
+ // Then initialization separately
+ newQueue = default(seq[machine]);
+ i = 0;
+
+ // Rest of function logic...
+}
+```
+
+Fix parser errors like "mismatched input '=' expecting ';'":
+```p
+// WRONG - Causes parser error
+var lockQueue: seq[tLockRequest] = default(seq[tLockRequest]);
+
+// CORRECT - Separate declaration and initialization
+var lockQueue: seq[tLockRequest];
+lockQueue = default(seq[tLockRequest]);
+```
+
+CORRECTION PROCESS:
+1. Scan the entire file to identify variable declaration and scope issues
+2. Create a list of required changes for variables only
+3. Make changes ensuring:
+ - All variables declared at scope start
+ - No inline initialization in declarations
+ - Proper variable scoping
+4. Maintain original functionality while fixing syntax
+
+OUTPUT FORMAT:
+Return only the corrected P code with variable declaration fixes applied.
+Return the P code enclosed in XML tags where the tag name is the filename.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/semantic-fix-sets/generic/analysis_prompt.txt b/Src/PeasyAI/resources/instructions/semantic-fix-sets/generic/analysis_prompt.txt
new file mode 100644
index 0000000000..fd28600e35
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/semantic-fix-sets/generic/analysis_prompt.txt
@@ -0,0 +1,16 @@
+### Error Trace
+When running {tool} on a P project, the following error trace was generated. {additional_error_info}
+
+{error_trace}
+
+### Relevant Code
+Below is the P project code where each .p file is provided within XML tags with its filename.
+
+{p_code}
+
+### ANALYSIS REQUEST
+
+Based on the code and error trace above, generate the following:
+1. A clear explanation of what's causing the error. Enclose the explanation in "error_understanding" xml tag
+2. Generate the single most recommended solution to fix this issue. Please provide fix description of only the best approach. Enclose this in "fix_description" xml tag
+3. Based on the recommended solution above, list only the files that need to be modified (separated by commas). Enclose this in "files_to_fix" xml tags
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/semantic-fix-sets/generic/fix_prompt.txt b/Src/PeasyAI/resources/instructions/semantic-fix-sets/generic/fix_prompt.txt
new file mode 100644
index 0000000000..6d69e01b12
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/semantic-fix-sets/generic/fix_prompt.txt
@@ -0,0 +1,17 @@
+Based on your previous analysis and fix recommendations, generate the fixed P code only for files that require changes.
+For each file that needs fixing:
+1. Apply the recommended fixes
+2. Identify and apply any necessary consequential changes required to maintain the functionality
+3. Ensure the complete fixed code is valid
+
+Here is the original p code of all p files in the project. The code for each file is enclosed in its filename xml tags:
+{p_code}
+
+Your recommended fixes:
+{fix_description}
+
+Please
+1. Fix the error in the necessary file
+2. Identify ALL other files that would need modifications as a result of this change
+3. Provide complete updated versions of each affected file
+4. Enclose each modified file in XML tags using the filename as the tag name. Don't generate any additional response or text.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/semantic-fix-sets/hot-state/analysis_prompt.txt b/Src/PeasyAI/resources/instructions/semantic-fix-sets/hot-state/analysis_prompt.txt
new file mode 100644
index 0000000000..5e6d5b669c
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/semantic-fix-sets/hot-state/analysis_prompt.txt
@@ -0,0 +1,34 @@
+### LIVENESS ERROR TRACE
+When running {tool} on a P project, the following hot state liveness violation was reported. This means that the system entered a state marked as "hot" (i.e., it must eventually be exited), but no progress was made out of that state by the end of execution. The error trace and context are below:
+
+{additional_error_info}
+{error_trace}
+
+### RELEVANT CODE
+Below is the P project code. Each `.p` file is wrapped in XML tags labeled by filename.
+
+{p_code}
+
+### ANALYSIS REQUEST
+You are to analyze the liveness issue and provide a structured response as described below.
+
+#### SPECIFIC ANALYSIS STEPS:
+
+- Locate the hot state in the state machine definition and identify which machine owns it
+- Trace backwards: What sequence of events should lead OUT of this hot state?
+- Check event generation: Are the required exit events being sent by other machines?
+- Verify event delivery: Can the events reach the target machine (proper addressing)?
+- Examine state transitions: Are there guards/conditions blocking valid transitions?
+- Look for crashed machines: Did any machine terminate due to UnhandledEventException?
+- Check initialization order: Are machines initialized with all required dependencies?
+- Consider message timing: Could late-arriving messages be causing issues?
+
+
+#### OUTPUT FORMAT:
+Please generate the following outputs based on the analysis:
+
+1. A clear explanation of what is causing the hot state to remain active. Mention which machine is stuck, what it was waiting for, and what is missing. Reference specific evidence from the trace. Enclose this in an `` XML tag.
+
+2. The single most recommended fix with detailed rationale. Explain exactly what code changes are needed and why this will resolve the liveness issue. Consider the common patterns from distributed systems debugging. Enclose this in a `` XML tag.
+
+3. Based on the fix, list the minimal set of `.p` files that need to be modified (filenames only, comma-separated). Enclose this in a `` XML tag.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/semantic-fix-sets/hot-state/fix_prompt.txt b/Src/PeasyAI/resources/instructions/semantic-fix-sets/hot-state/fix_prompt.txt
new file mode 100644
index 0000000000..6d69e01b12
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/semantic-fix-sets/hot-state/fix_prompt.txt
@@ -0,0 +1,17 @@
+Based on your previous analysis and fix recommendations, generate the fixed P code only for files that require changes.
+For each file that needs fixing:
+1. Apply the recommended fixes
+2. Identify and apply any necessary consequential changes required to maintain the functionality
+3. Ensure the complete fixed code is valid
+
+Here is the original p code of all p files in the project. The code for each file is enclosed in its filename xml tags:
+{p_code}
+
+Your recommended fixes:
+{fix_description}
+
+Please
+1. Fix the error in the necessary file
+2. Identify ALL other files that would need modifications as a result of this change
+3. Provide complete updated versions of each affected file
+4. Enclose each modified file in XML tags using the filename as the tag name. Don't generate any additional response or text.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/streamlit-snappy/1_ask_llm_which_files_it_needs.txt b/Src/PeasyAI/resources/instructions/streamlit-snappy/1_ask_llm_which_files_it_needs.txt
new file mode 100644
index 0000000000..f2099edd34
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/streamlit-snappy/1_ask_llm_which_files_it_needs.txt
@@ -0,0 +1,8 @@
+I need your help to solve a bug in my p project.
+Here is the error trace:
+{error_trace}
+
+Here are the files in this project:
+{file_list}
+
+Which files do you need to accurately diagnose this issue? Place each file on a new line.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/instructions/streamlit-snappy/2_generate_fix_patches_for_file.txt b/Src/PeasyAI/resources/instructions/streamlit-snappy/2_generate_fix_patches_for_file.txt
new file mode 100644
index 0000000000..beb6154e45
--- /dev/null
+++ b/Src/PeasyAI/resources/instructions/streamlit-snappy/2_generate_fix_patches_for_file.txt
@@ -0,0 +1,39 @@
+Here is the original p code of all p files in the project. The code for each file is enclosed in its filename xml tags:
+{p_code}
+
+Based on the following analysis and fix recommendations, generate the fixed P code only for files that require changes.
+{fix_description}
+
+Output guide:
+1. Fix the error in the necessary file
+2. Identify ALL other files that would need modifications as a result of this change
+3. ONLY provide patches for the files in the unified diff format. Here is an example of a patch:
+
+```
+--- transaction.p 2025-08-05 10:30:45.000000000 -0700
++++ transaction.p 2025-08-06 14:22:18.000000000 -0700
+@@ -1,6 +1,7 @@
+ event WithdrawRequest: (accountId: int, amount: int, reqId: int);
+-event WithdrawResponse: (success: bool, reqId: int);
++event WithdrawResponse: (success: bool, reqId: int, newBalance: int);
+
+ type TransactionRecord = (id: int, amount: int, timestamp: int);
++type AccountInfo = (balance: int, isActive: bool, lastAccess: int);
+
+ fun ValidateAmount(amount: int) : bool
+
+--- server.p 2025-08-05 10:30:45.000000000 -0700
++++ server.p 2025-08-06 14:22:18.000000000 -0700
+@@ -3,7 +3,8 @@
+ machine BankServer
+ {{
+ var accounts: map[int, int];
+- var totalFunds: int;
++ var totalFunds: int;
++ var transactionLog: seq[TransactionRecord];
+
+ start state Ready {{
+ on WithdrawRequest do (payload: WithdrawReq) {{
+```
+
+4. Respond ONLY in unified diff format, no talking.
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Advanced_3_RingLeaderVerification/PSrc/System.p b/Src/PeasyAI/resources/rag_examples/Advanced_3_RingLeaderVerification/PSrc/System.p
new file mode 100644
index 0000000000..7d6d54b5e2
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Advanced_3_RingLeaderVerification/PSrc/System.p
@@ -0,0 +1,93 @@
+type tNominate = (voteFor: machine);
+
+event eNominate : tNominate;
+
+pure le(x: machine, y: machine): bool;
+init-condition forall (x: machine) :: le(x, x);
+init-condition forall (x: machine, y: machine) :: le(x, y) || le(y, x);
+init-condition forall (x: machine, y: machine, z: machine) :: (le(x, y) && le(y, z)) ==> le(x, z);
+init-condition forall (x: machine, y: machine) :: (le(x, y) && le(y, x)) ==> (x == y);
+
+Lemma less_than {
+ invariant le_refl: forall (x: machine) :: le(x, x);
+ invariant le_symm: forall (x: machine, y: machine) :: le(x, y) || le(y, x);
+ invariant le_trans: forall (x: machine, y: machine, z: machine) :: (le(x, y) && le(y, z)) ==> le(x, z);
+ invariant le_antisymm: forall (x: machine, y: machine) :: (le(x, y) && le(y, x)) ==> (x == y);
+}
+Proof {
+ prove less_than;
+}
+
+pure btw(x: machine, y: machine, z: machine): bool;
+init-condition forall (w: machine, x: machine, y: machine, z: machine) :: (btw(w, x, y) && btw(w, y, z)) ==> btw(w, x, z);
+init-condition forall (x: machine, y: machine, z: machine) :: btw(x, y, z) ==> !btw(x, z, y);
+init-condition forall (x: machine, y: machine, z: machine) :: btw(x, y, z) || btw(x, z, y) || (x == y) || (x == z) || (y == z);
+init-condition forall (x: machine, y: machine, z: machine) :: btw(x, y, z) ==> btw(y, z, x);
+
+Lemma between_rel {
+ invariant btw_1: forall (w: machine, x: machine, y: machine, z: machine) :: (btw(w, x, y) && btw(w, y, z)) ==> btw(w, x, z);
+ invariant btw_2: forall (x: machine, y: machine, z: machine) :: btw(x, y, z) ==> !btw(x, z, y);
+ invariant btw_3: forall (x: machine, y: machine, z: machine) :: btw(x, y, z) || btw(x, z, y) || (x == y) || (x == z) || (y == z);
+ invariant btw_4: forall (x: machine, y: machine, z: machine) :: btw(x, y, z) ==> btw(y, z, x);
+}
+Proof {
+ prove between_rel;
+}
+
+pure right(x: machine): machine;
+init-condition forall (x: machine) :: x != right(x);
+init-condition forall (x: machine, y: machine) :: (x != y && y != right(x)) ==> btw(x, right(x), y);
+init-condition forall (x: machine, y: machine) :: !btw(x, y, right(x));
+init-condition forall (x: machine, n: machine, m: machine) :: m == right(n) ==> (btw(n, m, x) || x == m || x == n);
+
+Lemma right_rel {
+ invariant right_neq_self: forall (x: machine) :: x != right(x);
+ invariant btw_right: forall (x: machine, y: machine) :: (x != y && y != right(x)) ==> btw(x, right(x), y);
+ invariant Aux1: forall (x: machine, y: machine) :: !btw(x, y, right(x));
+ invariant right_btw: forall (x: machine, n: machine, m: machine) :: m == right(n) ==> (btw(n, m, x) || x == m || x == n);
+}
+Proof {
+ prove right_rel;
+}
+
+machine Server {
+ start state Proposing {
+ entry {
+ send right(this), eNominate, (voteFor=this,);
+ }
+ on eNominate do (n: tNominate) {
+ if (n.voteFor == this) {
+ goto Won;
+ } else if (le(this, n.voteFor)) {
+ send right(this), eNominate, (voteFor=n.voteFor,);
+ } else {
+ send right(this), eNominate, (voteFor=this,);
+ }
+ }
+ }
+ state Won {
+ ignore eNominate;
+ }
+}
+
+
+
+// voteFor is the running max
+Lemma lemmas {
+ invariant LeaderMax: forall (x: machine, y: machine) :: x is Won ==> le(y, x);
+ invariant Aux: forall (x: machine, y: machine) :: (le(x, y) && le(y, x)) ==> x == y;
+ invariant NoBypass: forall (n: machine, m: machine, e: eNominate) :: (inflight e && e targets m && btw(e.voteFor, n, m)) ==> le(n, e.voteFor);
+ invariant SelfPendingMax: forall (n: machine, m: machine, e: eNominate) :: (inflight e && e targets m && e.voteFor == m) ==> le(n, m);
+}
+Proof {
+ prove lemmas using less_than, between_rel, right_rel;
+}
+
+// Main theorems
+Theorem Safety {
+ invariant UniqueLeader: forall (x: machine, y: machine) :: (x is Won && y is Won) ==> x == y;
+}
+Proof {
+ prove Safety using lemmas_LeaderMax, lemmas_Aux;
+ prove default;
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Advanced_5_Consensus/PSrc/System.p b/Src/PeasyAI/resources/rag_examples/Advanced_5_Consensus/PSrc/System.p
new file mode 100644
index 0000000000..8b394d156c
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Advanced_5_Consensus/PSrc/System.p
@@ -0,0 +1,68 @@
+event eRequestVote: (src: Node);
+event eVote: (voter: Node);
+
+pure nodes(): set[machine];
+pure isQuorum(s: set[machine]): bool;
+
+init-condition forall (m: machine) :: m in nodes() == m is Node;
+axiom forall (q1: set[machine], q2: set[machine]) ::
+ isQuorum(q1) && isQuorum(q2) ==> exists (a: machine) :: a in q1 && a in q2;
+axiom forall (q: set[machine]) ::
+ isQuorum(q) ==> forall (a: machine) :: a in q ==> a in nodes();
+
+init-condition forall (n: Node) :: !n.voted && n.votes == default(set[machine]);
+
+machine Node {
+ var voted: bool;
+ var votes: set[machine];
+
+ start state RequestVoting {
+ entry {
+ var m: machine;
+ foreach (m in nodes())
+ invariant forall new (e: event) :: e is eRequestVote;
+ invariant forall new (e: eRequestVote) :: e.src == this;
+ {
+ send m, eRequestVote, (src=this,);
+ }
+ }
+
+ on eRequestVote do (payload: (src: Node)) {
+ if (!voted) {
+ voted = true;
+ send payload.src, eVote, (voter=this,);
+ }
+ }
+
+ on eVote do (payload: (voter: Node)) {
+ votes += (payload.voter);
+ if (isQuorum(votes)) {
+ goto Won;
+ }
+ }
+
+ }
+
+ state Won {
+ ignore eRequestVote, eVote;
+ }
+}
+
+Lemma quorum_votes {
+ invariant one_vote_per_node:
+ forall (e1: eVote, e2: eVote) :: e1.voter == e2.voter ==> e1 == e2;
+ invariant won_implies_quorum_votes:
+ forall (n: Node) :: n is Won ==> isQuorum(n.votes);
+}
+Proof {
+ prove quorum_votes;
+}
+
+Theorem election_safety {
+ invariant unique_leader:
+ forall (n1: Node, n2: Node) :: n1 is Won && n2 is Won ==> n1 == n2;
+}
+Proof {
+ prove election_safety using quorum_votes;
+ prove default;
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Advanced_6_DistributedLock/PSrc/System.p b/Src/PeasyAI/resources/rag_examples/Advanced_6_DistributedLock/PSrc/System.p
new file mode 100644
index 0000000000..79f8a2d7d5
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Advanced_6_DistributedLock/PSrc/System.p
@@ -0,0 +1,41 @@
+event eGrant: (node: Node, epoch: int);
+event eAccept: (epoch: int, source: Node);
+
+machine Node {
+ var epoch: int;
+ var held: bool;
+
+ start state Act {
+ on eGrant do (payload: (node: Node, epoch: int)) {
+ if (held && payload.epoch > epoch) {
+ held = false;
+ send payload.node, eAccept, (epoch=payload.epoch, source=this);
+ }
+ }
+
+ on eAccept do (payload: (epoch: int, source: Node)) {
+ if (payload.epoch > epoch) {
+ held = true;
+ epoch = payload.epoch;
+ }
+ }
+ }
+}
+
+init-condition exists (n: Node) :: n.held && n.epoch > 0 && forall (n1: Node) :: n1 != n ==> !n1.held && n1.epoch == 0;
+
+Theorem safety {
+ invariant unique_holder: forall (n1: Node, n2: Node) :: n1.held && n2.held ==> n1 == n2;
+ invariant no_lock_while_transfer:
+ forall (n: Node, e: eAccept) :: inflight e ==> !n.held;
+ invariant unique_accept:
+ forall (e1: eAccept, e2: eAccept) :: inflight e1 && inflight e2 ==> e1 == e2;
+ invariant not_held_after_release:
+ forall (n1: Node, e: eAccept) :: inflight e && e.source == n1 ==> !n1.held;
+ invariant transfer_to_higher:
+ forall (n1: Node, n2: Node, e: eAccept) :: inflight e && e.source == n1 && e targets n2 ==> e.epoch > n1.epoch;
+}
+Proof Safety {
+ prove safety;
+ prove default;
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Advanced_7_ShardedKV/PSrc/System.p b/Src/PeasyAI/resources/rag_examples/Advanced_7_ShardedKV/PSrc/System.p
new file mode 100644
index 0000000000..888374b3ce
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Advanced_7_ShardedKV/PSrc/System.p
@@ -0,0 +1,47 @@
+type tKey = int;
+type tValue = int;
+
+type tTransfer = (source: Node, key: tKey, value: tValue);
+type tReshard = (reshard_key: tKey, reshard_to: Node);
+
+event eTransfer: tTransfer;
+event eReshard: tReshard;
+
+pure owns(n: Node, k: tKey): bool = k in n.kv;
+
+machine Node {
+ var kv: map[tKey, tValue];
+
+ start state Serving {
+ on eReshard do (e: tReshard) {
+ var v: tValue;
+ if (e.reshard_key in kv) {
+ v = kv[e.reshard_key];
+ kv -= (e.reshard_key);
+ send e.reshard_to, eTransfer, (source=this, key=e.reshard_key, value=v);
+ }
+ }
+
+ on eTransfer do (e: tTransfer) {
+ kv[e.key] = e.value;
+ }
+ }
+}
+
+Theorem Safety {
+ invariant transfer_means_no_owner:
+ forall (e1: eTransfer, n: Node) ::
+ inflight e1 ==> !owns(n, e1.key);
+ invariant unique_key_transfer:
+ forall (e1: eTransfer, e2: eTransfer) ::
+ inflight e1 && inflight e2 && e1.key == e2.key ==> e1 == e2;
+ invariant transfer_means_not_own:
+ forall (e: eTransfer) :: inflight e ==> !owns(e.source, e.key);
+ invariant unique_owner:
+ forall (k: tKey, n1: Node, n2: Node) ::
+ owns(n1, k) && owns(n2, k) ==> n1 == n2;
+}
+Proof of_Safety {
+ prove Safety;
+ prove default;
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Advanced_8_LockServer/PSrc/System.p b/Src/PeasyAI/resources/rag_examples/Advanced_8_LockServer/PSrc/System.p
new file mode 100644
index 0000000000..97ad6ed61b
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Advanced_8_LockServer/PSrc/System.p
@@ -0,0 +1,96 @@
+event eLock: (sender: Node);
+event eUnlock: (sender: Node);
+event eGrant;
+
+event eAquire;
+event eRelease;
+
+pure lock_server(): machine;
+
+init-condition forall (m: machine) :: m is Server == (m == lock_server());
+init-condition forall (n: Node) :: n.server == lock_server() && !n.has_lock;
+init-condition forall (m: Server) :: m.has_lock;
+
+machine Server {
+ var has_lock: bool;
+
+ start state Serving {
+ on eLock do (p: (sender: Node)) {
+ if (has_lock) {
+ has_lock = false;
+ send p.sender, eGrant;
+ }
+ }
+
+ on eUnlock do (p: (sender: Node)) {
+ has_lock = true;
+ }
+ }
+}
+
+machine Node {
+ var has_lock: bool;
+ var server: machine;
+
+ start state Working {
+
+ on eAquire do {
+ send server, eLock, (sender=this,);
+ }
+
+ on eRelease do {
+ if (has_lock) {
+ has_lock = false;
+ send server, eUnlock, (sender=this,);
+ }
+ }
+
+ on eGrant do {
+ has_lock = true;
+ }
+ }
+}
+
+Lemma system_config {
+ invariant aquire_to_node: forall (e: eAquire, m: machine) :: e targets m && m is Server ==> !inflight e;
+ invariant release_to_node: forall (e: eRelease, m: machine) :: e targets m && m is Server ==>!inflight e;
+ invariant grant_to_node: forall (e: eGrant, m: machine) :: e targets m && m is Server ==> !inflight e;
+ invariant node_send_lock: forall (e: eLock) :: inflight e ==> e.sender is Node;
+ invariant node_send_unlock: forall (e: eUnlock) :: inflight e ==> e.sender is Node;
+
+ invariant lock_to_server: forall (e: eLock, m: Node) :: e targets m ==> !inflight e;
+ invariant unlock_to_server: forall (e: eUnlock, m: Node) :: e targets m ==> !inflight e;
+
+ invariant const_server: forall (m: machine) :: m is Server == (m == lock_server());
+ invariant unique_server: forall (m1: machine, m2: machine) ::
+ m1 is Server && m2 is Server ==> m1 == m2;
+ invariant const_server_ref: forall (n: Node) :: n.server == lock_server();
+}
+Proof {
+ prove system_config;
+}
+
+Theorem safety {
+ invariant unique_lock_holder:
+ forall (n1: Node, n2: Node) :: n1.has_lock && n2.has_lock ==> n1 == n2;
+
+ // Mutually relatively inductive lemmas
+ invariant unique_grant:
+ forall (e1: eGrant, e2: eGrant) :: inflight e1 && inflight e2 ==> e1 == e2;
+ invariant unique_unlock:
+ forall (e1: eUnlock, e2: eUnlock) :: inflight e1 && inflight e2 ==> e1 == e2;
+ invariant grant_server_unlocked:
+ forall (e: eGrant, m: Server) :: inflight e ==> !m.has_lock;
+ invariant no_lock_while_grant:
+ forall (e: eGrant, n: Node) :: inflight e ==> !n.has_lock;
+ invariant no_lock_while_unlock:
+ forall (e: eUnlock, n: Node, s: Server) :: inflight e ==> !n.has_lock && !s.has_lock;
+ invariant grant_not_unlock:
+ forall (e1: eGrant, e2: eUnlock) :: !(inflight e1 && inflight e2);
+ invariant node_server_mutex:
+ forall (n: Node, s: Server) :: !(n.has_lock && s.has_lock);
+}
+Proof {
+ prove safety using system_config;
+ prove default using system_config;
+}
diff --git a/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Coordinator.p b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Coordinator.p
new file mode 100644
index 0000000000..7edd1d8c26
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Coordinator.p
@@ -0,0 +1,66 @@
+// ============================================================================
+// BEST PRACTICES: Coordinator with Broadcast Pattern
+// ============================================================================
+//
+// Demonstrates:
+// 1. How to properly receive a component list via setup event
+// 2. How to broadcast to all components
+// 3. How to handle/ignore broadcast responses in all states
+// ============================================================================
+
+machine Coordinator {
+ var workers: seq[machine];
+ var allComponents: seq[machine];
+ var numWorkers: int;
+
+ start state Init {
+ entry InitEntry;
+ // BEST PRACTICE: Accept setup events in the start state.
+ // These arrive from the test driver after all machines are created.
+ on eSetupComponentList do HandleSetupComponents;
+ // BEST PRACTICE: Ignore protocol events that may arrive before setup.
+ ignore eResponse, eNotifyAll;
+ }
+
+ state Ready {
+ on eRequest do HandleRequest;
+ on eResponse do HandleResponse;
+ // BEST PRACTICE: When you broadcast eNotifyAll to all components,
+ // you might also be in the component list — handle or ignore your own broadcast.
+ ignore eNotifyAll;
+ }
+
+ state Done {
+ // BEST PRACTICE: Terminal states should ignore all events still in flight.
+ ignore eRequest, eResponse, eNotifyAll, eSetupComponentList;
+ }
+
+ fun InitEntry(config: tCoordinatorConfig) {
+ numWorkers = config.numWorkers;
+ // Don't goto Ready yet — wait for setup event with component list.
+ }
+
+ fun HandleSetupComponents(components: seq[machine]) {
+ allComponents = components;
+ goto Ready;
+ }
+
+ fun HandleRequest(req: (sender: machine, data: int)) {
+ // Process request and notify all components
+ BroadcastNotification(req.data);
+ }
+
+ fun HandleResponse(resp: (receiver: machine, result: int)) {
+ // Process response from worker
+ }
+
+ // BEST PRACTICE: Factor broadcast logic into a helper function.
+ fun BroadcastNotification(value: int) {
+ var i: int;
+ i = 0;
+ while (i < sizeof(allComponents)) {
+ send allComponents[i], eNotifyAll, (value = value);
+ i = i + 1;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Types_Events.p b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Types_Events.p
new file mode 100644
index 0000000000..6f02136933
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Types_Events.p
@@ -0,0 +1,34 @@
+// ============================================================================
+// BEST PRACTICES: Types, Events, and Setup Events
+// ============================================================================
+//
+// This file demonstrates best practices for defining types, events, and
+// setup/configuration events in P programs.
+//
+// KEY PRINCIPLES:
+// 1. Separate protocol events from setup events
+// 2. Use setup events for post-creation machine wiring
+// 3. Define clear payload types for each event
+// ============================================================================
+
+// --- Protocol events (used during normal operation) ---
+event eRequest: (sender: machine, data: int);
+event eResponse: (receiver: machine, result: int);
+event eNotifyAll: (value: int);
+
+// --- Setup/configuration events (used ONLY during initialization) ---
+// BEST PRACTICE: Define dedicated setup events for post-creation wiring.
+// NEVER reuse protocol events for setup purposes.
+//
+// Common patterns:
+// 1. Broadcast list setup: event eSetupComponents: seq[machine]
+// 2. Back-reference setup: event eSetupOwner: machine
+// 3. Peer list setup: event eSetupPeers: seq[machine]
+// 4. Full config setup: event eSetupConfig: tConfigType
+
+event eSetupComponentList: seq[machine];
+event eSetupCoordinatorRef: machine;
+
+// --- Configuration types ---
+type tWorkerConfig = (coordinator: machine, workerId: int);
+type tCoordinatorConfig = (numWorkers: int);
diff --git a/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Worker.p b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Worker.p
new file mode 100644
index 0000000000..a141bd75bd
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PSrc/Worker.p
@@ -0,0 +1,51 @@
+// ============================================================================
+// BEST PRACTICES: Worker with Proper Event Handling
+// ============================================================================
+//
+// Demonstrates:
+// 1. Handling broadcast events from coordinator (eNotifyAll)
+// 2. Ignoring events in states where they're not relevant
+// 3. Proper initialization via constructor payload
+// ============================================================================
+
+machine Worker {
+ var coordinator: machine;
+ var workerId: int;
+
+ start state Init {
+ entry InitEntry;
+ // BEST PRACTICE: Ignore broadcast events that may arrive during init.
+ ignore eNotifyAll;
+ }
+
+ state Working {
+ on eRequest do HandleRequest;
+ // BEST PRACTICE: Handle broadcast events from coordinator.
+ // Even if the Worker doesn't need to act on the notification,
+ // it MUST either handle or ignore it to prevent UnhandledEventException.
+ on eNotifyAll do HandleNotification;
+ }
+
+ state Idle {
+ on eRequest do HandleRequest;
+ // BEST PRACTICE: Consistent event handling across all states.
+ ignore eNotifyAll;
+ }
+
+ fun InitEntry(config: tWorkerConfig) {
+ coordinator = config.coordinator;
+ workerId = config.workerId;
+ goto Idle;
+ }
+
+ fun HandleRequest(req: (sender: machine, data: int)) {
+ // Process work and send response back
+ send coordinator, eResponse, (receiver = this, result = req.data * 2);
+ goto Working;
+ }
+
+ fun HandleNotification(notif: (value: int)) {
+ // React to coordinator broadcast
+ goto Idle;
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PTst/TestDriver.p b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PTst/TestDriver.p
new file mode 100644
index 0000000000..8b81a03839
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/BestPractices_MachineWiring/PTst/TestDriver.p
@@ -0,0 +1,65 @@
+// ============================================================================
+// BEST PRACTICES: Test Driver with Proper Machine Wiring
+// ============================================================================
+//
+// This test driver demonstrates the correct pattern for initializing a system
+// with circular dependencies (Coordinator needs component list, Workers need
+// Coordinator reference).
+//
+// KEY PRINCIPLES:
+// 1. Create machines in dependency order
+// 2. Use setup events for post-creation wiring
+// 3. NEVER use protocol events for initialization
+// 4. Build complete component lists AFTER all machines exist
+// 5. Keep scenario machines minimal — just configuration
+// ============================================================================
+
+machine TestSetup {
+ start state Init {
+ entry {
+ var coordinator: machine;
+ var workers: seq[machine];
+ var allComponents: seq[machine];
+ var i: int;
+ var worker: machine;
+
+ // STEP 1: Create the Coordinator first (with just config).
+ // Can't pass allComponents yet — Workers don't exist.
+ coordinator = new Coordinator((numWorkers = 3));
+ allComponents += (0, coordinator);
+
+ // STEP 2: Create Workers (they get the Coordinator reference at creation).
+ i = 0;
+ while (i < 3) {
+ worker = new Worker((coordinator = coordinator, workerId = i));
+ workers += (i, worker);
+ allComponents += (sizeof(allComponents), worker);
+ i = i + 1;
+ }
+
+ // STEP 3: CRITICAL — Send setup event with COMPLETE component list.
+ // Now that all machines exist, wire the Coordinator with the full list.
+ // This uses the dedicated eSetupComponentList event, NOT a protocol event.
+ send coordinator, eSetupComponentList, allComponents;
+
+ // STEP 4: Trigger the actual test scenario.
+ send coordinator, eRequest, (sender = coordinator, data = 42);
+ }
+ }
+}
+
+// ============================================================================
+// ANTI-PATTERN: What NOT to do
+// ============================================================================
+//
+// DON'T do this:
+// send coordinator, eNotifyAll, (value = 0); // Misusing protocol event for setup!
+//
+// The eNotifyAll event is a protocol event meant for broadcasting during operation.
+// Using it to pass initialization data is a SEMANTIC MISMATCH that causes:
+// 1. UnhandledEventException if the receiver state doesn't expect it
+// 2. Safety specs observing eNotifyAll may see spurious initialization events
+// 3. Confusing test traces that mix setup with protocol behavior
+//
+// INSTEAD, define a dedicated setup event (eSetupComponentList) and use it.
+// ============================================================================
diff --git a/Src/PeasyAI/resources/rag_examples/Common_FailureInjector/PSrc/FailureInjector.p b/Src/PeasyAI/resources/rag_examples/Common_FailureInjector/PSrc/FailureInjector.p
new file mode 100644
index 0000000000..888bda64ac
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Common_FailureInjector/PSrc/FailureInjector.p
@@ -0,0 +1,52 @@
+/*
+The failure injector machine randomly selects a replica machine and enqueues the special event "halt".
+*/
+event eDelayNodeFailure;
+// event: event sent by the failure injector to shutdown a node
+event eShutDown: machine;
+
+machine FailureInjector {
+ var nFailures: int;
+ var nodes: set[machine];
+
+ start state Init {
+ entry (config: (nodes: set[machine], nFailures: int)) {
+ nFailures = config.nFailures;
+ nodes = config.nodes;
+ assert nFailures < sizeof(nodes);
+ goto FailOneNode;
+ }
+ }
+
+ state FailOneNode {
+ entry {
+ var fail: machine;
+
+ if(nFailures == 0)
+ raise halt; // done with all failures
+ else
+ {
+ if($)
+ {
+ fail = choose(nodes);
+ send fail, eShutDown, fail;
+ nodes -= (fail);
+ nFailures = nFailures - 1;
+ }
+ else {
+ send this, eDelayNodeFailure;
+ }
+ }
+ }
+
+ on eDelayNodeFailure goto FailOneNode;
+ }
+}
+
+// function to create the failure injection
+fun CreateFailureInjector(config: (nodes: set[machine], nFailures: int)) {
+ new FailureInjector(config);
+}
+
+// failure injector module
+module FailureInjector = { FailureInjector };
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Common_FailureInjector/PSrc/NetworkFunctions.p b/Src/PeasyAI/resources/rag_examples/Common_FailureInjector/PSrc/NetworkFunctions.p
new file mode 100644
index 0000000000..3dad351077
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Common_FailureInjector/PSrc/NetworkFunctions.p
@@ -0,0 +1,24 @@
+// unreliable send operation that drops messages on the ether nondeterministically
+fun UnReliableSend(target: machine, message: event, payload: any) {
+ // nondeterministically drop messages
+ // $: choose()
+ if($) send target, message, payload;
+}
+
+// unrelialbe broadcast function
+fun UnReliableBroadCast(ms: set[machine], ev: event, payload: any) {
+ var i: int;
+ while (i < sizeof(ms)) {
+ UnReliableSend(ms[i], ev, payload);
+ i = i + 1;
+ }
+}
+
+// relialbe broadcast function
+fun ReliableBroadCast(ms: set[machine], ev: event, payload: any) {
+ var i: int;
+ while (i < sizeof(ms)) {
+ send ms[i], ev, payload;
+ i = i + 1;
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Common_Timer/PSrc/Timer.p b/Src/PeasyAI/resources/rag_examples/Common_Timer/PSrc/Timer.p
new file mode 100644
index 0000000000..a0ec2dd176
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Common_Timer/PSrc/Timer.p
@@ -0,0 +1,67 @@
+/*****************************************************************************************
+The timer state machine models the non-deterministic behavior of an OS timer
+******************************************************************************************/
+
+/************************************************
+Events used to interact with the timer machine
+************************************************/
+event eStartTimer;
+event eCancelTimer;
+event eTimeOut;
+event eDelayedTimeOut;
+
+machine Timer {
+ var client: machine;
+ var numDelays: int;
+
+ start state Init {
+ entry (_client : machine) {
+ client = _client;
+ goto WaitForTimerRequests;
+ }
+ }
+
+ state WaitForTimerRequests {
+ on eStartTimer goto TimerStarted with {
+ numDelays = 0;
+ };
+
+ ignore eCancelTimer, eDelayedTimeOut;
+ }
+
+ state TimerStarted {
+ entry {
+ // Bound the number of delays so the timer is guaranteed to
+ // eventually fire (required for liveness properties).
+ if (numDelays >= 3 || choose(10) == 0) {
+ send client, eTimeOut;
+ goto WaitForTimerRequests;
+ } else {
+ numDelays = numDelays + 1;
+ send this, eDelayedTimeOut;
+ }
+ }
+
+ on eDelayedTimeOut goto TimerStarted;
+ on eCancelTimer goto WaitForTimerRequests;
+ defer eStartTimer;
+ }
+}
+
+/************************************************
+Functions or API's to interact with the OS Timer
+*************************************************/
+// create timer
+fun CreateTimer(client: machine) : Timer {
+ return new Timer(client);
+}
+
+// start timer
+fun StartTimer(timer: Timer) {
+ send timer, eStartTimer;
+}
+
+// cancel timer
+fun CancelTimer(timer: Timer) {
+ send timer, eCancelTimer;
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Common_Timer/PSrc/TimerModules.p b/Src/PeasyAI/resources/rag_examples/Common_Timer/PSrc/TimerModules.p
new file mode 100644
index 0000000000..4bb33b3aa8
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Common_Timer/PSrc/TimerModules.p
@@ -0,0 +1,2 @@
+/* Create the timer module which consists of only the timer machine */
+module Timer = { Timer };
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Paxos/PSpec/Safety.p b/Src/PeasyAI/resources/rag_examples/Paxos/PSpec/Safety.p
new file mode 100644
index 0000000000..5780a67b34
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Paxos/PSpec/Safety.p
@@ -0,0 +1,137 @@
+// Safety Specification: Only one value can be chosen
+spec SafetyOnlyOneValueChosen observes eLearn {
+ var chosenValue: int;
+ var hasChosenValue: bool;
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ state Monitoring {
+ on eLearn do CheckSingleValueChosen;
+ }
+
+ fun InitEntry() {
+ hasChosenValue = false;
+ chosenValue = 0;
+ goto Monitoring;
+ }
+
+ fun CheckSingleValueChosen(learnMsg: tLearn) {
+ if (hasChosenValue) {
+ assert chosenValue == learnMsg.finalValue,
+ format("Safety violation: Multiple values chosen. Previously chosen: {0}, Now received: {1}",
+ chosenValue, learnMsg.finalValue);
+ } else {
+ chosenValue = learnMsg.finalValue;
+ hasChosenValue = true;
+ }
+ }
+}
+
+// Safety Specification: Acceptors only accept proposals with increasing proposal numbers
+spec SafetyMonotonicProposalNumbers observes ePromise, eAcceptRequest {
+ var acceptorHighestPromised: map[machine, int];
+ var acceptorHighestAccepted: map[machine, int];
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ state Monitoring {
+ on ePromise do CheckPromiseMonotonicity;
+ on eAcceptRequest do CheckAcceptMonotonicity;
+ }
+
+ fun InitEntry() {
+ goto Monitoring;
+ }
+
+ fun CheckPromiseMonotonicity(promise: tPromise) {
+ var acceptor: machine;
+ var prevHighest: int;
+
+ acceptor = promise.proposer;
+
+ if (acceptor in acceptorHighestPromised) {
+ prevHighest = acceptorHighestPromised[acceptor];
+ assert promise.highestProposalNumber >= prevHighest,
+ format("Safety violation: Acceptor promised lower proposal number. Previous: {0}, Current: {1}",
+ prevHighest, promise.highestProposalNumber);
+ }
+
+ acceptorHighestPromised[acceptor] = promise.highestProposalNumber;
+ }
+
+ fun CheckAcceptMonotonicity(acceptReq: tAcceptRequest) {
+ var proposer: machine;
+ var prevHighest: int;
+
+ proposer = acceptReq.proposer;
+
+ if (proposer in acceptorHighestAccepted) {
+ prevHighest = acceptorHighestAccepted[proposer];
+ }
+
+ acceptorHighestAccepted[proposer] = acceptReq.proposalNumber;
+ }
+}
+
+// Safety Specification: Once a value is accepted by a majority, all subsequent acceptances must be for the same value
+spec SafetyConsistentAcceptedValues observes eAccepted {
+ var acceptedValuesPerProposal: map[int, int];
+ var acceptedCountsPerProposal: map[int, int];
+ var majorityValue: int;
+ var hasMajorityValue: bool;
+ var majorityThreshold: int;
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ state Monitoring {
+ on eAccepted do CheckConsistentAcceptedValues;
+ }
+
+ fun InitEntry() {
+ hasMajorityValue = false;
+ majorityValue = 0;
+ majorityThreshold = 2;
+ goto Monitoring;
+ }
+
+ fun CheckConsistentAcceptedValues(accepted: tAccepted) {
+ var proposalNum: int;
+ var value: int;
+ var currentCount: int;
+
+ proposalNum = accepted.proposalNumber;
+ value = accepted.value;
+
+ if (proposalNum in acceptedValuesPerProposal) {
+ assert acceptedValuesPerProposal[proposalNum] == value,
+ format("Safety violation: Different values accepted for same proposal number {0}. Expected: {1}, Got: {2}",
+ proposalNum, acceptedValuesPerProposal[proposalNum], value);
+ } else {
+ acceptedValuesPerProposal[proposalNum] = value;
+ }
+
+ if (proposalNum in acceptedCountsPerProposal) {
+ currentCount = acceptedCountsPerProposal[proposalNum];
+ acceptedCountsPerProposal[proposalNum] = currentCount + 1;
+ } else {
+ acceptedCountsPerProposal[proposalNum] = 1;
+ }
+
+ if (acceptedCountsPerProposal[proposalNum] >= majorityThreshold) {
+ if (hasMajorityValue) {
+ assert value == majorityValue,
+ format("Safety violation: Different values reached majority. First: {0}, Second: {1}",
+ majorityValue, value);
+ } else {
+ majorityValue = value;
+ hasMajorityValue = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Acceptor.p b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Acceptor.p
new file mode 100644
index 0000000000..b09713d7a6
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Acceptor.p
@@ -0,0 +1,63 @@
+// Acceptor machine for Paxos protocol.
+// Participates in voting on proposals and forwards acceptances to learners.
+//
+// BEST PRACTICE: Handle or ignore ALL events that may be broadcast to this machine.
+// Since the Learner broadcasts eLearn to all components, we must ignore it here.
+machine Acceptor {
+ var highestProposalNumberSeen: int;
+ var acceptedProposalNumber: int;
+ var acceptedValue: int;
+ var hasAcceptedValue: bool;
+ var learners: seq[machine];
+
+ start state Init {
+ entry InitEntry;
+ // BEST PRACTICE: Ignore events that may arrive before initialization completes.
+ ignore eLearn;
+ }
+
+ state WaitingForProposals {
+ on ePropose do HandlePropose;
+ on eAcceptRequest do HandleAcceptRequest;
+ // BEST PRACTICE: When a Learner broadcasts eLearn to all components,
+ // the Acceptor should ignore it since it doesn't need to act on consensus results.
+ ignore eLearn;
+ }
+
+ // BEST PRACTICE: Use a payload tuple to pass initialization parameters.
+ // This avoids the need for setup events in simple cases.
+ fun InitEntry(payload: (learnerSet: seq[machine],)) {
+ highestProposalNumberSeen = -1;
+ acceptedProposalNumber = -1;
+ acceptedValue = 0;
+ hasAcceptedValue = false;
+ learners = payload.learnerSet;
+ goto WaitingForProposals;
+ }
+
+ fun HandlePropose(proposal: tProposal) {
+ if (proposal.proposalNumber > highestProposalNumberSeen) {
+ highestProposalNumberSeen = proposal.proposalNumber;
+ send proposal.proposer, ePromise, (proposer = proposal.proposer, highestProposalNumber = highestProposalNumberSeen, acceptedValue = acceptedValue, hasAcceptedValue = hasAcceptedValue);
+ }
+ }
+
+ fun HandleAcceptRequest(request: tAcceptRequest) {
+ var i: int;
+
+ if (request.proposalNumber >= highestProposalNumberSeen) {
+ highestProposalNumberSeen = request.proposalNumber;
+ acceptedProposalNumber = request.proposalNumber;
+ acceptedValue = request.value;
+ hasAcceptedValue = true;
+
+ // BEST PRACTICE: Forward acceptance to ALL learners.
+ // In Paxos, acceptors notify learners of accepted values directly.
+ i = 0;
+ while (i < sizeof(learners)) {
+ send learners[i], eAccepted, (learner = learners[i], proposalNumber = acceptedProposalNumber, value = acceptedValue);
+ i = i + 1;
+ }
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Client.p b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Client.p
new file mode 100644
index 0000000000..7f90f65664
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Client.p
@@ -0,0 +1,37 @@
+// Client machine for Paxos protocol.
+// Initiates requests and waits for consensus to be reached.
+machine Client {
+ var proposer: machine;
+ var valueToPropose: int;
+ var learnedValue: int;
+ var hasLearnedValue: bool;
+
+ start state Init {
+ entry InitEntry;
+ ignore eLearn;
+ }
+
+ state WaitingForConsensus {
+ on eLearn do HandleLearn;
+ }
+
+ state Done {
+ // BEST PRACTICE: In terminal states, ignore events that may still arrive.
+ ignore eLearn;
+ }
+
+ fun InitEntry(payload: (proposer: machine, value: int)) {
+ proposer = payload.proposer;
+ valueToPropose = payload.value;
+ learnedValue = 0;
+ hasLearnedValue = false;
+ send proposer, eProposeRequest, (client = this, value = valueToPropose);
+ goto WaitingForConsensus;
+ }
+
+ fun HandleLearn(learnMsg: tLearn) {
+ learnedValue = learnMsg.finalValue;
+ hasLearnedValue = true;
+ goto Done;
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Enums_Types_Events.p b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Enums_Types_Events.p
new file mode 100644
index 0000000000..342bd49ea4
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Enums_Types_Events.p
@@ -0,0 +1,31 @@
+// Enums
+enum tProposalStatus { PENDING, PROMISED, ACCEPTED, REJECTED }
+
+// Types
+type tProposal = (proposer: machine, proposalNumber: int, value: int);
+type tPromise = (proposer: machine, highestProposalNumber: int, acceptedValue: int, hasAcceptedValue: bool);
+type tAcceptRequest = (proposer: machine, proposalNumber: int, value: int);
+type tAccepted = (learner: machine, proposalNumber: int, value: int);
+type tLearn = (allComponents: machine, finalValue: int);
+type tProposeRequest = (client: machine, value: int);
+
+// Protocol events
+event ePropose: tProposal;
+event ePromise: tPromise;
+event eAcceptRequest: tAcceptRequest;
+event eAccepted: tAccepted;
+event eLearn: tLearn;
+event eProposeRequest: tProposeRequest;
+event eClientRequest: int;
+
+// BEST PRACTICE: Use dedicated setup/configuration events for post-creation
+// initialization. This solves circular dependency problems where machine A
+// needs machine B's reference at creation time, but B also needs A.
+//
+// Pattern: Create machine A -> Create machine B(A) -> send A, eSetupEvent, B
+//
+// This is especially useful for:
+// 1. Broadcast lists (e.g., Learner's allComponents)
+// 2. Back-references (e.g., Timer needing its owner)
+// 3. Ring topologies (each node needs its neighbor)
+event eSetupLearnerComponents: seq[machine];
diff --git a/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Learner.p b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Learner.p
new file mode 100644
index 0000000000..e7b35780d4
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Learner.p
@@ -0,0 +1,91 @@
+// Learner machine for Paxos protocol.
+// Learns the chosen value once a majority of acceptors agree.
+//
+// BEST PRACTICE: For circular dependency resolution (Learner needs allComponents
+// but components need Learner), use a setup event to pass the component list
+// AFTER all machines are created. See eSetupLearnerComponents below.
+machine Learner {
+ var acceptedValues: map[int, int];
+ var acceptedCounts: map[int, int];
+ var majorityThreshold: int;
+ var totalAcceptors: int;
+ var chosenValue: int;
+ var hasChosenValue: bool;
+ var allComponents: seq[machine];
+
+ start state Init {
+ entry InitEntry;
+ // BEST PRACTICE: Accept a setup event for post-creation initialization.
+ // This solves circular dependency: Learner needs to know about all components,
+ // but components (Acceptors, Proposers) need the Learner reference first.
+ on eSetupLearnerComponents do HandleSetupComponents;
+ ignore eLearn;
+ }
+
+ state WaitingForAcceptances {
+ on eAccepted do HandleAccepted;
+ // BEST PRACTICE: Also accept setup event in this state in case it
+ // arrives after we've transitioned (due to event ordering).
+ on eSetupLearnerComponents do HandleSetupComponents;
+ ignore eLearn;
+ }
+
+ state ValueChosen {
+ // Terminal state after consensus is reached.
+ // BEST PRACTICE: Ignore events that may still be in flight.
+ ignore eAccepted, eLearn, eSetupLearnerComponents;
+ }
+
+ fun InitEntry(payload: (acceptors: int,)) {
+ totalAcceptors = payload.acceptors;
+ majorityThreshold = (totalAcceptors / 2) + 1;
+ hasChosenValue = false;
+ chosenValue = 0;
+ goto WaitingForAcceptances;
+ }
+
+ // BEST PRACTICE: Use a dedicated setup event handler instead of misusing
+ // protocol events (like eLearn) for initialization.
+ fun HandleSetupComponents(components: seq[machine]) {
+ allComponents = components;
+ }
+
+ fun HandleAccepted(acceptance: tAccepted) {
+ var proposalNum: int;
+ var value: int;
+ var currentCount: int;
+
+ proposalNum = acceptance.proposalNumber;
+ value = acceptance.value;
+
+ acceptedValues[proposalNum] = value;
+
+ if (proposalNum in acceptedCounts) {
+ currentCount = acceptedCounts[proposalNum];
+ acceptedCounts[proposalNum] = currentCount + 1;
+ } else {
+ acceptedCounts[proposalNum] = 1;
+ }
+
+ if (acceptedCounts[proposalNum] >= majorityThreshold) {
+ if (!hasChosenValue) {
+ chosenValue = value;
+ hasChosenValue = true;
+ BroadcastLearn(chosenValue);
+ goto ValueChosen;
+ }
+ }
+ }
+
+ fun BroadcastLearn(value: int) {
+ var i: int;
+ var component: machine;
+
+ i = 0;
+ while (i < sizeof(allComponents)) {
+ component = allComponents[i];
+ send component, eLearn, (allComponents = component, finalValue = value);
+ i = i + 1;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Proposer.p b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Proposer.p
new file mode 100644
index 0000000000..8af22c5e37
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Paxos/PSrc/Proposer.p
@@ -0,0 +1,131 @@
+// Proposer machine for Paxos protocol.
+// Initiates proposals and drives consensus with acceptors.
+//
+// BEST PRACTICE: Every state should handle or ignore events that may arrive
+// from broadcast messages (e.g., eLearn broadcast by Learner).
+machine Proposer {
+ var currentProposalNumber: int;
+ var proposedValue: int;
+ var promisesReceived: int;
+ var acceptsReceived: int;
+ var majorityThreshold: int;
+ var totalAcceptors: int;
+ var acceptors: seq[machine];
+ var learner: machine;
+ var client: machine;
+ var highestAcceptedProposalNumber: int;
+ var highestAcceptedValue: int;
+ var hasHighestAcceptedValue: bool;
+ var isProposalActive: bool;
+
+ start state Init {
+ entry InitEntry;
+ ignore eLearn;
+ }
+
+ state WaitingForClientRequest {
+ on eProposeRequest do HandleClientRequest;
+ ignore eLearn;
+ }
+
+ state PreparingProposal {
+ on ePromise do HandlePromise;
+ on eAcceptRequest goto SendingAcceptRequests;
+ ignore eLearn;
+ }
+
+ state SendingAcceptRequests {
+ entry SendAcceptRequestsEntry;
+ on eAccepted do HandleAccepted;
+ // BEST PRACTICE: Use goto to transition when consensus is reached.
+ on eLearn goto Finished;
+ }
+
+ state Finished {
+ // BEST PRACTICE: In terminal states, ignore all events that may still arrive.
+ ignore ePromise, eAccepted, eLearn, eProposeRequest;
+ }
+
+ fun InitEntry(payload: (acceptors: seq[machine], learner: machine, totalAcceptors: int)) {
+ acceptors = payload.acceptors;
+ learner = payload.learner;
+ totalAcceptors = payload.totalAcceptors;
+ majorityThreshold = (totalAcceptors / 2) + 1;
+ currentProposalNumber = 0;
+ promisesReceived = 0;
+ acceptsReceived = 0;
+ highestAcceptedProposalNumber = -1;
+ highestAcceptedValue = 0;
+ hasHighestAcceptedValue = false;
+ isProposalActive = false;
+ goto WaitingForClientRequest;
+ }
+
+ fun HandleClientRequest(request: tProposeRequest) {
+ var i: int;
+ var acceptor: machine;
+
+ client = request.client;
+ proposedValue = request.value;
+ currentProposalNumber = currentProposalNumber + 1;
+ promisesReceived = 0;
+ acceptsReceived = 0;
+ hasHighestAcceptedValue = false;
+ highestAcceptedProposalNumber = -1;
+ isProposalActive = true;
+
+ i = 0;
+ while (i < sizeof(acceptors)) {
+ acceptor = acceptors[i];
+ send acceptor, ePropose, (proposer = this, proposalNumber = currentProposalNumber, value = proposedValue);
+ i = i + 1;
+ }
+
+ goto PreparingProposal;
+ }
+
+ fun HandlePromise(promise: tPromise) {
+ promisesReceived = promisesReceived + 1;
+
+ if (promise.hasAcceptedValue) {
+ if (promise.highestProposalNumber > highestAcceptedProposalNumber) {
+ highestAcceptedProposalNumber = promise.highestProposalNumber;
+ highestAcceptedValue = promise.acceptedValue;
+ hasHighestAcceptedValue = true;
+ }
+ }
+
+ if (promisesReceived >= majorityThreshold) {
+ raise eAcceptRequest, (proposer = this, proposalNumber = currentProposalNumber, value = proposedValue);
+ }
+ }
+
+ fun SendAcceptRequestsEntry() {
+ var valueToPropose: int;
+ var i: int;
+ var acceptor: machine;
+
+ if (hasHighestAcceptedValue) {
+ valueToPropose = highestAcceptedValue;
+ } else {
+ valueToPropose = proposedValue;
+ }
+
+ acceptsReceived = 0;
+
+ i = 0;
+ while (i < sizeof(acceptors)) {
+ acceptor = acceptors[i];
+ send acceptor, eAcceptRequest, (proposer = this, proposalNumber = currentProposalNumber, value = valueToPropose);
+ i = i + 1;
+ }
+ }
+
+ fun HandleAccepted(accepted: tAccepted) {
+ acceptsReceived = acceptsReceived + 1;
+
+ if (acceptsReceived >= majorityThreshold) {
+ isProposalActive = false;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Paxos/PTst/TestDriver.p b/Src/PeasyAI/resources/rag_examples/Paxos/PTst/TestDriver.p
new file mode 100644
index 0000000000..b6f8d121cd
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Paxos/PTst/TestDriver.p
@@ -0,0 +1,111 @@
+// Test Driver for Paxos Protocol.
+//
+// BEST PRACTICES demonstrated:
+// 1. Use a parameterized TestDriver to avoid code duplication across scenarios.
+// 2. Create machines in dependency order: Learner -> Acceptors -> Proposers -> Clients.
+// 3. Use setup events (eSetupLearnerComponents) for post-creation initialization
+// when there are circular dependencies.
+// 4. NEVER misuse protocol events (like eLearn) for initialization/setup.
+// 5. Build complete component lists AFTER all machines are created.
+
+machine TestDriver {
+ var numAcceptors: int;
+ var numProposers: int;
+ var acceptors: seq[machine];
+ var proposers: seq[machine];
+ var learner: machine;
+ var clients: seq[machine];
+ var allComponents: seq[machine];
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ fun InitEntry(config: (acceptors: int, proposers: int, clients: int)) {
+ var i: int;
+ var acceptor: machine;
+ var proposer: machine;
+ var client: machine;
+
+ numAcceptors = config.acceptors;
+ numProposers = config.proposers;
+
+ // STEP 1: Create the Learner first (with just the acceptor count).
+ // We can't pass allComponents yet because Proposers/Clients don't exist.
+ learner = new Learner((acceptors = numAcceptors,));
+ allComponents += (sizeof(allComponents), learner);
+
+ // STEP 2: Create Acceptors (they need the Learner reference).
+ i = 0;
+ while (i < numAcceptors) {
+ acceptor = new Acceptor((learnerSet = default(seq[machine]),));
+ acceptors += (i, acceptor);
+ allComponents += (sizeof(allComponents), acceptor);
+ i = i + 1;
+ }
+
+ // STEP 3: Create Proposers (they need Acceptor list and Learner).
+ i = 0;
+ while (i < numProposers) {
+ proposer = new Proposer((acceptors = acceptors, learner = learner, totalAcceptors = numAcceptors));
+ proposers += (i, proposer);
+ allComponents += (sizeof(allComponents), proposer);
+ i = i + 1;
+ }
+
+ // STEP 4: Create Clients (they need a Proposer).
+ i = 0;
+ while (i < config.clients) {
+ if (i < numProposers) {
+ client = new Client((proposer = proposers[i], value = i + 100));
+ clients += (i, client);
+ allComponents += (sizeof(allComponents), client);
+ }
+ i = i + 1;
+ }
+
+ // STEP 5: BEST PRACTICE — Use setup event for post-creation initialization.
+ // Now that ALL components are created, send the complete list to the Learner.
+ // This avoids the circular dependency problem.
+ send learner, eSetupLearnerComponents, allComponents;
+ }
+}
+
+// Scenario machines delegate to the parameterized TestDriver.
+// BEST PRACTICE: Keep scenario machines minimal — just configuration.
+
+machine TestDriverScenario1 {
+ start state Init {
+ entry {
+ var driver: machine;
+ driver = new TestDriver((acceptors = 3, proposers = 1, clients = 1));
+ }
+ }
+}
+
+machine TestDriverScenario2 {
+ start state Init {
+ entry {
+ var driver: machine;
+ driver = new TestDriver((acceptors = 5, proposers = 2, clients = 2));
+ }
+ }
+}
+
+machine TestDriverScenario3 {
+ start state Init {
+ entry {
+ var driver: machine;
+ driver = new TestDriver((acceptors = 5, proposers = 2, clients = 2));
+ }
+ }
+}
+
+// BEST PRACTICE: Test declarations should include the safety specs.
+test testPaxosScenario1 [main=TestDriverScenario1]:
+ assert SafetyOnlyOneValueChosen in
+ { Proposer, Acceptor, Learner, Client, TestDriver, TestDriverScenario1 };
+
+test testPaxosScenario2 [main=TestDriverScenario2]:
+ assert SafetyOnlyOneValueChosen in
+ { Proposer, Acceptor, Learner, Client, TestDriver, TestDriverScenario2 };
diff --git a/Src/PeasyAI/resources/rag_examples/Portfolio_BenOr/BenOr.p b/Src/PeasyAI/resources/rag_examples/Portfolio_BenOr/BenOr.p
new file mode 100644
index 0000000000..f044baa4e6
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Portfolio_BenOr/BenOr.p
@@ -0,0 +1,237 @@
+event eNext;
+event EvalP1 : (process : int, val : int);
+event EvalP2Case1 : (process : int, val : int);
+event EvalP2Case2 : (process : int, val : int);
+event EvalP2Case3 : int;
+
+machine BenOr {
+ var pc : map[int, int];
+ var r : map[int, int];
+ var p1v : map[int, int];
+ var p2v : map[int, int];
+ var decided : map[int, int];
+ var processes : seq[int];
+ var N : int;
+ var MAXROUND : int;
+ var F : int;
+ var driver : machine;
+ var SentP1Msgs : map[int, seq[(int, int)]];
+ var SentP1MsgsV : map[int, map[int, seq[int]]];
+ var ValSetP1Msgs : map[int, seq[int]];
+ var SentP2Msgs : map[int, seq[(int, int)]];
+ var SentP2MsgsV : map[int, map[int, seq[int]]];
+ var ValSetP2Msgs : map[int, seq[int]];
+
+ start state Init {
+ entry (pld : (driver : machine, N : int, INPUT : seq[int], MAXROUND : int, F : int)) {
+ var i : int;
+ var pairSeq : seq[(int, int)];
+ var intSeq : seq[int];
+ var valMap : map[int, seq[int]];
+ driver = pld.driver;
+ N = pld.N;
+ MAXROUND = pld.MAXROUND;
+ F = pld.F;
+ i = 0;
+ while (i < N) {
+ processes += (i, i + 1);
+ pc[i + 1] = 0;
+ r[i + 1] = 1;
+ p1v[i + 1] = pld.INPUT[i];
+ p2v[i + 1] = -1;
+ decided[i + 1] = -1;
+ i = i + 1;
+ }
+ i = -1;
+ while (i < 2) {
+ valMap[i] = intSeq;
+ i = i + 1;
+ }
+ i = -1;
+ while (i <= MAXROUND) {
+ SentP1Msgs[i] = pairSeq;
+ SentP1MsgsV[i] = valMap;
+ ValSetP1Msgs[i] = intSeq;
+ SentP2Msgs[i] = pairSeq;
+ SentP2MsgsV[i] = valMap;
+ ValSetP2Msgs[i] = intSeq;
+ i = i + 1;
+ }
+ }
+
+ on eNext do {
+ var i : int;
+ var j : int;
+ var choices : seq[(int, int, int)];
+ var choice : (int, int, int);
+ var added : bool;
+ var change : bool;
+ i = 0;
+ while (i < N) {
+ if (pc[processes[i]] == 0) {
+ if (r[processes[i]] <= MAXROUND) {
+ if (!((processes[i], p1v[processes[i]]) in SentP1Msgs[r[processes[i]]])) {
+ SentP1Msgs[r[processes[i]]] += (sizeof(SentP1Msgs[r[processes[i]]]), (processes[i], p1v[processes[i]]));
+ SentP1MsgsV[r[processes[i]]][p1v[processes[i]]] += (sizeof(SentP1MsgsV[r[processes[i]]][p1v[processes[i]]]), processes[i]);
+ if (!(p1v[processes[i]] in ValSetP1Msgs[r[processes[i]]])) {
+ ValSetP1Msgs[r[processes[i]]] += (sizeof(ValSetP1Msgs[r[processes[i]]]), p1v[processes[i]]);
+ }
+ change = true;
+ }
+ p2v[processes[i]] = -1;
+ pc[processes[i]] = 1;
+ }
+ }
+ else if (pc[processes[i]] == 1) {
+ if (sizeof(SentP1Msgs[r[processes[i]]]) >= N - F) {
+ // can do EvalP1(r)
+ if (2 * sizeof(SentP1MsgsV[r[processes[i]]][0]) > N ||
+ 2 * sizeof(SentP1MsgsV[r[processes[i]]][1]) > N) {
+ // make sure there is a choice that will change the value of p2v
+ j = 0;
+ while (j < sizeof(ValSetP1Msgs[r[processes[i]]])) {
+ if (2 * sizeof(SentP1MsgsV[r[processes[i]]][ValSetP1Msgs[r[processes[i]]][j]]) > N) {
+ if (ValSetP1Msgs[r[processes[i]]][j] != p2v[processes[i]]) {
+ choices += (sizeof(choices), (0, processes[i], ValSetP1Msgs[r[processes[i]]][j]));
+ }
+ }
+ j = j + 1;
+ }
+ } else {
+ // does not execute the branch in Eval1, so advance to CP2
+ if (!((processes[i], p2v[processes[i]]) in SentP2Msgs[r[processes[i]]])) {
+ change = true;
+ SentP2Msgs[r[processes[i]]] += (sizeof(SentP2Msgs[r[processes[i]]]), (processes[i], p2v[processes[i]]));
+ SentP2MsgsV[r[processes[i]]][p2v[processes[i]]] += (sizeof(SentP2MsgsV[r[processes[i]]][p2v[processes[i]]]), processes[i]);
+ if (!(p2v[processes[i]] in ValSetP2Msgs[r[processes[i]]])) {
+ ValSetP2Msgs[r[processes[i]]] += (sizeof(ValSetP2Msgs[r[processes[i]]]), p2v[processes[i]]);
+ }
+ pc[processes[i]] = 2;
+ }
+ }
+ }
+ }
+ else if (pc[processes[i]] == 2) {
+ if (sizeof(SentP2Msgs[r[processes[i]]]) >= N - F) {
+ // can do EvalP2(r)
+ if (sizeof(SentP2MsgsV[r[processes[i]]][0]) > F ||
+ sizeof(SentP2MsgsV[r[processes[i]]][1]) > F) {
+ j = 0;
+ while (j < sizeof(ValSetP2Msgs[r[processes[i]]])) {
+ if (ValSetP2Msgs[r[processes[i]]][j] != -1) {
+ if ((p1v[processes[i]] != ValSetP2Msgs[r[processes[i]]][j]) ||
+ (decided[processes[i]] != ValSetP2Msgs[r[processes[i]]][j])) {
+ choices += (sizeof(choices), (1, processes[i], ValSetP2Msgs[r[processes[i]]][j]));
+ }
+ }
+ j = j + 1;
+ }
+ } else {
+ j = 0;
+ added = false;
+ while (j < sizeof(ValSetP2Msgs[r[processes[i]]])) {
+ if (ValSetP2Msgs[r[processes[i]]][j] != -1) {
+ if (p1v[processes[i]] != ValSetP2Msgs[r[processes[i]]][j]) {
+ choices += (sizeof(choices), (2, processes[i], ValSetP2Msgs[r[processes[i]]][j]));
+ added = true;
+ }
+ }
+ j = j + 1;
+ }
+ if (!added) {
+ choices += (sizeof(choices), (3, processes[i], 0));
+ }
+ }
+ }
+ }
+ i = i + 1;
+ }
+ if (sizeof(choices) > 0) {
+ i = choose(sizeof(choices));
+ choice = choices[i];
+ if (choice.0 == 0) {
+ raise EvalP1, (process=choice.1, val=choice.2);
+ } else if (choice.0 == 1) {
+ raise EvalP2Case1, (process=choice.1, val=choice.2);
+ } else if (choice.0 == 2) {
+ raise EvalP2Case2, (process=choice.1, val=choice.2);
+ } else if (choice.0 == 3) {
+ raise EvalP2Case3, (choice.1);
+ }
+ } else if (change) {
+ send driver, eNext;
+ }
+ }
+
+ on EvalP1 do (pld: (process : int, val : int)) {
+ print("EvalP1");
+ p2v[pld.process] = pld.val;
+ if (!((pld.process, p2v[pld.process]) in SentP2Msgs[r[pld.process]])) {
+ SentP2Msgs[r[pld.process]] += (sizeof(SentP2Msgs[r[pld.process]]), (pld.process, p2v[pld.process]));
+ SentP2MsgsV[r[pld.process]][p2v[pld.process]] += (sizeof(SentP2MsgsV[r[pld.process]][p2v[pld.process]]), pld.process);
+ if (!(p2v[pld.process] in ValSetP2Msgs[r[pld.process]])) {
+ ValSetP2Msgs[r[pld.process]] += (sizeof(ValSetP2Msgs[r[pld.process]]), p2v[pld.process]);
+ }
+ }
+ pc[pld.process] = 2;
+ send driver, eNext;
+ }
+
+ on EvalP2Case1 do (pld: (process : int, val : int)) {
+ print("EvalP2 - case 1");
+ p1v[pld.process] = pld.val;
+ decided[pld.process] = pld.val;
+ r[pld.process] = r[pld.process] + 1;
+ pc[pld.process] = 0;
+ send driver, eNext;
+ }
+
+ on EvalP2Case2 do (pld: (process : int, val : int)) {
+ print("EvalP2 - case 2");
+ p1v[pld.process] = pld.val;
+ r[pld.process] = r[pld.process] + 1;
+ pc[pld.process] = 0;
+ send driver, eNext;
+ }
+
+ on EvalP2Case3 do (process : int) {
+ var i : int;
+ print("EvalP2 - case 3");
+ if (p1v[process] == 0) {
+ p1v[process] = 1;
+ } else if (p1v[process] == 1) {
+ p1v[process] = 0;
+ } else {
+ p1v[process] = choose(1);
+ }
+ r[process] = r[process] + 1;
+ pc[process] = 0;
+ send driver, eNext;
+ }
+ }
+}
+
+
+machine Main {
+ var benor : machine;
+ start state Init {
+ entry {
+ var N : int;
+ var INPUT : seq[int];
+ var MAXROUND : int;
+ var F : int;
+ F = 2;
+ N = 4;
+ INPUT += (0, 0);
+ INPUT += (1, 1);
+ INPUT += (2, 1);
+ INPUT += (3, 1);
+ MAXROUND = 3;
+ benor = new BenOr((driver=this, N=N, INPUT=INPUT, MAXROUND=MAXROUND, F=F));
+ send benor, eNext;
+ }
+ on eNext do {
+ send benor, eNext;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Portfolio_ChangRoberts/ChangRoberts.p b/Src/PeasyAI/resources/rag_examples/Portfolio_ChangRoberts/ChangRoberts.p
new file mode 100644
index 0000000000..e4844966d3
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Portfolio_ChangRoberts/ChangRoberts.p
@@ -0,0 +1,120 @@
+event eNext;
+
+machine ChangRoberts {
+ var nodes : seq[int];
+ var ids : map[int, int];
+ var msgs : map[int, seq[int]];
+ var newMsgs : seq[int];
+ var driver : machine;
+ var initiator : map[int, bool];
+ var st : map[int, int];
+ var pc : map[int, int];
+ start state Init {
+ entry (pld : (driver : machine, N : int, Id : map[int, int])) {
+ var i : int;
+ var emptySeq : seq[int];
+ driver = pld.driver;
+ ids = pld.Id;
+ while (i < pld.N) {
+ nodes += (i, i + 1);
+ msgs += (i + 1, emptySeq);
+ initiator += (i + 1, choose());
+ if (initiator[i + 1]) {
+ st += (i + 1, 0);
+ } else {
+ st += (i + 1, 1);
+ }
+ pc += (i + 1, 0);
+ i = i + 1;
+ }
+ }
+ on eNext do {
+ var i : int;
+ var succ : int;
+ var j : int;
+ var choices : seq[(int, int, int, int)];
+ var newMsgs : seq[int];
+ while (i < sizeof(nodes)) {
+ if (i + 1 == sizeof(nodes)) {
+ succ = 0;
+ } else {
+ succ = i + 1;
+ }
+ if (pc[nodes[i]] == 0) {
+ if(initiator[nodes[i]]) {
+ if (!(ids[nodes[i]] in msgs[nodes[succ]])) {
+ choices += (sizeof(choices), (0, nodes[i], nodes[succ], ids[nodes[i]]));
+ }
+ } else {
+ pc[nodes[i]] = 1;
+ }
+ } else {
+ while (j < sizeof(msgs[nodes[i]])) {
+ if (!(msgs[nodes[i]][j] in msgs[nodes[succ]]) &&
+ ((st[nodes[i]] == 1) || msgs[nodes[i]][j] < ids[nodes[i]])) {
+ choices += (sizeof(choices), (1, nodes[i], nodes[succ], msgs[nodes[i]][j]));
+ } else {
+ if (st[nodes[i]] != 2) {
+ if(msgs[nodes[i]][j] == ids[nodes[i]]) {
+ choices += (sizeof(choices), (2, nodes[i], 0, msgs[nodes[i]][j]));
+ }
+ }
+ }
+ j = j + 1;
+ }
+ }
+ i = i + 1;
+ }
+ if (sizeof(choices) > 0) {
+ i = choose(sizeof(choices));
+ if (choices[i].0 == 0) {
+ print("n0");
+ msgs[choices[i].2] += (sizeof(msgs[choices[i].2]), choices[i].3);
+ pc[choices[i].1] = 1;
+ } else {
+ // first msg update
+ j = 0;
+ while (j < sizeof(msgs[choices[i].1])) {
+ if (msgs[choices[i].1][j] != choices[i].3) {
+ newMsgs += (sizeof(newMsgs), msgs[choices[i].1][j]);
+ }
+ j = j + 1;
+ }
+ msgs[choices[i].1] = newMsgs;
+ if (choices[i].0 == 1) {
+ print("n1");
+ st[choices[i].1] = 1;
+ msgs[choices[i].2] += (sizeof(msgs[choices[i].2]), choices[i].3);
+ } else {
+ print("win");
+ st[choices[i].1] = 2;
+ }
+ }
+ send driver, eNext;
+ }
+ }
+ }
+}
+machine Main {
+ var changRoberts : machine;
+ start state Init {
+ entry {
+ var ids : map[int,int];
+ ids += (1, 3);
+ ids += (2, 2);
+ ids += (3, 7);
+ ids += (4, 8);
+ ids += (5, 9);
+ ids += (6, 10);
+ ids += (7, 1);
+ ids += (8, 4);
+ ids += (9, 6);
+ ids += (10, 5);
+ changRoberts = new ChangRoberts((driver=this, N=6, Id=ids));
+ send changRoberts, eNext;
+ }
+ on eNext do {
+ send changRoberts, eNext;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Portfolio_DAO/DAO.p b/Src/PeasyAI/resources/rag_examples/Portfolio_DAO/DAO.p
new file mode 100644
index 0000000000..f4f0c27e0f
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Portfolio_DAO/DAO.p
@@ -0,0 +1,59 @@
+event eNext;
+
+machine DAO {
+ var pc : int;
+ var driver : machine;
+ var bankBalance : int;
+ var amount : int;
+ var stackCount : int;
+ var malloryBalance : int;
+ var attack : int;
+
+ start state Init {
+ entry (pld : (driver : machine, BALANCE : int, AMOUNT : int)) {
+ pc = 0;
+ bankBalance = pld.BALANCE;
+ amount = pld.AMOUNT;
+ attack = 3;
+ driver = pld.driver;
+ }
+ on eNext do {
+ // withdraw
+ if (pc == 0 && (bankBalance >= amount)) {
+ pc = 1;
+ stackCount = stackCount + 1;
+ send driver, eNext;
+ }
+ else if (pc == 1) {
+ // dispense
+ malloryBalance = malloryBalance + amount;
+ if (attack > 0) {
+ attack = attack - 1;
+ stackCount = stackCount + 1;
+ pc = 0;
+ } else {
+ pc = 2;
+ }
+ send driver, eNext;
+ }
+ else if (pc == 2 && (stackCount > 0)) {
+ // update bank balance
+ bankBalance = bankBalance - amount;
+ stackCount = stackCount - 1;
+ send driver, eNext;
+ }
+ }
+ }
+}
+machine Main {
+ var dao : machine;
+ start state Init {
+ entry {
+ dao = new DAO((driver=this, BALANCE=500, AMOUNT=5));
+ send dao, eNext;
+ }
+ on eNext do {
+ send dao, eNext;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Portfolio_German/German.p b/Src/PeasyAI/resources/rag_examples/Portfolio_German/German.p
new file mode 100644
index 0000000000..6c8548e4e7
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Portfolio_German/German.p
@@ -0,0 +1,282 @@
+//Event declaration
+event unit;
+event req_share: machine;
+event req_excl: machine;
+event need_invalidate;
+event invalidate_ack;
+event grant;
+event ask_share;
+event ask_excl;
+event invalidate;
+event grant_excl;
+event grant_share;
+event normal;
+event wait;
+event invalidate_sharers: int;
+event sharer_id: machine;
+
+//Host machine
+machine Main {
+ var curr_client : machine;
+ var clients : seq[machine];
+ var curr_cpu : machine;
+ var sharer_list : seq[machine];
+ var is_curr_req_excl : bool;
+ var is_excl_granted : bool;
+ var i, s :int;
+ var temp: machine;
+ start state init {
+ entry {
+
+ temp = new Client(this, false);
+ clients += (0, temp);
+ temp = new Client(this, false);
+ clients += (0, temp);
+ temp = new Client(this, false);
+ clients += (0, temp);
+ curr_cpu = new CPU(clients);
+ assert(sizeof(sharer_list) == 0);
+ raise unit;
+ }
+ on unit goto receiveState;
+ }
+
+ state receiveState {
+ defer invalidate_ack;
+ entry {}
+
+ on req_share goto ShareRequest;
+ on req_excl goto ExclRequest;
+ }
+
+ state ShareRequest {
+ entry (payload: machine) {
+ curr_client = payload;
+ is_curr_req_excl = false;
+ raise unit;
+ }
+
+ on unit goto ProcessReq;
+ }
+
+ state ExclRequest {
+ entry (payload: machine) {
+ curr_client = payload;
+ is_curr_req_excl = true;
+ raise unit;
+ }
+
+ on unit goto ProcessReq;
+ }
+
+ state ProcessReq {
+ entry {
+ if (is_curr_req_excl || is_excl_granted)
+ {
+ // need to invalidate before giving access
+ raise need_invalidate;
+ }
+ else
+ raise grant;
+ }
+ on need_invalidate goto inv;
+ on grant goto grantAccess;
+ }
+
+ state inv {
+ defer req_share, req_excl;
+ entry {
+ i = 0;
+ s = sizeof(sharer_list);
+ if (s == 0)
+ raise grant;
+ while (i < s)
+ {
+ send sharer_list[i], invalidate;
+ i = i + 1;
+ }
+ }
+ on invalidate_ack do rec_ack;
+ on grant goto grantAccess;
+ }
+
+ fun rec_ack() {
+ sharer_list -= 0;
+ s = sizeof(sharer_list);
+ if (s == 0)
+ raise grant;
+ }
+
+ state grantAccess {
+ entry {
+ if (is_curr_req_excl)
+ {
+ is_excl_granted = true;
+ send curr_client, grant_excl;
+ }
+ else
+ {
+ send curr_client, grant_share;
+ }
+ sharer_list += (0, curr_client);
+ raise unit;
+ }
+ on unit goto receiveState;
+ }
+}
+
+//Client Machine
+machine Client {
+ var host : machine;
+ var pending : bool;
+ start state init {
+ entry (payload: (machine,bool)) {
+ host = payload.0;
+ pending = payload.1;
+ raise unit;
+ }
+ on unit goto invalid;
+ }
+
+ state invalid {
+ entry {
+
+ }
+ on ask_share goto asked_share;
+ on ask_excl goto asked_excl;
+ on invalidate goto invalidating;
+ on grant_excl goto exclusive;
+ on grant_share goto sharing;
+ }
+
+ state asked_share {
+ entry{
+ send host, req_share, this;
+ pending = true;
+ raise unit;
+ }
+ on unit goto invalid_wait;
+ }
+
+ state asked_excl {
+ entry {
+ send host, req_excl, this;
+ pending = true;
+ raise unit;
+ }
+ on unit goto invalid_wait;
+ }
+
+ state invalid_wait {
+ defer ask_share, ask_excl;
+ on invalidate goto invalidating;
+ on grant_excl goto exclusive;
+ on grant_share goto sharing;
+ }
+
+ state asked_ex2 {
+ entry {
+ send host, req_excl, this;
+ pending = true;
+ raise unit;
+ }
+ on unit goto sharing_wait;
+ }
+
+ state sharing {
+ entry {
+ pending = false;
+ }
+ on invalidate goto invalidating;
+ on grant_share goto sharing;
+ on grant_excl goto exclusive;
+ on ask_share goto sharing;
+ on ask_excl goto asked_ex2;
+ }
+
+ state sharing_wait {
+ defer ask_share, ask_excl;
+ entry {}
+ on invalidate goto invalidating;
+ on grant_share goto sharing_wait;
+ on grant_excl goto exclusive;
+
+ }
+
+ state exclusive {
+ ignore ask_share, ask_excl;
+ entry {
+ pending = false;
+ }
+ on invalidate goto invalidating;
+ on grant_share goto sharing;
+ on grant_excl goto exclusive;
+ }
+
+ state invalidating {
+ entry {
+ send host, invalidate_ack;
+ if (pending)
+ {
+ raise wait;
+ }
+ else
+ raise normal;
+ }
+ on wait goto invalid_wait;
+ on normal goto invalid;
+ }
+}
+
+
+//Environment machine in the form of a CPU which makes request to the clients
+machine CPU {
+ var cache : seq[machine];
+ var req_count : int;
+
+ start state init {
+ entry (payload: seq[machine]) {
+ cache = payload;
+ raise unit;
+ }
+ on unit goto makeReq;
+ }
+
+ state makeReq {
+ entry {
+ if ($)
+ {
+ if ($)
+ send cache[0], ask_share;
+ else
+ send cache[0], ask_excl;
+ }
+ else if ($)
+ {
+ if ($)
+ send cache[1], ask_share;
+ else
+ send cache[1], ask_excl;
+ }
+ else
+ {
+ if ($)
+ {
+ send cache[2], ask_share;
+ }
+ else
+ {
+ send cache[2], ask_excl;
+ }
+ }
+ if (req_count < 3) {
+ req_count = req_count + 1;
+ raise unit;
+ }
+ }
+ on unit goto makeReq;
+ }
+}
+
+
+
diff --git a/Src/PeasyAI/resources/rag_examples/Portfolio_Streamlet/str0.p b/Src/PeasyAI/resources/rag_examples/Portfolio_Streamlet/str0.p
new file mode 100644
index 0000000000..4a028c14e4
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Portfolio_Streamlet/str0.p
@@ -0,0 +1,164 @@
+event eNext;
+event UpdateNChain : (node : int, next : int, e : int);
+event Propose : (node : int, e : int);
+event Vote : (node : int, e : int);
+
+machine Str0 {
+ var exists : map[int, bool];
+ var nChain : map[int, seq[int]];
+ var Msgs : seq[(chain : seq[int], epoch : int, sender : int)];
+ var N : int;
+ var EMAX : int;
+ var driver : machine;
+
+ start state Init {
+ entry (pld : (driver : machine, N : int, EMAX : int)) {
+ var i : int;
+ var chain : seq[int];
+ N = pld.N;
+ EMAX = pld.EMAX;
+ driver = pld.driver;
+ while (i < N) {
+ nChain[i] = chain;
+ i = i + 1;
+ }
+ i = 0;
+ while (i <= EMAX) {
+ exists[i] = false;
+ i = i + 1;
+ }
+ exists[0] = true;
+ chain += (0, 0);
+ Msgs += (0, (chain=chain, epoch=0, sender=0));
+ Msgs += (1, (chain=chain, epoch=0, sender=1));
+ Msgs += (2, (chain=chain, epoch=0, sender=2));
+ }
+
+ on eNext do {
+ var i : int;
+ var e : int;
+ var choices : seq[(int, int, int)];
+ while (i < N) {
+ e = 0;
+ while (e <= EMAX) {
+ if ((mod(e, N) == i) && !exists[e]) {
+ choices += (sizeof(choices), (i, 0, e));
+ }
+ else if (exists[e]) {
+ choices += (sizeof(choices), (i, 1, e));
+ }
+ e = e + 1;
+ }
+ i = i + 1;
+ }
+ if (sizeof(choices) > 0) {
+ i = choose(sizeof(choices));
+ raise UpdateNChain, ((node=choices[i].0, next=choices[i].1, e=choices[i].2));
+ }
+ }
+ on UpdateNChain do (pld : (node : int, next : int, e : int)) {
+ var i : int;
+ var j : int;
+ var emptySeq : seq[int];
+ var chains : seq[int];
+ var chain : seq[int];
+ var maxLen : int;
+ var count : int;
+ var cond : bool;
+ print("update N Chain");
+ // first, find max chain length indices
+ while (i < sizeof(Msgs)) {
+ if(sizeof(Msgs[i].chain) > maxLen) {
+ chains = emptySeq;
+ maxLen = sizeof(Msgs[i].chain);
+ }
+ if (sizeof(Msgs[i].chain) == maxLen) {
+ chains += (sizeof(chains), i);
+ }
+ i = i + 1;
+ }
+ chain = Msgs[chains[choose(sizeof(chains))]].chain;
+ i = 0;
+ // conditional
+ while ((i <= EMAX) && !cond) {
+ j = 0;
+ count = 0;
+ while (j < sizeof(Msgs)) {
+ if ((Msgs[j].epoch == i) && (Msgs[j].chain == chain)) {
+ count = count + 1;
+ }
+ j = j + 1;
+ }
+ if (2 * count > N) {
+ cond = true;
+ }
+ i = i + 1;
+ }
+ if (cond) {
+ nChain[pld.node] = chain;
+ } else {
+ i = 1;
+ while (i < sizeof(chain)) {
+ nChain[pld.node] += ((i - 1), chain[i]);
+ i = i + 1;
+ }
+ }
+ if (pld.next == 0) {
+ raise Propose, (node=pld.node, e=pld.e);
+ } else {
+ raise Vote, (node=pld.node, e=pld.e);
+ }
+ }
+ on Propose do (pld : (node : int, e : int)) {
+ var i : int;
+ var chain : seq[int];
+ print("propose");
+ chain += (0, pld.e);
+ i = 1;
+ while (i - 1 < sizeof(nChain[pld.node])) {
+ chain += (i, nChain[pld.node][i - 1]);
+ i = i + 1;
+ }
+ Msgs += (sizeof(Msgs), (chain=chain, epoch=pld.e, sender=pld.node));
+ exists[pld.e] = true;
+ send driver, eNext;
+ }
+ on Vote do (pld : (node : int, e : int)) {
+ var i : int;
+ var chain : seq[int];
+ var choices : seq[int];
+ print("vote");
+ while (i < sizeof(Msgs)) {
+ if (Msgs[i].epoch == pld.e) {
+ choices += (sizeof(choices), i);
+ }
+ i = i + 1;
+ }
+ i = choose(sizeof(choices));
+ chain = Msgs[choices[i]].chain;
+ if (sizeof(chain) == sizeof(nChain[pld.node]) + 1) {
+ if (!((chain=chain, epoch=pld.e, sender=pld.node) in Msgs)) {
+ Msgs += (sizeof(Msgs), (chain=chain, epoch=pld.e, sender=pld.node));
+ }
+ exists[pld.e] = true;
+ send driver, eNext;
+ }
+ }
+ }
+ fun mod (a : int, b : int) : int {
+ return (a - b * (a / b));
+ }
+}
+
+machine Main {
+ var str0 : machine;
+ start state Init {
+ entry {
+ str0 = new Str0((driver=this, N=3, EMAX=5));
+ send str0, eNext;
+ }
+ on eNext do {
+ send str0, eNext;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Portfolio_TokenRing/TokenRing.p b/Src/PeasyAI/resources/rag_examples/Portfolio_TokenRing/TokenRing.p
new file mode 100644
index 0000000000..dd494febd9
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Portfolio_TokenRing/TokenRing.p
@@ -0,0 +1,164 @@
+event Empty;
+event Sending : machine ;
+event Done : machine ;
+event Unit;
+event Next : machine ;
+event Send : machine ;
+event Ready ;
+
+machine Node assume 100 {
+
+ var NextMachine : machine;
+ var MyRing : machine;
+
+ start state Init_Main_Node {
+ entry (payload: machine) { MyRing = payload; }
+ on Next goto SetNext_Main_Node;
+ }
+
+ state Wait_Main_Node {
+ on Empty goto SendEmpty_Main_Node;
+ on Send goto StartSending_Main_Node;
+ on Sending goto KeepSending_Main_Node;
+ on Done goto StopSending_Main_Node;
+ }
+
+ state SetNext_Main_Node {
+ entry (payload: machine) {
+ NextMachine = payload;
+ send MyRing, Ready;
+ raise Unit;
+ }
+
+ on Unit goto Wait_Main_Node;
+ }
+
+ state SendEmpty_Main_Node {
+ entry {
+ send NextMachine, Empty;
+ raise Unit;
+ }
+
+ on Unit goto Wait_Main_Node;
+ }
+
+ state StartSending_Main_Node {
+ entry (payload: machine) {
+ send NextMachine, Sending, payload;
+ raise Unit;
+ }
+
+ on Unit goto Wait_Main_Node;
+ }
+
+ state KeepSending_Main_Node {
+ entry (payload: machine) {
+ if (payload == this)
+ send NextMachine, Done, this;
+ else
+ send NextMachine, Sending, payload;
+ raise Unit;
+ }
+
+ on Unit goto Wait_Main_Node;
+ }
+
+ state StopSending_Main_Node {
+ entry (payload: machine) {
+ if (payload != this)
+ send NextMachine, Done, payload;
+ raise Unit;
+ }
+
+ on Unit goto Wait_Main_Node;
+ }
+}
+
+machine Main {
+
+ var N1 : machine;
+ var N2 : machine;
+ var N3 : machine;
+ var N4 : machine;
+ var ReadyCount : int;
+ var Rand1 : bool;
+ var Rand2 : bool;
+ var RandSrc : machine;
+ var RandDst : machine;
+ var loopCount : int;
+
+ start state Boot_Main_Ring4 {
+ entry {
+ N1 = new Node(this);
+ N2 = new Node(this);
+ N3 = new Node(this);
+ N4 = new Node(this);
+ send N1, Next, N2;
+ send N2, Next, N3;
+ send N3, Next, N4;
+ send N4, Next, N1;
+ ReadyCount = -1;
+ raise Unit;
+ }
+
+ defer Ready;
+ on Unit goto Stabilize_Main_Ring4;
+ }
+
+ state Stabilize_Main_Ring4 {
+ entry {
+ ReadyCount = ReadyCount + 1;
+ if (ReadyCount == 4)
+ raise Unit;
+ }
+
+ on Ready goto Stabilize_Main_Ring4;
+ on Unit goto RandomComm_Main_Ring4;
+ }
+
+ state RandomComm_Main_Ring4 {
+ entry {
+ if ($)
+ Rand1 = true;
+ else
+ Rand1 = false;
+ if ($)
+ Rand2 = true;
+ else
+ Rand2 = false;
+
+ if (!Rand1 && !Rand2)
+ RandSrc = N1;
+ if (!Rand1 && Rand2)
+ RandSrc = N2;
+ if (Rand1 && !Rand2)
+ RandSrc = N3;
+ else
+ RandSrc = N4;
+ if ($)
+ Rand1 = true;
+ else
+ Rand1 = false;
+ if ($)
+ Rand2 = true;
+ else
+ Rand2 = false;
+ if (!Rand1 && !Rand2)
+ RandDst = N1;
+ if (!Rand1 && Rand2)
+ RandDst = N2;
+ if (Rand1 && !Rand2)
+ RandDst = N3;
+ else
+ RandDst = N4;
+
+ send RandSrc, Send, RandDst;
+ if (loopCount < 1) {
+ loopCount = loopCount + 1;
+ raise Unit;
+ }
+ }
+
+ on Unit goto RandomComm_Main_Ring4;
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSpec/BankBalanceCorrect.p b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSpec/BankBalanceCorrect.p
new file mode 100644
index 0000000000..34e63b2680
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSpec/BankBalanceCorrect.p
@@ -0,0 +1,115 @@
+/*****************************************************
+This file defines two P specifications
+
+BankBalanceIsAlwaysCorrect (safety property):
+BankBalanceIsAlwaysCorrect checks the global invariant that the account-balance communicated
+to the client by the bank is always correct and the bank never removes more money from the account
+than that withdrawn by the client! Also, if the bank denies a withdraw request then it is only because
+the withdrawal would reduce the account balance to below 10.
+
+GuaranteedWithDrawProgress (liveness property):
+GuaranteedWithDrawProgress checks the liveness (or progress) property that all withdraw requests
+submitted by the client are eventually responded.
+
+Note: stating that "BankBalanceIsAlwaysCorrect checks that if the bank denies a withdraw request
+then the request would reduce the balance to below 10 (< 10)" is equivalent to state that "if there is enough money in the account - at least 10 (>= 10), then the request must not error".
+Hence, the two properties BankBalanceIsAlwaysCorrect and GuaranteedWithDrawProgress together ensure that every withdraw request if allowed will eventually succeed and the bank cannot block correct withdrawal requests.
+*****************************************************/
+
+// event: initialize the monitor with the initial account balances for all clients when the system starts
+event eSpec_BankBalanceIsAlwaysCorrect_Init: map[int, int];
+
+/****************************************************
+BankBalanceIsAlwaysCorrect checks the global invariant that the account balance communicated
+to the client by the bank is always correct and there is no error on the banks side with the
+implementation of the withdraw logic.
+
+For checking this property the spec machine observes the withdraw request (eWithDrawReq) and response (eWithDrawResp).
+- On receiving the eWithDrawReq, it adds the request in the pending-withdraws-map so that on receiving a
+response for this withdraw we can assert that the amount of money deducted from the account is same as
+what was requested by the client.
+
+- On receiving the eWithDrawResp, we look up the corresponding withdraw request and check that: the
+new account balance is correct and if the withdraw failed it is because the withdraw will make the account
+balance go below 10 dollars which is against the bank policies!
+****************************************************/
+spec BankBalanceIsAlwaysCorrect observes eWithDrawReq, eWithDrawResp, eSpec_BankBalanceIsAlwaysCorrect_Init {
+ // keep track of the bank balance for each client: map from accountId to bank balance.
+ var bankBalance: map[int, int];
+ // keep track of the pending withdraw requests that have not been responded yet.
+ // map from reqId -> withdraw request
+ var pendingWithDraws: map[int, tWithDrawReq];
+
+ start state Init {
+ on eSpec_BankBalanceIsAlwaysCorrect_Init goto WaitForWithDrawReqAndResp with (balance: map[int, int]){
+ bankBalance = balance;
+ }
+ }
+
+ state WaitForWithDrawReqAndResp {
+ on eWithDrawReq do (req: tWithDrawReq) {
+ assert req.accountId in bankBalance,
+ format ("Unknown accountId {0} in the withdraw request. Valid accountIds = {1}",
+ req.accountId, keys(bankBalance));
+ pendingWithDraws[req.rId] = req;
+ }
+
+ on eWithDrawResp do (resp: tWithDrawResp) {
+ assert resp.accountId in bankBalance,
+ format ("Unknown accountId {0} in the withdraw response!", resp.accountId);
+ assert resp.rId in pendingWithDraws,
+ format ("Unknown rId {0} in the withdraw response!", resp.rId);
+ assert resp.balance >= 10,
+ "Bank balance in all accounts must always be greater than or equal to 10!!";
+
+ if(resp.status == WITHDRAW_SUCCESS)
+ {
+ assert resp.balance == bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount,
+ format ("Bank balance for the account {0} is {1} and not the expected value {2}, Bank is lying!",
+ resp.accountId, resp.balance, bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount);
+ // update the new account balance
+ bankBalance[resp.accountId] = resp.balance;
+ }
+ else
+ {
+ // bank can only reject a request if it will drop the balance below 10
+ assert bankBalance[resp.accountId] - pendingWithDraws[resp.rId].amount < 10,
+ format ("Bank must accept the withdraw request for {0}, bank balance is {1}!",
+ pendingWithDraws[resp.rId].amount, bankBalance[resp.accountId]);
+ // if withdraw failed then the account balance must remain the same
+ assert bankBalance[resp.accountId] == resp.balance,
+ format ("Withdraw failed BUT the account balance changed! actual: {0}, bank said: {1}",
+ bankBalance[resp.accountId], resp.balance);
+ }
+ }
+ }
+}
+
+/**************************************************************************
+GuaranteedWithDrawProgress checks the liveness (or progress) property that all withdraw requests
+submitted by the client are eventually responded.
+***************************************************************************/
+spec GuaranteedWithDrawProgress observes eWithDrawReq, eWithDrawResp {
+ // keep track of the pending withdraw requests
+ var pendingWDReqs: set[int];
+
+ start state NopendingRequests {
+ on eWithDrawReq goto PendingReqs with (req: tWithDrawReq) {
+ pendingWDReqs += (req.rId);
+ }
+ }
+
+ hot state PendingReqs {
+ on eWithDrawResp do (resp: tWithDrawResp) {
+ assert resp.rId in pendingWDReqs,
+ format ("unexpected rId: {0} received, expected one of {1}", resp.rId, pendingWDReqs);
+ pendingWDReqs -= (resp.rId);
+ if(sizeof(pendingWDReqs) == 0) // all requests have been responded
+ goto NopendingRequests;
+ }
+
+ on eWithDrawReq goto PendingReqs with (req: tWithDrawReq){
+ pendingWDReqs += (req.rId);
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/AbstractBankServer.p b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/AbstractBankServer.p
new file mode 100644
index 0000000000..fea5042e68
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/AbstractBankServer.p
@@ -0,0 +1,37 @@
+/*********************************************************
+The AbstractBankServer provides an abstract implementation of the BankServer where it abstract away
+the interaction between the BankServer and Database.
+The AbstractBankServer machine is used to demonstrate how one can replace a complex component in P
+with its abstraction that hides a lot of its internal complexity.
+In this case, instead of storing the balance in a separate database the abstraction store the information
+locally and abstracts away the complexity of bank server interaction with the database.
+For the client, it still exposes the same interface/behavior. Hence, when checking the correctness
+of the client it doesnt matter whether we use BankServer or the AbstractBankServer
+**********************************************************/
+
+machine AbstractBankServer
+{
+ // account balance: map from account-id to balance
+ var balance: map[int, int];
+ start state WaitForWithdrawRequests {
+ entry (init_balance: map[int, int])
+ {
+ balance = init_balance;
+ }
+
+ on eWithDrawReq do (wReq: tWithDrawReq) {
+ assert wReq.accountId in balance, "Invalid accountId received in the withdraw request!";
+ if(balance[wReq.accountId] - wReq.amount > 10) /* hint: bug */
+ {
+ balance[wReq.accountId] = balance[wReq.accountId] - wReq.amount;
+ send wReq.source, eWithDrawResp,
+ (status = WITHDRAW_SUCCESS, accountId = wReq.accountId, balance = balance[wReq.accountId], rId = wReq.rId);
+ }
+ else
+ {
+ send wReq.source, eWithDrawResp,
+ (status = WITHDRAW_ERROR, accountId = wReq.accountId, balance = balance[wReq.accountId], rId = wReq.rId);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/Client.p b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/Client.p
new file mode 100644
index 0000000000..81b88b359e
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/Client.p
@@ -0,0 +1,99 @@
+/* User Defined Types */
+
+// payload type associated with the eWithDrawReq, where `source`: client sending the withdraw request,
+// `accountId`: account to withdraw from, `amount`: amount to withdraw, and `rId`: unique
+// request Id associated with each request.
+type tWithDrawReq = (source: Client, accountId: int, amount: int, rId:int);
+
+// payload type associated with the eWithDrawResp, where `status`: response status (below),
+// `accountId`: account withdrawn from, `balance`: bank balance after withdrawal, and
+// `rId`: request id for which this is the response.
+type tWithDrawResp = (status: tWithDrawRespStatus, accountId: int, balance: int, rId: int);
+
+// enum representing the response status for the withdraw request
+enum tWithDrawRespStatus {
+ WITHDRAW_SUCCESS,
+ WITHDRAW_ERROR
+}
+
+// event: withdraw request (from client to bank server)
+event eWithDrawReq : tWithDrawReq;
+// event: withdraw response (from bank server to client)
+event eWithDrawResp: tWithDrawResp;
+
+
+machine Client
+{
+ var server : BankServer;
+ var accountId: int;
+ var nextReqId : int;
+ var numOfWithdrawOps: int;
+ var currentBalance: int;
+
+ start state Init {
+
+ entry (input : (serv : BankServer, accountId: int, balance : int))
+ {
+ server = input.serv;
+ currentBalance = input.balance;
+ accountId = input.accountId;
+ // hacky: we would like request id's to be unique across all requests from clients
+ nextReqId = accountId*100 + 1; // each client has a unique account id
+ goto WithdrawMoney;
+ }
+ }
+
+ state WithdrawMoney {
+ entry {
+ // If current balance is <= 10 then we need more deposits before any more withdrawal
+ if(currentBalance <= 10)
+ goto NoMoneyToWithDraw;
+
+ // send withdraw request to the bank for a random amount between (1 to current balance + 1)
+ send server, eWithDrawReq, (source = this, accountId = accountId, amount = WithdrawAmount(), rId = nextReqId);
+ nextReqId = nextReqId + 1;
+ }
+
+ on eWithDrawResp do (resp: tWithDrawResp) {
+ // bank always ensures that a client has atleast 10 dollars in the account
+ assert resp.balance >= 10, "Bank balance must be greater than 10!!";
+ if(resp.status == WITHDRAW_SUCCESS) // withdraw succeeded
+ {
+ print format ("Withdrawal with rId = {0} succeeded, new account balance = {1}", resp.rId, resp.balance);
+ currentBalance = resp.balance;
+ }
+ else // withdraw failed
+ {
+ // if withdraw failed then the account balance must remain the same
+ assert currentBalance == resp.balance,
+ format ("Withdraw failed BUT the account balance changed! client thinks: {0}, bank balance: {1}", currentBalance, resp.balance);
+ print format ("Withdrawal with rId = {0} failed, account balance = {1}", resp.rId, resp.balance);
+ }
+
+ if (currentBalance > 10)
+/* Hint 3: Reduce the number of times WithdrawAmount() is called by changing the above line to the following:
+ if (currentBalance > 10 && nextReqId < (accountId*100 + 5))
+*/
+ {
+ print format ("Still have account balance = {0}, lets try and withdraw more", currentBalance);
+ goto WithdrawMoney;
+ }
+ }
+ }
+
+ // function that returns a random integer between (1 to current balance + 1)
+ fun WithdrawAmount() : int {
+ return choose(currentBalance) + 1;
+/* Hint 2: Reduce the number of choices by changing the above line to the following:
+ return ((choose(5) * currentBalance) / 4) + 1;
+*/
+ }
+
+ state NoMoneyToWithDraw {
+ entry {
+ // if I am here then the amount of money in my account should be exactly 10
+ assert currentBalance == 10, "Hmm, I still have money that I can withdraw but I have reached NoMoneyToWithDraw state!";
+ print format ("No Money to withdraw, waiting for more deposits!");
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/ClientServerModules.p b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/ClientServerModules.p
new file mode 100644
index 0000000000..f17c40d517
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/ClientServerModules.p
@@ -0,0 +1,8 @@
+// Client module
+module Client = { Client };
+
+// Bank module
+module Bank = { BankServer, Database };
+
+// Abstract Bank Server module
+module AbstractBank = { AbstractBankServer -> BankServer };
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/Server.p b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/Server.p
new file mode 100644
index 0000000000..0a86f29bcb
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PSrc/Server.p
@@ -0,0 +1,92 @@
+/** Events used to communicate between the bank server and the backend database **/
+// event: send update the database, i.e. the `balance` associated with the `accountId`
+event eUpdateQuery: (accountId: int, balance: int);
+// event: send a read request for the `accountId`.
+event eReadQuery: (accountId: int);
+// event: send a response (`balance`) corresponding to the read request for an `accountId`
+event eReadQueryResp: (accountId: int, balance: int);
+
+/*************************************************************
+The BankServer machine uses a database machine as a service to store the bank balance for all its clients.
+On receiving an eWithDrawReq (withdraw requests) from a client, it reads the current balance for the account,
+if there is enough money in the account then it updates the new balance in the database after withdrawal
+and sends a response back to the client.
+*************************************************************/
+machine BankServer
+{
+ var database: Database;
+
+ start state Init {
+ entry (initialBalance: map[int, int]){
+ database = new Database((server = this, initialBalance = initialBalance));
+ goto WaitForWithdrawRequests;
+ }
+ }
+
+ state WaitForWithdrawRequests {
+ on eWithDrawReq do (wReq: tWithDrawReq) {
+ var currentBalance: int;
+ var response: tWithDrawResp;
+
+ // read the current account balance from the database
+ currentBalance = ReadBankBalance(database, wReq.accountId);
+ // if there is enough money in account after withdrawal
+ if(currentBalance - wReq.amount >= 10)
+ {
+ UpdateBankBalance(database, wReq.accountId, currentBalance - wReq.amount);
+ response = (status = WITHDRAW_SUCCESS, accountId = wReq.accountId, balance = currentBalance - wReq.amount, rId = wReq.rId);
+ }
+ else // not enough money after withdraw
+ {
+ response = (status = WITHDRAW_ERROR, accountId = wReq.accountId, balance = currentBalance, rId = wReq.rId);
+ }
+
+ // send response to the client
+ send wReq.source, eWithDrawResp, response;
+ }
+ }
+}
+
+/***************************************************************
+The Database machine acts as a helper service for the Bank server and stores the bank balance for
+each account. There are two API's or functions to interact with the Database:
+ReadBankBalance and UpdateBankBalance.
+****************************************************************/
+machine Database
+{
+ var server: BankServer;
+ var balance: map[int, int];
+ start state Init {
+ entry(input: (server : BankServer, initialBalance: map[int, int])){
+ server = input.server;
+ balance = input.initialBalance;
+ }
+ on eUpdateQuery do (query: (accountId: int, balance: int)) {
+ assert query.accountId in balance, "Invalid accountId received in the update query!";
+ balance[query.accountId] = query.balance;
+ }
+ on eReadQuery do (query: (accountId: int))
+ {
+ assert query.accountId in balance, "Invalid accountId received in the read query!";
+ send server, eReadQueryResp, (accountId = query.accountId, balance = balance[query.accountId]);
+ }
+ }
+}
+
+// Function to read the bank balance corresponding to the accountId
+fun ReadBankBalance(database: Database, accountId: int) : int {
+ var currentBalance: int;
+ send database, eReadQuery, (accountId = accountId,);
+ receive {
+ case eReadQueryResp: (resp: (accountId: int, balance: int)) {
+ currentBalance = resp.balance;
+ }
+ }
+ return currentBalance;
+}
+
+// Function to update the account balance for the account Id
+fun UpdateBankBalance(database: Database, accId: int, bal: int)
+{
+ send database, eUpdateQuery, (accountId = accId, balance = bal);
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PTst/TestDriver.p b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PTst/TestDriver.p
new file mode 100644
index 0000000000..75c7a2a42f
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PTst/TestDriver.p
@@ -0,0 +1,73 @@
+
+// Test driver that checks the system with a single Client.
+machine TestWithSingleClient
+{
+ start state Init {
+ entry {
+ // since client
+ SetupClientServerSystem(1);
+ }
+ }
+}
+
+// Test driver that checks the system with multiple Clients.
+machine TestWithMultipleClients
+{
+ start state Init {
+ entry {
+ // multiple clients between (2, 4)
+ SetupClientServerSystem(choose(3) + 2);
+ }
+ }
+}
+
+param nClients: int;
+
+machine TestWithConfig {
+ start state Init {
+ entry {
+ SetupClientServerSystem(nClients);
+ }
+ }
+}
+
+// creates a random map from accountId's to account balance of size `numAccounts`
+fun CreateRandomInitialAccounts(numAccounts: int) : map[int, int]
+{
+ var i: int;
+ var bankBalance: map[int, int];
+ while(i < numAccounts) {
+ bankBalance[i] = choose(100) + 10; // min 10 in the account
+/* Hint 1: Reduce the number of choices by changing the above line to the following:
+ // bankBalance[i] = choose(10) + 10; // min 10 in the account
+*/
+ i = i + 1;
+ }
+ return bankBalance;
+}
+
+// setup the client server system with one bank server and `numClients` clients.
+fun SetupClientServerSystem(numClients: int)
+{
+ var i: int;
+ var server: BankServer;
+ var accountIds: seq[int];
+ var initAccBalance: map[int, int];
+
+ // randomly initialize the account balance for all clients
+ initAccBalance = CreateRandomInitialAccounts(numClients);
+ // create bank server with the init account balance
+ server = new BankServer(initAccBalance);
+
+ // before client starts sending any messages make sure we
+ // initialize the monitors or specifications
+ announce eSpec_BankBalanceIsAlwaysCorrect_Init, initAccBalance;
+
+ accountIds = keys(initAccBalance);
+
+ // create the clients
+ while(i < sizeof(accountIds)) {
+ new Client((serv = server, accountId = accountIds[i], balance = initAccBalance[accountIds[i]]));
+ i = i + 1;
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PTst/Testscript.p b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PTst/Testscript.p
new file mode 100644
index 0000000000..8443f7dfbe
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_ClientServer/PTst/Testscript.p
@@ -0,0 +1,21 @@
+/* This file contains three different model checking scenarios */
+
+// assert the properties for the single client and single server scenario
+test tcSingleClient [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithSingleClient });
+
+// assert the properties for the two clients and single server scenario
+test tcMultipleClients [main=TestWithMultipleClients]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithMultipleClients });
+
+// assert the properties for the single client and single server scenario but with abstract server
+ test tcAbstractServer [main=TestWithSingleClient]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, AbstractBank, { TestWithSingleClient });
+
+// test with a parameterized number of clients
+test param (nClients in [2, 3, 4]) tcParameterizedMultipleClients [main=TestWithConfig]:
+ assert BankBalanceIsAlwaysCorrect, GuaranteedWithDrawProgress in
+ (union Client, Bank, { TestWithConfig });
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSpec/Safety.p b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSpec/Safety.p
new file mode 100644
index 0000000000..ca94f737c8
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSpec/Safety.p
@@ -0,0 +1,53 @@
+/* Events used to inform monitor about the internal state of the CoffeeMaker */
+event eInWarmUpState;
+event eInReadyState;
+event eInBeansGrindingState;
+event eInCoffeeBrewingState;
+event eErrorHappened;
+event eResetPerformed;
+
+/*********************************************
+We would like to ensure that the coffee maker moves
+through the expected modes of operation. We want to make sure that the coffee maker always transitions
+through the following sequence of states:
+Steady operation:
+ WarmUp -> Ready -> GrindBeans -> MakeCoffee -> Ready
+With Error:
+If an error occurs in any of the states above then the Coffee machine stays in the error state until
+it is reset and after which it returns to the Warmup state.
+
+**********************************************/
+spec EspressoMachineModesOfOperation
+observes eInWarmUpState, eInReadyState, eInBeansGrindingState, eInCoffeeBrewingState, eErrorHappened, eResetPerformed
+{
+ start state StartUp {
+ on eInWarmUpState goto WarmUp;
+ }
+
+ state WarmUp {
+ on eErrorHappened goto Error;
+ on eInReadyState goto Ready;
+ }
+
+ state Ready {
+ ignore eInReadyState;
+ on eInBeansGrindingState goto BeanGrinding;
+ on eErrorHappened goto Error;
+ }
+
+ state BeanGrinding {
+ on eInCoffeeBrewingState goto MakingCoffee;
+ on eErrorHappened goto Error;
+ }
+
+ state MakingCoffee {
+ on eInReadyState goto Ready;
+ on eErrorHappened goto Error;
+ }
+
+ state Error {
+ on eResetPerformed goto StartUp;
+ ignore eErrorHappened;
+ }
+}
+
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/CoffeeMaker.p b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/CoffeeMaker.p
new file mode 100644
index 0000000000..7a263191ab
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/CoffeeMaker.p
@@ -0,0 +1,78 @@
+
+/* Requests or operations from the controller to coffee maker */
+
+// event: warmup request when the coffee maker starts or resets
+event eWarmUpReq;
+// event: grind beans request before making coffee
+event eGrindBeansReq;
+// event: start brewing coffee
+event eStartEspressoReq;
+// event start steamer
+event eStartSteamerReq;
+// event: stop steamer
+event eStopSteamerReq;
+
+/* Responses from the coffee maker to the controller */
+// event: completed grinding beans
+event eGrindBeansCompleted;
+// event: completed brewing and pouring coffee
+event eEspressoCompleted;
+// event: warmed up the machine and read to make coffee
+event eWarmUpCompleted;
+
+/* Error messages from the coffee maker to control panel or controller*/
+// event: no water for coffee, refill water!
+event eNoWaterError;
+// event: no beans for coffee, refill beans!
+event eNoBeansError;
+// event: the heater to warm the machine is broken!
+event eWarmerError;
+
+/*****************************************************
+EspressoCoffeeMaker receives requests from the control panel of the coffee machine and
+based on its state e.g., whether heater is working, or it has beans and water, the maker responds
+back to the controller if the operation succeeded or errored.
+*****************************************************/
+machine EspressoCoffeeMaker
+{
+ // control panel of the coffee machine that sends inputs to the coffee maker
+ var controller: CoffeeMakerControlPanel;
+
+ start state WaitForRequests {
+ entry (_controller: CoffeeMakerControlPanel) {
+ controller = _controller;
+ }
+
+ on eWarmUpReq do {
+ send controller, eWarmUpCompleted;
+ }
+
+ on eGrindBeansReq do {
+ if (!HasBeans()) {
+ send controller, eNoBeansError;
+ } else {
+ send controller, eGrindBeansCompleted;
+ }
+ }
+
+ on eStartEspressoReq do {
+ if (!HasWater()) {
+ send controller, eNoWaterError;
+ } else {
+ send controller, eEspressoCompleted;
+ }
+ }
+ on eStartSteamerReq do {
+ if (!HasWater()) {
+ send controller, eNoWaterError;
+ }
+ }
+ on eStopSteamerReq do { /* do nothing, steamer stopped */ }
+ }
+
+ // nondeterministic functions to trigger different behaviors
+ fun HasBeans() : bool { return $; }
+ fun HasWater() : bool { return $; }
+}
+
+
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/CoffeeMakerControlPanel.p b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/CoffeeMakerControlPanel.p
new file mode 100644
index 0000000000..bd38783058
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/CoffeeMakerControlPanel.p
@@ -0,0 +1,230 @@
+/* Events used by the user to interact with the control panel of the Coffee Machine */
+// event: make espresso button pressed
+event eEspressoButtonPressed;
+// event: steamer button turned off
+event eSteamerButtonOff;
+// event: steamer button turned on
+event eSteamerButtonOn;
+// event: door opened to empty grounds
+event eOpenGroundsDoor;
+// event: door closed after emptying grounds
+event eCloseGroundsDoor;
+// event: reset coffee maker button pressed
+event eResetCoffeeMaker;
+//event: error message from panel to the user
+event eCoffeeMakerError: tCoffeeMakerState;
+//event: coffee machine is ready
+event eCoffeeMakerReady;
+// event: coffee machine user
+event eCoffeeMachineUser: machine;
+
+// enum to represent the state of the coffee maker
+enum tCoffeeMakerState {
+ NotWarmedUp,
+ Ready,
+ NoBeansError,
+ NoWaterError
+}
+
+/*
+CoffeeMakerControlPanel acts as the interface between the CoffeeMaker and User
+It converts the inputs from the user to appropriate inputs to the CoffeeMaker and sends responses to
+the user.
+*/
+machine CoffeeMakerControlPanel
+{
+ var coffeeMaker: EspressoCoffeeMaker;
+ var coffeeMakerState: tCoffeeMakerState;
+ var currentUser: machine;
+
+ start state Init {
+ entry {
+ coffeeMakerState = NotWarmedUp;
+ coffeeMaker = new EspressoCoffeeMaker(this);
+ WaitForUser();
+ goto WarmUpCoffeeMaker;
+ }
+ }
+
+ // block until a user shows up
+ fun WaitForUser() {
+ receive {
+ case eCoffeeMachineUser: (user: machine) {
+ currentUser = user;
+ }
+ }
+ }
+
+ state WarmUpCoffeeMaker {
+ entry {
+ // inform the specification about current state of the coffee maker
+ announce eInWarmUpState;
+
+ BeginHeatingCoffeeMaker();
+ }
+
+ on eWarmUpCompleted goto CoffeeMakerReady;
+
+ // grounds door is opened or closed will handle it later after the coffee maker has warmed up
+ defer eOpenGroundsDoor, eCloseGroundsDoor;
+ // ignore these inputs from users until the maker has warmed up.
+ ignore eEspressoButtonPressed, eSteamerButtonOff, eSteamerButtonOn, eResetCoffeeMaker;
+ // ignore these errors and responses as they could be from previous state
+ ignore eNoBeansError, eNoWaterError, eGrindBeansCompleted;
+ }
+
+
+
+
+ state CoffeeMakerReady {
+ entry {
+ // inform the specification about current state of the coffee maker
+ announce eInReadyState;
+
+ coffeeMakerState = Ready;
+ send currentUser, eCoffeeMakerReady;
+ }
+
+ on eOpenGroundsDoor goto CoffeeMakerDoorOpened;
+ on eEspressoButtonPressed goto CoffeeMakerRunGrind;
+ on eSteamerButtonOn goto CoffeeMakerRunSteam;
+
+ // ignore these out of order commands, these must have happened because of an error
+ // from user or sensor
+ ignore eSteamerButtonOff, eCloseGroundsDoor;
+
+ // ignore commands and errors as they are from previous state
+ ignore eWarmUpCompleted, eResetCoffeeMaker, eNoBeansError, eNoWaterError;
+ }
+
+ state CoffeeMakerRunGrind {
+ entry {
+ // inform the specification about current state of the coffee maker
+ announce eInBeansGrindingState;
+
+ GrindBeans();
+ }
+ on eNoBeansError goto EncounteredError with {
+ coffeeMakerState = NoBeansError;
+ print "No beans to grind! Please refill beans and reset the machine!";
+ }
+
+ on eNoWaterError goto EncounteredError with {
+ coffeeMakerState = NoWaterError;
+ print "No Water! Please refill water and reset the machine!";
+ }
+
+ on eGrindBeansCompleted goto CoffeeMakerRunEspresso;
+
+ defer eOpenGroundsDoor, eCloseGroundsDoor, eEspressoButtonPressed;
+
+ // Can't make steam while we are making espresso
+ ignore eSteamerButtonOn, eSteamerButtonOff;
+
+ // ignore commands that are old or cannot be handled right now
+ ignore eWarmUpCompleted, eResetCoffeeMaker;
+ }
+
+ state CoffeeMakerRunEspresso {
+ entry {
+ // inform the specification about current state of the coffee maker
+ announce eInCoffeeBrewingState;
+
+ StartEspresso();
+ }
+ on eEspressoCompleted goto CoffeeMakerReady with { send currentUser, eEspressoCompleted; }
+
+ on eNoWaterError goto EncounteredError with {
+ coffeeMakerState = NoWaterError;
+ print "No Water! Please refill water and reset the machine!";
+ }
+
+ // the user commands will be handled next after finishing this espresso
+ defer eOpenGroundsDoor, eCloseGroundsDoor, eEspressoButtonPressed;
+
+ // Can't make steam while we are making espresso
+ ignore eSteamerButtonOn, eSteamerButtonOff;
+
+ // ignore old commands and cannot reset when making coffee
+ ignore eWarmUpCompleted, eResetCoffeeMaker;
+ }
+
+ state CoffeeMakerRunSteam {
+ entry {
+ StartSteamer();
+ }
+
+ on eSteamerButtonOff goto CoffeeMakerReady with {
+ StopSteamer();
+ }
+
+ on eNoWaterError goto EncounteredError with {
+ coffeeMakerState = NoWaterError;
+ print "No Water! Please refill water and reset the machine!";
+ }
+
+ // user might have cleaned grounds while steaming
+ defer eOpenGroundsDoor, eCloseGroundsDoor;
+
+ // can't make espresso while we are making steam
+ ignore eEspressoButtonPressed, eSteamerButtonOn;
+ }
+
+ state CoffeeMakerDoorOpened {
+ on eCloseGroundsDoor do {
+ assert coffeeMakerState != NotWarmedUp;
+ assert coffeeMakerState == Ready;
+ goto CoffeeMakerReady;
+ }
+
+ // grounds door is open cannot handle these requests just ignore them
+ ignore eEspressoButtonPressed, eSteamerButtonOn, eSteamerButtonOff;
+ }
+
+ state EncounteredError {
+ entry {
+ // inform the specification about current state of the coffee maker
+ announce eErrorHappened;
+
+ // send the error message to the client
+ send currentUser, eCoffeeMakerError, coffeeMakerState;
+ }
+
+ on eResetCoffeeMaker goto WarmUpCoffeeMaker with {
+ // inform the specification about current state of the coffee maker
+ announce eResetPerformed;
+ }
+
+ // error, ignore these requests until reset.
+ ignore eEspressoButtonPressed, eSteamerButtonOn, eSteamerButtonOff,
+ eOpenGroundsDoor, eCloseGroundsDoor, eWarmUpCompleted, eEspressoCompleted, eGrindBeansCompleted;
+
+ // ignore other simultaneous errors
+ ignore eNoBeansError, eNoWaterError;
+ }
+
+ fun BeginHeatingCoffeeMaker() {
+ // send an event to maker to start warming
+ send coffeeMaker, eWarmUpReq;
+ }
+
+ fun StartSteamer() {
+ // send an event to maker to start steaming
+ send coffeeMaker, eStartSteamerReq;
+ }
+
+ fun StopSteamer() {
+ // send an event to maker to stop steaming
+ send coffeeMaker, eStopSteamerReq;
+ }
+
+ fun GrindBeans() {
+ // send an event to maker to grind beans
+ send coffeeMaker, eGrindBeansReq;
+ }
+
+ fun StartEspresso() {
+ // send an event to maker to start espresso
+ send coffeeMaker, eStartEspressoReq;
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/EspressoMachineModules.p b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/EspressoMachineModules.p
new file mode 100644
index 0000000000..75b41306b8
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PSrc/EspressoMachineModules.p
@@ -0,0 +1 @@
+module EspressoMachine = { CoffeeMakerControlPanel, EspressoCoffeeMaker };
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/TestDrivers.p b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/TestDrivers.p
new file mode 100644
index 0000000000..ff9173d353
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/TestDrivers.p
@@ -0,0 +1,97 @@
+machine TestWithSaneUser
+{
+ start state Init {
+ entry {
+ // create a sane user
+ new SaneUser(new CoffeeMakerControlPanel());
+ }
+ }
+}
+
+machine TestWithCrazyUser
+{
+ start state Init {
+ entry {
+ // create a crazy user
+ new CrazyUser((coffeeMaker = new CoffeeMakerControlPanel(), nOps = 5));
+ }
+ }
+}
+
+// Parameters for comprehensive testing scenarios
+param nOps : int;
+param nUsers : int;
+param waterLevel : int;
+param beanLevel : int;
+param enableSteamer : bool;
+param cleaningMode : bool;
+
+machine TestWithConfig
+{
+ start state Init {
+ entry {
+ // create a crazy user
+ new CrazyUser((coffeeMaker = new CoffeeMakerControlPanel(), nOps = nOps));
+ }
+ }
+}
+
+// Test driver for multiple users, each with their own coffee machine
+machine TestWithMultipleUsers
+{
+ var i: int;
+ start state Init {
+ entry {
+ var coffeeMaker: CoffeeMakerControlPanel;
+
+ i = 0;
+ while (i < nUsers) {
+ coffeeMaker = new CoffeeMakerControlPanel();
+
+ if (i % 2 == 0) {
+ new SaneUser(coffeeMaker);
+ } else {
+ new CrazyUser((coffeeMaker = coffeeMaker, nOps = nOps));
+ }
+ i = i + 1;
+ }
+ }
+ }
+}
+
+// Test driver with resource constraints (water and bean levels)
+machine TestWithResourceConstraints
+{
+ start state Init {
+ entry {
+ var coffeeMaker: CoffeeMakerControlPanel;
+ coffeeMaker = new CoffeeMakerControlPanel();
+
+ if (waterLevel > 0 && beanLevel > 0) {
+ new SaneUser(coffeeMaker);
+ } else {
+ new CrazyUser((coffeeMaker = coffeeMaker, nOps = 2));
+ }
+ }
+ }
+}
+
+// Test driver with mixed configurations
+machine TestWithMixedConfiguration
+{
+ start state Init {
+ entry {
+ var coffeeMaker: CoffeeMakerControlPanel;
+ coffeeMaker = new CoffeeMakerControlPanel();
+
+ if (cleaningMode) {
+ new CrazyUser((coffeeMaker = coffeeMaker, nOps = 3));
+ } else if (enableSteamer) {
+ new SteamerTestUser(coffeeMaker);
+ } else {
+ new SaneUser(coffeeMaker);
+ }
+ }
+ }
+}
+
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/TestScripts.p b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/TestScripts.p
new file mode 100644
index 0000000000..1ac4be2942
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/TestScripts.p
@@ -0,0 +1,28 @@
+test tcSaneUserUsingCoffeeMachine [main=TestWithSaneUser]:
+ assert EspressoMachineModesOfOperation in (union { TestWithSaneUser }, EspressoMachine, Users);
+
+test tcCrazyUserUsingCoffeeMachine [main=TestWithCrazyUser]:
+ assert EspressoMachineModesOfOperation in (union { TestWithCrazyUser }, EspressoMachine, Users);
+
+// BASIC PARAMETER TESTS
+
+// test with a parameterized number of operations for the crazy user
+test param (nOps in [4, 5, 6]) tcCrazyUserParamOps [main=TestWithConfig]:
+ assert EspressoMachineModesOfOperation in (union EspressoMachine, Users, { TestWithConfig });
+
+// MULTI-PARAMETER TESTS
+
+// Test multiple users, each with their own coffee machine, with varying operations
+test param (nUsers in [1, 2, 3], nOps in [3, 5, 7]) tcMultiUserOperations [main=TestWithMultipleUsers]:
+ assert EspressoMachineModesOfOperation in (union EspressoMachine, Users, { TestWithMultipleUsers });
+
+// Test resource constraints with different levels
+test param (waterLevel in [0, 25, 50, 100], beanLevel in [0, 25, 50]) tcResourceConstraints [main=TestWithResourceConstraints]:
+ assert EspressoMachineModesOfOperation in (union EspressoMachine, Users, { TestWithResourceConstraints });
+
+// BOOLEAN PARAMETER TESTS
+
+// Test boolean configurations
+test param (enableSteamer in [true, false], cleaningMode in [true, false]) tcBooleanConfigs [main=TestWithMixedConfiguration]:
+ assert EspressoMachineModesOfOperation in (union EspressoMachine, Users, { TestWithMixedConfiguration, SteamerTestUser });
+
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/Users.p b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/Users.p
new file mode 100644
index 0000000000..4a8f95cba8
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_EspressoMachine/PTst/Users.p
@@ -0,0 +1,171 @@
+/*
+A SaneUser who knows how to use the CoffeeMaker
+*/
+machine SaneUser {
+ var coffeeMakerPanel: CoffeeMakerControlPanel;
+ var cups: int;
+ start state Init {
+ entry (coffeeMaker: CoffeeMakerControlPanel) {
+ coffeeMakerPanel = coffeeMaker;
+ // inform control panel that I am the user
+ send coffeeMakerPanel, eCoffeeMachineUser, this;
+ // want to make 2 cups of espresso
+ cups = 2;
+
+ goto LetsMakeCoffee;
+ }
+ }
+
+ state LetsMakeCoffee {
+ entry {
+ while (cups > 0)
+ {
+ // lets wait for coffee maker to be ready
+ WaitForCoffeeMakerToBeReady();
+
+ // press Espresso button
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressEspressoButton);
+
+ // check the status of the machine
+ receive {
+ case eEspressoCompleted: {
+ // lets make the next coffee
+ cups = cups - 1;
+ }
+ case eCoffeeMakerError: (status: tCoffeeMakerState){
+
+ // lets fill the beans or water and reset the machine
+ // and go back to making espresso
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressResetButton);
+ }
+ }
+ }
+
+ // I am a good user so I will clear the coffee grounds before leaving.
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_ClearGrounds);
+
+ // am done, let me exit
+ raise halt;
+ }
+ }
+}
+
+enum tCoffeeMakerOperations {
+ CM_PressEspressoButton,
+ CM_PressSteamerButton,
+ CM_PressResetButton,
+ CM_ClearGrounds
+}
+
+/*
+A crazy user who gets excited by looking at a coffee machine and starts stress testing the machine
+by pressing all sorts of random button and opening/closing doors
+*/
+// TODO: We do not support global constants currently, they can be encoded using global functions.
+
+machine CrazyUser {
+ var coffeeMakerPanel: CoffeeMakerControlPanel;
+ var numOperations: int;
+ start state StartPressingButtons {
+ entry (config: (coffeeMaker: CoffeeMakerControlPanel, nOps: int)) {
+ var pickedOps: tCoffeeMakerOperations;
+
+ numOperations = config.nOps;
+ coffeeMakerPanel = config.coffeeMaker;
+
+ // inform control panel that I am the user
+ send coffeeMakerPanel, eCoffeeMachineUser, this;
+
+ while(numOperations > 0)
+ {
+ pickedOps = PickRandomOperationToPerform();
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, pickedOps);
+ numOperations = numOperations - 1;
+ }
+ }
+
+ // I will ignore all the responses from the coffee maker
+ ignore eCoffeeMakerError, eEspressoCompleted, eCoffeeMakerReady;
+ }
+
+ // Pick a random enum value (hacky work around)
+ // Currently, the choose operation does not support choose over enum value
+ fun PickRandomOperationToPerform() : tCoffeeMakerOperations {
+ var op_i: int;
+ op_i = choose(3);
+ if(op_i == 0)
+ return CM_PressEspressoButton;
+ else if(op_i == 1)
+ return CM_PressSteamerButton;
+ else if(op_i == 2)
+ return CM_PressResetButton;
+ else
+ return CM_ClearGrounds;
+ }
+}
+
+// Specialized user for testing steamer functionality
+machine SteamerTestUser
+{
+ var coffeeMakerPanel: CoffeeMakerControlPanel;
+
+ start state Init {
+ entry (coffeeMaker: CoffeeMakerControlPanel) {
+ coffeeMakerPanel = coffeeMaker;
+ send coffeeMakerPanel, eCoffeeMachineUser, this;
+ goto TestSteamer;
+ }
+ }
+
+ state TestSteamer {
+ entry {
+ // Test steamer operations specifically
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressSteamerButton);
+
+ receive {
+ case eCoffeeMakerReady: {
+ // Continue with espresso after steamer test
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressEspressoButton);
+ }
+ case eCoffeeMakerError: (status: tCoffeeMakerState) {
+ // Reset and try again
+ PerformOperationOnCoffeeMaker(coffeeMakerPanel, CM_PressResetButton);
+ }
+ }
+ }
+
+ ignore eEspressoCompleted;
+ }
+}
+
+
+/* Function to perform an operation on the CoffeeMaker */
+fun PerformOperationOnCoffeeMaker(coffeeMakerCP: CoffeeMakerControlPanel, CM_Ops: tCoffeeMakerOperations)
+{
+ if(CM_Ops == CM_PressEspressoButton) {
+ send coffeeMakerCP, eEspressoButtonPressed;
+ }
+ else if(CM_Ops == CM_PressSteamerButton) {
+ send coffeeMakerCP, eSteamerButtonOn;
+ // wait for some time and then release the button
+ send coffeeMakerCP, eSteamerButtonOff;
+ }
+ else if(CM_Ops == CM_ClearGrounds)
+ {
+ send coffeeMakerCP, eOpenGroundsDoor;
+ // empty ground and close the door
+ send coffeeMakerCP, eCloseGroundsDoor;
+ }
+ else if(CM_Ops == CM_PressResetButton)
+ {
+ send coffeeMakerCP, eResetCoffeeMaker;
+ }
+}
+
+fun WaitForCoffeeMakerToBeReady() {
+ receive {
+ case eCoffeeMakerReady: {}
+ case eCoffeeMakerError: (status: tCoffeeMakerState){ raise halt; }
+ }
+}
+module Users = { SaneUser, CrazyUser };
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSpec/ReliableFailureDetector.p b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSpec/ReliableFailureDetector.p
new file mode 100644
index 0000000000..ac50af960d
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSpec/ReliableFailureDetector.p
@@ -0,0 +1,57 @@
+/***************************************************
+ReliableFailureDetector is a liveness property to assert that all nodes that have been shutdown
+by the failure injector will eventually be detected by the failure detector as a failed node
+***************************************************/
+
+spec ReliableFailureDetector observes eNotifyNodesDown, eShutDown {
+ // set of nodes that are shutdown by the failure injector but
+ // have not been detected.
+ var nodesShutdownAndNotDetected: set[Node];
+ // set of nodes that have been marked as down by the failure detector
+ var nodesDownDetected: set[Node];
+
+ // State where all the nodes that are shutdown by failure injector
+ // have been detected by the failure detector
+ start state AllShutdownNodesAreDetected {
+ on eNotifyNodesDown do (nodes: set[Node])
+ {
+ var i: int;
+ while(i < sizeof(nodes))
+ {
+ nodesShutdownAndNotDetected -= (nodes[i]);
+ nodesDownDetected += (nodes[i]);
+ i = i + 1;
+ }
+ }
+
+ on eShutDown do (node: machine) {
+ if(!((node as Node) in nodesDownDetected)) {
+ nodesShutdownAndNotDetected += (node as Node);
+ goto NodesShutDownButNotDetected;
+ }
+ }
+ }
+
+ // intermediate "unstable" state where the failure detector has not detected all the nodes
+ // that have been shutdown by the failure injector
+ hot state NodesShutDownButNotDetected {
+ on eNotifyNodesDown do (nodes: set[Node])
+ {
+ var i: int;
+ while(i < sizeof(nodes))
+ {
+ nodesShutdownAndNotDetected -= (nodes[i]);
+ nodesDownDetected += (nodes[i]);
+ i = i + 1;
+ }
+
+ if(sizeof(nodesShutdownAndNotDetected) == 0)
+ goto AllShutdownNodesAreDetected; // return to the stable state
+ }
+
+ on eShutDown do (node: machine) {
+ if(!((node as Node) in nodesDownDetected))
+ nodesShutdownAndNotDetected += (node as Node);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/Client.p b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/Client.p
new file mode 100644
index 0000000000..4e35ea3499
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/Client.p
@@ -0,0 +1,24 @@
+/******************************************
+Client machine that keeps track of the alive nodes in the system
+*******************************************/
+
+machine Client {
+ var myViewOfAliveNodes: set[Node];
+
+ start state Init {
+ entry (nodes: set[Node]){
+ myViewOfAliveNodes = nodes;
+ // do something with the alive nodes
+ }
+
+ on eNotifyNodesDown do (dead_nodes: set[Node]){
+ var i : int;
+ print format("Nodes {0} are down!", dead_nodes);
+ while(i < sizeof(dead_nodes))
+ {
+ myViewOfAliveNodes -= (dead_nodes[i]);
+ i = i + 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/FailureDetector.p b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/FailureDetector.p
new file mode 100644
index 0000000000..73a462a911
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/FailureDetector.p
@@ -0,0 +1,133 @@
+// event: ping nodes (from failure detector to nodes)
+event ePing: (fd: FailureDetector, trial: int);
+// event: pong detector (response to ping) (from nodes to failure detector)
+event ePong: (node: Node, trial: int);
+// event: failure notification to the client (from failure detector to client)
+event eNotifyNodesDown: set[Node];
+
+/***************************************************
+FailureDetector machine monitors whether a set of nodes in the system are alive (responsive).
+It periodically sends ping message to each node and waits for a pong message from the nodes.
+The nodes that do not send a pong message after multiple attempts are marked as down or failed
+and notified to the client nodes so that they can update their view of the system.
+***************************************************/
+machine FailureDetector {
+ // set of nodes to be monitored
+ var nodes: set[Node];
+ // set of registered clients
+ var clients: set[Client];
+ // num of ping attempts made
+ var attempts: int;
+ // set of alive nodes
+ var alive: set[Node];
+ // nodes that have responded in the current round
+ var respInCurrRound: set[machine];
+ // timer to wait for responses from nodes
+ var timer: Timer;
+
+ start state Init {
+ entry (config: (nodes: set[Node], clients: set[Client])) {
+ nodes = config.nodes;
+ alive = config.nodes;
+ clients = config.clients;
+ timer = CreateTimer(this);
+ goto SendPingsToAllNodes;
+ }
+ }
+
+ state SendPingsToAllNodes {
+ entry {
+ var notRespondedNodes: set[Node];
+
+ if(sizeof(alive) == 0)
+ raise halt; // stop myself, no work to do, there are no alive nodes!
+
+ // compute nodes that have not responded with pongs
+ notRespondedNodes = PotentiallyDownNodes();
+ // send ping events to machines that have not responded in the previous attempt
+ UnReliableBroadCast(notRespondedNodes, ePing, (fd = this, trial = attempts));
+ // start wait timer to wait for pong responses
+ StartTimer(timer);
+ }
+
+ on ePong do (pong: (node: Node, trial: int)) {
+ // collect pong responses from alive nodes
+ // no need to do any for pong messages from nodes that have marked failed.
+ if (pong.node in alive) {
+ respInCurrRound += (pong.node);
+ if (sizeof(respInCurrRound) == sizeof(alive)) {
+ // status of alive nodes has not changed
+ CancelTimer(timer);
+ goto ResetAndStartAgain;
+ }
+ }
+ }
+
+ on eTimeOut do {
+ var nodesDown: set[Node];
+ // one more attempt finished
+ attempts = attempts + 1;
+ // check if there are nodes that have not responded
+ if (sizeof(respInCurrRound) < sizeof(alive) ) {
+ // maximum number of attempts == 3
+ if(attempts < 3) {
+ // try again by re-pinging the nodes that have not responded
+ goto SendPingsToAllNodes;
+ }
+ else
+ {
+ // inform clients about the nodes down
+ nodesDown = ComputeNodesDownAndUpdateAliveSet();
+ // notification to the client is assumed to be a reliable send so that the client gets an updated view
+ ReliableBroadCast(clients, eNotifyNodesDown, nodesDown);
+ }
+ }
+ // lets reset and restart the failure detection
+ goto ResetAndStartAgain;
+ }
+ }
+
+ state ResetAndStartAgain {
+ entry {
+ // prepare for the next detection phase
+ attempts = 0;
+ respInCurrRound = default(set[Node]);
+ // start timer for inter-phase waiting
+ StartTimer(timer);
+ }
+ on eTimeOut goto SendPingsToAllNodes;
+ // detection has finish, these are all delayed pongs and must be ignored
+ ignore ePong;
+ }
+
+ // compute the potentially down nodes
+ // i.e., nodes that have not responded Pong in this round
+ fun PotentiallyDownNodes() : set[Node] {
+ var i: int;
+ var nodesNotResponded: set[Node];
+ while (i < sizeof(nodes)) {
+ if (nodes[i] in alive && !(nodes[i] in respInCurrRound)) {
+ nodesNotResponded += (nodes[i]);
+ }
+ i = i + 1;
+ }
+ return nodesNotResponded;
+ }
+
+ // compute the nodes that might be down and also update the alive set accordingly
+ fun ComputeNodesDownAndUpdateAliveSet() : set[Node] {
+ var i: int;
+ var nodesDown: set[Node];
+ while (i < sizeof(nodes)) {
+ if (nodes[i] in alive && !(nodes[i] in respInCurrRound)) {
+ alive -= (nodes[i]);
+ nodesDown += (nodes[i]);
+ }
+ i = i + 1;
+ }
+ return nodesDown;
+ }
+}
+
+
+
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/FailureDetectorModules.p b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/FailureDetectorModules.p
new file mode 100644
index 0000000000..f71f7d6033
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/FailureDetectorModules.p
@@ -0,0 +1,2 @@
+// module to represent the Failure Detector
+module FailureDetector = (union {FailureDetector, Node, Client }, Timer);
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/Node.p b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/Node.p
new file mode 100644
index 0000000000..a8ab804ee4
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PSrc/Node.p
@@ -0,0 +1,14 @@
+/****************************
+Node machine sends a pong message on receiving a ping
+*****************************/
+machine Node {
+ start state WaitForPing {
+ on ePing do (req: (fd: FailureDetector, trial: int)) {
+ UnReliableSend(req.fd, ePong, (node = this, trial = req.trial));
+ }
+
+ on eShutDown do {
+ raise halt;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PTst/TestDriver.p b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PTst/TestDriver.p
new file mode 100644
index 0000000000..43a81b07e5
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PTst/TestDriver.p
@@ -0,0 +1,55 @@
+type tSystemConfig = (
+ numNodes: int,
+ numClients: int
+);
+
+/*****************************************
+Setup a system with 3 nodes and 2 clients
+******************************************/
+machine TestMultipleClients {
+ start state Init {
+ entry {
+ var config: tSystemConfig;
+ config = (numNodes = 3, numClients = 2);
+ SetupSystemWithFailureInjector(config);
+ }
+ }
+}
+
+param numNodes: int;
+param numClients: int;
+
+machine TestWithConfig {
+ start state Init {
+ entry {
+ var config: tSystemConfig;
+ config = (numNodes = numNodes, numClients = numClients);
+ SetupSystemWithFailureInjector(config);
+ }
+ }
+}
+
+// setup the system for failure detection
+fun SetupSystemWithFailureInjector(config: tSystemConfig)
+{
+ var i : int;
+ var nodes: set[Node];
+ var clients: set[Client];
+ // create Nodes
+ while(i < config.numNodes) {
+ nodes += (new Node());
+ i = i + 1;
+ }
+
+ i = 0;
+ // create clients
+ while(i < config.numClients) {
+ clients += (new Client(nodes));
+ i = i + 1;
+ }
+ // create the failure detector
+ new FailureDetector((nodes = nodes, clients = clients));
+
+ // create the failure injector
+ new FailureInjector((nodes = nodes, nFailures = sizeof(nodes)/2 + 1));
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PTst/TestScript.p b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PTst/TestScript.p
new file mode 100644
index 0000000000..972d56b9c8
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_FailureDetector/PTst/TestScript.p
@@ -0,0 +1,8 @@
+test tcTest_FailureDetector [main=TestMultipleClients]:
+ assert ReliableFailureDetector in
+ union { TestMultipleClients }, FailureDetector, FailureInjector;
+
+// Test case that ensures clients don't outnumber nodes for better monitoring distribution
+test param (numNodes in [3, 4, 5], numClients in [2, 3, 4]) assume (numClients <= numNodes) tcTest_BalancedLoad [main=TestWithConfig]:
+ assert ReliableFailureDetector in
+ union { TestWithConfig }, FailureDetector, FailureInjector;
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSpec/common.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSpec/common.p
new file mode 100644
index 0000000000..e7d78a0a37
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSpec/common.p
@@ -0,0 +1,63 @@
+// Unreliably send event `e` with payload `p` to every `target`
+fun UnreliableBroadcast(target_machines: set[machine], e: event, payload: any) {
+ var i: int;
+ while (i < sizeof(target_machines)) {
+ if (choose()) {
+ send target_machines[i], e, payload;
+ }
+ i = i + 1;
+ }
+}
+
+// Unreliably send event `e` with payload `p` to every `target`, potentially multiple times
+fun UnreliableBroadcastMulti(target_machines: set[machine], e: event, payload: any) {
+ var i: int;
+ var n: int;
+ var k: int;
+
+ while (i < sizeof(target_machines)) {
+ // Each message is sent `k` is that number of times
+ k = choose(3);
+ // Times we've sent the packet so far
+ n = 0;
+ while (n < k) {
+ send target_machines[i], e, payload;
+ n = n + 1;
+ }
+ i = i + 1;
+ }
+}
+
+// Reliably send event `e` with payload `p` to every `target`
+fun ReliableBroadcast(target_machines: set[machine], e: event, payload: any) {
+ var i: int;
+ while (i < sizeof(target_machines)) {
+ send target_machines[i], e, payload;
+ i = i + 1;
+ }
+}
+
+// Reliably send event `e` with payload `p` to a majority of `target`. Unreliable send to remaining, potentially multiple times.
+fun ReliableBroadcastMajority(target_machines: set[machine], e: event, payload: any) {
+ var i: int;
+ var n: int;
+ var k: int;
+ var majority: int;
+
+ majority = sizeof(target_machines) / 2 + 1;
+
+ while (i < sizeof(target_machines)) {
+ // Each message is sent `k` is that number of times
+ k = 1;
+ if (i >= majority) {
+ k = choose(3);
+ }
+ // Times we've sent the packet so far
+ n = 0;
+ while (n < k) {
+ send target_machines[i], e, payload;
+ n = n + 1;
+ }
+ i = i + 1;
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSpec/spec.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSpec/spec.p
new file mode 100644
index 0000000000..f4eea9b38b
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSpec/spec.p
@@ -0,0 +1,18 @@
+// This safety spec ensures that the value that was taught never changes (though it can be taught multiple times)
+spec OneValueTaught observes eLearn {
+ var decided: int;
+
+ start state Init {
+ entry {
+ decided = -1;
+ }
+
+ on eLearn do (payload: (ballot: tBallot, v: tValue)) {
+ assert(payload.v != -1);
+ if (decided != -1) {
+ assert(decided == payload.v);
+ }
+ decided = payload.v;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/acceptor.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/acceptor.p
new file mode 100644
index 0000000000..f06c8527bf
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/acceptor.p
@@ -0,0 +1,57 @@
+type tBallot = int;
+type tValue = int;
+
+type tPrepareReq = (proposer: Proposer, ballot_n: tBallot, v: tValue);
+event ePrepareReq: tPrepareReq;
+type tPrepareRsp = (acceptor: Acceptor, promised: tBallot, v_accepted: tValue, n_accepted: tBallot);
+event ePrepareRsp: tPrepareRsp;
+type tAcceptReq = (proposer: Proposer, ballot_n: tBallot, v: tValue);
+event eAcceptReq: tAcceptReq;
+type tAcceptRsp = (acceptor: Acceptor, accepted: tBallot);
+event eAcceptRsp: tAcceptRsp;
+
+// The acceptor role in Paxos (see proposer.p for more details).
+machine Acceptor {
+ var n_prepared: tBallot;
+
+ var v_accepted: tValue;
+ var n_accepted: tBallot;
+
+ start state Init {
+ entry {
+ n_prepared = -1;
+ v_accepted = -1;
+ n_accepted = -1;
+ goto Accept;
+ }
+ }
+
+ state Accept {
+ // When we get a prepare request, we promise not to accept any prepare requests with a lower ballot number, and return any
+ // proposed value we've accepted, if any.
+ on ePrepareReq do (req: tPrepareReq) {
+ if (req.ballot_n > n_prepared) {
+ send req.proposer, ePrepareRsp, (acceptor = this, promised = req.ballot_n, v_accepted = v_accepted, n_accepted = n_accepted);
+ n_prepared = req.ballot_n;
+ // print format("{0}: ({1}:{2}, {3})", this, n_accepted, v_accepted, n_prepared);
+ }
+ // As an optimization, we could send a NACK here to let the proposer know we got its message, allowing it to tell the difference
+ // between losing it's proposal and packet loss. It's just an optimization, so we don't do it yet.
+ }
+
+ // Once a proposer has been made the 'leader', it sends us a proposed value. To avoid accepting values from old leaders, we simply
+ // discard any messages with ballot numbers lower than the one we prepared.
+ on eAcceptReq do (req: tAcceptReq) {
+ if (req.ballot_n >= n_prepared) {
+ v_accepted = req.v;
+ n_accepted = req.ballot_n;
+ // Treat accepting as "prepare, accept" the way that Lamport does in Part Time Parliament (but not in Paxos Made Simple)
+ // See https://brooker.co.za/blog/2021/11/16/paxos.html and https://stackoverflow.com/questions/29880949/contradiction-in-lamports-paxos-made-simple-paper
+ n_prepared = req.ballot_n;
+
+ send req.proposer, eAcceptRsp, (acceptor = this, accepted = req.ballot_n);
+ }
+ // Same story with NACKs here.
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/learner.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/learner.p
new file mode 100644
index 0000000000..0c171d410e
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/learner.p
@@ -0,0 +1,22 @@
+event eLearn: (ballot: tBallot, v: tValue);
+
+// The learner role in Paxos (see proposer.p for more details).
+machine Learner {
+ var learned_value: tValue;
+
+ start state Init {
+ entry {
+ learned_value = -1;
+ goto Learn;
+ }
+ }
+
+ state Learn {
+ on eLearn do (payload: (ballot: tBallot, v: tValue)) {
+ assert(payload.v != -1);
+ // This check is a belt-and-braces with the spec, but it's a useful piece of sanity
+ assert((learned_value == -1) || (learned_value == payload.v));
+ learned_value = payload.v;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/proposer.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/proposer.p
new file mode 100644
index 0000000000..20df7cb4de
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PSrc/proposer.p
@@ -0,0 +1,106 @@
+type tProposerConfig = (jury: set[Acceptor], school: set[Learner], value_to_propose: int, proposer_id: int);
+
+machine Proposer {
+ var jury: set[Acceptor];
+ var school: set[Learner];
+
+ // The ballot number we use for the prepares and accepts that this proposer sends out
+ var ballot_n: tBallot;
+ var highest_proposal_n: tBallot;
+ var value_to_propose: tValue;
+
+ // A number equal to more than half the number of acceptors in the jury
+ var majority: int;
+ // Acceptors we have received prepare ACKs from. This is a set rather than a counter to deal with the
+ // fact that messages can be delivered multiple times in our network model.
+ var prepare_acks: set[Acceptor];
+ // Acceptors we have recieved accept ACKs from.
+ var accept_acks: set[Acceptor];
+
+ start state Init {
+ entry (cfg : tProposerConfig) {
+ jury = cfg.jury;
+ school = cfg.school;
+ // For now, use our id as a ballot number
+ ballot_n = cfg.proposer_id;
+ value_to_propose = cfg.value_to_propose;
+ majority = sizeof(jury) / 2 + 1;
+ goto Prepare;
+ }
+ }
+
+ // Phase 1, prepare
+ state Prepare {
+ entry {
+ var acceptor: Acceptor;
+ highest_proposal_n = -1;
+ // Step 1a is to fire our proposal at the whole jury
+ // ReliableBroadcastMajority(jury, ePrepareReq, (proposer = this, ballot_n = ballot_n, v = value_to_propose));
+ foreach (acceptor in jury) {
+ send acceptor, ePrepareReq, (proposer = this, ballot_n = ballot_n, v = value_to_propose);
+ }
+ }
+
+ on ePrepareRsp do (rsp: tPrepareRsp) {
+ // The jury then gets back to us, saying whether they have accepted our proposal
+ if (rsp.promised == ballot_n) {
+ // If this acceptor has already accepted a proposal, we drop the one we're proposing and drive that forward instead.
+ if (rsp.n_accepted > highest_proposal_n) {
+ highest_proposal_n = rsp.n_accepted;
+ value_to_propose = rsp.v_accepted;
+ }
+ // Add this acceptor to the set of acceptors who have ACKed our proposal
+ prepare_acks += (rsp.acceptor);
+ // If more than half the acceptors have ACKed our proposal, we've been elected 'leader' and its time to move to phase 2
+ if (sizeof(prepare_acks) >= majority) {
+ goto Accept;
+ }
+ }
+ }
+ }
+
+ // Phase 2, accept
+ state Accept {
+ entry {
+ // We start the accept phase by firing off our accept requests at the jury
+ // ReliableBroadcastMajority(jury, eAcceptReq, (proposer = this, ballot_n = ballot_n, v = value_to_propose));
+ var acceptor: Acceptor;
+ foreach (acceptor in jury) {
+ send acceptor, eAcceptReq, (proposer = this, ballot_n = ballot_n, v = value_to_propose);
+ }
+ }
+
+ // Then get reponses from the acceptors
+ on eAcceptRsp do (rsp: tAcceptRsp) {
+ if (rsp.accepted == ballot_n) {
+ // Add this acceptor to the set that have accepted our value
+ accept_acks += (rsp.acceptor);
+ // And when more than half agree, it's been decided and it's time to teach
+ if (sizeof(accept_acks) >= majority) {
+ goto Teach;
+ }
+ }
+ }
+ // Stale prepares can be safely ignored
+ ignore ePrepareRsp;
+ }
+
+ // Phase 3, teaching.
+ // We diverge a little from the paper here, which puts the acceptors in the teacher role. Doing it here adds a round-trip,
+ // but makes the learners much simpler (they just accept whatever is thrown at them).
+ state Teach {
+ entry {
+ // ReliableBroadcast(school, eLearn, (v=value_to_propose,));
+ var learner: Learner;
+ foreach (learner in school) {
+ send learner, eLearn, (ballot=ballot_n, v=value_to_propose);
+ }
+ }
+
+ // At this point it's safe to ignore every message
+ ignore eAcceptRsp;
+ ignore ePrepareRsp;
+ }
+}
+
+module Paxos = { Proposer, Acceptor, Learner };
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PTst/test.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PTst/test.p
new file mode 100644
index 0000000000..ac113d949c
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Paxos/PTst/test.p
@@ -0,0 +1,119 @@
+test testBasicPaxos3on5 [main = BasicPaxos3on5]:
+ assert OneValueTaught in (union Paxos, { BasicPaxos3on5 });
+
+test testBasicPaxos3on3 [main = BasicPaxos3on3]:
+ assert OneValueTaught in (union Paxos, { BasicPaxos3on3 });
+
+test testBasicPaxos3on1 [main = BasicPaxos3on1]:
+ assert OneValueTaught in (union Paxos, { BasicPaxos3on1 });
+
+test testBasicPaxos2on3 [main = BasicPaxos2on3]:
+ assert OneValueTaught in (union Paxos, { BasicPaxos2on3 });
+
+test testBasicPaxos2on2 [main = BasicPaxos2on2]:
+ assert OneValueTaught in (union Paxos, { BasicPaxos2on2 });
+
+test testBasicPaxos1on1 [main = BasicPaxos1on1]:
+ assert OneValueTaught in (union Paxos, { BasicPaxos1on1 });
+
+type tPaxosConfig = (n_proposers: int, n_acceptors: int, n_learners: int);
+
+event ePaxosConfig: (quorum: int);
+
+fun SetupPaxos(cfg: tPaxosConfig) {
+ var i: int;
+ var proposers: set[Proposer];
+ var jury: set[Acceptor];
+ var school: set[Learner];
+
+ var proposerCfg: tProposerConfig;
+
+ announce ePaxosConfig, (quorum = cfg.n_acceptors / 2 + 1,);
+
+ i = 0;
+ while (i < cfg.n_acceptors) {
+ i = i + 1;
+ jury += (new Acceptor());
+ }
+ i = 0;
+ while (i < cfg.n_learners) {
+ i = i + 1;
+ school += (new Learner());
+ }
+ i = 0;
+ while (i < cfg.n_proposers) {
+ i = i + 1;
+ proposerCfg = (jury = jury, school = school, value_to_propose = i + 100 + choose(50), proposer_id = i + choose(50));
+ proposers += (new Proposer(proposerCfg));
+ }
+}
+
+machine BasicPaxos3on5 {
+ start state Init {
+ entry {
+ var config: tPaxosConfig;
+ config = (n_proposers = 3, n_acceptors = 5, n_learners = 1);
+ SetupPaxos(config);
+ }
+ }
+}
+
+machine BasicPaxos3on3 {
+ start state Init {
+ entry {
+ var config: tPaxosConfig;
+ config = (n_proposers = 3, n_acceptors = 3, n_learners = 1);
+ SetupPaxos(config);
+ }
+ }
+}
+
+machine BasicPaxos3on1 {
+ start state Init {
+ entry {
+ var config: tPaxosConfig;
+ config = (n_proposers = 3, n_acceptors = 1, n_learners = 1);
+ SetupPaxos(config);
+ }
+ }
+}
+
+machine BasicPaxos2on3 {
+ start state Init {
+ entry {
+ var config: tPaxosConfig;
+ config = (n_proposers = 2, n_acceptors = 3, n_learners = 1);
+ SetupPaxos(config);
+ }
+ }
+}
+
+machine BasicPaxos2on2 {
+ start state Init {
+ entry {
+ var config: tPaxosConfig;
+ config = (n_proposers = 2, n_acceptors = 2, n_learners = 1);
+ SetupPaxos(config);
+ }
+ }
+}
+
+machine BasicPaxos1on1 {
+ start state Init {
+ entry {
+ var config: tPaxosConfig;
+ config = (n_proposers = 1, n_acceptors = 1, n_learners = 1);
+ SetupPaxos(config);
+ }
+ }
+}
+
+machine BasicPaxos4on4 {
+ start state Init {
+ entry {
+ var config: tPaxosConfig;
+ config = (n_proposers = 4, n_acceptors = 4, n_learners = 1);
+ SetupPaxos(config);
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/LivenessClientsDone.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/LivenessClientsDone.p
new file mode 100644
index 0000000000..843240cc00
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/LivenessClientsDone.p
@@ -0,0 +1,60 @@
+spec LivenessClientsDone observes eClientPutRequest, eClientGetRequest, eClientFinishedMonitor {
+ var activeClients: set[Client];
+ var finishedClients: set[Client];
+
+ start state Init {
+ entry {
+ activeClients = default(set[Client]);
+ finishedClients = default(set[Client]);
+ goto AllClientsDone;
+ }
+ }
+
+ hot state ClientsActive {
+ on eClientFinishedMonitor do (c: Client) {
+ finishedClients += (c);
+ if (finishedClients == activeClients) {
+ goto AllClientsDone;
+ }
+ }
+
+ // on eClientRequest do (payload: tClientRequest) {
+ // if (payload.client == payload.sender) {
+ // activeClients += (payload.client);
+ // }
+ // }
+
+ on eClientPutRequest do (payload: tClientPutRequest) {
+ if (payload.client == payload.sender) {
+ activeClients += (payload.client);
+ }
+ }
+
+ on eClientGetRequest do (payload: tClientGetRequest) {
+ if (payload.client == payload.sender) {
+ activeClients += (payload.client);
+ }
+ }
+ }
+
+ cold state AllClientsDone {
+ on eClientPutRequest do (payload: tClientPutRequest) {
+ if (payload.client == payload.sender) {
+ activeClients += (payload.client);
+ goto ClientsActive;
+ }
+ }
+
+ on eClientGetRequest do (payload: tClientGetRequest) {
+ if (payload.client == payload.sender) {
+ activeClients += (payload.client);
+ goto ClientsActive;
+ }
+ }
+
+ on eClientFinishedMonitor do (c: Client) {
+ print format("Problematic: {0}", c);
+ assert false, "Received a client finished event after all clients are done.";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/LivenessProgress.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/LivenessProgress.p
new file mode 100644
index 0000000000..e2f8262f7e
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/LivenessProgress.p
@@ -0,0 +1,34 @@
+type tRequestMetadata = (client: machine, transId: int);
+
+spec LivenessProgress observes eClientWaitingResponse, eClientGotResponse {
+ var clientRequests: set[tRequestMetadata];
+
+ start state Init {
+ entry {
+ clientRequests = default(set[tRequestMetadata]);
+ goto Done;
+ }
+ }
+
+ hot state PendingRequestsExist {
+
+ on eClientWaitingResponse do (payload: tRequestMetadata) {
+ clientRequests += ((client=payload.client, transId=payload.transId));
+ }
+
+ on eClientGotResponse do (payload: tRequestMetadata) {
+ clientRequests -= ((client=payload.client, transId=payload.transId));
+ if (sizeof(clientRequests) == 0) {
+ goto Done;
+ }
+ }
+
+ }
+
+ cold state Done {
+ on eClientWaitingResponse do (payload: tRequestMetadata) {
+ clientRequests += ((client=payload.client, transId=payload.transId));
+ goto PendingRequestsExist;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyLeaderCompleteness.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyLeaderCompleteness.p
new file mode 100644
index 0000000000..975280647b
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyLeaderCompleteness.p
@@ -0,0 +1,27 @@
+spec SafetyLeaderCompleteness observes eBecomeLeader {
+ var committedLogs: set[tServerLog];
+
+ start state Init {
+ entry {
+ committedLogs = default(set[tServerLog]);
+ goto MonitoringLeaderChange;
+ }
+ }
+
+ state MonitoringLeaderChange {
+ on eBecomeLeader do (payload: (term:int, leader:Server, log: seq[tServerLog], commitIndex: int)) {
+ // check all already-committed logs is remain committed in the new leader's log
+ var i: int;
+ var e: tServerLog;
+ foreach (e in committedLogs) {
+ assert e in payload.log, format("{0} was committed before but not in the new leader's log", e);
+ }
+ // add the committed logs
+ i = 0;
+ while (i <= payload.commitIndex) {
+ committedLogs += (payload.log[i]);
+ i = i + 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyLogMatching.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyLogMatching.p
new file mode 100644
index 0000000000..f5dd340f4d
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyLogMatching.p
@@ -0,0 +1,59 @@
+spec SafetyLogMatching observes eNotifyLog {
+ var allLogs: map[Server, seq[tServerLog]];
+
+ start state MonitorLogUpdates {
+ entry {
+ allLogs = default(map[Server, seq[tServerLog]]);
+ }
+
+ on eNotifyLog do (payload: (timestamp: int, server: Server, log: seq[tServerLog])) {
+ var i: int;
+ var j: int;
+ var s1: Server;
+ var s2: Server;
+ if (!(payload.server in allLogs)) {
+ allLogs[payload.server] = payload.log;
+ }
+ i = 0;
+ while (i < sizeof(keys(allLogs))) {
+ s1 = keys(allLogs)[i];
+ j = i + 1;
+ while (j < sizeof(keys(allLogs))) {
+ s2 = keys(allLogs)[j];
+ if (sizeof(allLogs[s1]) > sizeof(allLogs[s2])) {
+ assert checkLogMatching(allLogs[s2], allLogs[s1]);
+ } else {
+ assert checkLogMatching(allLogs[s1], allLogs[s2]);
+ }
+ j = j + 1;
+ }
+ i = i + 1;
+ }
+ }
+ }
+
+ fun checkLogMatching(xs: seq[tServerLog], ys: seq[tServerLog]): bool {
+ var i: int;
+ var highestMatch: int;
+ var logsA: seq[tServerLog];
+ var logsB: seq[tServerLog];
+ if (sizeof(xs) > sizeof(ys)) {
+ logsA = ys;
+ logsB = xs;
+ } else {
+ logsA = xs;
+ logsB = ys;
+ }
+ i = sizeof(logsA) - 1;
+ while (i >= 0 && logsA[i] != logsB[i]) {
+ i = i - 1;
+ }
+ while (i >= 0) {
+ if (logsA[i] != logsB[i]) {
+ return false;
+ }
+ i = i - 1;
+ }
+ return true;
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyOneLeader.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyOneLeader.p
new file mode 100644
index 0000000000..dfb5c98d8b
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyOneLeader.p
@@ -0,0 +1,17 @@
+type tBecomeLeader = (term:TermId, leader:Server, log: seq[tServerLog], commitIndex: LogIndex);
+event eBecomeLeader: tBecomeLeader;
+
+spec SafetyOneLeader observes eBecomeLeader{
+ var termToLeader: map[int, Server];
+ start state Init {
+ entry {
+ termToLeader = default(map[int, Server]);
+ }
+ on eBecomeLeader do (payload: (term:int, leader:Server, log: seq[tServerLog], commitIndex: int)) {
+ if (payload.term in keys(termToLeader)) {
+ assert termToLeader[payload.term] == payload.leader, format("At term {0} there are multiple leaders: {1} and {2}.", payload.term, termToLeader[payload.term], payload.leader);
+ }
+ termToLeader += (payload.term, payload.leader);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyStateMachine.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyStateMachine.p
new file mode 100644
index 0000000000..4935e2cc96
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetyStateMachine.p
@@ -0,0 +1,20 @@
+event eEntryApplied: (logIndex: LogIndex, term: TermId, key: KeyT, value: ValueT, transId: TransId, client: Client);
+
+spec SafetyStateMachine observes eEntryApplied {
+ var appliedEntries: map[int, tServerLog];
+ start state MonitorEntryApplied {
+ entry {
+ appliedEntries = default(map[int, tServerLog]);
+ }
+
+ on eEntryApplied do (payload: (logIndex: LogIndex, term: TermId, key: KeyT, value: ValueT, transId: TransId, client: Client)) {
+ var log: tServerLog;
+ log = (term=payload.term, key=payload.key, value=payload.value, client=payload.client, transId=payload.transId);
+ if (!(payload.logIndex in keys(appliedEntries))) {
+ appliedEntries[payload.logIndex] = log;
+ }
+ assert appliedEntries[payload.logIndex] == log,
+ format("Entry@{0}={1} applied before does not match with {2}", payload.logIndex, appliedEntries[payload.logIndex], log);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetySynchronization.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetySynchronization.p
new file mode 100644
index 0000000000..72648d7f8e
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSpec/SafetySynchronization.p
@@ -0,0 +1,94 @@
+spec SafetySynchronization observes eClientPutRequest, eClientGetRequest, eRaftGetResponse, eRaftPutResponse, eBecomeLeader {
+ var localKVStore: KVStore;
+ var requestResultMap: map[Client, map[int, Result]];
+ var getRequestMap: map[Client, map[TransId, KeyT]];
+ var putRequestMap: map[Client, map[TransId, (key: KeyT, value: ValueT)]];
+ var seenId: map[Client, set[int]];
+ var respondedId : map[Client, set[int]];
+ var currentLeader: Server;
+ var term: TermId;
+
+ start state Init {
+ entry {
+ localKVStore = newStore();
+ requestResultMap = default(map[Client, map[int, Result]]);
+ getRequestMap = default(map[Client, map[TransId, KeyT]]);
+ putRequestMap = default(map[Client, map[TransId, (key: KeyT, value: ValueT)]]);
+ seenId = default(map[Client, set[int]]);
+ respondedId = default(map[Client, set[int]]);
+ currentLeader = default(Server);
+ term = -1;
+ goto Listening;
+ }
+ }
+
+ state Listening {
+ on eBecomeLeader do (payload: tBecomeLeader) {
+ if (payload.term > term) {
+ currentLeader = payload.leader;
+ term = payload.term;
+ }
+ }
+
+ on eClientGetRequest do (payload: tClientGetRequest) {
+ if (!(payload.client in keys(seenId))) {
+ seenId[payload.client] = default(set[int]);
+ }
+ if (!(payload.client in keys(respondedId))) {
+ respondedId[payload.client] = default(set[int]);
+ }
+ if (!(payload.client in keys(getRequestMap))) {
+ getRequestMap[payload.client] = default(map[TransId, KeyT]);
+ }
+ if (payload.client == payload.sender && !(payload.transId in seenId[payload.client])) {
+ seenId[payload.client] += (payload.transId);
+ }
+ getRequestMap[payload.client][payload.transId] = payload.key;
+ }
+
+ on eClientPutRequest do (payload: tClientPutRequest) {
+ if (!(payload.client in keys(seenId))) {
+ seenId[payload.client] = default(set[int]);
+ }
+ if (!(payload.client in keys(respondedId))) {
+ respondedId[payload.client] = default(set[int]);
+ }
+ if (!(payload.client in keys(putRequestMap))) {
+ putRequestMap[payload.client] = default(map[TransId, (key: KeyT, value: ValueT)]);
+ }
+ if (payload.client == payload.sender && !(payload.transId in seenId[payload.client])) {
+ seenId[payload.client] += (payload.transId);
+ }
+ putRequestMap[payload.client][payload.transId] = (key=payload.key, value=payload.value);
+ }
+
+ on eRaftGetResponse do (payload: tRaftGetResponse) {
+ var execResult: ExecutionResult;
+ checkResponseValid(payload.client, payload.sender, payload.transId);
+ if (!(payload.client in keys(respondedId)) && currentLeader == payload.sender) {
+ respondedId[payload.client] += (payload.transId);
+ execResult = executeGet(localKVStore, getRequestMap[payload.client][payload.transId]);
+ assert execResult.result.success == payload.success, format("Inconsistent status: {0}", getRequestMap[payload.client][payload.transId]);
+ assert execResult.result.value == payload.value, format("Inconsistent Get result! Expected {0}, got {1}", execResult.result.value, payload.value);
+ }
+ }
+
+ on eRaftPutResponse do (payload: tRaftPutResponse) {
+ var execResult: ExecutionResult;
+ checkResponseValid(payload.client, payload.sender, payload.transId);
+ if (!(payload.client in keys(respondedId)) && currentLeader == payload.sender) {
+ respondedId[payload.client] += (payload.transId);
+ assert payload.client in keys(putRequestMap);
+ execResult = executePut(localKVStore, putRequestMap[payload.client][payload.transId].key, putRequestMap[payload.client][payload.transId].value);
+ localKVStore = execResult.newState;
+ }
+ }
+ }
+ fun checkResponseValid(client: Client, sender: Server, tId: TransId) {
+ assert currentLeader != default(Server), "Leader is un-initialized but got an eRaftResponse!";
+ if (currentLeader == sender) {
+ assert client in keys(seenId), format("Responding to a client that has not sent any request: {0}", client);
+ assert tId in seenId[client], format("Responding to a non-existing transactionId {0} sent by {1}", tId, client);
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Application.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Application.p
new file mode 100644
index 0000000000..ed3aaa0134
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Application.p
@@ -0,0 +1,34 @@
+enum Op {
+ PUT,
+ GET
+}
+type KeyT = int;
+type ValueT = int;
+type Command = (op: Op, key: KeyT, value: ValueT);
+type Result = (success: bool, value: ValueT);
+type ExecutionResult = (newState: KVStore, result: Result);
+type KVStore = map[KeyT, ValueT];
+
+fun IsPut(op: Op): bool {
+ return op == PUT;
+}
+
+fun IsGet(op: Op): bool {
+ return op == GET;
+}
+
+fun newStore(): KVStore {
+ return default(KVStore);
+}
+
+fun executeGet(store: KVStore, key: KeyT): ExecutionResult {
+ if (!(key in store)) {
+ return (newState=store, result=(success=false, value=-1));
+ }
+ return (newState=store, result=(success=true, value=store[key]));
+}
+
+fun executePut(store: KVStore, key: KeyT, value: ValueT): ExecutionResult {
+ store[key] = value;
+ return (newState=store, result=(success=true, value=-1));
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Client.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Client.p
new file mode 100644
index 0000000000..f569713e00
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Client.p
@@ -0,0 +1,117 @@
+/*
+* A client that uses the Raft cluster.
+* The client sends a sequence of commands to the cluster.
+* It does not move on the next command until it receives a response from the cluster.
+* The client retries sending the command if a certain amount of heartbeats timeouts occur.
+*/
+
+// Client requests
+type tClientRequest = (transId: TransId, client: Client, cmd: Command, sender: Client);
+type tClientPutRequest = (transId: TransId, client: Client, key: KeyT, value: ValueT, sender: Client);
+type tClientGetRequest = (transId: TransId, client: Client, key: KeyT, sender: Client);
+event eClientPutRequest: tClientPutRequest;
+event eClientGetRequest: tClientGetRequest;
+// event eClientRequest: tClientRequest;
+// The event of notifying the monitor that the client is waiting for a response
+event eClientWaitingResponse: (client: Client, transId: int);
+// The event of notifying the monitor that the client got a response for a transaction
+event eClientGotResponse: (client: Client, transId: int);
+// The event of notifying the monitor that the client finished
+event eClientFinishedMonitor: Client;
+// The event of notifying the view service that the client finished
+event eClientFinished: Client;
+
+machine Client {
+ var worklist: seq[Command];
+ var servers: set[machine];
+ var ptr: int;
+ var tId: int;
+ var retries: int;
+ var currentCmd: Command;
+ var view: View;
+ var timer: Timer;
+
+ start state Init {
+ entry (config: (viewService: View, servers: set[machine], requests: seq[Command])) {
+ worklist = config.requests;
+ servers = config.servers;
+ view = config.viewService;
+ ptr = 0;
+ tId = 0;
+ timer = new Timer((user=this, timeoutEvent=eHeartbeatTimeout));
+ goto SendOne;
+ }
+ }
+
+ state SendOne {
+ entry {
+ var cmd: Command;
+ // print format("{0} is at {1}", this, ptr);
+ // print format("Worklist {0}", worklist);
+ if (sizeof(worklist) == ptr) {
+ // if no more work to do, go to Done
+ goto Done;
+ } else {
+ // get the current command and increase transaction id
+ currentCmd = worklist[ptr];
+ ptr = ptr + 1;
+ tId = tId + 1;
+ broadcastToCluster();
+ goto WaitForResponse;
+ }
+ }
+ }
+
+ state WaitForResponse {
+ entry {
+ announce eClientWaitingResponse, (client=this, transId=tId);
+ startTimer(timer);
+ retries = 0;
+ }
+
+ on eHeartbeatTimeout do {
+ // print format("Client {0} timed out waiting for response {1}; current retries: {2}", this, tId, retries / 50);
+ if (retries % 200 == 0) {
+ // retries every 200 heartbeats
+ broadcastToCluster();
+ }
+ retries = retries + 1;
+ startTimer(timer);
+ }
+
+ on eRaftGetResponse do (resp: tRaftGetResponse) {
+ handleResponse(resp.transId);
+ }
+
+ on eRaftPutResponse do (resp: tRaftPutResponse) {
+ handleResponse(resp.transId);
+ }
+ }
+
+ fun handleResponse(responseTransId: TransId) {
+ if (responseTransId == tId) {
+ announce eClientGotResponse, (client=this, transId=tId);
+ retries = 0;
+ goto SendOne;
+ }
+ }
+
+ fun broadcastToCluster() {
+ var s: machine;
+ foreach (s in servers) {
+ if (currentCmd.op == GET) {
+ send s, eClientGetRequest, (transId=tId, client=this, key=currentCmd.key, sender=this);
+ } else {
+ send s, eClientPutRequest, (transId=tId, client=this, key=currentCmd.key, value=currentCmd.value, sender=this);
+ }
+ }
+ }
+
+ state Done {
+ entry {
+ announce eClientFinishedMonitor, this;
+ send view, eClientFinished, this;
+ }
+ ignore eRaftGetResponse, eRaftPutResponse, eHeartbeatTimeout;
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/RaftModules.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/RaftModules.p
new file mode 100644
index 0000000000..a0961fc776
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/RaftModules.p
@@ -0,0 +1,4 @@
+module Server = { Server };
+module Timer = { Timer };
+module Client = { Client };
+module View = { View };
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Server.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Server.p
new file mode 100644
index 0000000000..c1fc914c8c
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Server.p
@@ -0,0 +1,806 @@
+/*****************************************************************************
+* A node in the Raft cluster.
+* The implementation follows the Raft paper (Ongaro and Ousterhout, 2014).
+* This implementation does not include log compaction / cluster reconfiguration.
+******************************************************************************/
+type ServerId = int;
+type TransId = int;
+
+// Response message to the client
+type tRaftResponse = (sender: Server, client: Client, transId: TransId, result: Result);
+// event eRaftResponse: tRaftResponse;
+type tRaftPutResponse = (sender: Server, client: Client, transId: TransId, key: KeyT);
+type tRaftGetResponse = (sender: Server, client: Client, success: bool, transId: TransId, value: ValueT);
+// type tRaftGetError = (sender: Server, client: Client, transId: TransId, key: KeyT);
+event eRaftPutResponse: tRaftPutResponse;
+event eRaftGetResponse: tRaftGetResponse;
+// event eRaftGetError: tRaftGetError;
+
+event ePutReq: (client: Client, transId: TransId, key: KeyT, value: ValueT);
+event eGetReq: (client: Client, transId: TransId, key: KeyT);
+event ePutResp: (client: Client, transId: TransId, key: KeyT, value: ValueT);
+event eGetResp: (client: Client, transId: TransId, success: bool, key: KeyT, value: ValueT);
+
+// Server initialization message
+event eServerInit: (myId: ServerId, cluster: set[Server], viewServer: View);
+
+// Vote request and response
+type TermId = int;
+type LogIndex = int;
+type tRequestVote = (term: TermId, candidate: Server, lastLogIndex: LogIndex, lastLogTerm: TermId);
+event eRequestVote: tRequestVote;
+type tRequestVoteReply = (sender: Server, term: TermId, voteGranted: bool);
+event eRequestVoteReply: tRequestVoteReply;
+
+// Heartbeat message and response
+type tAppendEntries = (term: TermId, leader: Server, prevLogIndex: LogIndex,
+ prevLogTerm: TermId, entries: seq[tServerLog], leaderCommit: LogIndex);
+
+event eAppendEntries: tAppendEntries;
+type tAppendEntriesReply = (sender: Server, term: TermId, success: bool, matchedIndex: LogIndex);
+event eAppendEntriesReply: tAppendEntriesReply;
+
+// Resetting a server (to model crashes)
+event eReset;
+
+// events to notify the server role change to the view server
+event eViewChangedFollower: Server;
+event eViewChangedLeader: Server;
+event eViewChangedCandidate: Server;
+
+// server logs
+type tServerLog = (term: TermId, key: KeyT, value: ValueT, client: Client, transId: TransId);
+
+fun TermLT(t1: TermId, t2: TermId): bool {
+ return t1 < t2;
+}
+
+fun IsSuccess(s: bool): bool {
+ return s;
+}
+
+fun IsFailure(s: bool): bool {
+ return !s;
+}
+
+// A node in the Raft cluster
+machine Server {
+ var serverId: ServerId;
+ // the application state
+ var kvStore: KVStore;
+ // the leader of the cluster
+ var leader: Server;
+ // size of the cluster
+ var clusterSize: int;
+ // nodes in the cluster
+ var peers: set[Server];
+ // the view service
+ var viewServer: View;
+
+ // Leader state (volatile)
+ var nextIndex: map[Server, int];
+ var matchIndex: map[Server, int];
+ // Leader state (persistent)
+ var currentTerm: int;
+ var logs: seq[tServerLog];
+
+ // States for voting
+ var votedFor: Server;
+ var votesReceived: set[Server];
+
+ // all servers
+ var commitIndex: int;
+ var lastApplied: int;
+
+ var role: string;
+ var logNotifyTimestamp: int;
+
+ // var heartbeatTimer: Timer;
+ // var clientRequestQueue: seq[tClientRequest];
+ var requestSeqNum: int;
+ var clientPutRequestQueue: seq[(seqNum: int, payload: tClientPutRequest)];
+ var clientGetRequestQueue: seq[(seqNum: int, payload: tClientGetRequest)];
+ var clientPutRequestCache: map[Client, map[int, tRaftPutResponse]];
+ var clientGetRequestCache: map[Client, map[int, tRaftGetResponse]];
+
+ start state Init {
+ entry {}
+ on eServerInit do (setup: (myId: ServerId, cluster: set[Server], viewServer: View)) {
+ kvStore = newStore();
+ serverId = setup.myId;
+ assert this in setup.cluster, "Server should be in the cluster";
+ clusterSize = sizeof(setup.cluster);
+ peers = setup.cluster;
+ nextIndex = default(map[Server, int]);
+ matchIndex = default(map[Server, int]);
+ votesReceived = default(set[Server]);
+ logs = default(seq[tServerLog]);
+ // clientRequestQueue = default(seq[tClientRequest]);
+ clientPutRequestQueue = default(seq[(seqNum: int, payload: tClientPutRequest)]);
+ clientGetRequestQueue = default(seq[(seqNum: int, payload: tClientGetRequest)]);
+ requestSeqNum = 0;
+ clientPutRequestCache = default(map[Client, map[int, tRaftPutResponse]]);
+ clientGetRequestCache = default(map[Client, map[int, tRaftGetResponse]]);
+ viewServer = setup.viewServer;
+
+ currentTerm = 0;
+ logNotifyTimestamp = 0;
+ votedFor = default(Server);
+ commitIndex = -1;
+ lastApplied = -1;
+ leader = default(Server);
+ role = "Uninitialized";
+ goto Follower;
+ }
+ ignore eReset, eElectionTimeout, eHeartbeatTimeout;
+ }
+
+ state Follower {
+ entry {
+ role = "F";
+ // reset the leader upon being a follower
+ leader = default(Server);
+ printLog("Become follower");
+ send viewServer, eViewChangedFollower, this;
+ }
+
+ on eShutdown do {
+ goto FinishedServing;
+ }
+
+ on eReset do {
+ reset();
+ }
+
+ on eRequestVote do (payload: tRequestVote) {
+ handleRequestVote(payload);
+ }
+
+ on eClientPutRequest do (payload: tClientPutRequest) {
+ // check if the request has been processed
+ respondOrForwardPut(payload);
+ }
+
+ on eClientGetRequest do (payload: tClientGetRequest) {
+ respondOrForwardGet(payload);
+ }
+
+ on eAppendEntries do (payload: tAppendEntries) {
+ if (payload.term >= currentTerm) {
+ printLog(format("heartbeat updated term from {0} to {1}", currentTerm, payload.term));
+ if (leader != default(Server) && payload.leader != leader) {
+ votedFor = default(Server);
+ }
+ leader = payload.leader;
+ updateTermAndVote(payload.term);
+ }
+ handleAppendEntries(payload);
+ if (leader != default(Server)) {
+ // propagate client requests to the leader
+ drainRequestQueuesTo(leader);
+ }
+ }
+
+ on eElectionTimeout do {
+ printLog("Election timeout");
+ goto Candidate;
+ }
+
+ on eHeartbeatTimeout do {
+ notifyViewLog();
+ }
+
+ ignore eRequestVoteReply, eAppendEntriesReply;
+ }
+
+ state Candidate {
+ entry {
+ var peer: Server;
+ var lastTerm: int;
+ role = "C";
+ printLog("Become candidate");
+ send viewServer, eViewChangedCandidate, this;
+ currentTerm = currentTerm + 1;
+ votedFor = default(Server);
+ votesReceived = default(set[Server]);
+ votesReceived += (this);
+ // if there is only 1 server, no need to request vote
+ if (sizeof(votesReceived) > clusterSize / 2) {
+ goto Leader;
+ } else {
+ printLog(format("RequestVote with term {0}", currentTerm));
+ broadcastRequest(this, peers, eRequestVote,
+ (term=currentTerm,
+ candidate=this,
+ lastLogIndex=lastLogIndex(logs),
+ lastLogTerm=lastLogTerm(logs)));
+ }
+ }
+
+ on eClientPutRequest do (payload: tClientPutRequest) {
+ if (checkClientRequestCache(payload.client, payload.transId)) {
+ return;
+ }
+ clientPutRequestQueue += (sizeof(clientPutRequestQueue), (seqNum=requestSeqNum, payload=payload));
+ requestSeqNum = requestSeqNum + 1;
+ }
+
+ on eShutdown do {
+ goto FinishedServing;
+ }
+
+ on eAppendEntries do (payload: tAppendEntries) {
+ if (payload.term >= currentTerm) {
+ // if there is a leader, convert to follower
+ printLog(format("Received heartbeat from a leader with higher term {0}", payload.term));
+ updateTermAndVote(payload.term);
+ handleAppendEntries(payload);
+ goto Follower;
+ } else {
+ printLog(format("Received smaller term heartbeat from {0}", payload.leader));
+ handleAppendEntries(payload);
+ }
+ }
+
+ on eAppendEntriesReply do (payload: tAppendEntriesReply) {
+ if (payload.term > currentTerm) {
+ printLog(format("Received AppendEntriesReply with higher term {0}", payload.term));
+ updateTermAndVote(payload.term);
+ goto Follower;
+ }
+ }
+
+ on eRequestVoteReply do (payload: tRequestVoteReply) {
+ if (payload.term > currentTerm) {
+ // if there is a vote with higher term, convert to follower
+ updateTermAndVote(payload.term);
+ goto Follower;
+ } else if (payload.voteGranted && payload.term == currentTerm) {
+ // if the vote is granted for me in this term, count it.
+ votesReceived += (payload.sender);
+ printLog(format("vote granted by {0}; now vote={1}", payload.sender, votesReceived));
+ if (sizeof(votesReceived) > clusterSize / 2) {
+ // if majority votes are received, become a leader
+ printLog(format("majority votes: {0}", votesReceived));
+ goto Leader;
+ }
+ } else {
+ printLog(format("Received rejection/invalid reply: {0}", payload));
+ }
+ }
+
+ on eRequestVote do (payload: tRequestVote) {
+ if (payload.term > currentTerm) {
+ updateTermAndVote(payload.term);
+ handleRequestVote(payload);
+ goto Follower;
+ } else {
+ send payload.candidate, eRequestVoteReply, (sender=this, term=currentTerm, voteGranted=false);
+ }
+ }
+
+ on eReset do {
+ reset();
+ }
+
+ on eElectionTimeout do {
+ goto Candidate;
+ }
+
+ on eHeartbeatTimeout do {
+ notifyViewLog();
+ }
+
+ ignore eClientGetRequest;
+ }
+
+ state Leader {
+ entry {
+ role = "L";
+ send viewServer, eViewChangedLeader, this;
+ leader = this;
+ // instantiate the nextIndex and matchIndex
+ nextIndex = fillMap(this, nextIndex, peers, sizeof(logs));
+ matchIndex = fillMap(this, matchIndex, peers, -1);
+ announce eBecomeLeader, (term=currentTerm, leader=this, log=logs, commitIndex=commitIndex);
+ // first exhaust the client request queu on its own
+ drainRequestQueuesTo(this);
+ printLog(format("Become leader with Log={0}, commiteIndex={1}, lastApplied={2}", logs, commitIndex, lastApplied));
+ broadcastAppendEntries();
+ }
+
+ on eShutdown do {
+ goto FinishedServing;
+ }
+
+ on eRequestVote do (payload: tRequestVote) {
+ if (currentTerm < payload.term) {
+ printLog(format("Saw higher term as a leader: currentTerm={0}, payload.term={1}", currentTerm, payload.term));
+ handleRequestVote(payload);
+ becomeFollower(payload.term);
+ } else {
+ printLog(format("Reject proposal from {0} with term {1}", payload.candidate, payload.term));
+ send payload.candidate, eRequestVoteReply, (sender=this, term=currentTerm, voteGranted=false);
+ }
+ }
+
+ on eRequestVoteReply do (payload: tRequestVoteReply) {
+ if (payload.term > currentTerm) {
+ printLog(format("received RequestVoteReply with higher term {0}", payload.term));
+ updateTermAndVote(payload.term);
+ goto Follower;
+ }
+ }
+
+ on eHeartbeatTimeout do {
+ broadcastAppendEntries();
+ leaderCommits();
+ notifyViewLog();
+ }
+
+ on eAppendEntries do (payload: tAppendEntries) {
+ if (payload.term > currentTerm) {
+ // if the leader receives an AppendEntries from another leader with a higher term
+ // step down to a follower
+ printLog(format("received AppendEntries with higher term {0}; handle then step down", payload.term));
+ updateTermAndVote(payload.term);
+ handleAppendEntries(payload);
+ becomeFollower(payload.term);
+ } else if (payload.term < currentTerm) {
+ send payload.leader, eAppendEntriesReply, (sender=this, term=currentTerm, success=false, matchedIndex=-1);
+ leaderCommits();
+ }
+ }
+
+ on eAppendEntriesReply do (payload: tAppendEntriesReply) {
+ printLog(format("Leader({0}, {1}) received AppendEntriesReply from {2}: {3}", this, currentTerm, payload.sender, payload));
+ if (payload.term < currentTerm) {
+ printLog("Ignore AppendEntriesReply with outdated term");
+ return;
+ }
+ if (payload.term > currentTerm) {
+ // if the leader receives a reply from another leader with a higher term
+ // step down to a follower
+ printLog(format("received reply from another leader with term {0} > currentTerm {1}", payload.term, currentTerm));
+ updateTermAndVote(payload.term);
+ goto Follower;
+ }
+ if (payload.success) {
+ // if the heartbeat is accepted,
+ // update the nextIndex and matchIndex for the corresponding node.
+ printLog(format("entries accepted by {0}; matchIndex={1}", payload.sender, payload.matchedIndex));
+ // Note that the leader can receive delayed message, so here we need to keep the
+ // larger values of nextIndex and matchIndex.
+ nextIndex[payload.sender] = Max(nextIndex[payload.sender], payload.matchedIndex + 1);
+ matchIndex[payload.sender] = Max(matchIndex[payload.sender], payload.matchedIndex);
+ leaderCommits();
+ } else {
+ if (payload.matchedIndex < 0) {
+ // a rejection of me as a leader in an earlier term
+ // this can happen when me and the other node simultaneously become a candidate
+ // the other node received my heartbeat from the previous term and sent me a rejection
+ // but currently, I am already at the newer term, so I can safely ignore this rejection
+ printLog(format("outdated rejection with term={0}, matchedIndex={1}", payload.term, payload.matchedIndex));
+ return;
+ }
+ // rejected because of log mismatch
+ // now, payload.matchedIndex is the index of the (potentially) first mismatched term
+ printLog(format("re-sync with {0} at index {1}", payload.sender, payload.matchedIndex));
+ // still, the leader can receive outdated message. To be conservative, it is safe to synchronize from
+ // a smaller index.
+ nextIndex[payload.sender] = Min(payload.matchedIndex, nextIndex[payload.sender]);
+ }
+ }
+
+ on eClientGetRequest do (payload: tClientGetRequest) {
+ var execResult: ExecutionResult;
+ if (checkClientRequestCache(payload.client, payload.transId)) {
+ return;
+ }
+ announce eGetReq, (client=payload.client, transId=payload.transId, key=payload.key);
+ execResult = executeGet(kvStore, payload.key);
+ send payload.client, eRaftGetResponse, (sender=this, client=payload.client, success=execResult.result.success, transId=payload.transId, value=execResult.result.value);
+ announce eGetResp, (client=payload.client, transId=payload.transId, success=execResult.result.success, key=payload.key, value=execResult.result.value);
+ if (!(payload.client in keys(clientGetRequestCache))) {
+ clientGetRequestCache[payload.client] = default(map[int, tRaftGetResponse]);
+ }
+ clientGetRequestCache[payload.client][payload.transId] = (sender=this, client=payload.client, success=execResult.result.success, transId=payload.transId, value=execResult.result.value);
+ }
+
+ on eClientPutRequest do (payload: tClientPutRequest) {
+ var newEntry: tServerLog;
+ var target: Server;
+ var entries: seq[tServerLog];
+ var i: int;
+ // print format("Received client request {0}", payload);
+ if (checkClientRequestCache(payload.client, payload.transId)) {
+ return;
+ }
+ newEntry = (term=currentTerm, key=payload.key, value=payload.value, client=payload.client, transId=payload.transId);
+ if (!(newEntry in logs)) {
+ logs += (sizeof(logs), newEntry);
+ announce ePutReq, (client=payload.client, transId=payload.transId, key=payload.key, value=payload.value);
+ }
+ printLog(format("Current leader logs={0}", logs));
+ printLog(format("nextIndex={0}, matchIndex={1}", nextIndex, matchIndex));
+ // use info of nextIndex and matchIndex to broadcast to peers
+ foreach (target in peers) {
+ if (target != this && nextIndex[target] < sizeof(logs)) {
+ entries = default(seq[tServerLog]);
+ i = nextIndex[target];
+ while (i < sizeof(logs)) {
+ entries += (i - nextIndex[target], logs[i]);
+ i = i + 1;
+ }
+ send target, eAppendEntries, (term=currentTerm,
+ leader=this,
+ prevLogIndex=nextIndex[target] - 1,
+ prevLogTerm=getLogTerm(logs, nextIndex[target] - 1),
+ entries=entries,
+ leaderCommit=commitIndex);
+ }
+ }
+ leaderCommits();
+ }
+
+ on eReset do {
+ reset();
+ }
+
+ ignore eElectionTimeout;
+ }
+
+ state FinishedServing {
+ entry {
+ printLog("Service ended. Shutdown.");
+ }
+ ignore eShutdown, eRequestVote, eRequestVoteReply, eAppendEntries, eAppendEntriesReply, eClientPutRequest, eClientGetRequest, eHeartbeatTimeout, eElectionTimeout;
+ }
+
+ fun drainRequestQueuesTo(target: Server) {
+ while (sizeof(clientPutRequestQueue) > 0 || sizeof(clientGetRequestQueue) > 0) {
+ if (sizeof(clientPutRequestQueue) == 0) {
+ send target, eClientGetRequest, forwardedGetRequest(clientGetRequestQueue[0].payload);
+ clientGetRequestQueue -= (0);
+ } else if (sizeof(clientGetRequestQueue) == 0) {
+ send target, eClientPutRequest, forwardedPutRequest(clientPutRequestQueue[0].payload);
+ clientPutRequestQueue -= (0);
+ } else {
+ if (clientPutRequestQueue[0].seqNum < clientGetRequestQueue[0].seqNum) {
+ send target, eClientPutRequest, forwardedPutRequest(clientPutRequestQueue[0].payload);
+ clientPutRequestQueue -= (0);
+ } else {
+ send target, eClientGetRequest, forwardedGetRequest(clientGetRequestQueue[0].payload);
+ clientGetRequestQueue -= (0);
+ }
+ }
+ }
+ }
+
+ fun respondOrForwardPut(payload: tClientPutRequest) {
+ if (checkClientRequestCache(payload.client, payload.transId)) {
+ return;
+ }
+ if (leader != default(Server)) {
+ send leader, eClientPutRequest, forwardedPutRequest(payload);
+ } else {
+ clientPutRequestQueue += (sizeof(clientPutRequestQueue), (seqNum=requestSeqNum, payload=payload));
+ requestSeqNum = requestSeqNum + 1;
+ }
+ }
+
+ fun respondOrForwardGet(payload: tClientGetRequest) {
+ if (checkClientRequestCache(payload.client, payload.transId)) {
+ return;
+ }
+ if (leader != default(Server)) {
+ send leader, eClientGetRequest, forwardedGetRequest(payload);
+ } else {
+ clientGetRequestQueue += (sizeof(clientGetRequestQueue), (seqNum=requestSeqNum, payload=payload));
+ requestSeqNum = requestSeqNum + 1;
+ }
+ }
+
+ fun forwardedPutRequest(req: tClientPutRequest): tClientPutRequest {
+ return (transId=req.transId, client=req.client, key=req.key, value=req.value, sender=this);
+ }
+
+ fun forwardedGetRequest(req: tClientGetRequest): tClientGetRequest {
+ return (transId=req.transId, client=req.client, key=req.key, sender=this);
+ }
+
+ fun handleRequestVote(reply: tRequestVote) {
+ // A handler for eRequestVote messages
+ leader = default(Server);
+ if (reply.term < currentTerm) {
+ // reject a vote with a smaller term
+ printLog(format("Reject vote with term {0} < currentTerm {1}", reply.term, currentTerm));
+ send reply.candidate, eRequestVoteReply, (sender=this, term=currentTerm, voteGranted=false);
+ } else {
+ updateTermAndVote(reply.term);
+ // if votedFor is null or the candidate it has voted for
+ // and the log is up-to-date, grant the vote. Reject otherwise.
+ if ((votedFor == default(Server) || votedFor == reply.candidate)
+ && logUpToDateCheck(reply.lastLogIndex, reply.lastLogTerm)) {
+ printLog(format("Grant vote to {0} (prev. votedFor={1})", reply.candidate, votedFor));
+ votedFor = reply.candidate;
+ send reply.candidate, eRequestVoteReply, (sender=this, term=currentTerm, voteGranted=true);
+ } else {
+ printLog(format("Reject vote with votedFor={0} and logUpToDate={1}", votedFor, logUpToDateCheck(reply.lastLogIndex, reply.lastLogTerm)));
+ send reply.candidate, eRequestVoteReply, (sender=this, term=currentTerm, voteGranted=false);
+ }
+ }
+ }
+
+ fun handleAppendEntries(resp: tAppendEntries) {
+ // handler of leader heartbeat, log synchronization
+ var myLastIndex: int;
+ var myLastTerm: int;
+ var ptr: int;
+ var mismatchedTerm: int;
+ var i: int;
+ var j: int;
+ printLog(format("Received AppendEntries({0}) from Leader({1}) with {2}; currentTerm={3}, log={4}", resp.entries, resp.leader, resp, currentTerm, logs));
+ if (resp.term < currentTerm) {
+ // if the heartbeat is from a leader with an outdated term, reject the heartbeat
+ printLog(format("Reject AppendEntries with term {0} < currentTerm {1}", resp.term, currentTerm));
+ send resp.leader, eAppendEntriesReply, (sender=this, term=currentTerm,
+ success=false, matchedIndex=-1);
+ } else if (sizeof(logs) <= resp.prevLogIndex) {
+ // If the log is very outdated, re-sync from the end of my log
+ leader = resp.leader;
+ printLog(format("prevLogIndex={0} is out of range of sizeof(log)={1}", resp.prevLogIndex, sizeof(logs)));
+ send resp.leader, eAppendEntriesReply, (sender=this, term=currentTerm, success=false, matchedIndex=sizeof(logs));
+ } else if (resp.prevLogIndex >= 0 && logs[resp.prevLogIndex].term != resp.prevLogTerm) {
+ // Log consistency check failed here;
+ // search for the first occurence of the mismatched
+ // term and notify the leader.
+ printLog(format("Log inconsistency at index {0}: log[i].term={1} v.s. prevTerm={2}", resp.prevLogIndex, logs[resp.prevLogIndex].term, resp.prevLogTerm));
+ leader = resp.leader;
+ ptr = resp.prevLogIndex;
+ mismatchedTerm = logs[ptr].term;
+ // find the first occurrence of the mismatched term
+ // this includes the optimization described in the paper
+ while (ptr > 0 && logs[ptr].term == mismatchedTerm) {
+ ptr = ptr - 1;
+ }
+ printLog(format("requesting sync. log terms={0}, mismatchedTerm={1}, prevLogIndex={2}, ptr={3}", termsOfLog(), mismatchedTerm, resp.prevLogIndex, ptr));
+ send resp.leader, eAppendEntriesReply, (sender=this, term=currentTerm,
+ success=false, matchedIndex=ptr);
+ } else {
+ // Check if any entries disagree; delete all entries after it.
+ printLog(format("Sync log with log={0} at index {1}. Entries={2}", logs, resp.prevLogIndex + 1, resp.entries));
+ leader = resp.leader;
+ i = resp.prevLogIndex + 1;
+ j = 0;
+ printLog(format("Before sync: logs={0}", logs));
+ // find the first mismatched entry
+ while (i < sizeof(logs) && j < sizeof(resp.entries)) {
+ if (logs[i].term != resp.entries[j].term) {
+ break;
+ }
+ i = i + 1;
+ j = j + 1;
+ }
+ // delete all entries after the mismatched entry
+ while (i < sizeof(logs)) {
+ logs -= (lastLogIndex(logs));
+ }
+ printLog(format("Inconsistency removal: logs={0}", logs));
+ // Append entries from the leader
+ j = 0;
+ while (j < sizeof(resp.entries)) {
+ if (!(resp.entries[j] in logs)) {
+ logs += (resp.prevLogIndex + 1 + j, resp.entries[j]);
+ } else {
+ assert resp.entries[j] == logs[resp.prevLogIndex + 1 + j], "Inconsistent log entry";
+ }
+ j = j + 1;
+ }
+ printLog(format("After sync: logs={0}, leaderCommit={1}, commitIndex={2}", logs, resp.leaderCommit, commitIndex));
+ if (resp.leaderCommit > commitIndex) {
+ if (resp.leaderCommit > lastLogIndex(logs)) {
+ commitIndex = lastLogIndex(logs);
+ } else {
+ commitIndex = resp.leaderCommit;
+ }
+ }
+ executeCommands();
+ send resp.leader, eAppendEntriesReply, (sender=this, term=currentTerm, success=true, matchedIndex=lastLogIndex(logs));
+ }
+ }
+
+ fun broadcastAppendEntries() {
+ // broadcasting of leader heartbeats
+ var target: Server;
+ var i: int;
+ var j: int;
+ var prevIndex: int;
+ var prevTerm: int;
+ var entries: seq[tServerLog];
+ assert this == leader, "Only leader can broadcast AppendEntries";
+ // print format("Broadcasting AppendEntries with log {0}", logs);
+ foreach (target in peers) {
+ if (this != target) {
+ entries = default(seq[tServerLog]);
+ prevIndex = nextIndex[target] - 1;
+ // get prevIndex and prevIndexTerm
+ if (prevIndex >= 0 && prevIndex < sizeof(logs)) {
+ prevTerm = logs[prevIndex].term;
+ } else {
+ prevTerm = 0;
+ }
+ if (nextIndex[target] < sizeof(logs)) {
+ // if the current lead has something to send (nextIndex within range of logs)
+ i = 0;
+ j = nextIndex[target];
+ // aggregate the logs to be synchronized
+ while (j < sizeof(logs)) {
+ entries += (i, logs[j]);
+ i = i + 1;
+ j = j + 1;
+ }
+ printLog(format("Entries for {0} is {1}", target, entries));
+ send target, eAppendEntries, (term=currentTerm,
+ leader=this,
+ prevLogIndex=prevIndex,
+ prevLogTerm=prevTerm,
+ entries=entries,
+ leaderCommit=commitIndex);
+ } else {
+ // send empty heartbeat
+ send target, eAppendEntries, (term=currentTerm,
+ leader=this,
+ prevLogIndex=prevIndex,
+ prevLogTerm=prevTerm,
+ entries=entries,
+ leaderCommit=commitIndex);
+
+ }
+ }
+ }
+ }
+
+ fun leaderCommits() {
+ // leader committing log entries
+ var execResult: ExecutionResult;
+ // the next commit index
+ var nextCommit: int;
+ // the number of match indices that are greater than or equal to nextCommit
+ var validMatchIndices: int;
+ var i: int;
+ var target: Server;
+ assert this == leader, "Only leader can execute the log on its own";
+ // iteratively search for that index
+ nextCommit = lastLogIndex(logs);
+ printLog(format("state: nextCommit={0} commitIndex={1} matchIndex={2}", nextCommit, commitIndex, matchIndex));
+ // Find the largest nextCommit such that the majority of matchIndex is greater than or equal to nextCommit
+ while (nextCommit > commitIndex) {
+ // counting itself first
+ validMatchIndices = 1;
+ foreach (target in peers) {
+ if (target == this) {
+ continue;
+ }
+ if (matchIndex[target] >= nextCommit) {
+ validMatchIndices = validMatchIndices + 1;
+ }
+ }
+ // if the majority of matchIndex is greater than or equal to nextCommit
+ // also the term is currentTerm in this entry
+ if (validMatchIndices > clusterSize / 2 && logs[nextCommit].term == currentTerm) {
+ commitIndex = nextCommit;
+ break;
+ }
+ nextCommit = nextCommit - 1;
+ }
+ // commitIndex = nextCommit;
+ printLog(format("leader commits decision: matchIndex={0} commitIndex={1} currentTerm={2} logs={3}", matchIndex, commitIndex, currentTerm, logs));
+ // commit all the logs from lastApplied + 1 to commitIndex
+ while (lastApplied < commitIndex) {
+ lastApplied = lastApplied + 1;
+ announce eEntryApplied, (logIndex=lastApplied, term=logs[lastApplied].term, key=logs[lastApplied].key, value=logs[lastApplied].value, transId=logs[lastApplied].transId, client=logs[lastApplied].client);
+ execResult = executePut(kvStore, logs[lastApplied].key, logs[lastApplied].value);
+ kvStore = execResult.newState;
+ printLog(format("leader committed and processed (by {0}), log: {1}", this, logs[lastApplied]));
+ // execute the command and send the result back to the client
+ announce ePutResp, (client=logs[lastApplied].client, transId=logs[lastApplied].transId, key=logs[lastApplied].key, value=logs[lastApplied].value);
+ send logs[lastApplied].client, eRaftPutResponse, (sender=this, client=logs[lastApplied].client,
+ transId=logs[lastApplied].transId, key=logs[lastApplied].key);
+ // update the cache
+ if (!(logs[lastApplied].client in keys(clientPutRequestCache))) {
+ clientPutRequestCache[logs[lastApplied].client] = default(map[int, tRaftPutResponse]);
+ }
+ clientPutRequestCache[logs[lastApplied].client][logs[lastApplied].transId] = (sender=this, client=logs[lastApplied].client, transId=logs[lastApplied].transId, key=logs[lastApplied].key);
+ }
+ }
+
+ fun checkClientRequestCache(client: Client, transactionId: TransId): bool {
+ // if the result is already in the cache, send it back
+ if (client in clientGetRequestCache && transactionId in keys(clientGetRequestCache[client])) {
+ send client, eRaftGetResponse, clientGetRequestCache[client][transactionId];
+ return true;
+ }
+ if (client in clientPutRequestCache && transactionId in keys(clientPutRequestCache[client])) {
+ send client, eRaftPutResponse, clientPutRequestCache[client][transactionId];
+ return true;
+ }
+ return false;
+ }
+
+ fun printLog(msg: string) {
+ // return;
+ // print format("{0}[{1}@{2}]: {3}", role, this, currentTerm, msg);
+ }
+
+ fun executeCommands() {
+ var execResult: ExecutionResult;
+ printLog(format("commitIndex={0} lastApplied={1}", commitIndex, lastApplied));
+ // Execute the command from lastApplied + 1 to commitIndex
+ while (lastApplied < commitIndex) {
+ lastApplied = lastApplied + 1;
+ announce eEntryApplied, (logIndex=lastApplied, term=logs[lastApplied].term, key=logs[lastApplied].key, value=logs[lastApplied].value, transId=logs[lastApplied].transId, client=logs[lastApplied].client);
+ execResult = executePut(kvStore, logs[lastApplied].key, logs[lastApplied].value);
+ kvStore = execResult.newState;
+ // clientRequestCache[logs[lastApplied].client][logs[lastApplied].transId] = (client=logs[lastApplied].client, transId=logs[lastApplied].transId, result=execResult.result);
+ }
+ }
+
+ fun logUpToDateCheck(lastIndex: LogIndex, lastTerm: TermId): bool {
+ // Given `lastIndex` and `lastTerm` from the other node,
+ // check if its logs is up-to-date.
+ if (lastTerm > lastLogTerm(logs)) {
+ // the lastTerm is greater than mine, then it is up-to-date
+ return true;
+ }
+ if (lastTerm < lastLogTerm(logs)) {
+ // the lastTerm is smaller than mine, then it is not up-to-date
+ return false;
+ }
+ // the lastTerm is the same, then whichever's log is longer is up-to-date
+ return lastIndex >= lastLogIndex(logs);
+ }
+
+ fun notifyViewLog() {
+ logNotifyTimestamp = logNotifyTimestamp + 1;
+ send viewServer, eNotifyLog, (timestamp=logNotifyTimestamp, server=this, log=logs);
+ }
+
+ fun becomeFollower(term: int) {
+ // cancelTimer(electionTimer);
+ updateTermAndVote(term);
+ goto Follower;
+ }
+
+ fun updateTermAndVote(term: int) {
+ if (term > currentTerm) {
+ // if the term is changed, then reset the votedFor since
+ // it has not voted in the new term.
+ votedFor = default(Server);
+ }
+ // the only case is currentTerm == term
+ currentTerm = term;
+ }
+
+ fun termsOfLog(): seq[int] {
+ var terms: seq[int];
+ var i: int;
+ terms = default(seq[int]);
+ i = 0;
+ while (i < sizeof(logs)) {
+ terms += (sizeof(terms), logs[i].term);
+ i = i + 1;
+ }
+ return terms;
+ }
+
+ fun reset() {
+ // crash a node, reset volatile states
+ // and become a follower (just as it starts up)
+ commitIndex = -1;
+ lastApplied = -1;
+ nextIndex = fillMap(this, nextIndex, peers, 0);
+ matchIndex = fillMap(this, matchIndex, peers, -1);
+ goto Follower;
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Timer.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Timer.p
new file mode 100644
index 0000000000..abaec9f1bd
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Timer.p
@@ -0,0 +1,57 @@
+/******************************************************************
+* A Timer machine that timeouts non-deterministically.
+*******************************************************************/
+
+event eStartTimer;
+event eElectionTimeout;
+event eHeartbeatTimeout;
+event eCancelTimer;
+event eTick;
+
+machine Timer {
+ var holder: machine;
+ var timeoutEvent: event;
+
+ start state Init {
+ entry (setup: (user: machine, timeoutEvent: event)) {
+ holder = setup.user;
+ timeoutEvent = setup.timeoutEvent;
+ goto TimerIdle;
+ }
+ ignore eCancelTimer, eTick, eStartTimer;
+ }
+
+ state TimerIdle {
+ on eStartTimer do {
+ goto TimerTick;
+ }
+ on eShutdown goto TimerShutdown;
+ ignore eCancelTimer, eTick;
+ }
+
+ state TimerTick {
+ entry {
+ checkTick();
+ }
+ on eTick do {
+ checkTick();
+ }
+ on eShutdown goto TimerShutdown;
+ on eCancelTimer goto TimerIdle;
+ ignore eStartTimer;
+ }
+
+ state TimerShutdown {
+ ignore eStartTimer, eCancelTimer, eTick;
+ }
+
+ fun checkTick() {
+ if ($) {
+ // non-deterministic timeout
+ send holder, timeoutEvent;
+ goto TimerIdle;
+ } else {
+ send this, eTick;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Utils.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Utils.p
new file mode 100644
index 0000000000..3f281f91be
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/Utils.p
@@ -0,0 +1,70 @@
+// Some utility functions
+fun startTimer(timer: Timer) {
+ send timer, eStartTimer;
+}
+
+fun cancelTimer(timer: Timer) {
+ send timer, eCancelTimer;
+}
+
+fun restartTimer(timer: Timer) {
+ cancelTimer(timer);
+ startTimer(timer);
+}
+
+fun lastLogIndex(log: seq[tServerLog]): LogIndex {
+ return sizeof(log) - 1;
+}
+
+fun getLogTerm(log: seq[tServerLog], index: int): int {
+ // get the term of the log entry at index.
+ if (index < 0 || index >= sizeof(log)) {
+ return 0;
+ } else {
+ return log[index].term;
+ }
+}
+
+fun lastLogTerm(log: seq[tServerLog]): int {
+ if (sizeof(log) == 0) {
+ return 0;
+ } else {
+ return log[sizeof(log) - 1].term;
+ }
+}
+
+fun Max(a: int, b: int): int {
+ if (a > b) {
+ return a;
+ } else {
+ return b;
+ }
+}
+
+fun Min(a: int, b: int): int {
+ if (a < b) {
+ return a;
+ } else {
+ return b;
+ }
+}
+
+fun fillMap(self: machine, m: map[Server, int], servers: set[Server], value: int): map[Server, int] {
+ var server: Server;
+ foreach(server in servers) {
+ if (server != self) {
+ m[server] = value;
+ }
+ }
+ return m;
+}
+
+fun broadcastRequest(self: Server, peers: set[Server], e: event, payload: any) {
+ // broadcast a message to nodes except for itself.
+ var peer: Server;
+ foreach(peer in peers) {
+ if (peer != self) {
+ send peer, e, payload;
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/View.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/View.p
new file mode 100644
index 0000000000..baf0e10de5
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PSrc/View.p
@@ -0,0 +1,241 @@
+/******************************************************************
+* A View service that manages the view of the cluster.
+* It is responsible for maintaining roles of, injecting failures to
+* and shutting down the nodes.
+*******************************************************************/
+
+// Shutdown message will be sent to the cluster when
+// client notifies the view service that it has finished all requests AND got all responses
+event eShutdown;
+// A node can notify the view service about its log entires
+// This is used for choosing servers that should receive an election timeout
+type tTS = int;
+event eNotifyLog: (timestamp: tTS, server: Server, log: seq[tServerLog]);
+
+machine View {
+ // the cluster nodes
+ var servers: set[Server];
+ // the probability that a node got an election timeout [0, 100]
+ // this is used to model network failure (i.e. a follower does not receive heartbeat)
+ var timeoutRate: int;
+ // the probability that a node crashes [0, 100]
+ var crashRate: int;
+ // the total number failures that can be injected
+ var numFailures: int;
+ // the timer for the view service to perform an action
+ var triggerTimer: Timer;
+ // the set of clients that have finished all requests
+ var clientsDone: set[machine];
+ // the number of clients
+ var numClients: int;
+
+ // the set of followers, leaders and candidates
+ var followers: set[Server];
+ var leaders: set[Server];
+ var candidates: set[Server];
+ // the set of servers that have not sent a `eRequestVote` message
+ // this set will be exahusted when the view service
+ // cannot detect any leader in the cluster
+ var requestVotePendingSet: set[Server];
+ // the logs of nodes
+ var serverLogs: map[Server, seq[tServerLog]];
+ // the last seen log of each node; prevent update the entry using older logs
+ var lastSeenLogs: map[Server, int];
+ var candidateRoundMap: map[Server, int];
+ // number of rounds of actions where the view service cannot detect any leader
+ var noLeaderRounds: int;
+
+ start state Init {
+ entry (setup: (numServers: int, numClients: int, timeoutRate: int, crashRate: int, numFailures: int)) {
+ var i: int;
+ var server: Server;
+ timeoutRate = setup.timeoutRate;
+ numClients = setup.numClients;
+ crashRate = setup.crashRate;
+ numFailures = setup.numFailures;
+ followers = default(set[Server]);
+ // at a moment, there can be multiple leaders, e.g. partitioned network
+ leaders = default(set[Server]);
+ candidates = default(set[Server]);
+ requestVotePendingSet = default(set[Server]);
+ serverLogs = default(map[Server, seq[tServerLog]]);
+ lastSeenLogs = default(map[Server, int]);
+ candidateRoundMap = default(map[Server, int]);
+ noLeaderRounds = 0;
+
+ while (i < setup.numServers) {
+ server = new Server();
+ servers += (server);
+ serverLogs += (server, default(seq[tServerLog]));
+ i = i + 1;
+ }
+ i = 0;
+ foreach (server in servers) {
+ send server, eServerInit, (myId=i, cluster=servers, viewServer=this);
+ followers += (server);
+ i = i + 1;
+ }
+ triggerTimer = new Timer((user=this, timeoutEvent=eHeartbeatTimeout));
+ clientsDone = default(set[machine]);
+ i = 0;
+ while (i < numClients) {
+ new Client((viewService=this, servers=servers, requests=randomWorkload(3)));
+ i = i + 1;
+ }
+ goto Monitoring;
+ }
+ }
+
+ state Monitoring {
+ entry {
+ var server: Server;
+ startTimer(triggerTimer);
+ server = choose(servers);
+ candidates += (server);
+ send server, eElectionTimeout;
+ }
+
+ on eNotifyLog do (payload: (timestamp:int, server: Server, log: seq[tServerLog])) {
+ // only track the most up-to-date logs
+ if (!(payload.server in keys(lastSeenLogs)) || lastSeenLogs[payload.server] < payload.timestamp) {
+ lastSeenLogs[payload.server] = payload.timestamp;
+ serverLogs[payload.server] = payload.log;
+ }
+ }
+
+ on eViewChangedLeader do (server: Server) {
+ // server change its role to a leader
+ noLeaderRounds = 0;
+ leaders += (server);
+ followers -= (server);
+ candidates -= (server);
+ }
+
+ on eViewChangedFollower do (server: Server) {
+ // server change its role to a follower
+ followers += (server);
+ candidates -= (server);
+ leaders -= (server);
+ }
+
+ on eViewChangedCandidate do (server: Server) {
+ // server change its role to a candidate
+ candidates += (server);
+ followers -= (server);
+ leaders -= (server);
+ }
+
+ on eHeartbeatTimeout do {
+ var server: Server;
+ // print format("Current view: leaders={0} followers={1} candidates={2}", leaders, followers, candidates);
+ if (sizeof(leaders) == 0) {
+ if (noLeaderRounds % 25 == 0 && sizeof(requestVotePendingSet) > 0) {
+ // non-deterministically choose a server to trigger an election
+ // either the current most up-to-date server or a random server
+ if ($) {
+ server = mostUpToDateServer(requestVotePendingSet);
+ } else {
+ server = choose(requestVotePendingSet);
+ }
+ requestVotePendingSet -= (server);
+ // print format("NoLeader rounds exceeded, trigger election on {0}", server);
+ send server, eElectionTimeout;
+ candidates += (server);
+ followers -= (server);
+ noLeaderRounds = 0;
+ }
+ if (sizeof(requestVotePendingSet) == 0) {
+ requestVotePendingSet = servers;
+ }
+ noLeaderRounds = noLeaderRounds + 1;
+ } else {
+ foreach (server in servers) {
+ send server, eHeartbeatTimeout;
+ }
+ foreach (server in followers) {
+ if (choose(100) < timeoutRate && numFailures > 0) {
+ // print format("Failure Injection: timeout a follower {0}", server);
+ send server, eElectionTimeout;
+ numFailures = numFailures - 1;
+ startTimer(triggerTimer);
+ return;
+ }
+ }
+ foreach (server in leaders) {
+ if (choose(100) < crashRate && numFailures > 0) {
+ // print format("Failure Injection: crash a leader {0}", server);
+ send server, eReset;
+ numFailures = numFailures - 1;
+ startTimer(triggerTimer);
+ return;
+ }
+ }
+ foreach (server in candidates) {
+ if (choose(100) < crashRate && numFailures > 0) {
+ // print format("Failure Injection: crash a candidate {0}", server);
+ send server, eReset;
+ numFailures = numFailures - 1;
+ startTimer(triggerTimer);
+ return;
+ }
+ }
+ }
+ startTimer(triggerTimer);
+ }
+
+ on eClientFinished do (client: machine) {
+ var i: int;
+ clientsDone += (client);
+ if (sizeof(clientsDone) == numClients) {
+ i = 0;
+ while (i < sizeof(servers)) {
+ send servers[i], eShutdown;
+ i = i + 1;
+ }
+ goto ViewServiceEnd;
+ }
+ }
+ }
+
+ state ViewServiceEnd {
+ entry {
+ send triggerTimer, eShutdown;
+ }
+
+ ignore eClientFinished, eHeartbeatTimeout, eViewChangedCandidate, eNotifyLog, eViewChangedLeader, eViewChangedFollower;
+ }
+
+ fun mostUpToDateServer(choices: set[Server]): Server {
+ // get the server with the most up-to-date logs (in the view of the view service)
+ // it might not be the real most up-to-date server in the cluster because of the message delay
+ var server: Server;
+ var candidate: Server;
+ var term: int;
+ var length: int;
+ term = 0;
+ length = 0;
+ candidate = default(Server);
+ // print format("Choose candidate: server |-> logs: {0}", serverLogs);
+ foreach (server in servers) {
+ if (candidate == default(Server)) {
+ candidate = server;
+ term = lastLogTerm(serverLogs[server]);
+ length = sizeof(serverLogs[server]);
+ } else {
+ if (sizeof(serverLogs[server]) > 0) {
+ if (term < lastLogTerm(serverLogs[server])) {
+ term = lastLogTerm(serverLogs[server]);
+ length = sizeof(serverLogs[server]);
+ candidate = server;
+ } else if (term == lastLogTerm(serverLogs[server])) {
+ if (length < sizeof(serverLogs[server])) {
+ length = sizeof(serverLogs[server]);
+ candidate = server;
+ }
+ }
+ }
+ }
+ }
+ return candidate;
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PTst/RaftTests.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PTst/RaftTests.p
new file mode 100644
index 0000000000..aa944156ea
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PTst/RaftTests.p
@@ -0,0 +1,109 @@
+event eRaftConfig: (quorumSize: int);
+
+fun setUpCluster(numServers: int, numClients: int, timeoutRate: int, crashRate: int, numFailures: int): View {
+ announce eRaftConfig, (quorumSize=numServers / 2 + 1,);
+ return new View((numServers=numServers, numClients=numClients,
+ timeoutRate=timeoutRate, crashRate=crashRate, numFailures=numFailures));
+}
+
+fun randomWorkload(numCmd: int): seq[Command] {
+ var cmds: seq[Command];
+ var puts: set[int];
+ var i: int;
+ var key: int;
+ assert numCmd <= 100, "Too many commands!";
+ cmds = default(seq[Command]);
+ puts = default(set[int]);
+ i = 0;
+ while (i < numCmd) {
+ // choose an existing key or a new key
+ // non-deterministically
+ if (sizeof(puts) > 0 && $) {
+ key = choose(puts);
+ } else {
+ key = choose(10);
+ puts += (key);
+ }
+ if (sizeof(puts) == 0 || $) {
+ // PUT
+ cmds += (i, (op=PUT, key=key, value=choose(1024)));
+ } else {
+ // GET
+ cmds += (i, (op=GET, key=key, value=-1));
+ }
+ i = i + 1;
+ }
+ return cmds;
+}
+
+machine OneClientOneServerReliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(1, 1, 0, 0, 0);
+ }
+ }
+}
+
+machine OneClientFiveServersReliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(5, 1, 0, 0, 0);
+ }
+ }
+}
+
+machine OneClientFiveServersUnreliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(5, 1, 5, 5, 100);
+ }
+ }
+}
+
+machine OneClientThreeServersReliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(3, 1, 0, 0, 0);
+ }
+ }
+}
+
+machine OneClientThreeServersUnreliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(3, 1, 5, 5, 100);
+ }
+ }
+}
+
+machine TwoClientsThreeServersReliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(3, 2, 0, 0, 0);
+ }
+ }
+}
+
+machine TwoClientsThreeServersUnreliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(3, 2, 5, 5, 100);
+ }
+ }
+}
+
+machine ThreeClientsOneServerReliable {
+ start state Init {
+ entry {
+ var view: View;
+ view = setUpCluster(1, 3, 0, 0, 0);
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PTst/Testscript.p b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PTst/Testscript.p
new file mode 100644
index 0000000000..d623630699
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_Raft/PTst/Testscript.p
@@ -0,0 +1,40 @@
+module ServingTests = { OneClientOneServerReliable,
+ OneClientThreeServersReliable,
+ OneClientThreeServersUnreliable,
+ OneClientFiveServersReliable,
+ OneClientFiveServersUnreliable,
+ TwoClientsThreeServersReliable,
+ TwoClientsThreeServersUnreliable,
+ ThreeClientsOneServerReliable};
+
+test oneClientOneServerReliable [main=OneClientOneServerReliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
+
+test oneClientThreeServersReliable [main=OneClientThreeServersReliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
+
+test oneClientThreeServersUnreliable [main=OneClientThreeServersUnreliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
+
+test oneClientFiveServersReliable [main=OneClientFiveServersReliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
+
+test oneClientFiveServersUnreliable [main=OneClientFiveServersUnreliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
+
+test threeClientsOneServerReliable [main=ThreeClientsOneServerReliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
+
+test twoClientsThreeServersReliable [main=TwoClientsThreeServersReliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
+
+test twoClientsThreeServersUnreliable [main=TwoClientsThreeServersUnreliable]:
+ assert SafetyOneLeader, SafetyLeaderCompleteness, SafetyStateMachine, SafetyLogMatching, LivenessClientsDone, LivenessProgress, SafetySynchronization in
+ (union Server, Timer, Client, View, ServingTests);
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSpec/Atomicity.p b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSpec/Atomicity.p
new file mode 100644
index 0000000000..89098dafc3
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSpec/Atomicity.p
@@ -0,0 +1,83 @@
+// event: initialize the AtomicityInvariant spec monitor
+event eMonitor_AtomicityInitialize: int;
+
+/**********************************
+We would like to assert the atomicity property that:
+if a transaction is committed by the coordinator then it was agreed on by all participants
+***********************************/
+spec AtomicityInvariant observes eWriteTransResp, ePrepareResp, eMonitor_AtomicityInitialize
+{
+ // a map from transaction id to a map from responses status to number of participants with that response
+ var participantsResponse: map[int, map[tTransStatus, int]];
+ var numParticipants: int;
+ start state Init {
+ on eMonitor_AtomicityInitialize goto WaitForEvents with (n: int) {
+ numParticipants = n;
+ }
+ }
+
+ state WaitForEvents {
+ on ePrepareResp do (resp: tPrepareResp){
+ var transId: int;
+ transId = resp.transId;
+
+ if(!(transId in participantsResponse))
+ {
+ participantsResponse[transId] = default(map[tTransStatus, int]);
+ participantsResponse[transId][SUCCESS] = 0;
+ participantsResponse[transId][ERROR] = 0;
+ }
+ participantsResponse[transId][resp.status] = participantsResponse[transId][resp.status] + 1;
+ }
+
+ on eWriteTransResp do (resp: tWriteTransResp) {
+ assert (resp.transId in participantsResponse || resp.status == TIMEOUT),
+ format ("Write transaction was responded to the client without receiving any responses from the participants!");
+
+ if(resp.status == SUCCESS)
+ {
+ assert participantsResponse[resp.transId][SUCCESS] == numParticipants,
+ format ("Write transaction was responded as committed before receiving success from all participants. ") +
+ format ("participants sent success: {0}, participants sent error: {1}", participantsResponse[resp.transId][SUCCESS],
+ participantsResponse[resp.transId][ERROR]);
+ }
+ else if(resp.status == ERROR)
+ {
+ assert participantsResponse[resp.transId][ERROR] > 0,
+ format ("Write transaction {0} was responded as failed before receiving error from atleast one participant.", resp.transId) +
+ format ("participants sent success: {0}, participants sent error: {1}", participantsResponse[resp.transId][SUCCESS],
+ participantsResponse[resp.transId][ERROR]);
+ }
+ // remove the transaction information
+ participantsResponse -= (resp.transId);
+ }
+ }
+}
+
+/**************************************************************************
+Every received transaction from a client must be eventually responded back.
+Note, the usage of hot and cold states.
+***************************************************************************/
+spec Progress observes eWriteTransReq, eWriteTransResp {
+ var pendingTransactions: int;
+ start state Init {
+ on eWriteTransReq goto WaitForResponses with { pendingTransactions = pendingTransactions + 1; }
+ }
+
+ hot state WaitForResponses
+ {
+ on eWriteTransResp do {
+ pendingTransactions = pendingTransactions - 1;
+ if(pendingTransactions == 0)
+ {
+ goto AllTransactionsFinished;
+ }
+ }
+
+ on eWriteTransReq do { pendingTransactions = pendingTransactions + 1; }
+ }
+
+ cold state AllTransactionsFinished {
+ on eWriteTransReq goto WaitForResponses with { pendingTransactions = pendingTransactions + 1; }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/Coordinator.p b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/Coordinator.p
new file mode 100644
index 0000000000..3201e3bf05
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/Coordinator.p
@@ -0,0 +1,178 @@
+/* User Defined Types */
+
+// a transaction consisting of the key, value, and the unique transaction id.
+type tTrans = (key: string, val: int, transId: int);
+// payload type associated with the `eWriteTransReq` event where `client`: client sending the
+// transaction, `trans`: transaction to be committed.
+type tWriteTransReq = (client: Client, trans: tTrans);
+// payload type associated with the `eWriteTransResp` event where `transId` is the transaction Id
+// and `status` is the return status of the transaction request.
+type tWriteTransResp = (transId: int, status: tTransStatus);
+// payload type associated with the `eReadTransReq` event where `client` is the Client machine sending
+// the read request and `key` is the key whose value the client wants to read.
+type tReadTransReq = (client: Client, key: string);
+// payload type associated with the `eReadTransResp` event where `val` is the value corresponding to
+// the `key` in the read request and `status` is the read status (e.g., success or failure)
+type tReadTransResp = (key: string, val: int, status: tTransStatus);
+
+// transaction status
+enum tTransStatus {
+ SUCCESS,
+ ERROR,
+ TIMEOUT
+}
+
+/* Events used by the 2PC clients to communicate with the 2PC coordinator */
+// event: write transaction request (client to coordinator)
+event eWriteTransReq : tWriteTransReq;
+// event: write transaction response (coordinator to client)
+event eWriteTransResp : tWriteTransResp;
+// event: read transaction request (client to coordinator)
+event eReadTransReq : tReadTransReq;
+// event: read transaction response (participant to client)
+event eReadTransResp: tReadTransResp;
+
+/* Events used for communication between the coordinator and the participants */
+// event: prepare request for a transaction (coordinator to participant)
+event ePrepareReq: tPrepareReq;
+// event: prepare response for a transaction (participant to coodinator)
+event ePrepareResp: tPrepareResp;
+// event: commit transaction (coordinator to participant)
+event eCommitTrans: int;
+// event: abort transaction (coordinator to participant)
+event eAbortTrans: int;
+
+/* User Defined Types */
+// payload type associated with the `ePrepareReq` event
+type tPrepareReq = tTrans;
+// payload type assocated with the `ePrepareResp` event where `participant` is the participant machine
+// sending the response, `transId` is the transaction id, and `status` is the status of the prepare
+// request for that transaction.
+type tPrepareResp = (participant: Participant, transId: int, status: tTransStatus);
+
+// event: inform participant about the coordinator
+event eInformCoordinator: Coordinator;
+
+/*****************************************************************************************
+The Coordinator machine receives write and read transactions from the clients. The coordinator machine
+services these transactions one by one in the order in which they were received. On receiving a write
+transaction the coordinator sends prepare request to all the participants and waits for prepare
+responses from all the participants. Based on the responses, the coordinator either commits or aborts
+the transaction. If the coordinator fails to receive agreement from participants in time, then it
+timesout and aborts the transaction. On receiving a read transaction, the coordinator randomly selects
+a participant and forwards the read request to that participant.
+******************************************************************************************/
+machine Coordinator
+{
+ // set of participants
+ var participants: set[Participant];
+ // current write transaction being handled
+ var currentWriteTransReq: tWriteTransReq;
+ // previously seen transaction ids
+ var seenTransIds: set[int];
+ var timer: Timer;
+
+ start state Init {
+ entry (payload: set[Participant]){
+ participants = payload;
+ timer = CreateTimer(this);
+ // inform all participants that I am the coordinator
+ BroadcastToAllParticipants(eInformCoordinator, this);
+ goto WaitForTransactions;
+ }
+ }
+
+ state WaitForTransactions {
+ on eWriteTransReq do (wTrans : tWriteTransReq) {
+ if(wTrans.trans.transId in seenTransIds) // transId have to be unique
+ {
+ send wTrans.client, eWriteTransResp, (transId = wTrans.trans.transId, status = TIMEOUT);
+ return;
+ }
+
+ currentWriteTransReq = wTrans;
+ BroadcastToAllParticipants(ePrepareReq, wTrans.trans);
+ //start timer while waiting for responses from all participants
+ StartTimer(timer);
+ goto WaitForPrepareResponses;
+ }
+
+ on eReadTransReq do (rTrans : tReadTransReq) {
+ // non-deterministically pick a participant to read from.
+ send choose(participants), eReadTransReq, rTrans;
+ }
+
+ // when in this state it is fine to drop these messages as they are from the previous transaction
+ ignore ePrepareResp, eTimeOut;
+ }
+
+ var countPrepareResponses: int;
+
+ state WaitForPrepareResponses {
+ // defer requests, we are going to process transactions sequentially
+ defer eWriteTransReq;
+
+ on ePrepareResp do (resp : tPrepareResp) {
+ // check if the response is for the current transaction else ignore it
+ if (currentWriteTransReq.trans.transId == resp.transId) {
+ if(resp.status == SUCCESS)
+ {
+ countPrepareResponses = countPrepareResponses + 1;
+ // check if we have received all responses
+ if(countPrepareResponses == sizeof(participants))
+ {
+ // lets commit the transaction
+ DoGlobalCommit();
+ // safe to go back and service the next transaction
+ goto WaitForTransactions;
+ }
+ }
+ else
+ {
+ DoGlobalAbort(ERROR);
+ // safe to go back and service the next transaction
+ goto WaitForTransactions;
+ }
+ }
+ }
+
+ // on timeout abort the transaction
+ on eTimeOut goto WaitForTransactions with { DoGlobalAbort(TIMEOUT); }
+
+ on eReadTransReq do (rTrans : tReadTransReq) {
+ // non-deterministically pick a participant to read from.
+ send choose(participants), eReadTransReq, rTrans;
+ }
+
+ exit {
+ countPrepareResponses = 0;
+ }
+ }
+
+ fun DoGlobalAbort(respStatus: tTransStatus) {
+ // ask all participants to abort and fail the transaction
+ BroadcastToAllParticipants(eAbortTrans, currentWriteTransReq.trans.transId);
+ send currentWriteTransReq.client, eWriteTransResp, (transId = currentWriteTransReq.trans.transId, status = respStatus);
+ if(respStatus != TIMEOUT)
+ CancelTimer(timer);
+ }
+
+ fun DoGlobalCommit() {
+ // ask all participants to commit and respond to client
+ BroadcastToAllParticipants(eCommitTrans, currentWriteTransReq.trans.transId);
+ send currentWriteTransReq.client, eWriteTransResp,
+ (transId = currentWriteTransReq.trans.transId, status = SUCCESS);
+ CancelTimer(timer);
+ }
+
+ //function to broadcast messages to all participants
+ fun BroadcastToAllParticipants(message: event, payload: any)
+ {
+ var i: int;
+ while (i < sizeof(participants)) {
+ send participants[i], message, payload;
+ i = i + 1;
+ }
+ }
+}
+
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/Participant.p b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/Participant.p
new file mode 100644
index 0000000000..60bc64d074
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/Participant.p
@@ -0,0 +1,77 @@
+/*****************************************************************************************
+Each participant maintains a local key-value store which is updated based on the
+transactions committed by the coordinator. On receiving a prepare request
+from the coordinator, the participant chooses to either accept or
+reject the transaction.
+******************************************************************************************/
+
+machine Participant {
+ // local key value store
+ var kvStore: map[string, tTrans];
+ // pending write transactions that have not been committed or aborted yet.
+ var pendingWriteTrans: map[int, tTrans];
+ // coordinator machine
+ var coordinator: Coordinator;
+
+ start state Init {
+ on eInformCoordinator goto WaitForRequests with (coor: Coordinator) {
+ coordinator = coor;
+ }
+ defer eShutDown;
+ }
+
+ state WaitForRequests {
+ on eAbortTrans do (transId: int) {
+ // check that abort transaction request is received for a pending transaction only.
+ assert transId in pendingWriteTrans,
+ format ("Abort request for a non-pending transaction, transId: {0}, pendingTrans set: {1}",
+ transId, pendingWriteTrans);
+ // remove the transaction from the pending transactions set
+ pendingWriteTrans -= (transId);
+ }
+
+ on eCommitTrans do (transId:int) {
+ // check that commit transaction request is received for a pending transactions only.
+ assert transId in pendingWriteTrans,
+ format ("Commit request for a non-pending transaction, transId: {0}, pendingTrans set: {1}",
+ transId, pendingWriteTrans);
+ // commit the transaction locally
+ kvStore[pendingWriteTrans[transId].key] = pendingWriteTrans[transId];
+ // remove the transaction from the pending transactions set
+ pendingWriteTrans -= (transId);
+ }
+
+ on ePrepareReq do (prepareReq :tPrepareReq) {
+ // cannot receive prepare for an already pending transaction
+ assert !(prepareReq.transId in pendingWriteTrans),
+ format ("Duplicate transaction ids not allowed!, received transId: {0}, pending transactions: {1}",
+ prepareReq.transId, pendingWriteTrans);
+ // add the transaction to the pending transactions set
+ pendingWriteTrans[prepareReq.transId] = prepareReq;
+ // non-deterministically pick whether to accept or reject the transaction
+ if (!(prepareReq.key in kvStore) || (prepareReq.key in kvStore && prepareReq.transId > kvStore[prepareReq.key].transId)) {
+ send coordinator, ePrepareResp, (participant = this, transId = prepareReq.transId, status = SUCCESS);
+ } else {
+ send coordinator, ePrepareResp, (participant = this, transId = prepareReq.transId, status = ERROR);
+ }
+ }
+
+ on eReadTransReq do (req: tReadTransReq) {
+ if(req.key in kvStore)
+ {
+ // read successful as the key exists
+ send req.client, eReadTransResp, (key = req.key, val = kvStore[req.key].val, status = SUCCESS);
+ }
+ else
+ {
+ // read failed as the key does not exist
+ send req.client, eReadTransResp, (key = "", val = -1, status = ERROR);
+ }
+ }
+
+ on eShutDown do {
+ raise halt;
+ }
+ }
+}
+
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/TwoPhaseCommitModules.p b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/TwoPhaseCommitModules.p
new file mode 100644
index 0000000000..c6c5b5e9ab
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PSrc/TwoPhaseCommitModules.p
@@ -0,0 +1,2 @@
+// the two phase commit module
+module TwoPhaseCommit = union { Coordinator, Participant }, Timer;
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/Client.p b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/Client.p
new file mode 100644
index 0000000000..1cb7548dc0
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/Client.p
@@ -0,0 +1,64 @@
+/*****************************************************************************************
+The client machine below implements the client of the two-phase-commit transaction service.
+Each client issues N non-deterministic write-transactions,
+if the transaction succeeds then it performs a read-transaction on the same key and asserts the value.
+******************************************************************************************/
+machine Client {
+ // the coordinator machine
+ var coordinator: Coordinator;
+ // current transaction issued by the client
+ var currTransaction : tTrans;
+ // number of transactions to be issued
+ var N: int;
+ // uniqie client Id
+ var id: int;
+
+ start state Init {
+ entry (payload : (coordinator: Coordinator, n : int, id: int)) {
+ coordinator = payload.coordinator;
+ N = payload.n;
+ id = payload.id;
+ goto SendWriteTransaction;
+ }
+ }
+
+ state SendWriteTransaction {
+ entry {
+ currTransaction = ChooseRandomTransaction(id * 100 + N /* hack for creating unique transaction id*/);
+ send coordinator, eWriteTransReq, (client = this, trans = currTransaction);
+ }
+ on eWriteTransResp goto ConfirmTransaction;
+ }
+
+ state ConfirmTransaction {
+ entry (writeResp: tWriteTransResp) {
+ // assert that if write transaction was successful then value read is the value written.
+ if(writeResp.status == SUCCESS)
+ {
+ send coordinator, eReadTransReq, (client= this, key = currTransaction.key);
+ // await response from the participant
+ receive {
+ case eReadTransResp: (readResp: tReadTransResp) {
+ assert readResp.key == currTransaction.key && readResp.val == currTransaction.val,
+ format ("Record read is not same as what was written by the client:: read - {0}, written - {1}",
+ readResp.val, currTransaction.val);
+ }
+ }
+ }
+ // has more work to do?
+ if(N > 0)
+ {
+ N = N - 1;
+ goto SendWriteTransaction;
+ }
+ }
+ }
+}
+
+fun ChooseRandomTransaction(uniqueId: int): tTrans {
+ return (key = format("{0}", choose(10)), val = choose(10), transId = uniqueId);
+}
+
+
+// two phase commit client module
+module TwoPCClient = { Client };
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/TestDriver.p b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/TestDriver.p
new file mode 100644
index 0000000000..4433b81f5b
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/TestDriver.p
@@ -0,0 +1,126 @@
+// type that represents the configuration of the system under test
+type t2PCConfig = (
+ numClients: int,
+ numParticipants: int,
+ numTransPerClient: int,
+ failParticipants: int
+);
+
+// function that creates the two phase commit system along with the machines in its
+// environment (test harness)
+fun SetUpTwoPhaseCommitSystem(config: t2PCConfig)
+{
+ var coordinator : Coordinator;
+ var participants: set[Participant];
+ var i : int;
+
+ // create participants
+ while (i < config.numParticipants) {
+ participants += (new Participant());
+ i = i + 1;
+ }
+
+ // initialize the monitors (specifications)
+ InitializeTwoPhaseCommitSpecifications(config.numParticipants);
+
+ // create the coordinator
+ coordinator = new Coordinator(participants);
+
+ // create the clients
+ i = 0;
+ while(i < config.numClients)
+ {
+ new Client((coordinator = coordinator, n = config.numTransPerClient, id = i + 1));
+ i = i + 1;
+ }
+
+ // create the failure injector if we want to inject failures
+ if(config.failParticipants > 0)
+ {
+ CreateFailureInjector((nodes = participants, nFailures = config.failParticipants));
+ }
+}
+
+fun InitializeTwoPhaseCommitSpecifications(numParticipants: int) {
+ // inform the monitor the number of participants in the system
+ announce eMonitor_AtomicityInitialize, numParticipants;
+}
+
+/*
+This machine creates the 3 participants, 1 coordinator, and 1 clients
+*/
+machine SingleClientNoFailure {
+ start state Init {
+ entry {
+ var config: t2PCConfig;
+
+ config = (numClients = 1,
+ numParticipants = 3,
+ numTransPerClient = 2,
+ failParticipants = 0);
+
+ SetUpTwoPhaseCommitSystem(config);
+ }
+ }
+}
+
+/*
+This machine creates the 3 participants, 1 coordinator, and 2 clients
+*/
+machine MultipleClientsNoFailure {
+ start state Init {
+ entry {
+ var config: t2PCConfig;
+ config =
+ (numClients = 2,
+ numParticipants = 3,
+ numTransPerClient = 2,
+ failParticipants = 0);
+
+ SetUpTwoPhaseCommitSystem(config);
+ }
+ }
+}
+
+/*
+This machine creates the 3 participants, 1 coordinator, 1 Failure injector, and 2 clients
+*/
+machine MultipleClientsWithFailure {
+ start state Init {
+ entry {
+ var config: t2PCConfig;
+ config =
+ (numClients = 2,
+ numParticipants = 3,
+ numTransPerClient = 2,
+ failParticipants = 1);
+
+ SetUpTwoPhaseCommitSystem(config);
+ }
+ }
+}
+
+// Parameters for system configuration
+param pNumClients: int;
+param pNumParticipants: int;
+param pNumTransPerClient: int;
+param pFailParticipants: int;
+/*
+This machine allows parameterized testing with configurable number of clients, participants, and failures
+*/
+
+machine TestWithConfig {
+ start state Init {
+ entry {
+ var config: t2PCConfig;
+ config = (
+ numClients = pNumClients,
+ numParticipants = pNumParticipants,
+ numTransPerClient = pNumTransPerClient,
+ failParticipants = pFailParticipants
+ );
+
+ SetUpTwoPhaseCommitSystem(config);
+ }
+ }
+}
diff --git a/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/TestScripts.p b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/TestScripts.p
new file mode 100644
index 0000000000..5e4bdb246a
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/Tutorial_TwoPhaseCommit/PTst/TestScripts.p
@@ -0,0 +1,20 @@
+// checks that all events are handled correctly and also the local assertions in the P machines.
+test tcSingleClientNoFailure [main = SingleClientNoFailure]:
+ union TwoPhaseCommit, TwoPCClient, FailureInjector, { SingleClientNoFailure };
+
+// asserts the liveness monitor along with the default properties
+test tcMultipleClientsNoFailure [main = MultipleClientsNoFailure]:
+ assert AtomicityInvariant, Progress in
+ (union TwoPhaseCommit, TwoPCClient, FailureInjector, { MultipleClientsNoFailure });
+
+// asserts the liveness monitor along with the default properties
+test tcMultipleClientsWithFailure [main = MultipleClientsWithFailure]:
+ assert Progress in (union TwoPhaseCommit, TwoPCClient, FailureInjector, { MultipleClientsWithFailure });
+
+// parametric testing of all parameters
+test param (pNumClients in [2, 3], pNumParticipants in [3, 4, 5],
+ pNumTransPerClient in [1, 2], pFailParticipants in [0, 1])
+ assume (pNumParticipants > pNumClients && pFailParticipants < pNumParticipants/2)
+ (2 wise) tcParametricTests [main=TestWithConfig]:
+ assert AtomicityInvariant, Progress in
+ (union TwoPhaseCommit, TwoPCClient, FailureInjector, { TestWithConfig });
diff --git a/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSpec/Safety.p b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSpec/Safety.p
new file mode 100644
index 0000000000..920611179b
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSpec/Safety.p
@@ -0,0 +1,101 @@
+// Safety Specification: Atomicity
+// If a transaction is committed by the coordinator, then it was agreed on by all participants.
+// If the transaction is aborted, then at least one participant must have rejected the transaction.
+
+spec Atomicity observes ePrepareResp, eCommitTrans, eAbortTrans {
+ var currentTransaction: int;
+ var prepareResponses: map[int, map[machine, tTransStatus]];
+ var transactionDecision: map[int, tTransStatus];
+
+ start state WaitingForEvents {
+ on ePrepareResp do HandlePrepareResp;
+ on eCommitTrans do HandleCommit;
+ on eAbortTrans do HandleAbort;
+ }
+
+ fun HandlePrepareResp(resp: tPrepareResp) {
+ var participantResponses: map[machine, tTransStatus];
+
+ if (resp.transId in prepareResponses) {
+ participantResponses = prepareResponses[resp.transId];
+ participantResponses[resp.participant] = resp.status;
+ prepareResponses[resp.transId] = participantResponses;
+ } else {
+ participantResponses[resp.participant] = resp.status;
+ prepareResponses[resp.transId] = participantResponses;
+ }
+ }
+
+ fun HandleCommit(msg: tCommitTrans) {
+ var participantResponses: map[machine, tTransStatus];
+ var participant: machine;
+ var allSuccess: bool;
+
+ assert msg.transId in prepareResponses,
+ format("Commit decision for transaction {0} without any prepare responses", msg.transId);
+
+ participantResponses = prepareResponses[msg.transId];
+ allSuccess = true;
+
+ foreach (participant in keys(participantResponses)) {
+ if (participantResponses[participant] != SUCCESS) {
+ allSuccess = false;
+ }
+ }
+
+ assert allSuccess,
+ format("Transaction {0} committed but not all participants agreed", msg.transId);
+
+ transactionDecision[msg.transId] = SUCCESS;
+ }
+
+ fun HandleAbort(msg: tAbortTrans) {
+ var participantResponses: map[machine, tTransStatus];
+ var participant: machine;
+ var someRejected: bool;
+
+ if (msg.transId in prepareResponses) {
+ participantResponses = prepareResponses[msg.transId];
+ someRejected = false;
+
+ foreach (participant in keys(participantResponses)) {
+ if (participantResponses[participant] != SUCCESS) {
+ someRejected = true;
+ }
+ }
+ }
+
+ transactionDecision[msg.transId] = ERROR;
+ }
+}
+
+// Liveness Specification: Progress
+// Every transaction request from a client must be eventually responded to.
+
+spec Progress observes eWriteTransReq, eWriteTransResp {
+ var pendingTransactions: set[int];
+
+ start state NoPendingTransactions {
+ on eWriteTransReq goto PendingTransactions with HandleWriteRequest;
+ }
+
+ hot state PendingTransactions {
+ on eWriteTransReq goto PendingTransactions with HandleWriteRequest;
+ on eWriteTransResp do HandleWriteResponse;
+ }
+
+ fun HandleWriteRequest(req: tWriteTransReq) {
+ pendingTransactions += (req.trans.transId);
+ }
+
+ fun HandleWriteResponse(resp: tWriteTransResp) {
+ assert resp.transId in pendingTransactions,
+ format("Response for transaction {0} which was not requested", resp.transId);
+
+ pendingTransactions -= (resp.transId);
+
+ if (sizeof(pendingTransactions) == 0) {
+ goto NoPendingTransactions;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Client.p b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Client.p
new file mode 100644
index 0000000000..a6589dc256
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Client.p
@@ -0,0 +1,88 @@
+// Client Machine
+machine Client {
+ var coordinator: machine;
+ var numTransactions: int;
+ var transactionsSent: int;
+ var currentTransId: int;
+ var currentKey: string;
+ var currentValue: int;
+ var pendingWrite: bool;
+
+ start state Init {
+ entry InitializeClient;
+ }
+
+ state SendWriteTransaction {
+ entry SendWriteTransactionEntry;
+ on eWriteTransResp do HandleWriteTransResp;
+ }
+
+ state SendReadTransaction {
+ entry SendReadTransactionEntry;
+ on eReadTransResp do HandleReadTransResp;
+ }
+
+ state Done {
+ entry DoneEntry;
+ }
+
+ fun InitializeClient(payload: (coord: machine, numTrans: int)) {
+ coordinator = payload.coord;
+ numTransactions = payload.numTrans;
+ transactionsSent = 0;
+ goto SendWriteTransaction;
+ }
+
+ fun SendWriteTransactionEntry() {
+ var trans: tWriteTransaction;
+
+ if (transactionsSent >= numTransactions) {
+ goto Done;
+ }
+
+ currentTransId = transactionsSent;
+ currentKey = format("key{0}", currentTransId);
+ currentValue = currentTransId * 100;
+
+ trans = (key = currentKey, value = currentValue, transId = currentTransId);
+
+ send coordinator, eWriteTransReq, (client = this, trans = trans);
+
+ pendingWrite = true;
+ transactionsSent = transactionsSent + 1;
+ }
+
+ fun HandleWriteTransResp(resp: tWriteTransResp) {
+ assert resp.transId == currentTransId,
+ format("Response transaction ID {0} does not match current transaction ID {1}",
+ resp.transId, currentTransId);
+
+ pendingWrite = false;
+
+ if (resp.status == SUCCESS) {
+ goto SendReadTransaction;
+ } else {
+ goto SendWriteTransaction;
+ }
+ }
+
+ fun SendReadTransactionEntry() {
+ send coordinator, eReadTransReq, (client = this, key = currentKey);
+ }
+
+ fun HandleReadTransResp(resp: tReadTransResp) {
+ assert resp.key == currentKey,
+ format("Response key {0} does not match current key {1}", resp.key, currentKey);
+
+ if (resp.status == READ_SUCCESS) {
+ assert resp.value == currentValue,
+ format("Response value {0} does not match current value {1}", resp.value, currentValue);
+ }
+
+ goto SendWriteTransaction;
+ }
+
+ fun DoneEntry() {
+ print format("Client completed all {0} transactions", numTransactions);
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Coordinator.p b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Coordinator.p
new file mode 100644
index 0000000000..d70ef8f2f0
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Coordinator.p
@@ -0,0 +1,166 @@
+machine Coordinator {
+ var participants: seq[machine];
+ var numParticipants: int;
+ var currentTransId: int;
+ var currentKey: string;
+ var currentValue: int;
+ var currentClient: machine;
+ var prepareResponses: int;
+ var prepareSuccess: bool;
+ var timer: machine;
+ var pendingWriteRequests: seq[tWriteTransReq];
+ var usedTransIds: set[int];
+
+ start state Init {
+ entry InitEntry;
+ on eInformCoordinator goto WaitForRequests;
+ }
+
+ state WaitForRequests {
+ on eWriteTransReq do HandleWriteTransReq;
+ on eReadTransReq do HandleReadTransReq;
+ }
+
+ state ProcessingWriteTransaction {
+ entry ProcessingWriteTransactionEntry;
+ on ePrepareResp do HandlePrepareResp;
+ on eTimeOut do HandleTimeout;
+ defer eWriteTransReq, eReadTransReq;
+ }
+
+ state CommitTransaction {
+ entry CommitTransactionEntry;
+ defer eWriteTransReq, eReadTransReq;
+ ignore ePrepareResp, eTimeOut;
+ }
+
+ state AbortTransaction {
+ entry AbortTransactionEntry;
+ defer eWriteTransReq, eReadTransReq;
+ ignore ePrepareResp, eTimeOut;
+ }
+
+ fun InitEntry(payload: (parts: seq[machine], timerMachine: machine)) {
+ var i: int;
+
+ numParticipants = sizeof(payload.parts);
+ i = 0;
+ while (i < numParticipants) {
+ participants += (i, payload.parts[i]);
+ i = i + 1;
+ }
+
+ timer = payload.timerMachine;
+ currentTransId = -1;
+ prepareResponses = 0;
+ prepareSuccess = true;
+ }
+
+ fun HandleWriteTransReq(req: tWriteTransReq) {
+ var i: int;
+ var prepareReq: tPrepareReq;
+
+ if (req.trans.transId in usedTransIds) {
+ send req.client, eWriteTransResp, (transId = req.trans.transId, status = TIMEOUT);
+ } else {
+ currentTransId = req.trans.transId;
+ currentKey = req.trans.key;
+ currentValue = req.trans.value;
+ currentClient = req.client;
+
+ i = 0;
+ while (i < numParticipants) {
+ prepareReq = (key = currentKey, value = currentValue, transId = currentTransId);
+ send participants[i], ePrepareReq, prepareReq;
+ i = i + 1;
+ }
+
+ send timer, eStartTimer;
+ goto ProcessingWriteTransaction;
+ }
+ }
+
+ fun HandleReadTransReq(req: tReadTransReq) {
+ var selectedParticipant: machine;
+ var participantIndex: int;
+
+ participantIndex = choose(numParticipants);
+ selectedParticipant = participants[participantIndex];
+ send selectedParticipant, eReadTransReq, (client = req.client, key = req.key);
+ }
+
+ fun ProcessingWriteTransactionEntry() {
+ prepareResponses = 0;
+ prepareSuccess = true;
+ }
+
+ fun HandlePrepareResp(resp: tPrepareResp) {
+ if (resp.transId == currentTransId) {
+ prepareResponses = prepareResponses + 1;
+
+ if (resp.status != SUCCESS) {
+ prepareSuccess = false;
+ }
+
+ if (prepareResponses == numParticipants) {
+ if (prepareSuccess) {
+ goto CommitTransaction;
+ } else {
+ goto AbortTransaction;
+ }
+ }
+ }
+ }
+
+ fun HandleTimeout() {
+ send timer, eCancelTimer;
+ goto AbortTransaction;
+ }
+
+ fun CommitTransactionEntry() {
+ var i: int;
+ var commitMsg: tCommitTrans;
+
+ send timer, eCancelTimer;
+
+ commitMsg = (transId = currentTransId,);
+ i = 0;
+ while (i < numParticipants) {
+ send participants[i], eCommitTrans, commitMsg;
+ i = i + 1;
+ }
+
+ usedTransIds += (currentTransId);
+ send currentClient, eWriteTransResp, (transId = currentTransId, status = SUCCESS);
+
+ if (sizeof(pendingWriteRequests) > 0) {
+ HandleWriteTransReq(pendingWriteRequests[0]);
+ pendingWriteRequests -= (0);
+ } else {
+ goto WaitForRequests;
+ }
+ }
+
+ fun AbortTransactionEntry() {
+ var i: int;
+ var abortMsg: tAbortTrans;
+
+ send timer, eCancelTimer;
+
+ abortMsg = (transId = currentTransId,);
+ i = 0;
+ while (i < numParticipants) {
+ send participants[i], eAbortTrans, abortMsg;
+ i = i + 1;
+ }
+
+ send currentClient, eWriteTransResp, (transId = currentTransId, status = ERROR);
+
+ if (sizeof(pendingWriteRequests) > 0) {
+ HandleWriteTransReq(pendingWriteRequests[0]);
+ pendingWriteRequests -= (0);
+ } else {
+ goto WaitForRequests;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Enums_Types_Events.p b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Enums_Types_Events.p
new file mode 100644
index 0000000000..a49f8a9ae5
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Enums_Types_Events.p
@@ -0,0 +1,37 @@
+// Enums
+enum tTransStatus { SUCCESS, ERROR, TIMEOUT }
+enum tReadStatus { READ_SUCCESS, READ_ERROR }
+
+// Types
+type tWriteTransaction = (key: string, value: int, transId: int);
+type tReadTransaction = (key: string);
+
+type tWriteTransReq = (client: machine, trans: tWriteTransaction);
+type tWriteTransResp = (transId: int, status: tTransStatus);
+
+type tReadTransReq = (client: machine, key: string);
+type tReadTransResp = (key: string, value: int, status: tReadStatus);
+
+type tPrepareReq = (key: string, value: int, transId: int);
+type tPrepareResp = (participant: machine, transId: int, status: tTransStatus);
+
+type tCommitTrans = (transId: int);
+type tAbortTrans = (transId: int);
+
+type tInformCoordinator = (coordinator: machine);
+
+// Events
+event eWriteTransReq: tWriteTransReq;
+event eWriteTransResp: tWriteTransResp;
+event eReadTransReq: tReadTransReq;
+event eReadTransResp: tReadTransResp;
+event ePrepareReq: tPrepareReq;
+event ePrepareResp: tPrepareResp;
+event eCommitTrans: tCommitTrans;
+event eAbortTrans: tAbortTrans;
+event eInformCoordinator: tInformCoordinator;
+
+// Timer events
+event eStartTimer;
+event eCancelTimer;
+event eTimeOut;
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Participant.p b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Participant.p
new file mode 100644
index 0000000000..04fdec8026
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Participant.p
@@ -0,0 +1,73 @@
+machine Participant {
+ var coordinator: machine;
+ var dataStore: map[string, int];
+ var pendingTransactions: map[int, (key: string, value: int)];
+
+ start state Init {
+ on eInformCoordinator do HandleInformCoordinator;
+ }
+
+ state WaitForRequests {
+ on ePrepareReq do HandlePrepareReq;
+ on eCommitTrans do HandleCommitTrans;
+ on eAbortTrans do HandleAbortTrans;
+ on eReadTransReq do HandleReadTransReq;
+ }
+
+ fun HandleInformCoordinator(payload: tInformCoordinator) {
+ coordinator = payload.coordinator;
+ goto WaitForRequests;
+ }
+
+ fun HandlePrepareReq(req: tPrepareReq) {
+ var status: tTransStatus;
+ var resp: tPrepareResp;
+
+ if (!(req.transId in pendingTransactions)) {
+ pendingTransactions[req.transId] = (key = req.key, value = req.value);
+ }
+
+ if (!(req.key in dataStore)) {
+ status = SUCCESS;
+ } else {
+ if (req.transId > dataStore[req.key]) {
+ status = SUCCESS;
+ } else {
+ status = ERROR;
+ }
+ }
+
+ resp = (participant = this, transId = req.transId, status = status);
+ send coordinator, ePrepareResp, resp;
+ }
+
+ fun HandleCommitTrans(msg: tCommitTrans) {
+ var trans: (key: string, value: int);
+
+ assert msg.transId in pendingTransactions,
+ format("Commit request received for non-pending transaction ID {0}", msg.transId);
+
+ trans = pendingTransactions[msg.transId];
+ dataStore[trans.key] = trans.value;
+ pendingTransactions -= (msg.transId);
+ }
+
+ fun HandleAbortTrans(msg: tAbortTrans) {
+ assert msg.transId in pendingTransactions,
+ format("Abort request received for non-pending transaction ID {0}", msg.transId);
+
+ pendingTransactions -= (msg.transId);
+ }
+
+ fun HandleReadTransReq(req: tReadTransReq) {
+ var readResp: tReadTransResp;
+
+ if (req.key in dataStore) {
+ readResp = (key = req.key, value = dataStore[req.key], status = READ_SUCCESS);
+ send req.client, eReadTransResp, readResp;
+ } else {
+ readResp = (key = req.key, value = 0, status = READ_ERROR);
+ send req.client, eReadTransResp, readResp;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Timer.p b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Timer.p
new file mode 100644
index 0000000000..77e351b3c6
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PSrc/Timer.p
@@ -0,0 +1,38 @@
+machine Timer {
+ var client: machine;
+ var timerActive: bool;
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ state WaitForTimerRequests {
+ on eStartTimer goto TimerStarted;
+ ignore eCancelTimer, eTimeOut;
+ }
+
+ state TimerStarted {
+ entry TimerStartedEntry;
+ on eTimeOut goto TimerStarted;
+ on eCancelTimer goto WaitForTimerRequests;
+ defer eStartTimer;
+ }
+
+ fun InitEntry(payload: machine) {
+ client = payload;
+ goto WaitForTimerRequests;
+ }
+
+ fun TimerStartedEntry() {
+ var shouldFire: bool;
+
+ shouldFire = choose();
+
+ if (shouldFire) {
+ send client, eTimeOut;
+ goto WaitForTimerRequests;
+ } else {
+ send this, eTimeOut;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PTst/TestDriver.p b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PTst/TestDriver.p
new file mode 100644
index 0000000000..10bbbe4783
--- /dev/null
+++ b/Src/PeasyAI/resources/rag_examples/TwoPhaseCommit/PTst/TestDriver.p
@@ -0,0 +1,99 @@
+machine TestWithSingleClient {
+ var coordinator: machine;
+ var participants: seq[machine];
+ var timer: machine;
+ var client: machine;
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ fun InitEntry() {
+ var i: int;
+ var participantsList: seq[machine];
+
+ // Create timer first
+ timer = new Timer(default(machine));
+
+ // Create 3 participants
+ i = 0;
+ while (i < 3) {
+ participants += (i, new Participant());
+ i = i + 1;
+ }
+
+ // Prepare participants list for coordinator
+ i = 0;
+ while (i < 3) {
+ participantsList += (i, participants[i]);
+ i = i + 1;
+ }
+
+ // Create coordinator
+ coordinator = new Coordinator((parts = participantsList, timerMachine = timer));
+
+ // Update timer with coordinator as client
+ send timer, eInformCoordinator, (coordinator = coordinator,);
+
+ // Inform all participants about coordinator
+ i = 0;
+ while (i < 3) {
+ send participants[i], eInformCoordinator, (coordinator = coordinator,);
+ i = i + 1;
+ }
+
+ // Create and start client with 5 transactions
+ client = new Client((coord = coordinator, numTrans = 5));
+ }
+}
+
+machine TestWithMultipleClients {
+ var coordinator: machine;
+ var participants: seq[machine];
+ var timer: machine;
+ var client1: machine;
+ var client2: machine;
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ fun InitEntry() {
+ var i: int;
+ var participantsList: seq[machine];
+
+ // Create timer first
+ timer = new Timer(default(machine));
+
+ // Create 3 participants
+ i = 0;
+ while (i < 3) {
+ participants += (i, new Participant());
+ i = i + 1;
+ }
+
+ // Prepare participants list for coordinator
+ i = 0;
+ while (i < 3) {
+ participantsList += (i, participants[i]);
+ i = i + 1;
+ }
+
+ // Create coordinator
+ coordinator = new Coordinator((parts = participantsList, timerMachine = timer));
+
+ // Update timer with coordinator as client
+ send timer, eInformCoordinator, (coordinator = coordinator,);
+
+ // Inform all participants about coordinator
+ i = 0;
+ while (i < 3) {
+ send participants[i], eInformCoordinator, (coordinator = coordinator,);
+ i = i + 1;
+ }
+
+ // Create and start two clients with 3 transactions each
+ client1 = new Client((coord = coordinator, numTrans = 3));
+ client2 = new Client((coord = coordinator, numTrans = 3));
+ }
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/resources/system_design_docs/DESIGN_DOC_TEMPLATE.md b/Src/PeasyAI/resources/system_design_docs/DESIGN_DOC_TEMPLATE.md
new file mode 100644
index 0000000000..350e3eae9a
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/DESIGN_DOC_TEMPLATE.md
@@ -0,0 +1,93 @@
+# [System Name]
+
+## Introduction
+
+[One paragraph overview: what the system does, what problem it solves, and the core protocol or algorithm it implements.]
+
+**Assumptions:**
+1. [Assumption about communication model, e.g., reliable message delivery, no Byzantine faults]
+2. [Assumption about failure model, e.g., crash-stop failures, no network partitions]
+3. [Assumption about concurrency, e.g., requests are serialized, at most N concurrent clients]
+4. [Assumption about reusable modules, e.g., "The Timer machine is a pre-existing reusable module — do NOT re-implement it. Use CreateTimer(this), StartTimer(timer), and CancelTimer(timer)."]
+
+## Components
+
+
+
+### Source Components
+
+#### 1. [MachineName]
+- **Role:** [One-sentence role description.]
+- **States:** [List known states, e.g., Init, WaitingForPrepare, Committed, Aborted]
+- **Local state:**
+ - `[variableName]`: [what it tracks]
+ - `[variableName]`: [what it tracks]
+- **Initialization:** [Describe in plain English what information this machine needs to start and how it receives it, e.g., "Created with a reference to all acceptors, all learners, and a unique proposer ID." or "Waits for a configuration event from the coordinator before becoming operational."]
+- **Behavior:**
+ - [Key behavior point, e.g., "Sends prepare requests to all participants and waits for responses."]
+ - [Key behavior point, e.g., "Times out and aborts if responses are not received in time."]
+- **Event handling notes:**
+ - In [StateName]: ignore `[eStaleEvent1]`, `[eStaleEvent2]`
+ - In [StateName]: defer `[eDeferredEvent1]`, `[eDeferredEvent2]`
+
+#### 2. [MachineName]
+- ...
+
+### Test Components
+
+#### 3. [ClientOrDriverName]
+- **Role:** [One-sentence role description.]
+- **States:** [List known states]
+- **Local state:**
+ - `[variableName]`: [what it tracks]
+- **Initialization:** [Describe what information this machine needs to start, e.g., "Created with a reference to the server and the number of requests to send."]
+- **Behavior:**
+ - [Key behavior point, e.g., "Issues N write transactions with random key/value using choose()."]
+ - [Key behavior point, e.g., "On success, performs a read and asserts the value matches."]
+
+## Interactions
+
+1. **[eEventName]**
+ - **Source:** [MachineName]
+ - **Target:** [MachineName(s)]
+ - **Payload:** [Describe the data carried by this event in plain English, e.g., "the proposer's reference, the proposal number, and the proposed value"]
+ - **Description:** [What this event represents and when it is sent.]
+ - **Effects:**
+ - [Effect on the receiver, including state transitions.]
+ - [Any conditional behavior, e.g., "If the proposal number is higher than any previously seen..."]
+
+2. **[eEventName]**
+ - **Source:** [MachineName]
+ - **Target:** [MachineName(s)]
+ - **Payload:** none
+ - **Description:** [What this event represents.]
+ - **Effects:**
+ - [Effect on the receiver.]
+
+## Specifications
+
+
+
+1. **[PropertyName]** (safety property):
+ [Precise statement that naturally references the relevant events. E.g., "Whenever an eAccepted is sent for a value, no future eAccepted may carry a different value — only one value can ever be chosen."]
+
+2. **[PropertyName]** (liveness property):
+ [Precise statement that naturally references the relevant events. E.g., "If a majority of participants are alive, an eLearn event carrying the consensus value is eventually delivered."]
+
+## Test Scenarios
+
+
+
+1. [N] [machine_type_1], [M] [machine_type_2], [K] [machine_type_3] — [description of what is being tested and expected outcome].
+2. [N] [machine_type_1], [M] [machine_type_2] — [description of failure scenario, e.g., "one proposer fails mid-protocol, remaining machines still reach consensus"].
+3. [Description of edge case or stress scenario].
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Basic Paxos Protocol.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Basic Paxos Protocol.md
new file mode 100644
index 0000000000..f254e44e90
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Basic Paxos Protocol.md
@@ -0,0 +1,123 @@
+# Basic Paxos Protocol
+
+## Introduction
+
+The goal of this system is to model a basic version of the Paxos consensus algorithm. Paxos is designed to achieve consensus among a group of participants (referred to as proposers, acceptors, and learners) in a distributed system, even in the presence of failures. The protocol ensures that multiple participants agree on a single value, which is crucial for consistency in distributed systems.
+
+**Assumptions:**
+1. Our system models a basic version of Paxos, focusing on single-value consensus without optimizations like multi-Paxos.
+2. The system assumes reliable delivery of messages, though participants may fail or recover.
+3. Our model does not consider network partitions or Byzantine failures.
+
+## Components
+
+### Source Components
+
+#### 1. Proposer
+- **Role:** Initiates the proposal of values to reach consensus.
+- **States:** Init, ProposalPhase, AcceptPhase
+- **Local state:**
+ - `acceptors`: list of acceptor machines
+ - `learners`: list of learner machines
+ - `proposalNumber`: current proposal number
+ - `proposedValue`: the value being proposed
+ - `promiseCount`: number of promises received
+ - `majoritySize`: number needed for a majority
+- **Initialization:** Created with references to all acceptors, all learners, and a unique proposer ID.
+- **Behavior:**
+ - Generates proposals with unique proposal numbers.
+ - Sends proposals to a majority of acceptors and waits for responses.
+ - If a majority of promises are received, sends accept requests.
+
+#### 2. Acceptor
+- **Role:** Participates in voting on proposals.
+- **States:** Init, Ready
+- **Local state:**
+ - `highestProposalSeen`: the highest proposal number it has seen
+ - `acceptedProposal`: the proposal number of the accepted proposal
+ - `acceptedValue`: the value associated with the highest accepted proposal
+ - `learners`: list of learner machines
+- **Initialization:** Created with references to all learner machines.
+- **Behavior:**
+ - Responds to proposals with either a promise not to accept lower-numbered proposals or an acceptance of the proposal.
+ - Maintains the highest proposal number it has seen and the value associated with the highest accepted proposal.
+
+#### 3. Learner
+- **Role:** Learns the chosen value once consensus is reached.
+- **States:** Init, Learning
+- **Local state:**
+ - `acceptedValues`: maps proposal numbers to accepted values
+ - `acceptCount`: counts of acceptances per proposal
+ - `majoritySize`: number needed for a majority
+ - `learnedValue`: the final agreed-upon value
+- **Initialization:** Created with the majority size (number of acceptors needed for consensus).
+- **Behavior:**
+ - Collects accepted proposals from acceptors.
+ - Learns the final agreed-upon value once a majority of acceptors have accepted the same proposal.
+
+### Test Components
+
+#### 4. Client
+- **Role:** Acts as a client of the Paxos protocol, initiating requests for consensus on values.
+- **Initialization:** Created with a reference to the proposer and the value to propose for consensus.
+- **Behavior:**
+ - Interacts with the Proposer to start the consensus process.
+
+## Interactions
+
+1. **ePropose**
+ - **Source:** Proposer
+ - **Target:** Acceptors
+ - **Payload:** the proposer's reference, the proposal number, and the proposed value
+ - **Description:** Proposer sends a proposal to acceptors for consideration.
+ - **Effects:**
+ - Acceptor checks if the proposal number is higher than any previously seen proposal number.
+ - If higher, the acceptor promises not to accept any lower-numbered proposals and may accept the proposal.
+
+2. **ePromise**
+ - **Source:** Acceptor
+ - **Target:** Proposer
+ - **Payload:** the proposer's reference, the highest proposal number seen, and any previously accepted value
+ - **Description:** Acceptor sends a promise to the proposer, agreeing not to accept proposals with lower numbers.
+ - **Effects:**
+ - Proposer may proceed with the proposal if a majority of acceptors send promises.
+
+3. **eAcceptRequest**
+ - **Source:** Proposer
+ - **Target:** Acceptors
+ - **Payload:** the proposer's reference, the proposal number, and the proposed value
+ - **Description:** Proposer requests acceptors to accept the proposal.
+ - **Effects:**
+ - Acceptor accepts the proposal if it has not already promised a higher-numbered proposal.
+ - Acceptor sends an acceptance notification to the proposer and learners.
+
+4. **eAccepted**
+ - **Source:** Acceptor
+ - **Target:** Learners
+ - **Payload:** the learner's reference, the proposal number, and the accepted value
+ - **Description:** Acceptor notifies learners that it has accepted a proposal.
+ - **Effects:**
+ - Learners collect accepted proposals.
+ - Once a majority of acceptors have accepted the same proposal, learners conclude that consensus has been reached.
+
+5. **eLearn**
+ - **Source:** Learner
+ - **Target:** All components
+ - **Payload:** the final agreed-upon value
+ - **Description:** Learner announces the final value agreed upon by the majority of acceptors.
+ - **Effects:**
+ - All components update their state to reflect the chosen value.
+
+## Specifications
+
+1. **OnlyOneValueChosen** (safety property):
+ Once an eAccepted carries a value and an eLearn announces it, no subsequent eAccepted or eLearn may carry a different value — only one value can ever be chosen.
+
+2. **EventualConsensus** (liveness property):
+ If a majority of participants are functioning correctly, the sequence ePropose → ePromise → eAccepted → eLearn eventually completes, reaching consensus.
+
+## Test Scenarios
+
+1. 3 acceptors, 1 proposer, 1 learner, and no failures — basic consensus reached successfully.
+2. 5 acceptors, 2 proposers, and 1 learner with one proposer failing — remaining proposer reaches consensus.
+3. Network partition scenarios with the recovery of proposers and acceptors.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Client Server.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Client Server.md
new file mode 100644
index 0000000000..a1d00ba01d
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Client Server.md
@@ -0,0 +1,71 @@
+# Client Server
+
+## Introduction
+
+The goal of this system is to model a simple client-server interaction where clients send requests to a server and the server processes those requests and sends back responses. This is the most basic distributed system pattern.
+
+**Assumptions:**
+1. The server processes requests one at a time in the order they are received.
+2. Communication between clients and server is reliable.
+3. The server does not fail.
+4. Each client sends a fixed number of requests.
+
+## Components
+
+### Source Components
+
+#### 1. Server
+- **Role:** Receives requests from clients, processes them, and sends responses.
+- **States:** Init, Ready
+- **Local state:**
+ - `requestCount`: number of requests processed
+- **Initialization:** No external configuration needed.
+- **Behavior:**
+ - Receives requests from clients.
+ - Processes each request and sends a response back to the requesting client.
+ - Keeps count of the number of requests processed.
+
+### Test Components
+
+#### 2. Client
+- **Role:** Sends requests to the server and tracks responses.
+- **States:** Init, Sending, Done
+- **Local state:**
+ - `server`: reference to the server
+ - `numRequests`: number of requests to send
+ - `requestsSent`: count of requests sent so far
+ - `responsesReceived`: count of responses received
+- **Initialization:** Created with a reference to the server and the number of requests to send.
+- **Behavior:**
+ - Sends requests to the server with a unique request identifier.
+ - Waits for the server's response before sending the next request.
+ - Keeps track of responses received.
+
+## Interactions
+
+1. **eRequest**
+ - **Source:** Client
+ - **Target:** Server
+ - **Payload:** the requesting client's reference and a unique request ID
+ - **Description:** Client sends a request to the server.
+ - **Effects:**
+ - Server processes the request and prepares a response.
+
+2. **eResponse**
+ - **Source:** Server
+ - **Target:** Client
+ - **Payload:** the request ID and whether the request succeeded
+ - **Description:** Server sends a response back to the client after processing the request.
+ - **Effects:**
+ - Client receives the response and may send the next request.
+
+## Specifications
+
+1. **EveryRequestGetsResponse** (safety property):
+ Every eRequest sent by a client must eventually be matched by exactly one eResponse from the server with the same requestId. The server must not drop or duplicate eResponse messages.
+
+## Test Scenarios
+
+1. 1 server, 1 client (3 requests) — a single client sends multiple requests to the server sequentially.
+2. 1 server, 3 clients (2 requests each) — multiple clients send requests to the same server concurrently.
+3. 1 server, 3 clients (different request counts: 1, 3, 5) — clients with different request counts interact with the server.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Distributed Lock Server.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Distributed Lock Server.md
new file mode 100644
index 0000000000..52fdd8f20d
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Distributed Lock Server.md
@@ -0,0 +1,87 @@
+# Distributed Lock Server
+
+## Introduction
+
+The goal of this system is to model a distributed lock server, which is used to manage access to shared resources in a distributed environment. The lock server ensures that only one client can hold a lock on a resource at any given time, preventing conflicts and ensuring consistency.
+
+**Assumptions:**
+1. The system allows multiple clients to request and release locks concurrently.
+2. The lock server is assumed to be reliable; it does not fail, and it always responds to client requests.
+3. The system does not support lock server replication; there is only a single lock server managing all locks.
+4. The system models reliable communication between clients and the lock server.
+
+## Components
+
+### Source Components
+
+#### 1. LockServer
+- **Role:** Manages locks on resources, granting and releasing them in response to client requests.
+- **States:** Init, Ready
+- **Local state:**
+ - `lockTable`: maps resource ID to the client currently holding the lock
+ - `waitQueues`: maps resource ID to a queue of clients waiting for the lock
+- **Initialization:** No external configuration needed.
+- **Behavior:**
+ - Processes lock requests and release requests from clients in the order they are received.
+ - Grants a lock if it is available, otherwise queues the request.
+ - Releases a lock when requested by the client holding it, and if there are pending requests for the same lock, grants the lock to the next client in the queue.
+
+### Test Components
+
+#### 2. Client
+- **Role:** Requests and releases locks on specific resources.
+- **Local state:**
+ - `lockServer`: reference to the lock server
+ - `clientId`: unique client identifier
+ - `resourceId`: the resource this client wants to lock
+- **Initialization:** Created with a reference to the lock server, a unique client ID, and the resource ID it wants to lock.
+- **Behavior:**
+ - Sends lock requests to the Lock Server for a specific resource.
+ - After acquiring a lock, performs operations on the resource.
+ - Sends a release request to the Lock Server after the operations are complete.
+
+## Interactions
+
+1. **eLockRequest**
+ - **Source:** Client
+ - **Target:** LockServer
+ - **Payload:** the client's reference, client ID, and resource ID
+ - **Description:** Client requests a lock on a specific resource.
+ - **Effects:**
+ - If the lock is available, grant it to the client.
+ - If the lock is held by another client, queue the request.
+
+2. **eLockResponse**
+ - **Source:** LockServer
+ - **Target:** Client
+ - **Payload:** the client ID, resource ID, and lock status (granted or queued)
+ - **Description:** Lock server responds to the client's lock request.
+ - **Effects:**
+ - Client either proceeds with operations on the resource if the lock is granted or waits if queued.
+
+3. **eReleaseRequest**
+ - **Source:** Client
+ - **Target:** LockServer
+ - **Payload:** the client's reference, client ID, and resource ID
+ - **Description:** Client sends a request to release the lock on a specific resource.
+ - **Effects:**
+ - Lock Server releases the lock and grants it to the next client in the queue if any.
+
+4. **eReleaseResponse**
+ - **Source:** LockServer
+ - **Target:** Client
+ - **Payload:** the client ID, resource ID, and release status
+ - **Description:** Lock Server confirms the release of the lock.
+
+## Specifications
+
+1. **MutualExclusion** (safety property):
+ Between an eLockResponse granting a lock on a resource and the corresponding eReleaseRequest from that client, no other eLockResponse may grant the same resource to a different client.
+
+2. **DeadlockFreedom** (liveness property):
+ Every eLockRequest must eventually result in an eLockResponse granting the lock, provided clients eventually send eReleaseRequest for resources they hold.
+
+## Test Scenarios
+
+1. 1 lock server, 3 clients, 1 resource — multiple clients request the same lock concurrently, and the lock server handles the contention.
+2. 1 lock server, 3 clients, 3 resources — clients request and release locks on different resources, with no contention.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Espresso Machine.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Espresso Machine.md
new file mode 100644
index 0000000000..5bf8822f86
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Espresso Machine.md
@@ -0,0 +1,111 @@
+# Espresso Machine
+
+## Introduction
+
+The goal of this system is to model a coffee/espresso machine with a control panel interface. The espresso machine manages the brewing process through a series of states (idle, grinding, brewing, ready), while the control panel allows users to interact with it. This is a non-distributed state machine example focused on sequential state transitions and hardware-like behavior.
+
+**Assumptions:**
+1. The espresso machine operates sequentially — it can only make one coffee at a time.
+2. The machine must complete the current brewing process before accepting new requests.
+3. The grinding and brewing steps each take a fixed time simulated by a timer.
+4. The machine has a water tank that must be refilled when empty.
+5. The Timer machine is a pre-existing reusable module — do NOT re-implement it. Use CreateTimer(this), StartTimer(timer), and CancelTimer(timer).
+
+## Components
+
+### Source Components
+
+#### 1. CoffeeMaker
+- **Role:** The core espresso machine that manages the brewing process.
+- **States:** Idle, Grinding, Brewing, CoffeeReady, Error
+- **Local state:**
+ - `waterLevel`: current water level in the tank
+ - `timer`: timer for grinding and brewing durations
+ - `controlPanel`: reference to the control panel
+- **Initialization:** Created with a reference to the control panel.
+- **Behavior:**
+ - Receives commands from the CoffeeMakerControlPanel.
+ - Uses a timer to simulate grinding and brewing durations.
+ - Tracks water level and reports errors when water is depleted.
+- **Event handling notes:**
+ - In Grinding: ignore `eMakeCoffee`
+ - In Brewing: ignore `eMakeCoffee`
+
+#### 2. CoffeeMakerControlPanel
+- **Role:** Interface between users and the CoffeeMaker.
+- **States:** Init, Ready
+- **Local state:**
+ - `coffeeMaker`: reference to the coffee maker
+- **Initialization:** No external configuration needed; creates the CoffeeMaker internally.
+- **Behavior:**
+ - Forwards user requests (make coffee, refill water) to the CoffeeMaker.
+ - Receives status updates from the CoffeeMaker and reports them to the user.
+
+### Test Components
+
+#### 3. User
+- **Role:** Simulates a user interacting with the control panel.
+- **Initialization:** Created with a reference to the control panel.
+- **Behavior:**
+ - Sends make coffee and refill water requests to the control panel.
+
+## Interactions
+
+1. **eMakeCoffee**
+ - **Source:** CoffeeMakerControlPanel
+ - **Target:** CoffeeMaker
+ - **Payload:** none
+ - **Description:** Request to start making a coffee.
+ - **Effects:**
+ - If idle and water available, transitions to Grinding state and starts timer.
+ - If busy or no water, sends error response.
+
+2. **eCoffeeReady**
+ - **Source:** CoffeeMaker
+ - **Target:** CoffeeMakerControlPanel
+ - **Payload:** none
+ - **Description:** Coffee has been brewed and is ready for pickup.
+
+3. **eError**
+ - **Source:** CoffeeMaker
+ - **Target:** CoffeeMakerControlPanel
+ - **Payload:** a message describing the error
+ - **Description:** An error occurred (e.g., no water, machine busy).
+
+4. **eRefillWater**
+ - **Source:** CoffeeMakerControlPanel
+ - **Target:** CoffeeMaker
+ - **Payload:** none
+ - **Description:** Request to refill the water tank.
+ - **Effects:**
+ - Machine resets its water level to full.
+
+5. **eGrindingDone**
+ - **Source:** Timer (internal)
+ - **Target:** CoffeeMaker
+ - **Payload:** none
+ - **Description:** Grinding phase timer expired, coffee is ground.
+ - **Effects:**
+ - Machine transitions from Grinding to Brewing state.
+
+6. **eBrewingDone**
+ - **Source:** Timer (internal)
+ - **Target:** CoffeeMaker
+ - **Payload:** none
+ - **Description:** Brewing phase timer expired, coffee is ready.
+ - **Effects:**
+ - Machine transitions from Brewing to CoffeeReady state.
+
+## Specifications
+
+1. **NoCoffeeWhileBusy** (safety property):
+ Between an eMakeCoffee that starts grinding and the eventual eCoffeeReady, no second eMakeCoffee may be accepted. The eGrindingDone and eBrewingDone transitions must complete in order before the machine returns to idle.
+
+2. **WaterLevelTracking** (safety property):
+ An eMakeCoffee must never succeed when the water level is zero. Only after an eRefillWater restores the tank should a subsequent eMakeCoffee be allowed to proceed to brewing.
+
+## Test Scenarios
+
+1. 1 user, 1 control panel, 1 coffee maker — a single user makes one coffee successfully.
+2. 1 user, 1 control panel, 1 coffee maker — user tries to make coffee when the water tank is empty, refills, then makes coffee.
+3. 2 users, 1 control panel, 1 coffee maker — two users try to make coffee in sequence, the second waits for the first to complete.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Failure Detector.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Failure Detector.md
new file mode 100644
index 0000000000..bc926fb239
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Failure Detector.md
@@ -0,0 +1,144 @@
+# Failure Detector
+
+## Introduction
+
+The goal of this system is to model an eventually perfect failure detector using heartbeat-based monitoring. Nodes periodically send heartbeats to a failure detector, which monitors node liveness and reports suspected failures to clients.
+
+**Assumptions:**
+1. Nodes can fail by stopping (crash failures).
+2. The failure detector uses periodic heartbeat timeouts to detect failures.
+3. The failure detector may temporarily suspect a live node (unreliable detection) but will eventually be accurate for permanently failed nodes.
+4. Communication is reliable between live nodes.
+5. A Timer machine models non-deterministic OS timer behavior. It provides helper functions: CreateTimer(client), StartTimer(timer), CancelTimer(timer). The Timer sends eTimeOut to its client when it fires.
+
+## Components
+
+### Source Components
+
+#### 1. FailureDetector
+- **Role:** Monitors a set of nodes by expecting periodic heartbeats and reports suspected failures.
+- **States:** Init, Monitoring
+- **Local state:**
+ - `nodes`: the set of nodes being monitored
+ - `aliveNodes`: nodes that have sent heartbeats since last check
+ - `suspectedNodes`: nodes currently suspected of failure
+ - `clients`: registered clients to notify
+ - `timer`: timer for periodic liveness checks
+- **Initialization:** Created with references to all the nodes it will monitor.
+- **Behavior:**
+ - Uses a timer to trigger periodic liveness checks.
+ - If a suspected node later sends a heartbeat, it is restored to alive status.
+ - Reports suspected failures to registered clients.
+
+#### 2. Node
+- **Role:** Periodically sends heartbeats to the failure detector.
+- **States:** Init, Alive, Crashed
+- **Local state:**
+ - `failureDetector`: reference to the failure detector
+ - `timer`: timer for heartbeat interval
+- **Initialization:** Created with a reference to the failure detector.
+- **Behavior:**
+ - Periodically sends heartbeats to the failure detector.
+ - Can be instructed to crash (stop sending heartbeats) to simulate failure.
+
+#### 3. Timer
+- **Role:** Models non-deterministic OS timer behavior for periodic timeouts.
+- **States:** Init, WaitForTimerRequests, TimerStarted
+- **Local state:**
+ - `client`: the machine that created this timer (receives eTimeOut)
+ - `numDelays`: counter tracking how many times the timer has delayed (bounded)
+- **Initialization:** Created with a reference to the client machine via `CreateTimer(client)`.
+- **Behavior:**
+ - Waits for eStartTimer, then non-deterministically fires eTimeOut to the client.
+ - The timer bounds the number of delays (e.g., max 3) so it is guaranteed to eventually fire. This is required for liveness properties. After `numDelays >= 3`, the timer must fire unconditionally.
+ - Supports eCancelTimer to cancel a pending timeout.
+ - Uses eDelayedTimeOut internally to model non-deterministic delay.
+- **Helper functions (declared at file scope, outside the machine):**
+ - `fun CreateTimer(client: machine) : Timer` — creates a new Timer for the given client
+ - `fun StartTimer(timer: Timer)` — sends eStartTimer to the timer
+ - `fun CancelTimer(timer: Timer)` — sends eCancelTimer to the timer
+
+### Test Components
+
+#### 4. Client
+- **Role:** Registers with the failure detector to receive failure notifications.
+- **Local state:**
+ - `failureDetector`: reference to the failure detector
+ - `suspectedNodes`: nodes reported as suspected
+- **Initialization:** Created with a reference to the failure detector.
+- **Behavior:**
+ - Registers with the failure detector to receive failure notifications.
+ - Receives notifications when a node is suspected of failure.
+
+## Interactions
+
+1. **eHeartbeat**
+ - **Source:** Node
+ - **Target:** FailureDetector
+ - **Payload:** the source node's reference
+ - **Description:** Node sends a heartbeat to indicate it is alive.
+ - **Effects:**
+ - FailureDetector marks the node as alive and removes it from suspected set.
+
+2. **eTimeOut**
+ - **Source:** Timer
+ - **Target:** FailureDetector
+ - **Payload:** none
+ - **Description:** Timer fires to trigger a liveness check round.
+ - **Effects:**
+ - FailureDetector checks which nodes have not sent heartbeats since last check.
+ - Nodes that missed heartbeats are added to the suspected set.
+ - Suspected nodes are reported to registered clients.
+
+3. **eNodeSuspected**
+ - **Source:** FailureDetector
+ - **Target:** Client
+ - **Payload:** the suspected node's reference
+ - **Description:** FailureDetector notifies the client that a node is suspected of failure.
+
+4. **eCrash**
+ - **Source:** Test driver
+ - **Target:** Node
+ - **Payload:** none
+ - **Description:** Instructs a node to stop sending heartbeats (simulating a crash).
+
+5. **eRegisterClient**
+ - **Source:** Client
+ - **Target:** FailureDetector
+ - **Payload:** the client's reference
+ - **Description:** Client registers to receive failure notifications.
+
+6. **eStartTimer**
+ - **Source:** Any machine (via StartTimer helper)
+ - **Target:** Timer
+ - **Payload:** none
+ - **Description:** Starts the timer. The Timer will eventually send eTimeOut to its client.
+
+7. **eCancelTimer**
+ - **Source:** Any machine (via CancelTimer helper)
+ - **Target:** Timer
+ - **Payload:** none
+ - **Description:** Cancels a pending timer.
+
+8. **eDelayedTimeOut**
+ - **Source:** Timer (internal)
+ - **Target:** Timer (self)
+ - **Payload:** none
+ - **Description:** Internal event used by Timer to model non-deterministic delay.
+
+## Specifications
+
+1. **ReliableDetection** (liveness property):
+ If a node crashes (receives eCrash and stops sending eHeartbeat), the failure detector must **eventually** suspect that node (issue eNodeSuspected).
+
+ This is a liveness property — it asserts that something good eventually happens. In P, express this using a **hot state** in the spec monitor:
+ - When a node crashes, transition to a **hot** state.
+ - A hot state means "the system must eventually leave this state" — PChecker will flag it as a liveness violation if the system stays in a hot state forever.
+ - When the crashed node is suspected (eNodeSuspected received for that node), transition back to a cold (normal) state.
+ - The spec must observe: eCrash, eHeartbeat, eNodeSuspected.
+ - The spec must NOT observe eTimeOut. The eTimeOut event is shared by all Timer instances (Node timers and the FD timer), so the spec cannot distinguish which timer fired. Observing eTimeOut would cause the spec to incorrectly process Node timer events as FD timeout rounds.
+
+## Test Scenarios
+
+1. 3 nodes, 1 failure detector, 1 client — one node crashes and is detected.
+2. 3 nodes, 1 failure detector, 1 client — two nodes crash at different times, both are eventually detected.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Hotel Management Application.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Hotel Management Application.md
new file mode 100644
index 0000000000..2d3c34be89
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Hotel Management Application.md
@@ -0,0 +1,108 @@
+# Hotel Management Application
+
+## Introduction
+
+The goal of this system is to develop a hotel service booking application that enables the Front Desk to handle client requests, including room reservations, special requests, and cancellations. The application will maintain a centralized database to record room bookings and prevent double booking of rooms.
+
+**Assumptions:**
+1. The Front Desk processes requests one at a time in the order they are received.
+2. Client ids are unique.
+3. The number of rooms is fixed and cannot be altered.
+4. Communication between clients and the Front Desk is reliable.
+
+## Components
+
+### Source Components
+
+#### 1. FrontDesk
+- **Role:** Manages client requests, including room reservations, special requests, and cancellations.
+- **States:** Init, Ready
+- **Local state:**
+ - `roomAssignments`: maps room number to client name
+ - `specialRequests`: maps room number to list of special requests
+ - `numRooms`: total number of rooms available
+- **Initialization:** Created with the total number of rooms available.
+- **Behavior:**
+ - Keeps track of room availability and existing reservations.
+ - Assigns rooms to clients on reservation requests if available.
+ - Validates client identity before processing cancellations and special requests.
+ - Communicates with clients regarding their requests and reservations.
+
+### Test Components
+
+#### 2. Client
+- **Role:** Makes room reservation requests, submits special requests, and cancels reservations.
+- **Local state:**
+ - `frontDesk`: reference to the front desk
+ - `clientId`: unique client identifier
+ - `clientName`: client name
+ - `assignedRoom`: room number assigned to this client
+- **Initialization:** Created with a reference to the front desk, a unique client ID, and the client's name.
+- **Behavior:**
+ - Makes room reservation requests.
+ - Submits special requests (e.g., preferences for room placement).
+ - Cancels room reservations.
+ - Waits for responses from the Front Desk.
+ - May make additional requests based on responses received.
+
+## Interactions
+
+1. **eRoomReservationRequest**
+ - **Source:** Client
+ - **Target:** FrontDesk
+ - **Payload:** the client's reference, client ID, and client name
+ - **Description:** A client requests a room. If successful, the client is assigned a room. If unsuccessful, the client is informed of the failure.
+ - **Effects:**
+ - If a room is available, the Front Desk assigns it to the client and provides the room number.
+ - If no rooms are available, the Front Desk informs the client of the inability to reserve a room.
+
+2. **eRoomReservationResponse**
+ - **Source:** FrontDesk
+ - **Target:** Client
+ - **Payload:** the client ID, the assigned room number, and the status (SUCCESS or FAILURE)
+ - **Description:** Front Desk responds to the reservation request with the assigned room number and status.
+
+3. **eCancellationRequest**
+ - **Source:** Client
+ - **Target:** FrontDesk
+ - **Payload:** the client's reference, client ID, client name, and room number
+ - **Description:** A client requests to cancel their reservation. The request is processed if the client's name matches the reservation for the given room.
+ - **Effects:**
+ - If the client's name matches the reservation, the room is freed up, and the client is notified of successful cancellation.
+ - If the client's name does not match the reservation, the cancellation is rejected, and the client is informed.
+
+4. **eCancellationResponse**
+ - **Source:** FrontDesk
+ - **Target:** Client
+ - **Payload:** the client ID and the status (SUCCESS or FAILURE)
+ - **Description:** Front Desk responds to the cancellation request.
+
+5. **eSpecialRequest**
+ - **Source:** Client
+ - **Target:** FrontDesk
+ - **Payload:** the client's reference, client ID, client name, room number, and the special request details
+ - **Description:** A client with an existing reservation submits a special request. The request is recorded if the client is staying in the room mentioned.
+ - **Effects:**
+ - If the client's name matches the reservation for the specified room, the request is noted, and the client is informed of its successful recording.
+ - If the client's name does not match the reservation, the request is discarded, and the client is informed.
+
+6. **eSpecialResponse**
+ - **Source:** FrontDesk
+ - **Target:** Client
+ - **Payload:** the client ID and the status (SUCCESS or FAILURE)
+ - **Description:** Front Desk responds to the special request.
+
+## Specifications
+
+1. **NoDoubleBooking** (safety property):
+ Between a successful eRoomReservationResponse assigning a room and a successful eCancellationResponse freeing it, no other eRoomReservationRequest may result in that same room being assigned to a different client.
+
+2. **ReservationIntegrity** (safety property):
+ An eSpecialResponse must never alter room assignments. While an eRoomReservationRequest or eCancellationRequest is being processed, existing room assignments must remain unchanged.
+
+## Test Scenarios
+
+1. 1 front desk (3 rooms), 2 clients — both clients reserve rooms successfully.
+2. 1 front desk (1 room), 2 clients — second client's reservation fails due to no availability.
+3. 1 front desk (2 rooms), 1 client — client reserves a room, then cancels, then reserves again.
+4. 1 front desk (2 rooms), 2 clients — one client tries to cancel a reservation they do not hold.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Raft Leader Election.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Raft Leader Election.md
new file mode 100644
index 0000000000..88709aa695
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Raft Leader Election.md
@@ -0,0 +1,107 @@
+# Raft Leader Election
+
+## Introduction
+
+The goal of this system is to model the leader election component of the Raft consensus protocol. In Raft, a cluster of servers elects a single leader which is responsible for managing the replicated log. If the leader fails, a new election is triggered. This models only the election mechanism, not log replication.
+
+**Assumptions:**
+1. Servers communicate via reliable message passing.
+2. Each server starts as a Follower.
+3. If a Follower does not hear from a leader within a timeout, it becomes a Candidate and starts an election.
+4. A Candidate requests votes from all other servers. A server grants its vote to the first valid candidate in a given term.
+5. A Candidate that receives votes from a majority becomes the Leader.
+6. If a Candidate discovers a higher term, it reverts to Follower.
+7. The Leader sends periodic heartbeats to maintain authority.
+8. The Timer machine is a pre-existing reusable module — do NOT re-implement it. Use CreateTimer(this), StartTimer(timer), and CancelTimer(timer).
+
+## Components
+
+### Source Components
+
+#### 1. Server
+- **Role:** Participates in leader election, cycling through Follower, Candidate, and Leader roles.
+- **States:** Follower, Candidate, Leader
+- **Local state:**
+ - `currentTerm`: the current election term
+ - `votedFor`: which candidate this server voted for in the current term
+ - `peers`: references to all other servers
+ - `voteCount`: number of votes received as a candidate
+ - `majoritySize`: number needed for a majority
+ - `electionTimer`: timer for election timeout
+ - `heartbeatTimer`: timer for heartbeat interval
+- **Initialization:** Created with references to all peer servers and a unique server ID.
+- **Behavior:**
+ - As Follower: waits for heartbeats; starts election on timeout.
+ - As Candidate: increments term, votes for self, sends RequestVote to all peers.
+ - As Leader: sends periodic heartbeats (AppendEntries with no data) to all peers.
+- **Event handling notes:**
+ - In Leader: ignore `eVoteResponse` (stale from election phase)
+ - In Follower: ignore `eVoteResponse`
+
+#### 2. Timer
+- **Role:** A generic timer machine that sends eTimeOut after a configurable period.
+- **Initialization:** Pre-existing reusable module. Created by the server that owns it.
+- **Behavior:**
+ - Used for election timeouts (followers/candidates) and heartbeat intervals (leaders).
+
+### Test Components
+
+#### 3. TestDriver
+- **Role:** Creates the cluster of servers and optionally injects failures.
+- **Initialization:** No configuration needed. Creates the cluster of servers directly.
+- **Behavior:**
+ - Creates N servers, each with references to all peers.
+ - Optionally stops a leader to trigger re-election.
+
+## Interactions
+
+1. **eRequestVote**
+ - **Source:** Candidate Server
+ - **Target:** All peer Servers
+ - **Payload:** the candidate's reference and its current term
+ - **Description:** Candidate requests a vote from a peer.
+ - **Effects:**
+ - If the receiver's term is less than or equal and it hasn't voted yet in this term, it grants its vote.
+ - If the receiver's term is higher, the candidate updates its term and reverts to follower.
+
+2. **eVoteResponse**
+ - **Source:** Peer Server
+ - **Target:** Candidate Server
+ - **Payload:** the responder's current term and whether the vote was granted
+ - **Description:** Server responds to a vote request.
+ - **Effects:**
+ - If majority votes received, candidate becomes leader.
+ - If term in response is higher, candidate reverts to follower.
+
+3. **eAppendEntries**
+ - **Source:** Leader Server
+ - **Target:** Follower Server
+ - **Payload:** the leader's term and the leader's reference
+ - **Description:** Leader heartbeat to maintain authority.
+ - **Effects:**
+ - Follower resets its election timer.
+ - If leader's term >= follower's term, follower acknowledges the leader.
+ - If leader's term < follower's term, follower rejects (stale leader).
+
+4. **eAppendEntriesResponse**
+ - **Source:** Follower Server
+ - **Target:** Leader Server
+ - **Payload:** the follower's current term and whether the heartbeat was accepted
+ - **Description:** Follower acknowledges or rejects the heartbeat.
+
+5. **eTimeOut**
+ - **Source:** Timer
+ - **Target:** Server
+ - **Payload:** none
+ - **Description:** Timer expiration triggers election (for followers/candidates) or heartbeat sending (for leaders).
+
+## Specifications
+
+1. **AtMostOneLeaderPerTerm** (safety property):
+ In any given term, at most one server may transition to the Leader role. Tracking eRequestVote and eVoteResponse exchanges ensures no two servers collect a majority in the same term, and eAppendEntries heartbeats must never originate from two different leaders in the same term.
+
+## Test Scenarios
+
+1. 3 servers, one becomes leader after initial election with no contention.
+2. 5 servers, leader crashes (stops heartbeating), a new leader is elected.
+3. 3 servers with split vote — no majority in first round, re-election succeeds.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Simple Message Broker.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Simple Message Broker.md
new file mode 100644
index 0000000000..7c18ca3d0e
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Simple Message Broker.md
@@ -0,0 +1,148 @@
+# Simple Message Broker
+
+## Introduction
+
+This document describes the design of a simplified distributed message broker inspired by Apache Kafka. The system models the core mechanics of a publish-subscribe log: producers write keyed messages into an append-only topic log managed by a central broker, and consumers independently poll that log, each tracking their own read position. The broker is the single source of truth for the message log and for each consumer's committed offset.
+
+The design intentionally constrains itself to a single topic with a single partition. This keeps the state space tractable for model checking while still exercising the interesting protocol interactions — in particular, the interplay between concurrent producers racing to append, multiple consumers progressing at different speeds, and the offset-commit protocol that ensures consumers can resume from a consistent position after a restart.
+
+**Assumptions:**
+1. There is exactly one topic backed by a single ordered partition; multi-partition fan-out is out of scope.
+2. The network is reliable: every message sent between components is eventually delivered, and there are no duplicates or reorderings at the transport level.
+3. Consumers read the log sequentially. Each consumer maintains a local cursor that advances through the log; random seeks are not supported.
+4. The broker acknowledges every publish synchronously before the producer moves on, giving us a clear linearization point for each write.
+5. Message delivery follows a pull model: consumers poll the broker at their own pace rather than having messages pushed to them, mirroring Kafka's real consumer protocol.
+
+## Components
+
+### Source Components
+
+#### 1. Broker
+- **Role:** The heart of the system — owns an append-only log and manages consumer offsets.
+- **States:** Init, Ready
+- **Local state:**
+ - `log`: append-only log of (key, value) records stamped with monotonically increasing offsets
+ - `consumerOffsets`: last committed offset per registered consumer
+ - `registeredConsumers`: set of consumers that have registered
+ - `batchSize`: maximum number of records to return per poll
+- **Initialization:** Created with the maximum batch size for poll responses.
+- **Behavior:**
+ - When a producer publishes a message, appends it to the tail of the log and replies with the assigned offset.
+ - Maintains a registry of known consumers. A consumer must register before it can issue its first poll.
+ - Tracks each consumer's last committed offset so that, if the consumer restarts, it can resume from where it left off.
+ - When a consumer polls, slices the log from the requested offset forward (up to batchSize) and returns the batch.
+ - On offset-commit, persists the new offset and acknowledges.
+
+#### 2. Producer
+- **Role:** Publishes a fixed number of key-value messages to the broker, one at a time.
+- **States:** Init, Publishing, Done
+- **Local state:**
+ - `broker`: reference to the broker
+ - `numMessages`: number of messages to publish
+ - `messagesSent`: count of messages sent so far
+- **Initialization:** Created with a reference to the broker and the number of messages to publish.
+- **Behavior:**
+ - After sending each publish request, blocks until it receives the broker's acknowledgment, ensuring messages from a single producer are appended in order.
+
+### Test Components
+
+#### 3. Consumer
+- **Role:** Registers with the broker and polls for messages in a loop.
+- **States:** Init, Registering, Polling, Processing, Done
+- **Local state:**
+ - `broker`: reference to the broker
+ - `currentOffset`: current read position in the log
+ - `targetCount`: number of messages to consume before stopping
+ - `consumed`: count of messages consumed so far
+- **Initialization:** Created with a reference to the broker and the target number of messages to consume.
+- **Behavior:**
+ - Begins by registering with the broker, then enters a poll loop.
+ - On each iteration, asks the broker for the next batch of messages starting from its current offset.
+ - Processes messages in order and commits the new offset back to the broker before polling again.
+ - Validates that the offsets received are strictly increasing.
+ - The loop terminates once the consumer has consumed the target number of messages.
+
+## Interactions
+
+1. **ePublish**
+ - **Source:** Producer
+ - **Target:** Broker
+ - **Payload:** the producer's reference, the message key, and the message value
+ - **Description:** A producer sends ePublish to write a single keyed message into the topic. The broker appends the record to the tail of its log, assigns the next sequential offset, and replies with ePublishAck.
+ - **Effects:**
+ - Broker appends the record and assigns a monotonically increasing offset.
+ - Broker replies with ePublishAck containing the assigned offset.
+
+2. **ePublishAck**
+ - **Source:** Broker
+ - **Target:** Producer
+ - **Payload:** the assigned offset in the log
+ - **Description:** The broker's confirmation that the message has been durably appended. The offset tells the producer exactly where in the log its message landed.
+ - **Effects:**
+ - Producer is free to send its next message.
+
+3. **ePoll**
+ - **Source:** Consumer
+ - **Target:** Broker
+ - **Payload:** the consumer's reference and the starting offset to read from
+ - **Description:** A consumer issues a poll to request the next slice of the log starting at fromOffset.
+ - **Effects:**
+ - Broker reads from that position up to its internal batch-size limit and responds with ePollResp.
+ - If fromOffset is already at or past the end of the log, the broker returns an empty batch.
+
+4. **ePollResp**
+ - **Source:** Broker
+ - **Target:** Consumer
+ - **Payload:** a batch of log records (each with offset, key, and value) and the end offset just past the last message
+ - **Description:** The broker's response carrying a batch of log records. Each record includes its offset so the consumer can verify ordering. The endOffset indicates the offset just past the last message in the batch.
+ - **Effects:**
+ - Consumer processes messages and then issues an eCommitOffset.
+
+5. **eCommitOffset**
+ - **Source:** Consumer
+ - **Target:** Broker
+ - **Payload:** the consumer's reference and the offset to commit
+ - **Description:** After processing a batch, the consumer commits its new read position. The broker persists the offset in its consumer registry.
+ - **Effects:**
+ - Broker records the committed offset and acknowledges with eCommitOffsetAck.
+
+6. **eCommitOffsetAck**
+ - **Source:** Broker
+ - **Target:** Consumer
+ - **Payload:** the committed offset
+ - **Description:** Confirmation that the broker has recorded the consumer's committed offset.
+ - **Effects:**
+ - Consumer proceeds to its next poll iteration.
+
+7. **eRegisterConsumer**
+ - **Source:** Consumer
+ - **Target:** Broker
+ - **Payload:** the consumer's reference
+ - **Description:** A one-time handshake that a consumer must complete before it can poll. The broker adds the consumer to its registry with an initial committed offset of zero.
+ - **Effects:**
+ - Broker registers the consumer and responds with eRegisterConsumerAck.
+
+8. **eRegisterConsumerAck**
+ - **Source:** Broker
+ - **Target:** Consumer
+ - **Payload:** none
+ - **Description:** Acknowledgment that the consumer has been registered.
+ - **Effects:**
+ - Consumer transitions into its polling loop.
+
+## Specifications
+
+1. **OrderedDelivery** (safety property):
+ Every ePollResp delivered to a consumer must carry offsets that are strictly greater than all offsets in any previously received ePollResp for that consumer. A violation would mean the broker is serving stale or out-of-order data.
+
+2. **NoMessageLoss** (safety property):
+ Every ePublish that the broker acknowledges with an ePublishAck must eventually appear in some ePollResp. The log must grow monotonically — an acknowledged offset must never disappear, and no gaps may appear in the offset sequence.
+
+3. **ConsistentOffsets** (safety property):
+ The offset in any eCommitOffset from a consumer must not exceed the highest offset assigned by any ePublishAck. Otherwise the consumer would believe it had read messages that do not yet exist.
+
+## Test Scenarios
+
+1. 1 producer, 1 consumer, 5 messages — the happy-path baseline that exercises the full publish, poll, commit cycle end to end.
+2. 2 producers, 1 consumer, 3 messages each — tests that concurrent appends from different producers are correctly serialized in the log and that the consumer sees a consistent total order.
+3. 1 producer, 2 consumers, 5 messages — verifies that independent consumers can progress through the same log at different rates without interfering with each other's offsets.
diff --git a/Src/PeasyAI/resources/system_design_docs/[Design Doc] Two Phase Commit.md b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Two Phase Commit.md
new file mode 100644
index 0000000000..877a2155cd
--- /dev/null
+++ b/Src/PeasyAI/resources/system_design_docs/[Design Doc] Two Phase Commit.md
@@ -0,0 +1,127 @@
+# Two Phase Commit Protocol
+
+## Introduction
+
+The goal of this system is to model a simplified version of the classic two phase commit protocol. The two phase commit protocol uses a coordinator to gain consensus for any transaction spanning across multiple participants. A transaction in our case is simply a write operation for a key-value data store where the data store is replicated across multiple participants. More concretely, a write transaction must be committed by the coordinator only if it's accepted by all the participant replicas, and must be aborted if any one of the participant replicas rejects the write request.
+
+**Assumptions:**
+1. Our system allows multiple concurrent clients to issue transactions in parallel, but the coordinator serializes these transactions and services them one-by-one.
+2. Our system is not fault-tolerant to node failures, failure of either the coordinator or any of the participants will block the progress forever.
+3. Our system models assume reliable delivery of messages.
+4. The Timer machine is a pre-existing reusable module — do NOT re-implement it. Use CreateTimer(this), StartTimer(timer), and CancelTimer(timer) to interact with the timer.
+
+## Components
+
+### Source Components
+
+#### 1. Coordinator
+- **Role:** Receives write and read transactions from the clients and orchestrates the two-phase commit protocol.
+- **States:** WaitForRequests, WaitForPrepareResponses
+- **Local state:**
+ - `participants`: the list of participant machines
+ - `timer`: timer for prepare-phase timeout
+ - `pendingTransaction`: the currently active transaction
+ - `prepareResponses`: responses from participants
+ - `currentClient`: the client that issued the current transaction
+- **Initialization:** Created with references to all participant machines. On startup, creates a timer and sends its own reference to all participants so they know who the coordinator is.
+ - On initialization: creates a timer using CreateTimer(this), then sends eInformCoordinator to all participants with its own reference so participants know who the coordinator is.
+- **Behavior:**
+ - Processes transactions (write and read) from clients one by one in the order received.
+ - On receiving a write transaction: sends prepare requests to all participants, starts the timer, and waits for prepare responses. Commits if all participants accepted, aborts if any rejected. Times out and aborts if responses are not received in time.
+ - On receiving a read transaction: randomly selects a participant using choose() and forwards the read request.
+- **Event handling notes:**
+ - In WaitForRequests: ignore stale `ePrepareResp` and `eTimeOut`
+ - In WaitForPrepareResponses: defer `eWriteTransReq` and `eReadTransReq`
+
+#### 2. Participant
+- **Role:** Maintains a local key-value store replica and votes on transactions.
+- **States:** WaitForCoordinator, Ready
+- **Local state:**
+ - `coordinator`: reference to the coordinator
+ - `kvStore`: local key-value store
+ - `pendingTransactions`: pending prepare requests
+- **Initialization:** Waits for the coordinator to send its reference before becoming operational.
+- **Behavior:**
+ - Waits for eInformCoordinator from the Coordinator (blocks in Init state until informed).
+ - On ePrepareReq: non-deterministically accepts (using choose()) or rejects the transaction. Sends ePrepareResp back to coordinator.
+ - On eCommitTrans: commits the transaction to the local store.
+ - On eAbortTrans: removes the transaction from pending.
+
+### Test Components
+
+#### 3. Client
+- **Role:** Issues transactions to the coordinator and validates results.
+- **Local state:**
+ - `coordinator`: reference to the coordinator
+ - `numTransactions`: number of transactions to issue
+- **Initialization:** Created with a reference to the coordinator and the number of transactions to issue.
+- **Behavior:**
+ - Issues N non-deterministic write-transactions (with random key and value using choose()).
+ - On success response: performs a read-transaction on the same key and asserts the value read matches what was written.
+ - On failure response: moves on to the next transaction.
+
+## Interactions
+
+1. **eWriteTransReq**
+ - **Source:** Client
+ - **Target:** Coordinator
+ - **Payload:** the client's reference and the transaction (key, value, transaction ID)
+ - **Description:** Client sends a write transaction request to the coordinator.
+
+2. **eWriteTransResp**
+ - **Source:** Coordinator
+ - **Target:** Client
+ - **Payload:** the transaction ID and the status (SUCCESS or ERROR)
+ - **Description:** Coordinator sends the result of the write transaction request back to the client.
+
+3. **eReadTransReq**
+ - **Source:** Client
+ - **Target:** Coordinator
+ - **Payload:** the client's reference and the key to read
+ - **Description:** Client requests to read a specific key.
+
+4. **eReadTransResp**
+ - **Source:** Participant
+ - **Target:** Client
+ - **Payload:** the key, the value read, and the status (SUCCESS or ERROR)
+ - **Description:** Participant responds with the value read and status.
+
+5. **ePrepareReq**
+ - **Source:** Coordinator
+ - **Target:** Participant
+ - **Payload:** the coordinator's reference, the transaction ID, the key, and the value
+ - **Description:** Coordinator requests the participant to prepare for a transaction.
+
+6. **ePrepareResp**
+ - **Source:** Participant
+ - **Target:** Coordinator
+ - **Payload:** the participant's reference, the transaction ID, and the status (SUCCESS or ERROR)
+ - **Description:** Participant responds to the prepare request with SUCCESS or ERROR.
+
+7. **eCommitTrans**
+ - **Source:** Coordinator
+ - **Target:** Participant
+ - **Payload:** the transaction ID to commit
+ - **Description:** Coordinator requests the participant to commit a transaction.
+
+8. **eAbortTrans**
+ - **Source:** Coordinator
+ - **Target:** Participant
+ - **Payload:** the transaction ID to abort
+ - **Description:** Coordinator requests the participant to abort a transaction.
+
+9. **eInformCoordinator**
+ - **Source:** Coordinator
+ - **Target:** Participant
+ - **Payload:** the coordinator's machine reference
+ - **Description:** Coordinator sends its own reference to the participant so the participant knows who to respond to. Sent during initialization after the coordinator is created.
+
+## Specifications
+
+1. **Atomicity** (safety property):
+ If the coordinator sends an eWriteTransResp with SUCCESS to the client, then every ePrepareResp received for that transaction must have been SUCCESS, and an eCommitTrans must have been sent to all participants. If the coordinator sends an eWriteTransResp with ERROR, an eAbortTrans must have been sent — meaning at least one participant rejected or the timer expired.
+
+## Test Scenarios
+
+1. 3 participants, 1 coordinator, 1 client, no failure — test basic commit and abort paths.
+2. 3 participants, 1 coordinator, 2 clients — test concurrent transaction serialization.
diff --git a/Src/PeasyAI/run_mcp.sh b/Src/PeasyAI/run_mcp.sh
new file mode 100755
index 0000000000..5b2aea3c33
--- /dev/null
+++ b/Src/PeasyAI/run_mcp.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# PeasyAI MCP Server launcher.
+# Configuration is loaded from ~/.peasyai/settings.json
+# Run peasyai-mcp init to create the config file.
+
+cd /Users/adesai/workspace/public/P/Src/PeasyAI
+
+# Add P compiler (dotnet tools) and dotnet SDK to PATH
+export PATH="$HOME/.dotnet/tools:/usr/local/share/dotnet:$PATH"
+export DOTNET_ROOT="/usr/local/share/dotnet"
+
+exec /Users/adesai/workspace/public/P/Src/PeasyAI/.venv/bin/peasyai-mcp
diff --git a/Src/PeasyAI/scripts/check_dependencies.py b/Src/PeasyAI/scripts/check_dependencies.py
new file mode 100755
index 0000000000..15c0fd64a2
--- /dev/null
+++ b/Src/PeasyAI/scripts/check_dependencies.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""
+Dependency preflight for PeasyAI development and MCP contract tests.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+import shutil
+import sys
+
+
+REQUIRED_PYTHON_MODULES = [
+ "pydantic",
+ "fastmcp",
+ "dotenv",
+]
+
+OPTIONAL_PYTHON_MODULES = [
+ "pytest",
+]
+
+REQUIRED_BINARIES = [
+ "python3",
+]
+
+OPTIONAL_BINARIES = [
+ "p",
+ "dotnet",
+]
+
+
+def has_module(module_name: str) -> bool:
+ return importlib.util.find_spec(module_name) is not None
+
+
+def has_binary(binary_name: str) -> bool:
+ return shutil.which(binary_name) is not None
+
+
+def main() -> int:
+ missing_required = []
+
+ print("== PeasyAI dependency preflight ==")
+
+ for mod in REQUIRED_PYTHON_MODULES:
+ ok = has_module(mod)
+ print(f"[{'OK' if ok else 'MISSING'}] python module: {mod}")
+ if not ok:
+ missing_required.append(f"python module '{mod}'")
+
+ for mod in OPTIONAL_PYTHON_MODULES:
+ ok = has_module(mod)
+ print(f"[{'OK' if ok else 'WARN'}] optional python module: {mod}")
+
+ for binary in REQUIRED_BINARIES:
+ ok = has_binary(binary)
+ print(f"[{'OK' if ok else 'MISSING'}] binary: {binary}")
+ if not ok:
+ missing_required.append(f"binary '{binary}'")
+
+ for binary in OPTIONAL_BINARIES:
+ ok = has_binary(binary)
+ print(f"[{'OK' if ok else 'WARN'}] optional binary: {binary}")
+
+ if missing_required:
+ print("\nMissing required dependencies:")
+ for item in missing_required:
+ print(f"- {item}")
+ print("\nInstall them with:")
+ print(" python3 -m pip install -r requirements.txt")
+ return 1
+
+ print("\nAll required dependencies are present.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/Src/PeasyAI/scripts/evaluate_peasyai.py b/Src/PeasyAI/scripts/evaluate_peasyai.py
new file mode 100644
index 0000000000..ac071252c9
--- /dev/null
+++ b/Src/PeasyAI/scripts/evaluate_peasyai.py
@@ -0,0 +1,292 @@
+#!/usr/bin/env python3
+
+"""
+ EXAMPLE USAGE:
+ python evaluate_peasyai.py --metric pass_at_k -k 1 -n 1 -t 0.9 --trials 5 --benchmark-dir evaluation/p-model-benchmark
+"""
+
+import sys
+import argparse
+import os
+import evaluation.metrics.pass_at_k as pass_at_k
+import evaluation.visualization.viz_pass_at_k as viz_pass_at_k
+import traceback
+import json
+from datetime import datetime
+import random
+import tests.pipeline.pipeline_tests as tests
+from core.pipelining.prompting_pipeline import PromptingPipeline
+from compute_metrics import compute_average_token_usage
+import time
+
+CONFIG_MAX_TEMP = 1.2
+CONFIG_MODEL = "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
+
+TEST_SETS = {
+ 'base_old': [tests.taskgen_base, tests.test_base_old, tests.oracle_base],
+ 'base_all_docs': [tests.taskgen_base, tests.test_base_all_docs, tests.oracle_base],
+ 'base_few_shot': [tests.taskgen_base, tests.test_base_few_shot, tests.oracle_base],
+ 'base_RAG1000_inline': [tests.taskgen_base, tests.test_base_RAG1000_inline, tests.oracle_base],
+ 'base_RAG2000_inline': [tests.taskgen_base, tests.test_base_RAG2000_inline, tests.oracle_base],
+ 'base_RAG2000_inline_fewshot': [tests.taskgen_base, tests.test_base_RAG2000_inline_fewshot, tests.oracle_base],
+ 'base_RAG2000_asdoc': [tests.taskgen_base, tests.test_base_RAG2000_asdoc, tests.oracle_base],
+ 'base_RAG2000_inline_aMLML12v2': [tests.taskgen_base, tests.test_base_RAG2000_inline_aMLML12v2, tests.oracle_base],
+ 'dd2proj_legacy': [tests.taskgen_dd2proj, tests.test_dd2proj_legacy, tests.oracle_dd2proj],
+ 'dd2proj_replicated': [tests.taskgen_dd2proj, tests.test_dd2proj_replicated, tests.oracle_dd2proj_replicated],
+ 'dd2psrc': [tests.test_taskgen_dd2psrc, tests.test_dd2proj_psrc, tests.oracle_dd2psrc_correctness],
+ 'dd2proj_current': [tests.taskgen_dd2proj, tests.test_dd2proj_current, tests.oracle_dd2proj_current],
+ 'pchecker_fix_basic_one': [tests.taskgen_pchecker_fix_single, tests.test_fix_pchecker_errors, tests.oracle_fix_pchecker_errors],
+ 'pchecker_fix_basic_full': [tests.taskgen_pchecker_fix, tests.test_fix_pchecker_errors, tests.oracle_fix_pchecker_errors],
+}
+
+CURRENT_TEST_NAME = "pchecker_fix_basic_full"
+CURRENT_TEST_SET = TEST_SETS[CURRENT_TEST_NAME]
+TASKGEN, PIPELINE, ORACLE = CURRENT_TEST_SET
+
+def process_args():
+ parser = argparse.ArgumentParser(description="Evaluate PeasyAI using different metrics")
+ parser.add_argument("--metric", type=str, choices=list(METRIC_HANDLERS.keys()), required=True,
+ help="Which metric to compute")
+ parser.add_argument("-k", type=int, help="Value for k in pass@k metric")
+ parser.add_argument("-t", type=str, help="Temperature to be used for the model. 'random' for random sampling")
+ parser.add_argument("-n", type=int, help="Number of samples")
+ parser.add_argument("--trials", type=int, help="Number of trails", default=1)
+ parser.add_argument("--benchmark-dir", type=str, help="Path to the benchmark directory")
+ parser.add_argument("--out-dir", type=str, help="Path to the output directory where results will be stored", default="results")
+
+ return parser.parse_args()
+
+def model_caller(task, **kwargs):
+
+# ---------------------------------------------------------
+ # pipeline = tests.test_interactive_one_pass_new(
+ # task=task,
+ # model=CONFIG_MODEL,
+ # **kwargs
+ # )
+# ---------------------------------------------------------
+ pipeline = PIPELINE(
+ task=task,
+ model=CONFIG_MODEL,
+ **kwargs
+ )
+
+
+
+ # ======= TO REPLAY A PREVIOUS RUN ======================
+ # test_name, _ = task
+ # trial = kwargs['trial']
+ # result_dir = "key-results/2025-06-24-15-04-43"
+ # with open(f"{result_dir}/trial_{trial}/{test_name}/conversation.json", "r") as f:
+ # conversation = json.load(f)
+
+ # with open(f"{result_dir}/trial_{trial}/{test_name}/token_usage.json", "r") as f:
+ # token_usage = json.load(f)
+
+ # pipeline = PromptingPipeline()
+ # pipeline.conversation = conversation
+ # pipeline.usage_stats = token_usage
+ # ========================================================
+ return pipeline
+
+def is_float(t):
+ try:
+ float(t)
+ return True
+ except:
+ return False
+
+def compute_pass_at_k(args, out_dir=None, **kwargs):
+ tasks = TASKGEN(args.benchmark_dir)
+
+ temp = float(args.t) if is_float(args.t) else random.uniform(0, CONFIG_MAX_TEMP)
+ return pass_at_k.compute(
+ model_caller,
+ lambda n, out: ORACLE(n, out, out_dir=out_dir),
+ args.k,
+ args.n,
+ temp,
+ tasks,
+ out_dir=out_dir,
+ **kwargs
+ )
+
+def compute_avg_p_at_k(results):
+ sum_p_at_k = 0
+ for _, p_at_k in results:
+ sum_p_at_k += p_at_k
+ avg_p_at_k = sum_p_at_k/len(results)
+ return avg_p_at_k
+
+def assert_same_keys(dict_list, msg):
+ if not dict_list:
+ return
+
+ reference_keys = set(dict_list[0].keys())
+
+ assert all(set(d.keys()) == reference_keys for d in dict_list), msg
+
+
+def compute_avg_passrate_per_subtest(subtest_dict, totals):
+ avg_dict = {}
+ for subtest_name, summed in subtest_dict.items():
+ avg_dict[subtest_name] = summed/totals[subtest_name]
+
+ return avg_dict
+
+def initialize_subdicts(sum_passrate_dict, one_test_dict):
+ for test_name in sum_passrate_dict:
+ for oracle in one_test_dict:
+ sum_passrate_dict[test_name] = {**sum_passrate_dict[test_name], oracle:0}
+
+ return sum_passrate_dict
+
+def compute_avg_passrate_per_test(results):
+ all_trial_dicts = [ d for d,_ in results ]
+ assert_same_keys(all_trial_dicts, "[compute_avg_passrate_per_test] All trials must have the same set of tests!")
+
+ # sample value, all_trial_dicts = [{'1_lightswitch': {'compile': True, 'tcSingleSwitchLight': False, 'tcMultipleSwitchesOneLight': False, 'tcMultipleSwitchesAndLights': False}}, {'1_lightswitch': {'compile': True, 'tcToggleOffToOn': False, 'tcToggleOnToOff': False, 'tcMultipleSwitchesSameLight': False, 'tcAllScenarios': False}}]
+ # tests_from_sample_trial = [d for _,d in all_trial_dicts[0].items()]
+ # assert_same_keys(tests_from_sample_trial, "[compute_avg_passrate_per_test] Each test in a trial must have the same set of oracles!")
+
+ # sum_passrate_dict = { k:{} for k,_ in all_trial_dicts[0].items() }
+ # sum_passrate_dict = initialize_subdicts(sum_passrate_dict, tests_from_sample_trial[0])
+ sum_passrate_dict = {}
+ totals = {}
+ for result_dict, _ in results:
+ for test_name, subtest_dict in result_dict.items():
+ if test_name not in sum_passrate_dict:
+ sum_passrate_dict[test_name] = {}
+ totals[test_name] = {}
+ for subtest_name in subtest_dict:
+ if subtest_name not in sum_passrate_dict[test_name]:
+ sum_passrate_dict[test_name][subtest_name] = 0
+ totals[test_name][subtest_name] = 0
+
+ sum_passrate_dict[test_name][subtest_name] += 1 if result_dict[test_name][subtest_name] else 0
+ totals[test_name][subtest_name] += 1
+
+ avg_passrate_dict = { k:compute_avg_passrate_per_subtest(v,totals[k]) for k,v in sum_passrate_dict.items() }
+ return avg_passrate_dict
+
+def construct_lines(key, subdict):
+ lines =[f"{(value * 100):.2f}% : {key}->{subkey} avg. pass rate" for subkey, value in subdict.items()]
+ return "\n".join(lines)
+
+def pp_avg_passrate_per_test(results_dict):
+ formatted_lines = [
+ construct_lines(key, subdict)
+ for key, subdict in sorted(results_dict.items(), key=lambda x: x[0])
+ ]
+
+ return "\n".join(formatted_lines)
+
+def pretty_print_report(report):
+
+ k = report['args']['k']
+ n = report['args']['n']
+ t = report['args']['t']
+ trials = report['args']['trials']
+ avg_p_at_k = report['results']['avg_p_at_k']
+ avg_pass_rates = report['results']['avg_pass_rates']
+
+ lines = []
+ lines.append("---- RESULTS SUMMARIZED ----")
+ lines.append(f"pass@{k}(n={n},t={t},trials={trials}) = {avg_p_at_k} (avg)")
+ lines.append(pp_avg_passrate_per_test(avg_pass_rates))
+
+ return "\n".join(lines)
+
+
+def save_report_json(report, save_dir):
+ os.makedirs(save_dir, exist_ok=True)
+ filename = f"report.json"
+ filepath = os.path.join(save_dir, filename)
+
+ with open(filepath, 'w') as f:
+ json.dump(report, f, indent=4)
+
+ return filepath
+
+def process_p_at_k_results(args, results, save_dir="/tmp", **external_metrics):
+
+ avg_p_at_k = compute_avg_p_at_k(results)
+ avg_pass_rates = compute_avg_passrate_per_test(results)
+
+
+ report = {
+ "name": CURRENT_TEST_NAME,
+ "args": {
+ **vars(args)
+ },
+
+ "results": {
+ "avg_p_at_k": avg_p_at_k,
+ "avg_pass_rates": avg_pass_rates,
+ **external_metrics
+ }
+ }
+
+ print(results)
+ print(pretty_print_report(report))
+
+ saved_json = save_report_json(report, save_dir)
+ print(f"Report saved to {saved_json}")
+ return saved_json
+
+METRIC_HANDLERS = {
+ "pass_at_k": (compute_pass_at_k, process_p_at_k_results, viz_pass_at_k.visualize_json_results),
+}
+
+def write_and_return_result(result, filepath):
+ result_dict, _ = result
+ with open(filepath, 'w') as f:
+ json.dump(result_dict, f, indent=4)
+
+ return result
+
+def main():
+ args = process_args()
+ timestamp = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
+ out_dir = f"{args.out_dir}/{args.metric}/{timestamp}"
+
+ handler, result_processor, visualizer = METRIC_HANDLERS[args.metric]
+
+ try:
+ # Time the list comprehension
+ start_time = time.time()
+ results = [
+ write_and_return_result(handler(
+ args,
+ out_dir=f"{out_dir}/trial_{trial}",
+ trial=trial,
+ ), f"{out_dir}/trial_{trial}/result.json")
+ for trial in range(args.trials)
+ ]
+
+ end_time = time.time()
+ total_exec_time = end_time - start_time
+ avg_exec_time = total_exec_time/args.trials
+
+ report_json = result_processor(
+ args,
+ results,
+ save_dir=out_dir,
+ avg_exec_time=avg_exec_time,
+ total_exec_time=total_exec_time,
+ )
+ visualizer(report_json, "p_at_k.png")
+
+ if args.metric == "pass_at_k":
+ compute_average_token_usage(out_dir)
+
+ with open(f"{out_dir}/{CURRENT_TEST_NAME}.txt", "w") as f:
+ f.write(CURRENT_TEST_NAME)
+
+ except Exception as e:
+ print(f"Error running {args.metric}: {e}")
+ traceback.print_exc()
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/Src/PeasyAI/scripts/mcp_e2e_protocols.py b/Src/PeasyAI/scripts/mcp_e2e_protocols.py
new file mode 100644
index 0000000000..46c7469774
--- /dev/null
+++ b/Src/PeasyAI/scripts/mcp_e2e_protocols.py
@@ -0,0 +1,392 @@
+#!/usr/bin/env python3
+"""
+End-to-end MCP validation for protocol examples.
+
+Runs complete MCP tool flows for:
+1) Basic Paxos Protocol
+2) Two Phase Commit
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import sys
+from datetime import datetime
+from pathlib import Path
+from typing import Dict, Any, List
+
+logger = logging.getLogger(__name__)
+# Suppress noisy Streamlit warnings when running outside Streamlit
+logging.getLogger("streamlit").setLevel(logging.ERROR)
+
+PROJECT_ROOT = Path(__file__).resolve().parents[1]
+SRC_ROOT = PROJECT_ROOT / "src"
+sys.path.insert(0, str(PROJECT_ROOT))
+sys.path.insert(0, str(SRC_ROOT))
+
+from src.ui.mcp import server as mcp_server
+from src.ui.mcp.tools.env import register_env_tools, ValidateEnvironmentParams
+from src.ui.mcp.tools.generation import (
+ register_generation_tools,
+ GenerateProjectParams,
+ GenerateTypesEventsParams,
+ GenerateMachineParams,
+ GenerateSpecParams,
+ GenerateTestParams,
+ GenerateCompleteProjectParams,
+ SavePFileParams,
+)
+from src.ui.mcp.tools.compilation import (
+ register_compilation_tools,
+ PCompileParams,
+ PCheckParams,
+)
+from src.ui.mcp.tools.fixing import register_fixing_tools, FixIterativelyParams, FixBuggyProgramParams
+from src.core.workflow.factory import extract_machine_names_from_design_doc
+
+
+class DummyMCP:
+ def tool(self, *args, **kwargs):
+ def decorator(fn):
+ return fn
+ return decorator
+
+ def resource(self, *args, **kwargs):
+ def decorator(fn):
+ return fn
+ return decorator
+
+
+def _load_design_doc(path: Path) -> str:
+ return path.read_text(encoding="utf-8")
+
+
+def _register_tools() -> Dict[str, Any]:
+ dummy_mcp = DummyMCP()
+ env_tools = register_env_tools(dummy_mcp, mcp_server._with_metadata)
+ gen_tools = register_generation_tools(
+ dummy_mcp, mcp_server.get_services, mcp_server._with_metadata
+ )
+ comp_tools = register_compilation_tools(
+ dummy_mcp, mcp_server.get_services, mcp_server._with_metadata
+ )
+ fix_tools = register_fixing_tools(
+ dummy_mcp, mcp_server.get_services, mcp_server._with_metadata
+ )
+ all_tools = {
+ "validate_environment": env_tools,
+ **gen_tools,
+ **comp_tools,
+ **fix_tools,
+ }
+ return all_tools
+
+
+def _save_generated(tools: Dict[str, Any], generated: Dict[str, Dict[str, Any]]) -> None:
+ for _, payload in generated.items():
+ if payload.get("success") and payload.get("file_path") and payload.get("code"):
+ tools["save_p_file"](
+ SavePFileParams(
+ file_path=payload["file_path"],
+ code=payload["code"],
+ )
+ )
+
+
+def run_protocol_ensemble(
+ tools: Dict[str, Any],
+ design_doc: str,
+ out_root: Path,
+ project_name: str,
+ ensemble_size: int = 3,
+) -> Dict[str, Any]:
+ """
+ Run a protocol end-to-end using ``generate_complete_project`` with
+ ensemble generation. This is the primary flow — it generates N candidates
+ per file, picks the best, compiles, fixes, and runs PChecker in one call.
+ """
+ result: Dict[str, Any] = {
+ "project_name": project_name,
+ "success": False,
+ "mode": "ensemble",
+ "ensemble_size": ensemble_size,
+ "steps": [],
+ "generated_files": [],
+ "compile": None,
+ "check": None,
+ "errors": [],
+ }
+
+ resp = tools["generate_complete_project"](
+ GenerateCompleteProjectParams(
+ design_doc=design_doc,
+ output_dir=str(out_root),
+ project_name=project_name,
+ include_spec=True,
+ include_test=True,
+ auto_fix=True,
+ run_checker=True,
+ ensemble_size=ensemble_size,
+ )
+ )
+
+ result["steps"].append({
+ "name": "generate_complete_project",
+ "success": resp.get("success"),
+ })
+ result["project_path"] = resp.get("project_path")
+ result["generated_files"] = sorted(resp.get("generated_files", {}).values())
+ result["compile"] = resp.get("compilation")
+ result["check"] = resp.get("checker")
+ result["errors"] = resp.get("errors", [])
+ result["warnings"] = resp.get("warnings", [])
+ result["success"] = bool(resp.get("success"))
+
+ # If generate_complete_project didn't run checker or checker failed,
+ # try the explicit fix_buggy_program loop as a fallback.
+ project_path = resp.get("project_path")
+ check_info = resp.get("checker")
+ compile_info = resp.get("compilation", {})
+ if (
+ project_path
+ and compile_info.get("success")
+ and check_info
+ and not check_info.get("success")
+ and check_info.get("failed_tests")
+ ):
+ MAX_CHECKER_FIX_ROUNDS = 2
+ for fix_round in range(1, MAX_CHECKER_FIX_ROUNDS + 1):
+ logger.info(f"[CHECKER-FIX] Round {fix_round}: attempting fix_buggy_program")
+ fix_bug_resp = tools["fix_buggy_program"](
+ FixBuggyProgramParams(project_path=project_path)
+ )
+ result.setdefault("checker_fixes", []).append(fix_bug_resp)
+
+ if not fix_bug_resp.get("fixed"):
+ break
+
+ recompile = tools["p_compile"](PCompileParams(path=project_path))
+ if not recompile.get("success"):
+ result["errors"].append(
+ f"Recompilation failed after checker fix round {fix_round}"
+ )
+ break
+
+ recheck = tools["p_check"](
+ PCheckParams(path=project_path, schedules=100, timeout=90)
+ )
+ result["check"] = {
+ "success": recheck.get("success"),
+ "failed_tests": recheck.get("failed_tests", []),
+ "error": recheck.get("error"),
+ }
+ if recheck.get("success"):
+ result["success"] = True
+ break
+
+ return result
+
+
+def run_protocol(tools: Dict[str, Any], design_doc: str, out_root: Path, project_name: str, ensemble_size: int = 3) -> Dict[str, Any]:
+ """
+ Run a protocol end-to-end using the step-by-step flow.
+ Now uses ensemble generation (best-of-N) for each file by default.
+ For the single-call ensemble flow, prefer ``run_protocol_ensemble``.
+ """
+ result: Dict[str, Any] = {
+ "project_name": project_name,
+ "success": False,
+ "mode": "step_by_step",
+ "steps": [],
+ "generated_files": [],
+ "compile": None,
+ "fix": None,
+ "check": None,
+ "errors": [],
+ }
+
+ create = tools["generate_project_structure"](
+ GenerateProjectParams(
+ design_doc=design_doc,
+ output_dir=str(out_root),
+ project_name=project_name,
+ )
+ )
+ result["steps"].append({"name": "generate_project_structure", "success": create.get("success")})
+ if not create.get("success"):
+ result["errors"].append(create.get("error"))
+ return result
+
+ project_path = create["project_path"]
+
+ generated: Dict[str, Dict[str, Any]] = {}
+ types_resp = tools["generate_types_events"](
+ GenerateTypesEventsParams(
+ design_doc=design_doc,
+ project_path=project_path,
+ )
+ )
+ generated["types"] = types_resp
+ result["steps"].append({"name": "generate_types_events", "success": types_resp.get("success")})
+ if not types_resp.get("success"):
+ result["errors"].append(types_resp.get("error"))
+ return result
+
+ machine_names: List[str] = extract_machine_names_from_design_doc(design_doc)
+ context_files = {"Enums_Types_Events.p": types_resp["code"]}
+
+ for machine_name in machine_names:
+ machine_resp = tools["generate_machine"](
+ GenerateMachineParams(
+ machine_name=machine_name,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ )
+ )
+ generated[f"machine:{machine_name}"] = machine_resp
+ result["steps"].append({"name": f"generate_machine:{machine_name}", "success": machine_resp.get("success")})
+ if machine_resp.get("success") and machine_resp.get("filename") and machine_resp.get("code"):
+ context_files[machine_resp["filename"]] = machine_resp["code"]
+
+ spec_resp = tools["generate_spec"](
+ GenerateSpecParams(
+ spec_name="Safety",
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ )
+ )
+ generated["spec"] = spec_resp
+ result["steps"].append({"name": "generate_spec", "success": spec_resp.get("success")})
+
+ test_resp = tools["generate_test"](
+ GenerateTestParams(
+ test_name="TestDriver",
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ )
+ )
+ generated["test"] = test_resp
+ result["steps"].append({"name": "generate_test", "success": test_resp.get("success")})
+
+ _save_generated(tools, generated)
+ result["generated_files"] = sorted(
+ [v["file_path"] for v in generated.values() if v.get("success") and v.get("file_path")]
+ )
+
+ compile_resp = tools["p_compile"](PCompileParams(path=project_path))
+ result["compile"] = {
+ "success": compile_resp.get("success"),
+ "error": compile_resp.get("error"),
+ }
+
+ if not compile_resp.get("success"):
+ fix_resp = tools["fix_iteratively"](
+ FixIterativelyParams(project_path=project_path, max_iterations=8)
+ )
+ result["fix"] = fix_resp
+ compile_resp = tools["p_compile"](PCompileParams(path=project_path))
+ result["compile_after_fix"] = {
+ "success": compile_resp.get("success"),
+ "error": compile_resp.get("error"),
+ }
+
+ if not compile_resp.get("success"):
+ result["errors"].append("Compilation failed after fix attempts")
+ return result
+
+ check_resp = tools["p_check"](
+ PCheckParams(path=project_path, schedules=100, timeout=90)
+ )
+ result["check"] = {
+ "success": check_resp.get("success"),
+ "failed_tests": check_resp.get("failed_tests", []),
+ "error": check_resp.get("error"),
+ }
+
+ # If PChecker found bugs, attempt automated fix and re-check (up to 2 rounds)
+ MAX_CHECKER_FIX_ROUNDS = 2
+ checker_fix_round = 0
+ while not check_resp.get("success") and check_resp.get("failed_tests") and checker_fix_round < MAX_CHECKER_FIX_ROUNDS:
+ checker_fix_round += 1
+ logger.info(f"[CHECKER-FIX] Round {checker_fix_round}: attempting fix_buggy_program")
+ fix_bug_resp = tools["fix_buggy_program"](
+ FixBuggyProgramParams(project_path=project_path)
+ )
+ result.setdefault("checker_fixes", []).append(fix_bug_resp)
+
+ if not fix_bug_resp.get("fixed"):
+ break
+
+ # Recompile after fix
+ recompile = tools["p_compile"](PCompileParams(path=project_path))
+ if not recompile.get("success"):
+ result["errors"].append(f"Recompilation failed after checker fix round {checker_fix_round}")
+ break
+
+ # Re-check
+ check_resp = tools["p_check"](
+ PCheckParams(path=project_path, schedules=100, timeout=90)
+ )
+ result["check"] = {
+ "success": check_resp.get("success"),
+ "failed_tests": check_resp.get("failed_tests", []),
+ "error": check_resp.get("error"),
+ }
+
+ result["success"] = bool(check_resp.get("success"))
+ if not result["success"]:
+ result["errors"].append("PChecker reported failing tests")
+ return result
+
+
+def main() -> int:
+ root = PROJECT_ROOT
+ docs_dir = root / "resources" / "system_design_docs"
+ out_root = root / "generated_code" / "mcp_e2e" / datetime.now().strftime("%Y%m%d_%H%M%S")
+ out_root.mkdir(parents=True, exist_ok=True)
+
+ tools = _register_tools()
+ env = tools["validate_environment"](ValidateEnvironmentParams())
+
+ report: Dict[str, Any] = {
+ "timestamp": datetime.now().isoformat(),
+ "output_root": str(out_root),
+ "environment": env,
+ "protocols": [],
+ }
+
+ runs = [
+ ("Paxos", docs_dir / "[Design Doc] Basic Paxos Protocol.txt"),
+ ("TwoPhaseCommit", docs_dir / "[Design Doc] Two Phase Commit.txt"),
+ ("MessageBroker", docs_dir / "[Design Doc] Simple Message Broker.txt"),
+ ("DistributedLock", docs_dir / "[Design Doc] Distributed Lock Server.txt"),
+ ("HotelManagement", docs_dir / "[Design Doc] Hotel Management Application.txt"),
+ ]
+
+ ensemble_size = 3 # Default ensemble size for higher reliability
+
+ for name, path in runs:
+ design_doc = _load_design_doc(path)
+ proto_result = run_protocol_ensemble(
+ tools, design_doc, out_root, name,
+ ensemble_size=ensemble_size,
+ )
+ report["protocols"].append(proto_result)
+
+ report_path = out_root / "mcp_e2e_report.json"
+ report_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
+ print(json.dumps(report, indent=2))
+ print(f"\nReport written to: {report_path}")
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Src/PeasyAI/scripts/regression_test.py b/Src/PeasyAI/scripts/regression_test.py
new file mode 100644
index 0000000000..822ba7b800
--- /dev/null
+++ b/Src/PeasyAI/scripts/regression_test.py
@@ -0,0 +1,818 @@
+#!/usr/bin/env python3
+"""
+PeasyAI MCP Regression Test Suite.
+
+Runs all protocol design docs through the full MCP pipeline and produces
+a scored report. Supports baseline comparison to detect regressions.
+
+Usage:
+ # Run and establish baseline:
+ python scripts/regression_test.py --save-baseline
+
+ # Run and compare against baseline:
+ python scripts/regression_test.py
+
+ # Run a single protocol:
+ python scripts/regression_test.py --protocol Paxos
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import logging
+import os
+import sys
+import time
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+PROJECT_ROOT = Path(__file__).resolve().parents[1]
+SRC_ROOT = PROJECT_ROOT / "src"
+sys.path.insert(0, str(PROJECT_ROOT))
+sys.path.insert(0, str(SRC_ROOT))
+
+from src.ui.mcp import server as mcp_server
+from src.ui.mcp.tools.env import register_env_tools, ValidateEnvironmentParams
+from src.ui.mcp.tools.generation import (
+ register_generation_tools,
+ GenerateProjectParams,
+ GenerateTypesEventsParams,
+ GenerateMachineParams,
+ GenerateSpecParams,
+ GenerateTestParams,
+ SavePFileParams,
+)
+from src.ui.mcp.tools.compilation import (
+ register_compilation_tools,
+ PCompileParams,
+ PCheckParams,
+)
+from src.ui.mcp.tools.fixing import (
+ register_fixing_tools,
+ FixIterativelyParams,
+ FixBuggyProgramParams,
+)
+from src.core.workflow.factory import extract_machine_names_from_design_doc
+from src.core.services.fixer import build_checker_feedback
+
+logger = logging.getLogger(__name__)
+
+BASELINE_PATH = PROJECT_ROOT / "tests" / "regression_baseline.json"
+RESULTS_DIR = PROJECT_ROOT / "generated_code" / "regression"
+
+# ============================================================================
+# Scoring
+# ============================================================================
+
+SCORE_WEIGHTS = {
+ "generation_all_ok": 20, # All files generated
+ "compile_first_try": 25, # Compiled without fixes
+ "compile_after_fix": 15, # Compiled after fix iterations
+ "tests_discovered": 15, # PChecker found > 0 test cases
+ "all_tests_pass": 25, # All PChecker tests pass
+}
+
+
+def score_protocol(result: Dict[str, Any]) -> Dict[str, Any]:
+ """Score a single protocol run on a 0-100 scale."""
+ scores: Dict[str, int] = {}
+
+ # Generation
+ steps = result.get("steps", [])
+ gen_ok = all(s.get("success") for s in steps)
+ scores["generation_all_ok"] = SCORE_WEIGHTS["generation_all_ok"] if gen_ok else 0
+
+ # Compilation
+ compile_ok = (result.get("compile") or {}).get("success", False)
+ fix_info = result.get("fix")
+ fix_ok = fix_info.get("success") if fix_info else False
+ compile_after_fix = (result.get("compile_after_fix") or {}).get("success", False)
+
+ if compile_ok:
+ scores["compile_first_try"] = SCORE_WEIGHTS["compile_first_try"]
+ scores["compile_after_fix"] = SCORE_WEIGHTS["compile_after_fix"]
+ elif compile_after_fix or fix_ok:
+ scores["compile_first_try"] = 0
+ scores["compile_after_fix"] = SCORE_WEIGHTS["compile_after_fix"]
+ else:
+ scores["compile_first_try"] = 0
+ scores["compile_after_fix"] = 0
+
+ # Tests
+ check = result.get("check") or {}
+ failed = check.get("failed_tests", [])
+ passed = check.get("passed_tests", [])
+ has_tests = bool(failed or passed)
+ all_pass = check.get("success", False)
+
+ scores["tests_discovered"] = SCORE_WEIGHTS["tests_discovered"] if has_tests else 0
+ scores["all_tests_pass"] = SCORE_WEIGHTS["all_tests_pass"] if all_pass else 0
+
+ total = sum(scores.values())
+ return {"scores": scores, "total": total, "max": 100}
+
+
+# ============================================================================
+# Protocol runner (reused from mcp_e2e_protocols.py)
+# ============================================================================
+
+class DummyMCP:
+ def tool(self, *args, **kwargs):
+ def decorator(fn): return fn
+ return decorator
+ def resource(self, *args, **kwargs):
+ def decorator(fn): return fn
+ return decorator
+
+
+def register_tools() -> Dict[str, Any]:
+ dummy = DummyMCP()
+ env = register_env_tools(dummy, mcp_server._with_metadata)
+ gen = register_generation_tools(dummy, mcp_server.get_services, mcp_server._with_metadata)
+ comp = register_compilation_tools(dummy, mcp_server.get_services, mcp_server._with_metadata)
+ fix = register_fixing_tools(dummy, mcp_server.get_services, mcp_server._with_metadata)
+ return {"validate_environment": env, **gen, **comp, **fix}
+
+
+def run_protocol(
+ tools: Dict[str, Any],
+ design_doc: str,
+ out_root: Path,
+ project_name: str,
+ ensemble_size: int = 1,
+) -> Dict[str, Any]:
+ """Run full pipeline for one protocol. Returns structured result."""
+ result: Dict[str, Any] = {
+ "project_name": project_name,
+ "success": False,
+ "steps": [],
+ "generated_files": [],
+ "compile": None,
+ "fix": None,
+ "check": None,
+ "checker_fixes": [],
+ "errors": [],
+ "timing": {},
+ }
+
+ t0 = time.time()
+
+ # --- Generate project structure ---
+ create = tools["generate_project_structure"](
+ GenerateProjectParams(design_doc=design_doc, output_dir=str(out_root), project_name=project_name)
+ )
+ result["steps"].append({"name": "generate_project_structure", "success": create.get("success")})
+ if not create.get("success"):
+ result["errors"].append(create.get("error"))
+ result["timing"]["total_s"] = round(time.time() - t0, 1)
+ return result
+ project_path = create["project_path"]
+
+ # --- Generate types/events ---
+ types_resp = tools["generate_types_events"](
+ GenerateTypesEventsParams(design_doc=design_doc, project_path=project_path)
+ )
+ result["steps"].append({"name": "generate_types_events", "success": types_resp.get("success")})
+ if not types_resp.get("success"):
+ result["errors"].append(types_resp.get("error"))
+ result["timing"]["total_s"] = round(time.time() - t0, 1)
+ return result
+
+ # --- Generate machines ---
+ # Use the LLM to extract machine names for robust handling of
+ # multi-word names, prose descriptions, etc.
+ services = mcp_server.get_services()
+ machine_names: List[str] = extract_machine_names_from_design_doc(
+ design_doc, llm_provider=services.get("llm_provider")
+ )
+ context_files = {"Enums_Types_Events.p": types_resp["code"]}
+ generated: Dict[str, Dict[str, Any]] = {"types": types_resp}
+
+ for mn in machine_names:
+ resp = tools["generate_machine"](
+ GenerateMachineParams(
+ machine_name=mn,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ )
+ )
+ generated[f"machine:{mn}"] = resp
+ result["steps"].append({"name": f"generate_machine:{mn}", "success": resp.get("success")})
+ if resp.get("success") and resp.get("filename") and resp.get("code"):
+ context_files[resp["filename"]] = resp["code"]
+
+ # --- Generate spec ---
+ spec_resp = tools["generate_spec"](
+ GenerateSpecParams(
+ spec_name="Safety",
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ )
+ )
+ generated["spec"] = spec_resp
+ result["steps"].append({"name": "generate_spec", "success": spec_resp.get("success")})
+
+ # --- Apply spec fixes (LLM review may have corrected the spec) ---
+ spec_fixes = spec_resp.get("spec_fixes") or {}
+ for sf_filename, sf_code in spec_fixes.items():
+ for key, payload in generated.items():
+ if payload.get("filename") == sf_filename or (payload.get("file_path") or "").endswith(sf_filename):
+ payload["code"] = sf_code
+ break
+ # Update context_files so test generation sees the corrected spec
+ context_files[sf_filename] = sf_code
+ # Also ensure the spec itself is in context_files for the test generator
+ if spec_resp.get("success") and spec_resp.get("filename") and spec_resp.get("code"):
+ context_files[spec_resp["filename"]] = spec_resp["code"]
+
+ # --- Generate test ---
+ test_resp = tools["generate_test"](
+ GenerateTestParams(
+ test_name="TestDriver",
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ )
+ )
+ generated["test"] = test_resp
+ result["steps"].append({"name": "generate_test", "success": test_resp.get("success")})
+
+ # --- Apply wiring fixes (LLM review may have modified other files) ---
+ wiring_fixes = test_resp.get("wiring_fixes") or {}
+ for wf_filename, wf_code in wiring_fixes.items():
+ # Determine the correct subdirectory for the fixed file
+ if "Enums" in wf_filename or "Types" in wf_filename:
+ wf_path = os.path.join(project_path, "PSrc", wf_filename)
+ elif "Spec" in wf_filename or "Safety" in wf_filename:
+ wf_path = os.path.join(project_path, "PSpec", wf_filename)
+ else:
+ wf_path = os.path.join(project_path, "PSrc", wf_filename)
+ # Update the generated dict so the corrected code gets saved
+ for key, payload in generated.items():
+ if payload.get("filename") == wf_filename or (payload.get("file_path") or "").endswith(wf_filename):
+ payload["code"] = wf_code
+ break
+ else:
+ # File not in generated dict — save it directly
+ generated[f"wiring_fix:{wf_filename}"] = {
+ "success": True, "file_path": wf_path, "code": wf_code, "filename": wf_filename,
+ }
+ # Also update context_files so subsequent operations see the fix
+ context_files[wf_filename] = wf_code
+
+ # --- Save all files ---
+ for _, payload in generated.items():
+ if payload.get("success") and payload.get("file_path") and payload.get("code"):
+ tools["save_p_file"](SavePFileParams(file_path=payload["file_path"], code=payload["code"]))
+
+ result["generated_files"] = sorted(
+ [v["file_path"] for v in generated.values() if v.get("success") and v.get("file_path")]
+ )
+
+ # --- Compile ---
+ compile_resp = tools["p_compile"](PCompileParams(path=project_path))
+ result["compile"] = {"success": compile_resp.get("success"), "error": compile_resp.get("error")}
+
+ if not compile_resp.get("success"):
+ fix_resp = tools["fix_iteratively"](FixIterativelyParams(project_path=project_path, max_iterations=8))
+ result["fix"] = fix_resp
+ compile_resp = tools["p_compile"](PCompileParams(path=project_path))
+ result["compile_after_fix"] = {"success": compile_resp.get("success"), "error": compile_resp.get("error")}
+
+ if not compile_resp.get("success"):
+ result["errors"].append("Compilation failed after fix attempts")
+ result["timing"]["total_s"] = round(time.time() - t0, 1)
+ return result
+
+ # --- PChecker ---
+ check_resp = tools["p_check"](PCheckParams(path=project_path, schedules=100, timeout=90, max_steps=10000))
+ result["check"] = {
+ "success": check_resp.get("success"),
+ "failed_tests": check_resp.get("failed_tests", []),
+ "passed_tests": check_resp.get("passed_tests", []),
+ "error": check_resp.get("error"),
+ }
+
+ # --- Auto-fix checker bugs (up to 2 rounds) ---
+ last_fix_bug_resp: Optional[Dict[str, Any]] = None
+ for rnd in range(2):
+ if check_resp.get("success") or not check_resp.get("failed_tests"):
+ break
+ fix_bug = tools["fix_buggy_program"](FixBuggyProgramParams(project_path=project_path))
+ last_fix_bug_resp = fix_bug
+ result["checker_fixes"].append({"round": rnd + 1, "fixed": fix_bug.get("fixed")})
+ if not fix_bug.get("fixed"):
+ break
+ recomp = tools["p_compile"](PCompileParams(path=project_path))
+ if not recomp.get("success"):
+ break
+ check_resp = tools["p_check"](PCheckParams(path=project_path, schedules=100, timeout=90, max_steps=10000))
+ result["check"] = {
+ "success": check_resp.get("success"),
+ "failed_tests": check_resp.get("failed_tests", []),
+ "passed_tests": check_resp.get("passed_tests", []),
+ "error": check_resp.get("error"),
+ }
+
+ # Stash the trace analysis so the adaptive retry can feed it to regen
+ if last_fix_bug_resp and not check_resp.get("success"):
+ result["_checker_analysis"] = last_fix_bug_resp.get("analysis")
+ result["_checker_root_cause"] = last_fix_bug_resp.get("root_cause")
+ result["_checker_suggested_fixes"] = last_fix_bug_resp.get("suggested_fixes")
+
+ result["success"] = bool(check_resp.get("success"))
+ if not result["success"]:
+ result["errors"].append("PChecker reported failing tests")
+
+ result["timing"]["total_s"] = round(time.time() - t0, 1)
+ return result
+
+
+def _read_project_context(project_path: str, tools: Dict[str, Any]) -> Dict[str, str]:
+ """Read all .p files from a project as context for targeted regeneration."""
+ from pathlib import Path as _P
+ ctx: Dict[str, str] = {}
+ for folder in ("PSrc", "PSpec", "PTst"):
+ folder_path = _P(project_path) / folder
+ if folder_path.exists():
+ for p_file in folder_path.glob("*.p"):
+ try:
+ ctx[p_file.name] = p_file.read_text(encoding="utf-8")
+ except Exception:
+ pass
+ return ctx
+
+
+def _diagnose_checker_failure(check_result: Dict[str, Any]) -> str:
+ """
+ Guess which artifact type is most likely responsible for a checker failure.
+
+ Returns one of: "spec", "test", "machine", or "unknown".
+
+ Heuristics (from most to least common root causes):
+ - "unhandled event" / "deadlock" → usually a machine missing defer/ignore
+ - "assertion" in trace → usually a spec with wrong invariant logic
+ - 0 tests discovered → test driver is broken
+ - all tests fail with no error context → try regenerating both spec and test
+ """
+ error_text = str(check_result.get("error", "")).lower()
+ failed = check_result.get("failed_tests", [])
+ passed = check_result.get("passed_tests", [])
+
+ if not failed and not passed:
+ return "test"
+
+ if "unhandled" in error_text or "deadlock" in error_text:
+ return "machine"
+
+ if "assert" in error_text:
+ return "spec"
+
+ # All tests fail, no passed tests → the whole spec + test combo is likely
+ # misconfigured. Regenerate both via "machine" culprit which triggers
+ # spec + test regen in the adaptive strategy.
+ if len(passed) == 0 and len(failed) > 0:
+ return "machine"
+
+ return "test"
+
+
+def _build_checker_hint(result: Dict[str, Any]) -> str:
+ """
+ Thin wrapper around ``build_checker_feedback`` from the core fixer
+ service. Reconstructs the ``check_result`` and ``fix_response``
+ dicts from the stashed fields in *result*.
+ """
+ check_result = result.get("check")
+ fix_response: Optional[Dict[str, Any]] = None
+
+ analysis = result.get("_checker_analysis")
+ if analysis:
+ fix_response = {
+ "analysis": analysis,
+ "root_cause": result.get("_checker_root_cause", ""),
+ "suggested_fixes": result.get("_checker_suggested_fixes"),
+ }
+
+ return build_checker_feedback(
+ check_result=check_result,
+ fix_response=fix_response,
+ )
+
+
+def run_protocol_adaptive(
+ tools: Dict[str, Any],
+ design_doc: str,
+ out_root: Path,
+ project_name: str,
+) -> Dict[str, Any]:
+ """
+ Failure-type-aware adaptive strategy:
+
+ 1. Fast pass: ensemble_size=1 for all files.
+ 2. If checker fails, diagnose which artifact is likely broken and
+ regenerate only that file (spec/test/machine) with ensemble_size=3.
+ 3. If compile fails entirely, regenerate the whole project with ensemble.
+ """
+ fast = run_protocol(tools, design_doc, out_root, project_name, ensemble_size=1)
+ fast["ensemble_used"] = 1
+
+ gen_ok = all(s.get("success") for s in fast.get("steps", []))
+ compile_ok = (
+ (fast.get("compile") or {}).get("success")
+ or (fast.get("compile_after_fix") or {}).get("success")
+ )
+ check_ok = (fast.get("check") or {}).get("success", False)
+
+ if gen_ok and compile_ok and check_ok:
+ fast["adaptive_retry"] = False
+ return fast
+
+ fast_score = score_protocol(fast)["total"]
+ fast["score"] = score_protocol(fast)
+
+ # ── If compilation completely failed, full regeneration with ensemble ──
+ if not compile_ok:
+ logger.info(f"[ADAPTIVE] {project_name}: compile failed, full regen with ensemble=3")
+ retry_root = out_root / f"{project_name}_full_retry"
+ retry_root.mkdir(parents=True, exist_ok=True)
+ robust = run_protocol(tools, design_doc, retry_root, project_name, ensemble_size=3)
+ robust["ensemble_used"] = 3
+ robust["adaptive_retry"] = "full_regen"
+ robust["prior_fast_score"] = fast_score
+ return robust
+
+ # ── Compiled OK but checker failed → targeted regeneration ────────────
+ if compile_ok and not check_ok:
+ check_result = fast.get("check") or {}
+ culprit = _diagnose_checker_failure(check_result)
+ logger.info(
+ f"[ADAPTIVE] {project_name}: checker failed, diagnosed culprit={culprit}, "
+ f"regenerating with ensemble=3"
+ )
+
+ project_path = None
+ for step in fast.get("steps", []):
+ if step.get("name") == "generate_project_structure":
+ break
+ # Recover project_path from generated_files
+ gen_files = fast.get("generated_files", [])
+ if gen_files:
+ from pathlib import Path as _P
+ first_file = _P(gen_files[0])
+ project_path = str(first_file.parent.parent)
+
+ if not project_path:
+ logger.info(f"[ADAPTIVE] {project_name}: could not find project_path, full regen")
+ retry_root = out_root / f"{project_name}_full_retry"
+ retry_root.mkdir(parents=True, exist_ok=True)
+ robust = run_protocol(tools, design_doc, retry_root, project_name, ensemble_size=3)
+ robust["ensemble_used"] = 3
+ robust["adaptive_retry"] = "full_regen_fallback"
+ robust["prior_fast_score"] = fast_score
+ return robust
+
+ # Read current project files as context
+ ctx = _read_project_context(project_path, tools)
+
+ # Build a checker-bug summary from the trace analysis so the LLM
+ # can avoid the same mistake during regeneration.
+ checker_hint = _build_checker_hint(fast)
+ if checker_hint:
+ logger.info(f"[ADAPTIVE] Injecting checker bug context ({len(checker_hint)} chars)")
+
+ regen_count = 0
+
+ if culprit == "spec":
+ logger.info(f"[ADAPTIVE] {project_name}: regenerating spec only")
+ spec_resp = tools["generate_spec"](
+ GenerateSpecParams(
+ spec_name="Safety",
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=ctx,
+ ensemble_size=3,
+ checker_feedback=checker_hint or None,
+ )
+ )
+ if spec_resp.get("success") and spec_resp.get("file_path") and spec_resp.get("code"):
+ tools["save_p_file"](SavePFileParams(file_path=spec_resp["file_path"], code=spec_resp["code"]))
+ regen_count += 1
+
+ elif culprit == "test":
+ logger.info(f"[ADAPTIVE] {project_name}: regenerating test only")
+ test_resp = tools["generate_test"](
+ GenerateTestParams(
+ test_name="TestDriver",
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=ctx,
+ ensemble_size=3,
+ checker_feedback=checker_hint or None,
+ )
+ )
+ if test_resp.get("success") and test_resp.get("file_path") and test_resp.get("code"):
+ tools["save_p_file"](SavePFileParams(file_path=test_resp["file_path"], code=test_resp["code"]))
+ regen_count += 1
+
+ elif culprit == "machine":
+ logger.info(f"[ADAPTIVE] {project_name}: regenerating spec + test (machine bugs often surface there)")
+ for regen_type, regen_params in [
+ ("spec", GenerateSpecParams(
+ spec_name="Safety", design_doc=design_doc,
+ project_path=project_path, context_files=ctx, ensemble_size=3,
+ checker_feedback=checker_hint or None,
+ )),
+ ("test", GenerateTestParams(
+ test_name="TestDriver", design_doc=design_doc,
+ project_path=project_path, context_files=ctx, ensemble_size=3,
+ checker_feedback=checker_hint or None,
+ )),
+ ]:
+ if regen_type == "spec":
+ resp = tools["generate_spec"](regen_params)
+ else:
+ resp = tools["generate_test"](regen_params)
+ if resp.get("success") and resp.get("file_path") and resp.get("code"):
+ tools["save_p_file"](SavePFileParams(file_path=resp["file_path"], code=resp["code"]))
+ regen_count += 1
+
+ # Recompile after targeted regen
+ if regen_count > 0:
+ compile_resp = tools["p_compile"](PCompileParams(path=project_path))
+ if not compile_resp.get("success"):
+ fix_resp = tools["fix_iteratively"](
+ FixIterativelyParams(project_path=project_path, max_iterations=5)
+ )
+ compile_resp = tools["p_compile"](PCompileParams(path=project_path))
+
+ if compile_resp.get("success"):
+ check_resp = tools["p_check"](PCheckParams(path=project_path, schedules=100, timeout=90, max_steps=10000))
+
+ # One more fix_buggy_program attempt if still failing
+ if not check_resp.get("success") and check_resp.get("failed_tests"):
+ fix_bug = tools["fix_buggy_program"](FixBuggyProgramParams(project_path=project_path))
+ if fix_bug.get("fixed"):
+ recomp = tools["p_compile"](PCompileParams(path=project_path))
+ if recomp.get("success"):
+ check_resp = tools["p_check"](PCheckParams(path=project_path, schedules=100, timeout=90, max_steps=10000))
+
+ fast["check"] = {
+ "success": check_resp.get("success"),
+ "failed_tests": check_resp.get("failed_tests", []),
+ "passed_tests": check_resp.get("passed_tests", []),
+ "error": check_resp.get("error"),
+ }
+ fast["success"] = bool(check_resp.get("success"))
+
+ fast["adaptive_retry"] = f"targeted_{culprit}"
+ fast["prior_fast_score"] = fast_score
+ fast["regen_count"] = regen_count
+ return fast
+
+ # Fallback: return the fast result as-is
+ fast["adaptive_retry"] = False
+ return fast
+
+
+# ============================================================================
+# Baseline comparison
+# ============================================================================
+
+def compare_with_baseline(
+ current: Dict[str, Any], baseline: Dict[str, Any]
+) -> Dict[str, Any]:
+ """Compare current run against baseline. Returns diff report."""
+ diff: Dict[str, Any] = {"regressions": [], "improvements": [], "unchanged": []}
+
+ current_protos = {p["project_name"]: p for p in current.get("protocols", [])}
+ baseline_protos = {p["project_name"]: p for p in baseline.get("protocols", [])}
+
+ for name in sorted(set(list(current_protos.keys()) + list(baseline_protos.keys()))):
+ cur = current_protos.get(name)
+ base = baseline_protos.get(name)
+
+ if not base:
+ diff["improvements"].append({"protocol": name, "reason": "new protocol added"})
+ continue
+ if not cur:
+ diff["regressions"].append({"protocol": name, "reason": "protocol removed"})
+ continue
+
+ cur_score = cur.get("score", {}).get("total", 0)
+ base_score = base.get("score", {}).get("total", 0)
+ delta = cur_score - base_score
+
+ entry = {
+ "protocol": name,
+ "baseline_score": base_score,
+ "current_score": cur_score,
+ "delta": delta,
+ }
+
+ # Check specific regressions
+ details = []
+ if (base.get("compile") or {}).get("success") and not (cur.get("compile") or {}).get("success"):
+ details.append("compile regressed")
+ if (base.get("check") or {}).get("success") and not (cur.get("check") or {}).get("success"):
+ details.append("checker regressed")
+ base_gen = all(s.get("success") for s in (base.get("steps") or []))
+ cur_gen = all(s.get("success") for s in (cur.get("steps") or []))
+ if base_gen and not cur_gen:
+ details.append("generation regressed")
+
+ entry["details"] = details
+
+ if delta < 0:
+ diff["regressions"].append(entry)
+ elif delta > 0:
+ diff["improvements"].append(entry)
+ else:
+ diff["unchanged"].append(entry)
+
+ return diff
+
+
+def print_report(report: Dict[str, Any], diff: Optional[Dict[str, Any]] = None):
+ """Print a human-readable report."""
+ print("\n" + "=" * 70)
+ print(" PeasyAI MCP Regression Test Report")
+ print(f" {report['timestamp']}")
+ print("=" * 70)
+
+ total_score = 0
+ max_score = 0
+
+ for p in report["protocols"]:
+ name = p["project_name"]
+ sc = p.get("score", {})
+ total = sc.get("total", 0)
+ total_score += total
+ max_score += 100
+
+ gen_ok = all(s.get("success") for s in p.get("steps", []))
+ comp_ok = (p.get("compile") or {}).get("success", False)
+ check = p.get("check") or {}
+ check_ok = check.get("success", False)
+ passed = len(check.get("passed_tests", []))
+ failed = len(check.get("failed_tests", []))
+ timing = p.get("timing", {}).get("total_s", "?")
+
+ status = "✅ PASS" if p["success"] else "❌ FAIL"
+ retries = p.get("retries_used", 0)
+ retry_tag = f" [retry {retries}x]" if retries else ""
+
+ print(f"\n {name}: {status} (score: {total}/100, {timing}s{retry_tag})")
+ print(f" generate: {'✅' if gen_ok else '❌'} "
+ f"compile: {'✅' if comp_ok else '❌'} "
+ f"check: {'✅' if check_ok else '❌'} ({passed} passed, {failed} failed)")
+
+ if sc.get("scores"):
+ breakdown = ", ".join(f"{k}={v}" for k, v in sc["scores"].items())
+ print(f" scores: {breakdown}")
+
+ pct = (total_score / max_score * 100) if max_score else 0
+ print(f"\n AGGREGATE: {total_score}/{max_score} ({pct:.0f}%)")
+
+ if diff:
+ print(f"\n --- Baseline Comparison ---")
+ if diff["improvements"]:
+ for d in diff["improvements"]:
+ print(f" ⬆️ {d['protocol']}: +{d.get('delta', '?')} pts {d.get('details', d.get('reason', ''))}")
+ if diff["regressions"]:
+ for d in diff["regressions"]:
+ print(f" ⬇️ {d['protocol']}: {d.get('delta', '?')} pts {d.get('details', d.get('reason', ''))}")
+ if diff["unchanged"]:
+ for d in diff["unchanged"]:
+ print(f" ➡️ {d['protocol']}: unchanged ({d.get('current_score', '?')}/100)")
+ if not diff["regressions"]:
+ print(" ✅ No regressions detected!")
+ else:
+ print(f" ⚠️ {len(diff['regressions'])} regression(s) detected!")
+
+ print("\n" + "=" * 70)
+
+
+# ============================================================================
+# Main
+# ============================================================================
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="PeasyAI MCP Regression Tests")
+ parser.add_argument("--save-baseline", action="store_true", help="Save results as new baseline")
+ parser.add_argument("--protocol", type=str, help="Run a single protocol by name")
+ parser.add_argument("--protocols", type=str, help="Comma-separated list of protocols to run")
+ parser.add_argument("--no-compare", action="store_true", help="Skip baseline comparison")
+ args = parser.parse_args()
+
+ logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
+ # Suppress noisy Streamlit warnings when running outside Streamlit
+ logging.getLogger("streamlit").setLevel(logging.ERROR)
+
+ docs_dir = PROJECT_ROOT / "resources" / "system_design_docs"
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ out_root = RESULTS_DIR / timestamp
+ out_root.mkdir(parents=True, exist_ok=True)
+
+ tools = register_tools()
+ env = tools["validate_environment"](ValidateEnvironmentParams())
+
+ all_runs = [
+ ("Paxos", docs_dir / "[Design Doc] Basic Paxos Protocol.md"),
+ ("TwoPhaseCommit", docs_dir / "[Design Doc] Two Phase Commit.md"),
+ ("MessageBroker", docs_dir / "[Design Doc] Simple Message Broker.md"),
+ ("DistributedLock", docs_dir / "[Design Doc] Distributed Lock Server.md"),
+ ("HotelManagement", docs_dir / "[Design Doc] Hotel Management Application.md"),
+ ("ClientServer", docs_dir / "[Design Doc] Client Server.md"),
+ ("FailureDetector", docs_dir / "[Design Doc] Failure Detector.md"),
+ ("EspressoMachine", docs_dir / "[Design Doc] Espresso Machine.md"),
+ ("RaftLeaderElection", docs_dir / "[Design Doc] Raft Leader Election.md"),
+ ]
+
+ if args.protocol:
+ all_runs = [(n, p) for n, p in all_runs if n == args.protocol]
+ if not all_runs:
+ print(f"Unknown protocol: {args.protocol}")
+ return 1
+ elif args.protocols:
+ names = {s.strip() for s in args.protocols.split(",")}
+ all_runs = [(n, p) for n, p in all_runs if n in names]
+ if not all_runs:
+ print(f"No matching protocols for: {args.protocols}")
+ return 1
+
+ report: Dict[str, Any] = {
+ "timestamp": datetime.now().isoformat(),
+ "output_root": str(out_root),
+ "environment": env,
+ "protocols": [],
+ }
+
+ # Only retry when generation or compilation completely failed (score ≤ 35).
+ # Checker bugs are better addressed by fix_buggy_program, not regeneration.
+ RETRY_THRESHOLD = 35
+ MAX_PROTOCOL_RETRIES = 2
+
+ for name, path in all_runs:
+ design_doc = path.read_text(encoding="utf-8")
+ best_result = None
+ best_score = -1
+
+ for attempt in range(1, MAX_PROTOCOL_RETRIES + 1):
+ attempt_out = out_root if attempt == 1 else out_root / f"{name}_retry{attempt}"
+ if attempt > 1:
+ attempt_out.mkdir(parents=True, exist_ok=True)
+ logger.info(f"[RETRY] {name} attempt {attempt} (previous score: {best_score})")
+
+ result = run_protocol_adaptive(tools, design_doc, attempt_out, name)
+ result["score"] = score_protocol(result)
+ result["attempt"] = attempt
+ current_score = result["score"]["total"]
+
+ if current_score > best_score:
+ best_score = current_score
+ best_result = result
+
+ if current_score > RETRY_THRESHOLD:
+ break # Compiled successfully, no need to retry full generation
+
+ assert best_result is not None
+ if best_result.get("attempt", 1) > 1:
+ best_result["retries_used"] = best_result["attempt"] - 1
+ report["protocols"].append(best_result)
+
+ # Save report
+ report_path = out_root / "regression_report.json"
+ report_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
+
+ # Baseline comparison
+ diff = None
+ if not args.no_compare and BASELINE_PATH.exists():
+ baseline = json.loads(BASELINE_PATH.read_text(encoding="utf-8"))
+ diff = compare_with_baseline(report, baseline)
+ report["baseline_diff"] = diff
+
+ # Print report
+ print_report(report, diff)
+
+ # Save baseline if requested
+ if args.save_baseline:
+ BASELINE_PATH.parent.mkdir(parents=True, exist_ok=True)
+ BASELINE_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
+ print(f"\n Baseline saved to: {BASELINE_PATH}")
+
+ # Exit code: 1 if regressions detected
+ if diff and diff.get("regressions"):
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Src/PeasyAI/scripts/run_contract_tests.sh b/Src/PeasyAI/scripts/run_contract_tests.sh
new file mode 100755
index 0000000000..9f8c225a1a
--- /dev/null
+++ b/Src/PeasyAI/scripts/run_contract_tests.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+
+cd "${PROJECT_ROOT}"
+
+python3 scripts/check_dependencies.py
+python3 -m unittest tests.test_mcp_contracts -v
diff --git a/Src/PeasyAI/src/app.py b/Src/PeasyAI/src/app.py
new file mode 100644
index 0000000000..03f0b901f0
--- /dev/null
+++ b/Src/PeasyAI/src/app.py
@@ -0,0 +1,69 @@
+import streamlit as st
+from core.modes.DesignDocInputModeV2 import DesignDocInputModeV2Wrapper
+from ui.stlit.SideNav import SideNav
+from core.modes.pchecker_mode import PCheckerMode
+
+# Set up the browser page configurations, including tab title and tab icon
+st.set_page_config(page_title='PeasyAI', layout='wide', page_icon = "ui/assets/p_icon.ico")
+
+# Title and containers for different sections of the webpage
+st.title('PeasyAI')
+back_button, clear_button = st.columns([1.5, 11]) # Container for the topmost "Back to Start Page" button and "Clear" button
+coding_language = "kotlin"
+
+def is_home_page():
+ return 'p_easyai_state' not in st.session_state or st.session_state['p_easyai_state'] is None
+
+def is_chat_history_page():
+ return "display_history" in st.session_state and st.session_state["display_history"]
+
+def change_app_state(new_state):
+ """
+ Change the app state where each state contains different ways to interact with the app.
+
+ Parameters:
+ new_state (int): The new state to set for the app.
+ """
+ st.session_state['p_easyai_state'] = new_state
+
+def back():
+ st.session_state['p_easyai_state'] = None
+ if "mode_state" in st.session_state:
+ del st.session_state["mode_state"]
+ # chat_history.save_conversation()
+ # chat_history.clear_conversation()
+
+
+def display_page():
+ st.empty()
+ SideNav()
+
+ # If showing past interactions from chat history, don't display any app mode
+ if is_chat_history_page():
+ return
+
+ if is_home_page():
+ display_home_page()
+ else:
+ # back_button.button('Back to Start Page', on_click=back)
+ if st.session_state['p_easyai_state'] == "DesignDocInputMode":
+ DesignDocInputModeV2Wrapper()
+ elif st.session_state['p_easyai_state'] == "PCheckerMode":
+ PCheckerMode()
+ else:
+ display_home_page()
+
+
+def display_home_page():
+ st.session_state['p_easyai_state'] = None
+ st.session_state["display_history"] = False
+
+ st.write("Hi! I'm an AI Assistant trained on writing P Code. What would you like me to do today?")
+ st.button("Generate P Code from a design doc", on_click=change_app_state, args = ["DesignDocInputMode"])
+ st.button("Run PChecker on P project", on_click=change_app_state, args=["PCheckerMode"])
+
+
+def main():
+ display_page()
+
+main()
diff --git a/Src/PeasyAI/src/core/compilation/__init__.py b/Src/PeasyAI/src/core/compilation/__init__.py
new file mode 100644
index 0000000000..9381ac12d0
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/__init__.py
@@ -0,0 +1,91 @@
+"""
+P Compilation Module
+
+Provides compilation, error parsing, and error fixing capabilities.
+"""
+
+from .error_parser import (
+ PCompilerError,
+ PCompilerErrorParser,
+ ErrorType,
+ ErrorCategory,
+ CompilationResult,
+ parse_compilation_output,
+)
+
+from .error_fixers import (
+ CodeFix,
+ PErrorFixer,
+ apply_fix,
+ fix_all_errors,
+)
+
+from .environment import (
+ EnvironmentInfo,
+ EnvironmentDetector,
+ ensure_environment,
+ get_compile_command,
+ get_check_command,
+)
+
+from .checker_error_parser import (
+ CheckerErrorCategory,
+ CheckerError,
+ TraceAnalysis,
+ PCheckerErrorParser,
+ MachineState,
+ EventInfo,
+ SenderInfo,
+ CascadingImpact,
+)
+
+from .checker_fixers import (
+ CheckerFix,
+ FilePatch,
+ PCheckerErrorFixer,
+ analyze_and_suggest_fix,
+)
+
+from .p_post_processor import (
+ PCodePostProcessor,
+ PostProcessResult,
+ post_process_file,
+)
+
+__all__ = [
+ # Error parsing
+ "PCompilerError",
+ "PCompilerErrorParser",
+ "ErrorType",
+ "ErrorCategory",
+ "CompilationResult",
+ "parse_compilation_output",
+ # Error fixing
+ "CodeFix",
+ "PErrorFixer",
+ "apply_fix",
+ "fix_all_errors",
+ # Environment
+ "EnvironmentInfo",
+ "EnvironmentDetector",
+ "ensure_environment",
+ "get_compile_command",
+ "get_check_command",
+ # Checker error handling
+ "CheckerErrorCategory",
+ "CheckerError",
+ "TraceAnalysis",
+ "PCheckerErrorParser",
+ "MachineState",
+ "EventInfo",
+ "SenderInfo",
+ "CascadingImpact",
+ "CheckerFix",
+ "FilePatch",
+ "PCheckerErrorFixer",
+ "analyze_and_suggest_fix",
+ # Post-processing
+ "PCodePostProcessor",
+ "PostProcessResult",
+ "post_process_file",
+]
diff --git a/Src/PeasyAI/src/core/compilation/checker_error_parser.py b/Src/PeasyAI/src/core/compilation/checker_error_parser.py
new file mode 100644
index 0000000000..37490d4551
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/checker_error_parser.py
@@ -0,0 +1,760 @@
+"""
+PChecker Error Parser and Analyzer
+
+Parses PChecker trace logs to identify error types and extract actionable information.
+"""
+
+import re
+import logging
+from typing import Dict, List, Optional, Any, Tuple
+from dataclasses import dataclass, field
+from enum import Enum
+
+logger = logging.getLogger(__name__)
+
+
+class CheckerErrorCategory(Enum):
+ """Categories of PChecker errors."""
+ NULL_TARGET = "null_target"
+ UNHANDLED_EVENT = "unhandled_event"
+ ASSERTION_FAILURE = "assertion_failure"
+ DEADLOCK = "deadlock"
+ LIVENESS_VIOLATION = "liveness_violation"
+ QUEUE_OVERFLOW = "queue_overflow"
+ UNKNOWN = "unknown"
+
+
+@dataclass
+class MachineState:
+ """Represents a machine's state at a point in execution."""
+ machine_id: str
+ machine_type: str
+ state: str
+
+ @classmethod
+ def from_log(cls, log_line: str) -> Optional["MachineState"]:
+ """Parse machine state from a log line."""
+ # Pattern: 'MachineName(ID)' in state 'StateName'
+ match = re.search(r"'?(\w+)\((\d+)\)'?\s+(?:in state|enters state|exits state)\s+'([^']+)'", log_line)
+ if match:
+ return cls(
+ machine_id=match.group(2),
+ machine_type=match.group(1),
+ state=match.group(3).split('.')[-1] # Get just the state name
+ )
+ return None
+
+
+@dataclass
+class EventInfo:
+ """Information about an event in the trace."""
+ event_name: str
+ payload: Optional[str] = None
+ sender: Optional[str] = None
+ receiver: Optional[str] = None
+
+ @classmethod
+ def from_log(cls, log_line: str) -> Optional["EventInfo"]:
+ """Parse event info from a log line."""
+ # SendLog pattern
+ send_match = re.search(
+ r"'(\w+)\((\d+)\)'\s+.*sent event '(\w+)(?:\s+with payload \(([^)]+)\))?'\s+to '(\w+)\((\d+)\)'",
+ log_line
+ )
+ if send_match:
+ return cls(
+ event_name=send_match.group(3),
+ payload=send_match.group(4),
+ sender=f"{send_match.group(1)}({send_match.group(2)})",
+ receiver=f"{send_match.group(5)}({send_match.group(6)})"
+ )
+
+ # DequeueLog pattern
+ dequeue_match = re.search(
+ r"'(\w+)\((\d+)\)'\s+dequeued event '(\w+)(?:\s+with payload \(([^)]+)\))?'",
+ log_line
+ )
+ if dequeue_match:
+ return cls(
+ event_name=dequeue_match.group(3),
+ payload=dequeue_match.group(4),
+ receiver=f"{dequeue_match.group(1)}({dequeue_match.group(2)})"
+ )
+
+ return None
+
+
+@dataclass
+class SenderInfo:
+ """Information about who sent the problematic event."""
+ machine_type: Optional[str] = None
+ machine_id: Optional[str] = None
+ state: Optional[str] = None
+ is_test_driver: bool = False
+ event_payload: Optional[str] = None
+ is_initialization_pattern: bool = False
+ semantic_mismatch: Optional[str] = None
+
+ @property
+ def machine(self) -> Optional[str]:
+ if self.machine_type and self.machine_id:
+ return f"{self.machine_type}({self.machine_id})"
+ return self.machine_type
+
+
+@dataclass
+class CascadingImpact:
+ """Analysis of which other machines/states would also be affected."""
+ # machines that also lack handlers for the event: {machine_type: [states]}
+ unhandled_in: Dict[str, List[str]] = field(default_factory=dict)
+ # machines that broadcast this event
+ broadcasters: List[str] = field(default_factory=list)
+ # all receiver machines for this event
+ all_receivers: List[str] = field(default_factory=list)
+
+
+@dataclass
+class CheckerError:
+ """Parsed PChecker error with analysis."""
+ category: CheckerErrorCategory
+ message: str
+ machine_type: Optional[str] = None
+ machine_id: Optional[str] = None
+ machine_state: Optional[str] = None
+ event_name: Optional[str] = None
+ target_field: Optional[str] = None
+ raw_error_line: Optional[str] = None
+
+ # Analysis results
+ root_cause: Optional[str] = None
+ affected_machines: List[str] = field(default_factory=list)
+ suggested_fixes: List[str] = field(default_factory=list)
+
+ # Enhanced analysis fields
+ sender_info: Optional[SenderInfo] = None
+ cascading_impact: Optional[CascadingImpact] = None
+ is_test_driver_bug: bool = False
+ requires_new_event: bool = False
+ requires_multi_file_fix: bool = False
+
+ @property
+ def machine(self) -> Optional[str]:
+ if self.machine_type and self.machine_id:
+ return f"{self.machine_type}({self.machine_id})"
+ return self.machine_type
+
+
+@dataclass
+class TraceAnalysis:
+ """Complete analysis of a PChecker trace."""
+ error: CheckerError
+ execution_steps: int
+ machines_involved: Dict[str, List[str]] # machine_type -> [states visited]
+ events_sent: List[EventInfo]
+ last_actions: List[str]
+
+ def get_summary(self) -> str:
+ """Get a human-readable summary."""
+ lines = [
+ f"## PChecker Error Analysis",
+ f"",
+ f"**Error Category:** {self.error.category.value}",
+ f"**Error Message:** {self.error.message}",
+ ]
+
+ if self.error.machine:
+ lines.append(f"**Affected Machine:** {self.error.machine} in state '{self.error.machine_state}'")
+
+ if self.error.sender_info:
+ sender = self.error.sender_info
+ lines.append(f"**Sender:** {sender.machine or 'unknown'} in state '{sender.state or 'unknown'}'")
+ if sender.is_test_driver:
+ lines.append(f"**Bug Location:** Test driver (not protocol logic)")
+ if sender.is_initialization_pattern:
+ lines.append(f"**Pattern:** Event used for initialization (semantic mismatch)")
+ if sender.semantic_mismatch:
+ lines.append(f"**Semantic Mismatch:** {sender.semantic_mismatch}")
+
+ if self.error.cascading_impact and self.error.cascading_impact.unhandled_in:
+ lines.append(f"")
+ lines.append(f"### Cascading Impact")
+ for machine, states in self.error.cascading_impact.unhandled_in.items():
+ lines.append(f" - {machine} also lacks handler in states: {', '.join(states)}")
+
+ if self.error.root_cause:
+ lines.append(f"")
+ lines.append(f"### Root Cause Analysis")
+ lines.append(self.error.root_cause)
+
+ if self.error.suggested_fixes:
+ lines.append(f"")
+ lines.append(f"### Suggested Fixes")
+ for i, fix in enumerate(self.error.suggested_fixes, 1):
+ lines.append(f"{i}. {fix}")
+
+ if self.last_actions:
+ lines.append(f"")
+ lines.append(f"### Last Actions Before Error")
+ for action in self.last_actions[-5:]:
+ lines.append(f" - {action}")
+
+ return "\n".join(lines)
+
+
+class PCheckerErrorParser:
+ """Parses and analyzes PChecker error traces."""
+
+ # Error patterns
+ NULL_TARGET_PATTERN = re.compile(
+ r"Target in send cannot be null\. Machine (\w+)\((\d+)\) trying to send event (\w+) to null target in state (\w+)"
+ )
+
+ UNHANDLED_EVENT_PATTERN = re.compile(
+ r"(\w+)\((\d+)\) received event '([^']+)' that cannot be handled"
+ )
+
+ ASSERTION_PATTERN = re.compile(
+ r"Assertion .* failed|assert .* failed|AssertionFailure"
+ )
+
+ DEADLOCK_PATTERN = re.compile(
+ r"[Dd]eadlock|potential deadlock"
+ )
+
+ LIVENESS_PATTERN = re.compile(
+ r"[Ll]iveness|hot state|[Ll]iveness monitor"
+ )
+
+ def parse(self, trace_log: str) -> CheckerError:
+ """Parse a trace log and return a CheckerError."""
+ lines = trace_log.strip().split('\n')
+
+ # Find the error line
+ error_line = None
+ for line in lines:
+ if '' in line:
+ error_line = line
+ break
+
+ if not error_line:
+ # Try to find any error indicator
+ for line in reversed(lines):
+ if 'error' in line.lower() or 'bug' in line.lower():
+ error_line = line
+ break
+
+ if not error_line:
+ return CheckerError(
+ category=CheckerErrorCategory.UNKNOWN,
+ message="Could not identify error from trace",
+ raw_error_line=lines[-1] if lines else None
+ )
+
+ # Try to match specific error patterns
+
+ # 1. Null target error
+ null_match = self.NULL_TARGET_PATTERN.search(error_line)
+ if null_match:
+ return CheckerError(
+ category=CheckerErrorCategory.NULL_TARGET,
+ message=error_line,
+ machine_type=null_match.group(1),
+ machine_id=null_match.group(2),
+ event_name=null_match.group(3),
+ machine_state=null_match.group(4),
+ raw_error_line=error_line,
+ )
+
+ # 2. Unhandled event error
+ unhandled_match = self.UNHANDLED_EVENT_PATTERN.search(error_line)
+ if unhandled_match:
+ raw_event = unhandled_match.group(3)
+ # Strip namespace prefix (e.g., PImplementation.eLearn -> eLearn)
+ clean_event = raw_event.split('.')[-1] if '.' in raw_event else raw_event
+ return CheckerError(
+ category=CheckerErrorCategory.UNHANDLED_EVENT,
+ message=error_line,
+ machine_type=unhandled_match.group(1),
+ machine_id=unhandled_match.group(2),
+ event_name=clean_event,
+ raw_error_line=error_line,
+ )
+
+ # 3. Assertion failure
+ if self.ASSERTION_PATTERN.search(error_line):
+ return CheckerError(
+ category=CheckerErrorCategory.ASSERTION_FAILURE,
+ message=error_line,
+ raw_error_line=error_line,
+ )
+
+ # 4. Deadlock
+ if self.DEADLOCK_PATTERN.search(error_line):
+ return CheckerError(
+ category=CheckerErrorCategory.DEADLOCK,
+ message=error_line,
+ raw_error_line=error_line,
+ )
+
+ # 5. Liveness violation
+ if self.LIVENESS_PATTERN.search(error_line):
+ return CheckerError(
+ category=CheckerErrorCategory.LIVENESS_VIOLATION,
+ message=error_line,
+ raw_error_line=error_line,
+ )
+
+ return CheckerError(
+ category=CheckerErrorCategory.UNKNOWN,
+ message=error_line,
+ raw_error_line=error_line,
+ )
+
+ def analyze(self, trace_log: str, project_files: Dict[str, str] = None) -> TraceAnalysis:
+ """Perform full analysis of a trace log."""
+ error = self.parse(trace_log)
+
+ lines = trace_log.strip().split('\n')
+
+ # Extract execution info
+ machines_involved: Dict[str, List[str]] = {}
+ events_sent: List[EventInfo] = []
+ last_actions: List[str] = []
+
+ for line in lines:
+ # Track machine states
+ state = MachineState.from_log(line)
+ if state:
+ if state.machine_type not in machines_involved:
+ machines_involved[state.machine_type] = []
+ if state.state not in machines_involved[state.machine_type]:
+ machines_involved[state.machine_type].append(state.state)
+
+ # Track events
+ event = EventInfo.from_log(line)
+ if event:
+ events_sent.append(event)
+
+ # Track recent actions
+ if any(x in line for x in ['', '', '', '']):
+ # Clean up the line
+ clean = line.replace('', '').replace('', '')
+ clean = clean.replace('', '').replace('', '').strip()
+ last_actions.append(clean)
+
+ # Analyze root cause based on error type
+ self._analyze_root_cause(error, trace_log, project_files or {}, machines_involved)
+
+ return TraceAnalysis(
+ error=error,
+ execution_steps=len([l for l in lines if '<' in l and 'Log>' in l]),
+ machines_involved=machines_involved,
+ events_sent=events_sent,
+ last_actions=last_actions[-10:], # Keep last 10
+ )
+
+ def _analyze_root_cause(
+ self,
+ error: CheckerError,
+ trace_log: str,
+ project_files: Dict[str, str],
+ machines_involved: Dict[str, List[str]]
+ ):
+ """Analyze the root cause and generate suggestions."""
+
+ if error.category == CheckerErrorCategory.NULL_TARGET:
+ self._analyze_null_target(error, trace_log, project_files)
+
+ elif error.category == CheckerErrorCategory.UNHANDLED_EVENT:
+ self._analyze_unhandled_event(error, trace_log, project_files)
+
+ elif error.category == CheckerErrorCategory.ASSERTION_FAILURE:
+ self._analyze_assertion_failure(error, trace_log, project_files)
+
+ elif error.category == CheckerErrorCategory.DEADLOCK:
+ self._analyze_deadlock(error, trace_log, project_files, machines_involved)
+
+ def _analyze_null_target(
+ self,
+ error: CheckerError,
+ trace_log: str,
+ project_files: Dict[str, str]
+ ):
+ """Analyze null target error."""
+ error.root_cause = (
+ f"Machine '{error.machine_type}' tried to send event '{error.event_name}' "
+ f"to a null machine reference in state '{error.machine_state}'. "
+ f"This typically means a machine field was not initialized before use."
+ )
+
+ # Find the machine file
+ machine_file = None
+ for filename, content in project_files.items():
+ if f"machine {error.machine_type}" in content:
+ machine_file = filename
+ break
+
+ if machine_file:
+ # Try to identify the field
+ content = project_files[machine_file]
+
+ # Look for the state where the error occurred
+ state_pattern = rf"state\s+{error.machine_state}\s*\{{"
+ state_match = re.search(state_pattern, content)
+
+ # Look for send statements with the event
+ send_pattern = rf"send\s+(\w+)\s*,\s*{error.event_name}"
+ send_matches = re.findall(send_pattern, content)
+
+ if send_matches:
+ target_var = send_matches[0]
+ error.target_field = target_var
+
+ error.suggested_fixes = [
+ f"Initialize the '{target_var}' field before entering state '{error.machine_state}'",
+ f"Add a configuration event (e.g., eConfigureXXX) to set '{target_var}' during initialization",
+ f"Use a 'with' clause on the transition to pass the target machine reference",
+ f"Check if '{target_var}' is set in the machine's start state entry function",
+ ]
+ else:
+ error.suggested_fixes = [
+ "Ensure all machine references are initialized before sending events",
+ "Add configuration events to wire machines together at startup",
+ "Check the test driver/harness to ensure proper machine initialization order",
+ ]
+
+ def _analyze_unhandled_event(
+ self,
+ error: CheckerError,
+ trace_log: str,
+ project_files: Dict[str, str]
+ ):
+ """Analyze unhandled event error with sender trace-back and cascading impact."""
+ # Find what state the machine was in
+ state_match = re.search(
+ rf"{error.machine_type}\({error.machine_id}\).*state\s+'([^']+)'",
+ trace_log
+ )
+ if state_match:
+ error.machine_state = state_match.group(1).split('.')[-1]
+
+ # --- Sender trace-back analysis ---
+ sender_info = self._trace_back_to_sender(error, trace_log, project_files)
+ error.sender_info = sender_info
+
+ # --- Cascading impact analysis ---
+ cascading = self._analyze_cascading_impact(error, project_files)
+ error.cascading_impact = cascading
+
+ # --- Build root cause with enhanced context ---
+ root_parts = [
+ f"Machine '{error.machine_type}' received event '{error.event_name}' "
+ f"in state '{error.machine_state or 'unknown'}' but has no handler for it."
+ ]
+
+ if sender_info:
+ if sender_info.is_test_driver:
+ root_parts.append(
+ f" The event was sent by test driver '{sender_info.machine_type}' "
+ f"during setup (not by protocol logic)."
+ )
+ error.is_test_driver_bug = True
+ else:
+ root_parts.append(
+ f" The event was sent by '{sender_info.machine or 'unknown'}' "
+ f"in state '{sender_info.state or 'unknown'}'."
+ )
+
+ if sender_info.is_initialization_pattern:
+ root_parts.append(
+ f" This appears to be a SEMANTIC MISMATCH: the event '{error.event_name}' "
+ f"is a protocol event being misused for initialization/setup."
+ )
+ error.requires_new_event = True
+
+ if sender_info.semantic_mismatch:
+ root_parts.append(f" {sender_info.semantic_mismatch}")
+
+ if cascading and cascading.unhandled_in:
+ other_machines = [m for m in cascading.unhandled_in if m != error.machine_type]
+ if other_machines:
+ root_parts.append(
+ f" Additionally, machines [{', '.join(other_machines)}] also lack "
+ f"handlers for '{error.event_name}' — this is a multi-file issue."
+ )
+ error.requires_multi_file_fix = True
+
+ error.root_cause = "".join(root_parts)
+
+ # --- Build suggested fixes with enhanced context ---
+ fixes = []
+ if error.is_test_driver_bug:
+ fixes.append(
+ f"Fix the test driver: introduce a dedicated setup event "
+ f"(e.g., eSetup{error.machine_type}) instead of reusing "
+ f"the protocol event '{error.event_name}' for initialization"
+ )
+ fixes.append(
+ f"Define the new setup event in Enums_Types_Events.p and "
+ f"add a handler for it in {error.machine_type}'s Init state"
+ )
+ if error.requires_new_event:
+ fixes.append(
+ "This requires a design-level change: add a new event type "
+ "to the types file, not just a handler in the receiving machine"
+ )
+ if error.requires_multi_file_fix:
+ fixes.append(
+ f"Add 'ignore {error.event_name};' to ALL machines that may "
+ f"receive it: {', '.join(cascading.all_receivers) if cascading else 'unknown'}"
+ )
+ # Always include the mechanical fixes as fallback
+ fixes.append(f"Add 'on {error.event_name} do HandleXXX;' to state '{error.machine_state}'")
+ fixes.append(f"Add 'ignore {error.event_name};' if this event should be silently dropped")
+ fixes.append(f"Add 'defer {error.event_name};' if this event should be handled later")
+
+ error.suggested_fixes = fixes
+
+ def _trace_back_to_sender(
+ self,
+ error: CheckerError,
+ trace_log: str,
+ project_files: Dict[str, str]
+ ) -> Optional[SenderInfo]:
+ """
+ Trace back through the execution log to find who sent the problematic event
+ and analyze their intent.
+ """
+ event_name = error.event_name
+ if not event_name:
+ return None
+
+ # Strip namespace prefix (e.g. PImplementation.eLearn -> eLearn)
+ clean_event = event_name.split('.')[-1] if '.' in event_name else event_name
+
+ # Find the SendLog that sent this event to the error machine
+ receiver_pattern = rf"\s*'(\w+)\((\d+)\)'\s+in state '([^']+)'.*sent event '{clean_event}.*to '{error.machine_type}\({error.machine_id}\)'"
+ send_match = re.search(receiver_pattern, trace_log)
+
+ if not send_match:
+ # Try with namespace prefix
+ receiver_pattern2 = rf"\s*'(\w+)\((\d+)\)'\s+in state '([^']+)'.*sent event '.*{clean_event}.*to '{error.machine_type}\({error.machine_id}\)'"
+ send_match = re.search(receiver_pattern2, trace_log)
+
+ if not send_match:
+ return None
+
+ sender_type = send_match.group(1)
+ sender_id = send_match.group(2)
+ sender_state = send_match.group(3).split('.')[-1]
+
+ sender_info = SenderInfo(
+ machine_type=sender_type,
+ machine_id=sender_id,
+ state=sender_state,
+ )
+
+ # --- Test driver detection ---
+ # Heuristics: test driver machines are in PTst/ and often have names like
+ # "Scenario*", "TestDriver*", "Test*", or are the main machine in a test decl.
+ sender_info.is_test_driver = self._is_test_driver_machine(
+ sender_type, sender_state, project_files
+ )
+
+ # --- Initialization pattern detection ---
+ # If the sender is a test driver and the event was sent during the start
+ # state's entry action, this is likely an initialization pattern.
+ if sender_info.is_test_driver and sender_state == "Init":
+ sender_info.is_initialization_pattern = True
+
+ # --- Semantic mismatch detection ---
+ # Check if the event has a "real" protocol purpose by looking at
+ # whether protocol machines (non-test) also send this event.
+ protocol_senders = self._find_protocol_senders(clean_event, project_files)
+ if sender_info.is_test_driver and protocol_senders:
+ sender_info.semantic_mismatch = (
+ f"Event '{clean_event}' is used in protocol logic by "
+ f"[{', '.join(protocol_senders)}], but the test driver is also "
+ f"sending it with a dummy payload for setup purposes. "
+ f"This is a semantic mismatch — use a dedicated setup event instead."
+ )
+
+ # Extract the payload from the SendLog line
+ payload_match = re.search(r'with payload \(([^)]+)\)', send_match.group(0))
+ if payload_match:
+ sender_info.event_payload = payload_match.group(1)
+
+ # Check for dummy payload indicators (e.g., value:0, default values)
+ payload = sender_info.event_payload
+ if sender_info.is_initialization_pattern:
+ # Count how many fields look like defaults (0, false, null, default)
+ default_indicators = re.findall(r':\s*(?:0|false|null|default)\b', payload)
+ if default_indicators:
+ sender_info.semantic_mismatch = (
+ sender_info.semantic_mismatch or ""
+ ) + (
+ f" The payload contains {len(default_indicators)} default/zero value(s), "
+ f"suggesting this is a dummy initialization message, not a real protocol event."
+ )
+
+ return sender_info
+
+ def _is_test_driver_machine(
+ self,
+ machine_type: str,
+ state: str,
+ project_files: Dict[str, str]
+ ) -> bool:
+ """Determine if a machine type is a test driver (lives in PTst/)."""
+ # Check if this machine is defined in PTst/ files
+ for filepath, content in project_files.items():
+ if filepath.startswith('PTst/') or '/PTst/' in filepath:
+ if f"machine {machine_type}" in content:
+ return True
+
+ # Heuristic: common test driver naming patterns
+ test_patterns = [
+ r'^Scenario\d*',
+ r'^Test',
+ r'TestDriver',
+ r'TestHarness',
+ r'_Test$',
+ ]
+ for pattern in test_patterns:
+ if re.match(pattern, machine_type, re.IGNORECASE):
+ return True
+
+ return False
+
+ def _find_protocol_senders(
+ self,
+ event_name: str,
+ project_files: Dict[str, str]
+ ) -> List[str]:
+ """Find non-test machines that send a given event."""
+ senders = []
+ for filepath, content in project_files.items():
+ if filepath.startswith('PTst/') or '/PTst/' in filepath:
+ continue
+ if filepath.startswith('PSpec/') or '/PSpec/' in filepath:
+ continue
+ # Look for send statements with this event
+ # Use [^,]+ to match targets like allComponents[i]
+ send_pattern = rf"send\s+[^,]+\s*,\s*{re.escape(event_name)}\b"
+ if re.search(send_pattern, content):
+ # Extract machine name from this file
+ machine_match = re.search(r'machine\s+(\w+)', content)
+ if machine_match:
+ senders.append(machine_match.group(1))
+ return senders
+
+ def _analyze_cascading_impact(
+ self,
+ error: CheckerError,
+ project_files: Dict[str, str]
+ ) -> Optional[CascadingImpact]:
+ """
+ Analyze which other machines and states also lack handlers for this event.
+ This identifies multi-file fix requirements.
+ """
+ event_name = error.event_name
+ if not event_name:
+ return None
+
+ # Strip namespace prefix
+ clean_event = event_name.split('.')[-1] if '.' in event_name else event_name
+
+ impact = CascadingImpact()
+
+ # Find all machines that could receive this event
+ # (look for send statements targeting various machines)
+ for filepath, content in project_files.items():
+ if filepath.startswith('PSpec/') or '/PSpec/' in filepath:
+ continue
+
+ # Find machines defined in this file
+ machine_matches = re.finditer(r'machine\s+(\w+)\s*\{', content)
+ for mm in machine_matches:
+ machine_name = mm.group(1)
+
+ # Check if this machine sends the event (broadcaster)
+ # Use [^,]+ to match targets like allComponents[i]
+ send_pattern = rf'send\s+[^,]+\s*,\s*{re.escape(clean_event)}\b'
+ if re.search(send_pattern, content):
+ if machine_name not in impact.broadcasters:
+ impact.broadcasters.append(machine_name)
+
+ # Find all states in this machine
+ states = re.findall(
+ r'(?:start\s+)?state\s+(\w+)\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}',
+ content
+ )
+
+ unhandled_states = []
+ for state_name, state_body in states:
+ # Check if this state handles the event
+ handlers = [
+ rf'on\s+{re.escape(clean_event)}\b',
+ rf'ignore\s+[^;]*\b{re.escape(clean_event)}\b',
+ rf'defer\s+[^;]*\b{re.escape(clean_event)}\b',
+ ]
+ handled = any(re.search(h, state_body) for h in handlers)
+ if not handled:
+ unhandled_states.append(state_name)
+
+ if unhandled_states:
+ impact.unhandled_in[machine_name] = unhandled_states
+ if machine_name not in impact.all_receivers:
+ impact.all_receivers.append(machine_name)
+
+ return impact
+
+ def _analyze_assertion_failure(
+ self,
+ error: CheckerError,
+ trace_log: str,
+ project_files: Dict[str, str]
+ ):
+ """Analyze assertion failure."""
+ # Try to extract assertion details
+ assert_match = re.search(r"assert\s+([^,;]+)", trace_log)
+
+ error.root_cause = (
+ "An assertion in the P code failed during execution. "
+ "This indicates the system reached an unexpected state."
+ )
+
+ if assert_match:
+ error.root_cause += f" The assertion was: '{assert_match.group(1)}'"
+
+ error.suggested_fixes = [
+ "Review the assertion condition and ensure prerequisites are met",
+ "Add guards or checks before the assertion to prevent invalid states",
+ "Check if the assertion is overly strict for the current test scenario",
+ ]
+
+ def _analyze_deadlock(
+ self,
+ error: CheckerError,
+ trace_log: str,
+ project_files: Dict[str, str],
+ machines_involved: Dict[str, List[str]]
+ ):
+ """Analyze deadlock error."""
+ # List machines and their final states
+ final_states = []
+ for machine, states in machines_involved.items():
+ if states:
+ final_states.append(f"{machine}: {states[-1]}")
+
+ error.root_cause = (
+ "The system reached a deadlock where no machine can make progress. "
+ f"Machines involved: {', '.join(final_states)}"
+ )
+
+ error.suggested_fixes = [
+ "Check for circular dependencies between machines",
+ "Ensure all expected events are being sent",
+ "Add timeout mechanisms to break potential deadlocks",
+ "Review the test driver to ensure all necessary events are triggered",
+ ]
diff --git a/Src/PeasyAI/src/core/compilation/checker_fixers.py b/Src/PeasyAI/src/core/compilation/checker_fixers.py
new file mode 100644
index 0000000000..653b4962f3
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/checker_fixers.py
@@ -0,0 +1,814 @@
+"""
+Specialized PChecker Error Fixers
+
+Implements automatic fixes for common PChecker runtime errors.
+
+Enhanced with:
+- Multi-file fix support (fixes across types, machines, tests)
+- Sender trace-back analysis (fixes the sender, not just receiver)
+- Test-driver vs protocol bug distinction
+- Cascading impact analysis (fix all affected machines)
+- Semantic mismatch detection (new event introduction)
+"""
+
+import re
+import logging
+from typing import Dict, List, Optional, Tuple
+from dataclasses import dataclass, field
+from pathlib import Path
+
+from .checker_error_parser import (
+ CheckerError, CheckerErrorCategory, TraceAnalysis,
+ SenderInfo, CascadingImpact,
+)
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class FilePatch:
+ """A single file modification within a multi-file fix."""
+ file_path: str
+ original_code: str
+ fixed_code: str
+ description: str
+
+
+@dataclass
+class CheckerFix:
+ """A fix for a PChecker error, potentially spanning multiple files."""
+ file_path: str
+ original_code: str
+ fixed_code: str
+ description: str
+ confidence: float # 0.0 to 1.0
+ requires_review: bool = False
+ review_notes: Optional[str] = None
+ # Multi-file fix support
+ additional_patches: List[FilePatch] = field(default_factory=list)
+ is_multi_file: bool = False
+ fix_strategy: Optional[str] = None # e.g., "ignore", "new_event", "test_driver_fix"
+
+
+class PCheckerErrorFixer:
+ """Fixes common PChecker errors automatically."""
+
+ def __init__(self, project_path: str, project_files: Dict[str, str]):
+ """
+ Initialize fixer with project context.
+
+ Args:
+ project_path: Path to the P project root
+ project_files: Dict mapping filenames to their content
+ """
+ self.project_path = project_path
+ self.project_files = project_files
+
+ def can_fix(self, error: CheckerError) -> bool:
+ """Check if we can automatically fix this error."""
+ return error.category in [
+ CheckerErrorCategory.NULL_TARGET,
+ CheckerErrorCategory.UNHANDLED_EVENT,
+ CheckerErrorCategory.ASSERTION_FAILURE,
+ ]
+
+ def fix(self, analysis: TraceAnalysis) -> Optional[CheckerFix]:
+ """
+ Attempt to fix the error.
+
+ Args:
+ analysis: Full trace analysis
+
+ Returns:
+ CheckerFix if successful, None otherwise
+ """
+ error = analysis.error
+
+ if error.category == CheckerErrorCategory.NULL_TARGET:
+ return self._fix_null_target(error, analysis)
+
+ elif error.category == CheckerErrorCategory.UNHANDLED_EVENT:
+ return self._fix_unhandled_event(error, analysis)
+
+ elif error.category == CheckerErrorCategory.ASSERTION_FAILURE:
+ return self._fix_assertion_failure(error, analysis)
+
+ return None
+
+ # ------------------------------------------------------------------
+ # Assertion failure fix
+ # ------------------------------------------------------------------
+
+ def _fix_assertion_failure(
+ self,
+ error: CheckerError,
+ analysis: TraceAnalysis,
+ ) -> Optional[CheckerFix]:
+ """
+ Attempt to fix an assertion failure in a spec monitor.
+
+ Strategy:
+ 1. Locate the spec file that contains the failing assertion.
+ 2. Find the ``assert`` statement that failed.
+ 3. If the assertion looks like a simple comparison that might be
+ off-by-one or an inverted condition, produce a low-confidence
+ fix that relaxes it.
+ 4. Otherwise return a high-detail ``CheckerFix`` with
+ ``requires_review=True`` and concrete guidance.
+ """
+ # Find the spec file
+ spec_file = None
+ spec_content = None
+ spec_machine = error.machine_type or ""
+
+ for filename, content in self.project_files.items():
+ if "PSpec" in filename or "spec " in content:
+ if spec_machine and f"spec {spec_machine}" in content:
+ spec_file = filename
+ spec_content = content
+ break
+ if not spec_file:
+ spec_file = filename
+ spec_content = content
+
+ if not spec_file or not spec_content:
+ return None
+
+ # Identify the failing assert line(s). The error message often
+ # contains the assertion text or the machine/state where it fired.
+ assert_lines: List[Tuple[int, str]] = []
+ for i, line in enumerate(spec_content.splitlines()):
+ if re.search(r'\bassert\b', line):
+ assert_lines.append((i + 1, line.strip()))
+
+ if not assert_lines:
+ return None
+
+ # Build a descriptive fix that flags the assertion for review and
+ # wraps each assert in a comment showing the trace context.
+ lines = spec_content.splitlines()
+ changed = False
+ for line_no, _ in assert_lines:
+ idx = line_no - 1
+ if idx < len(lines):
+ original_line = lines[idx]
+ indent = len(original_line) - len(original_line.lstrip())
+ pad = " " * indent
+
+ # Common pattern: assert(x == y) when the protocol can
+ # legitimately have x != y transiently. We can't auto-fix
+ # the logic, but we can guard the assertion with extra
+ # state tracking comments.
+ if "assert" in original_line and "//" not in original_line:
+ lines[idx] = (
+ f"{pad}// REVIEW: PChecker assertion failure — "
+ f"{error.message[:80]}\n"
+ f"{original_line}"
+ )
+ changed = True
+
+ if not changed:
+ return None
+
+ fixed_content = "\n".join(lines)
+
+ return CheckerFix(
+ file_path=self._get_full_path(spec_file),
+ original_code=spec_content,
+ fixed_code=fixed_content,
+ description=(
+ f"Flagged assertion(s) in spec '{spec_machine or spec_file}' for review. "
+ f"PChecker error: {error.message[:120]}"
+ ),
+ confidence=0.3,
+ requires_review=True,
+ review_notes=(
+ f"The spec monitor has an assertion that fails under the "
+ f"PChecker schedule. Common causes:\n"
+ f" - The spec tracks events that arrive in an unexpected order\n"
+ f" - The assertion condition is too strict (e.g., == instead of >=)\n"
+ f" - The spec doesn't account for all protocol scenarios\n"
+ f"Trace error: {error.message[:200]}"
+ ),
+ fix_strategy="assertion_review",
+ )
+
+ def _fix_null_target(
+ self,
+ error: CheckerError,
+ analysis: TraceAnalysis
+ ) -> Optional[CheckerFix]:
+ """
+ Fix null target error by identifying and suggesting initialization.
+
+ This is a complex fix that may require:
+ 1. Adding a configuration event
+ 2. Modifying the test driver
+ 3. Adding initialization in the machine
+ """
+ # Find the machine file
+ machine_file = None
+ machine_content = None
+
+ for filename, content in self.project_files.items():
+ if f"machine {error.machine_type}" in content:
+ machine_file = filename
+ machine_content = content
+ break
+
+ if not machine_file or not machine_content:
+ logger.warning(f"Could not find machine file for {error.machine_type}")
+ return None
+
+ # Identify the target field being sent to
+ send_pattern = rf"send\s+(\w+)\s*,\s*{error.event_name}"
+ send_matches = re.findall(send_pattern, machine_content)
+
+ if not send_matches:
+ return None
+
+ target_field = send_matches[0]
+
+ # Check if there's already a configuration event handler
+ config_event = f"eConfig{error.machine_type}"
+ has_config = config_event.lower() in machine_content.lower()
+
+ if has_config:
+ # Config event exists but field not being set properly
+ return CheckerFix(
+ file_path=self._get_full_path(machine_file),
+ original_code=machine_content,
+ fixed_code=machine_content, # No auto-fix possible
+ description=f"Field '{target_field}' is null. Check that {config_event} properly sets this field.",
+ confidence=0.3,
+ requires_review=True,
+ review_notes=f"The machine has a configuration event but '{target_field}' is still null. "
+ f"Verify the test driver sends the config event with a valid machine reference."
+ )
+
+ # Generate a fix that adds configuration handling
+ fixed_content = self._add_config_event_handler(
+ machine_content,
+ error.machine_type,
+ target_field
+ )
+
+ if fixed_content == machine_content:
+ return None
+
+ return CheckerFix(
+ file_path=self._get_full_path(machine_file),
+ original_code=machine_content,
+ fixed_code=fixed_content,
+ description=f"Added configuration event handler to set '{target_field}' in {error.machine_type}",
+ confidence=0.6,
+ requires_review=True,
+ review_notes=f"Added eConfig{error.machine_type} handler. You also need to:\n"
+ f"1. Define the event 'eConfig{error.machine_type}' in Enums_Types_Events.p\n"
+ f"2. Update the test driver to send this event with the target machine reference"
+ )
+
+ def _fix_unhandled_event(
+ self,
+ error: CheckerError,
+ analysis: TraceAnalysis
+ ) -> Optional[CheckerFix]:
+ """
+ Fix unhandled event with deep analysis.
+
+ Strategy selection (in priority order):
+ 1. If sender is a test driver misusing a protocol event → fix test driver
+ 2. If event is broadcast to many machines → add ignore to ALL affected machines
+ 3. Simple case → add ignore to the single affected state
+ """
+ # Clean event name
+ clean_event = error.event_name
+ if clean_event and '.' in clean_event:
+ clean_event = clean_event.split('.')[-1]
+
+ if not clean_event:
+ return None
+
+ # Determine the best fix strategy based on enhanced analysis
+ if error.is_test_driver_bug and error.sender_info:
+ return self._fix_test_driver_misuse(error, analysis, clean_event)
+
+ if error.requires_multi_file_fix and error.cascading_impact:
+ return self._fix_cascading_unhandled_event(error, analysis, clean_event)
+
+ # Fallback: simple single-state ignore fix
+ return self._fix_single_state_unhandled(error, analysis, clean_event)
+
+ def _fix_test_driver_misuse(
+ self,
+ error: CheckerError,
+ analysis: TraceAnalysis,
+ clean_event: str,
+ ) -> Optional[CheckerFix]:
+ """
+ Fix a test driver that misuses a protocol event for initialization.
+
+ Strategy: Remove the bad send from the test driver and add a dedicated
+ setup event with proper handler in the receiving machine.
+ """
+ sender = error.sender_info
+ if not sender:
+ return None
+
+ # Find the test driver file
+ test_file = None
+ test_content = None
+ for filename, content in self.project_files.items():
+ if f"machine {sender.machine_type}" in content:
+ test_file = filename
+ test_content = content
+ break
+
+ if not test_file or not test_content:
+ return self._fix_single_state_unhandled(error, analysis, clean_event)
+
+ # Find the receiver machine file
+ receiver_file = None
+ receiver_content = None
+ for filename, content in self.project_files.items():
+ if f"machine {error.machine_type}" in content:
+ receiver_file = filename
+ receiver_content = content
+ break
+
+ if not receiver_file or not receiver_content:
+ return self._fix_single_state_unhandled(error, analysis, clean_event)
+
+ # Find the types/events file
+ types_file = None
+ types_content = None
+ for filename, content in self.project_files.items():
+ if 'Enums_Types_Events' in filename or ('event ' in content and 'machine ' not in content):
+ types_file = filename
+ types_content = content
+ break
+
+ if not types_file or not types_content:
+ return self._fix_single_state_unhandled(error, analysis, clean_event)
+
+ # --- Determine the payload type of the protocol event ---
+ event_type_match = re.search(
+ rf'event\s+{re.escape(clean_event)}\s*:\s*(\w+)',
+ types_content
+ )
+ event_payload_type = event_type_match.group(1) if event_type_match else None
+
+ # --- Find what field the test driver was trying to set ---
+ # Look at the send statement in the test driver for the event
+ send_pattern = rf'send\s+\w+\s*,\s*{re.escape(clean_event)}\s*,\s*\(([^)]+)\)'
+ send_match = re.search(send_pattern, test_content)
+
+ if not send_match:
+ return self._fix_single_state_unhandled(error, analysis, clean_event)
+
+ # Determine what the test was trying to accomplish
+ # e.g., send learner, eLearn, (allComponents = learner, agreedValue = 0)
+ # → trying to set allComponents on the Learner
+ payload_str = send_match.group(1)
+
+ # Create a new setup event name
+ setup_event_name = f"eSetup{error.machine_type}Components"
+
+ # --- Determine the appropriate type for the setup event ---
+ # Parse the payload fields to figure out what the test was setting
+ # Look for fields with non-default values (not = 0, not = false)
+ field_matches = re.findall(r'(\w+)\s*=\s*([^,]+)', payload_str)
+ setup_fields = []
+ for field_name, field_value in field_matches:
+ field_value = field_value.strip()
+ # Skip fields with default/dummy values
+ if field_value in ('0', 'false', 'null', 'default(int)', 'default(bool)'):
+ continue
+ setup_fields.append(field_name)
+
+ # Determine the type for the setup event based on receiver machine vars
+ # Look at what variable types the receiver has
+ setup_event_type = "seq[machine]" # reasonable default for component setup
+ var_pattern = rf'var\s+(\w+)\s*:\s*([^;]+);'
+ var_matches = re.findall(var_pattern, receiver_content)
+ for var_name, var_type in var_matches:
+ if var_name in setup_fields:
+ setup_event_type = var_type.strip()
+ break
+
+ # --- Build the multi-file fix ---
+ patches = []
+
+ # Patch 1: Add new setup event to types file
+ new_types = types_content.rstrip()
+ new_types += f"\n\n// Setup event for {error.machine_type} initialization\n"
+ new_types += f"event {setup_event_name}: {setup_event_type};\n"
+ patches.append(FilePatch(
+ file_path=self._get_full_path(types_file),
+ original_code=types_content,
+ fixed_code=new_types,
+ description=f"Added setup event '{setup_event_name}: {setup_event_type}' to types file"
+ ))
+
+ # Patch 2: Add handler in receiver machine
+ fixed_receiver = self._add_setup_event_handler(
+ receiver_content,
+ error.machine_type,
+ setup_event_name,
+ setup_event_type,
+ setup_fields,
+ clean_event,
+ )
+ if fixed_receiver != receiver_content:
+ patches.append(FilePatch(
+ file_path=self._get_full_path(receiver_file),
+ original_code=receiver_content,
+ fixed_code=fixed_receiver,
+ description=f"Added handler for '{setup_event_name}' and 'ignore {clean_event}' in {error.machine_type}"
+ ))
+
+ # Patch 3: Fix test driver to use new setup event
+ # Replace the bad send with the new setup event
+ old_send = send_match.group(0)
+ # Build the new send — extract the target variable from the old send
+ target_match = re.search(rf'send\s+(\w+)\s*,\s*{re.escape(clean_event)}', old_send)
+ target_var = target_match.group(1) if target_match else error.machine_type.lower()
+
+ # Build new send payload from non-default fields
+ new_payload_parts = []
+ for field_name, field_value in field_matches:
+ field_value = field_value.strip()
+ if field_value not in ('0', 'false', 'null'):
+ new_payload_parts.append(field_value)
+
+ if new_payload_parts and setup_event_type.startswith('seq['):
+ # For sequence types, we need to send the sequence variable
+ new_send = f"send {target_var}, {setup_event_name}, {new_payload_parts[0]}"
+ elif new_payload_parts:
+ new_send = f"send {target_var}, {setup_event_name}, {new_payload_parts[0]}"
+ else:
+ # Comment out the bad send instead
+ new_send = f"// Removed: {old_send} (was misusing protocol event for setup)"
+
+ fixed_test = test_content.replace(old_send, new_send)
+ if fixed_test != test_content:
+ patches.append(FilePatch(
+ file_path=self._get_full_path(test_file),
+ original_code=test_content,
+ fixed_code=fixed_test,
+ description=f"Replaced misused '{clean_event}' send with '{setup_event_name}' in test driver"
+ ))
+
+ # Patch 4: Add ignore for the protocol event in all affected machines
+ cascading = error.cascading_impact
+ if cascading:
+ for machine_name, states in cascading.unhandled_in.items():
+ if machine_name == error.machine_type:
+ continue # Already handled in Patch 2
+ if machine_name == sender.machine_type:
+ continue # Test driver, doesn't need ignore
+
+ for filename, content in self.project_files.items():
+ if f"machine {machine_name}" in content:
+ patched = self._add_ignore_to_all_states(
+ content, machine_name, clean_event, states
+ )
+ if patched != content:
+ patches.append(FilePatch(
+ file_path=self._get_full_path(filename),
+ original_code=content,
+ fixed_code=patched,
+ description=f"Added 'ignore {clean_event};' to {machine_name} states: {', '.join(states)}"
+ ))
+ break
+
+ if not patches:
+ return self._fix_single_state_unhandled(error, analysis, clean_event)
+
+ # Use the first patch as the primary fix, rest as additional
+ primary = patches[0]
+ return CheckerFix(
+ file_path=primary.file_path,
+ original_code=primary.original_code,
+ fixed_code=primary.fixed_code,
+ description=f"Multi-file fix: {'; '.join(p.description for p in patches)}",
+ confidence=0.7,
+ requires_review=True,
+ review_notes=(
+ f"This fix introduces a new setup event '{setup_event_name}' and modifies "
+ f"{len(patches)} files. The test driver was misusing protocol event "
+ f"'{clean_event}' for initialization."
+ ),
+ additional_patches=patches[1:],
+ is_multi_file=True,
+ fix_strategy="new_event",
+ )
+
+ def _fix_cascading_unhandled_event(
+ self,
+ error: CheckerError,
+ analysis: TraceAnalysis,
+ clean_event: str,
+ ) -> Optional[CheckerFix]:
+ """
+ Fix an unhandled event that affects multiple machines.
+ Adds ignore statements to ALL affected machines and states.
+ """
+ cascading = error.cascading_impact
+ if not cascading:
+ return self._fix_single_state_unhandled(error, analysis, clean_event)
+
+ patches = []
+ for machine_name, states in cascading.unhandled_in.items():
+ for filename, content in self.project_files.items():
+ if f"machine {machine_name}" in content:
+ patched = self._add_ignore_to_all_states(
+ content, machine_name, clean_event, states
+ )
+ if patched != content:
+ patches.append(FilePatch(
+ file_path=self._get_full_path(filename),
+ original_code=content,
+ fixed_code=patched,
+ description=(
+ f"Added 'ignore {clean_event};' to {machine_name} "
+ f"states: {', '.join(states)}"
+ ),
+ ))
+ break
+
+ if not patches:
+ return None
+
+ primary = patches[0]
+ return CheckerFix(
+ file_path=primary.file_path,
+ original_code=primary.original_code,
+ fixed_code=primary.fixed_code,
+ description=f"Multi-file fix: added 'ignore {clean_event}' across {len(patches)} files",
+ confidence=0.75,
+ requires_review=True,
+ review_notes=(
+ f"Event '{clean_event}' was unhandled in {len(patches)} machine files. "
+ f"All affected states now ignore it. Review to ensure this is the correct behavior."
+ ),
+ additional_patches=patches[1:],
+ is_multi_file=True,
+ fix_strategy="cascading_ignore",
+ )
+
+ def _fix_single_state_unhandled(
+ self,
+ error: CheckerError,
+ analysis: TraceAnalysis,
+ clean_event: str,
+ ) -> Optional[CheckerFix]:
+ """Original simple fix: add ignore to the single affected state."""
+ machine_file = None
+ machine_content = None
+
+ for filename, content in self.project_files.items():
+ if f"machine {error.machine_type}" in content:
+ machine_file = filename
+ machine_content = content
+ break
+
+ if not machine_file or not machine_content:
+ return None
+
+ state_name = error.machine_state
+ if not state_name:
+ return None
+ if '.' in state_name:
+ state_name = state_name.split('.')[-1]
+
+ fixed_content = self._add_ignore_to_all_states(
+ machine_content, error.machine_type, clean_event, [state_name]
+ )
+
+ if fixed_content == machine_content:
+ return None
+
+ return CheckerFix(
+ file_path=self._get_full_path(machine_file),
+ original_code=machine_content,
+ fixed_code=fixed_content,
+ description=f"Added 'ignore {clean_event};' to state '{state_name}' in {error.machine_type}",
+ confidence=0.8,
+ requires_review=True,
+ review_notes=(
+ f"Event '{clean_event}' will now be silently ignored in state '{state_name}'. "
+ f"If you need to handle it differently, change 'ignore' to 'defer' or add a proper handler."
+ ),
+ fix_strategy="simple_ignore",
+ )
+
+ # =========================================================================
+ # Helper methods for building fixes
+ # =========================================================================
+
+ def _add_setup_event_handler(
+ self,
+ machine_content: str,
+ machine_type: str,
+ setup_event_name: str,
+ setup_event_type: str,
+ fields_to_set: List[str],
+ protocol_event_to_ignore: str,
+ ) -> str:
+ """Add a setup event handler and ignore for the misused protocol event."""
+ # Find all states and add ignore for the protocol event
+ result = self._add_ignore_to_all_states(
+ machine_content, machine_type, protocol_event_to_ignore, None # None = all states
+ )
+
+ # Find the start state to add the setup event handler
+ start_match = re.search(r'(start\s+state\s+\w+\s*\{)', result)
+ if not start_match:
+ return result
+
+ # Find where to insert the handler (after entry or at start of state)
+ state_start = start_match.end()
+ # Find the entry statement
+ entry_match = re.search(r'entry\s+\w+\s*;', result[state_start:])
+
+ if entry_match:
+ insert_pos = state_start + entry_match.end()
+ else:
+ insert_pos = state_start
+
+ # Build handler based on the type
+ if fields_to_set:
+ field_assignments = "\n".join(
+ f" {f} = payload;" for f in fields_to_set[:1]
+ )
+ handler = f"""
+ on {setup_event_name} do (payload: {setup_event_type}) {{
+{field_assignments}
+ }}"""
+ else:
+ handler = f"\n on {setup_event_name} do (payload: {setup_event_type}) {{ }}"
+
+ result = result[:insert_pos] + handler + result[insert_pos:]
+ return result
+
+ def _add_ignore_to_all_states(
+ self,
+ content: str,
+ machine_type: str,
+ event_name: str,
+ states: Optional[List[str]],
+ ) -> str:
+ """
+ Add 'ignore event_name;' to specified states (or all states if states is None)
+ in the given machine content.
+ """
+ # Find all states in the machine
+ # We need to handle both "start state X {" and "state X {"
+ state_pattern = r'((?:start\s+)?state\s+(\w+)\s*\{)'
+
+ result = content
+ offset = 0
+
+ for match in re.finditer(state_pattern, content):
+ state_decl = match.group(1)
+ state_name = match.group(2)
+
+ # Skip if we only want specific states and this isn't one
+ if states is not None and state_name not in states:
+ continue
+
+ # Check if this event is already handled in this state
+ # Find the state body (everything between the opening { and matching })
+ state_start = match.start()
+ brace_start = match.end() - 1 # Position of opening {
+
+ # Find matching closing brace
+ depth = 1
+ pos = brace_start + 1
+ while pos < len(result) and depth > 0:
+ if result[pos] == '{':
+ depth += 1
+ elif result[pos] == '}':
+ depth -= 1
+ pos += 1
+
+ state_body = result[brace_start:pos]
+
+ # Check if event is already handled
+ handlers = [
+ rf'on\s+{re.escape(event_name)}\b',
+ rf'ignore\s+[^;]*\b{re.escape(event_name)}\b',
+ rf'defer\s+[^;]*\b{re.escape(event_name)}\b',
+ ]
+ if any(re.search(h, state_body) for h in handlers):
+ continue
+
+ # Determine indentation from existing state content
+ indent = " "
+ for line in state_body.split('\n'):
+ stripped = line.lstrip()
+ if stripped.startswith('on ') or stripped.startswith('ignore ') or stripped.startswith('entry '):
+ indent = line[:len(line) - len(stripped)]
+ break
+
+ # Check if there's an existing ignore statement we can extend
+ existing_ignore = re.search(r'(ignore\s+[^;]+);', state_body)
+ if existing_ignore:
+ old_ignore = existing_ignore.group(0)
+ # Add the event to the existing ignore list
+ new_ignore = old_ignore.replace(';', f', {event_name};')
+ result = result[:brace_start] + state_body.replace(old_ignore, new_ignore) + result[pos:]
+ else:
+ # Insert a new ignore statement before the closing brace
+ ignore_line = f"{indent}ignore {event_name};\n"
+ # Insert before the last line (closing brace)
+ insert_pos = brace_start + len(state_body) - 1
+ # Find the position just before the closing }
+ while insert_pos > brace_start and result[insert_pos - 1] in (' ', '\t', '\n'):
+ insert_pos -= 1
+ insert_pos += 1 # After the last newline
+
+ result = result[:insert_pos] + ignore_line + result[insert_pos:]
+
+ return result
+
+ def _add_config_event_handler(
+ self,
+ machine_content: str,
+ machine_type: str,
+ target_field: str
+ ) -> str:
+ """Add a configuration event handler to the machine."""
+ config_event = f"eConfig{machine_type}"
+ config_type = f"tConfig{machine_type}"
+
+ # Find the start state
+ start_match = re.search(r"(start\s+state\s+\w+\s*\{)", machine_content)
+ if not start_match:
+ return machine_content
+
+ # Add the handler after the entry statement or at the start of the state
+ state_start = start_match.end()
+
+ # Find entry statement
+ entry_match = re.search(r"entry\s+\w+\s*;", machine_content[state_start:])
+ if entry_match:
+ insert_pos = state_start + entry_match.end()
+ else:
+ insert_pos = state_start
+
+ # Generate handler
+ handler = f"""
+ on {config_event} do (config: {config_type}) {{
+ {target_field} = config.{target_field};
+ }}"""
+
+ # Insert handler
+ fixed_content = machine_content[:insert_pos] + handler + machine_content[insert_pos:]
+
+ return fixed_content
+
+ def _get_full_path(self, filename: str) -> str:
+ """Get full path for a filename or relative path."""
+ # If filename already includes folder prefix, use it directly
+ if '/' in filename:
+ full_path = Path(self.project_path) / filename
+ if full_path.exists():
+ return str(full_path)
+ # Extract just the filename and search
+ filename = Path(filename).name
+
+ # Check each standard folder
+ for folder in ['PSrc', 'PSpec', 'PTst']:
+ full_path = Path(self.project_path) / folder / filename
+ if full_path.exists():
+ return str(full_path)
+
+ # Default to PSrc
+ return str(Path(self.project_path) / 'PSrc' / filename)
+
+
+def analyze_and_suggest_fix(
+ trace_log: str,
+ project_path: str,
+ project_files: Dict[str, str]
+) -> Tuple[TraceAnalysis, Optional[CheckerFix]]:
+ """
+ Analyze a trace and attempt to generate a fix.
+
+ Returns:
+ Tuple of (TraceAnalysis, Optional[CheckerFix])
+ """
+ from .checker_error_parser import PCheckerErrorParser
+
+ parser = PCheckerErrorParser()
+ analysis = parser.analyze(trace_log, project_files)
+
+ fixer = PCheckerErrorFixer(project_path, project_files)
+
+ fix = None
+ if fixer.can_fix(analysis.error):
+ fix = fixer.fix(analysis)
+
+ return analysis, fix
diff --git a/Src/PeasyAI/src/core/compilation/environment.py b/Src/PeasyAI/src/core/compilation/environment.py
new file mode 100644
index 0000000000..23d263e33a
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/environment.py
@@ -0,0 +1,259 @@
+"""
+Environment Detection for P Compiler and .NET SDK
+
+Auto-detects the location of P compiler and dotnet SDK.
+"""
+
+import os
+import shutil
+import subprocess
+import logging
+from pathlib import Path
+from typing import Optional, Dict, Any
+from dataclasses import dataclass
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class EnvironmentInfo:
+ """Information about the P development environment."""
+ p_compiler_path: Optional[str] = None
+ dotnet_path: Optional[str] = None
+ dotnet_version: Optional[str] = None
+ p_version: Optional[str] = None
+ is_valid: bool = False
+ issues: list = None
+
+ def __post_init__(self):
+ if self.issues is None:
+ self.issues = []
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "p_compiler_path": self.p_compiler_path,
+ "dotnet_path": self.dotnet_path,
+ "dotnet_version": self.dotnet_version,
+ "p_version": self.p_version,
+ "is_valid": self.is_valid,
+ "issues": self.issues,
+ }
+
+
+class EnvironmentDetector:
+ """Detects and validates the P development environment."""
+
+ # Common paths to search for P compiler
+ P_SEARCH_PATHS = [
+ Path.home() / ".dotnet" / "tools" / "p",
+ Path("/usr/local/bin/p"),
+ Path("/opt/homebrew/bin/p"),
+ ]
+
+ # Common paths to search for dotnet
+ DOTNET_SEARCH_PATHS = [
+ Path("/usr/local/share/dotnet/dotnet"),
+ Path.home() / ".dotnet" / "dotnet",
+ Path("/opt/homebrew/bin/dotnet"),
+ Path("/usr/bin/dotnet"),
+ ]
+
+ @classmethod
+ def detect(cls) -> EnvironmentInfo:
+ """Detect the P development environment."""
+ info = EnvironmentInfo()
+
+ # Find P compiler
+ info.p_compiler_path = cls._find_p_compiler()
+ if not info.p_compiler_path:
+ info.issues.append("P compiler not found. Install with: dotnet tool install -g P")
+
+ # Find dotnet
+ info.dotnet_path = cls._find_dotnet()
+ if not info.dotnet_path:
+ info.issues.append("dotnet SDK not found. Install from: https://dotnet.microsoft.com/download")
+ else:
+ info.dotnet_version = cls._get_dotnet_version(info.dotnet_path)
+
+ # Get P version if available
+ if info.p_compiler_path:
+ info.p_version = cls._get_p_version(info.p_compiler_path)
+
+ # Validate
+ info.is_valid = info.p_compiler_path is not None and info.dotnet_path is not None
+
+ return info
+
+ @classmethod
+ def _find_p_compiler(cls) -> Optional[str]:
+ """Find the P compiler."""
+ # First try which/where
+ p_path = shutil.which("p")
+ if p_path:
+ return p_path
+
+ # Search common paths
+ for path in cls.P_SEARCH_PATHS:
+ if path.exists():
+ return str(path)
+
+ # Check if it's in PATH but not found by which (Windows edge case)
+ try:
+ result = subprocess.run(
+ ["p", "--version"],
+ capture_output=True,
+ timeout=5,
+ )
+ if result.returncode == 0:
+ return "p" # It's in PATH
+ except (subprocess.SubprocessError, FileNotFoundError):
+ pass
+
+ return None
+
+ @classmethod
+ def _find_dotnet(cls) -> Optional[str]:
+ """Find the dotnet SDK."""
+ # First try which/where
+ dotnet_path = shutil.which("dotnet")
+ if dotnet_path:
+ return dotnet_path
+
+ # Search common paths
+ for path in cls.DOTNET_SEARCH_PATHS:
+ if path.exists():
+ return str(path)
+
+ # Check DOTNET_ROOT env var
+ dotnet_root = os.environ.get("DOTNET_ROOT")
+ if dotnet_root:
+ dotnet_exe = Path(dotnet_root) / "dotnet"
+ if dotnet_exe.exists():
+ return str(dotnet_exe)
+
+ return None
+
+ @classmethod
+ def _get_dotnet_version(cls, dotnet_path: str) -> Optional[str]:
+ """Get the dotnet version."""
+ try:
+ result = subprocess.run(
+ [dotnet_path, "--version"],
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ if result.returncode == 0:
+ return result.stdout.strip()
+ except (subprocess.SubprocessError, FileNotFoundError):
+ pass
+ return None
+
+ @classmethod
+ def _get_p_version(cls, p_path: str) -> Optional[str]:
+ """Get the P compiler version."""
+ try:
+ # P doesn't have a direct --version flag, but we can check
+ # This is a placeholder - adjust based on actual P CLI
+ return "installed"
+ except Exception:
+ pass
+ return None
+
+ @classmethod
+ def get_environment_vars(cls) -> Dict[str, str]:
+ """Get environment variables needed for P compilation."""
+ info = cls.detect()
+ env = os.environ.copy()
+
+ if info.p_compiler_path:
+ # Add P compiler directory to PATH
+ p_dir = str(Path(info.p_compiler_path).parent)
+ env["PATH"] = f"{p_dir}:{env.get('PATH', '')}"
+
+ if info.dotnet_path:
+ # Add dotnet directory to PATH
+ dotnet_dir = str(Path(info.dotnet_path).parent)
+ env["PATH"] = f"{dotnet_dir}:{env.get('PATH', '')}"
+ env["DOTNET_ROOT"] = dotnet_dir
+
+ return env
+
+ @classmethod
+ def setup_environment(cls) -> bool:
+ """
+ Set up the environment for P compilation.
+ Returns True if successful.
+ """
+ info = cls.detect()
+
+ if not info.is_valid:
+ logger.error(f"Invalid P environment: {info.issues}")
+ return False
+
+ # Update os.environ
+ if info.p_compiler_path:
+ p_dir = str(Path(info.p_compiler_path).parent)
+ current_path = os.environ.get("PATH", "")
+ if p_dir not in current_path:
+ os.environ["PATH"] = f"{p_dir}:{current_path}"
+
+ if info.dotnet_path:
+ dotnet_dir = str(Path(info.dotnet_path).parent)
+ current_path = os.environ.get("PATH", "")
+ if dotnet_dir not in current_path:
+ os.environ["PATH"] = f"{dotnet_dir}:{current_path}"
+ os.environ["DOTNET_ROOT"] = dotnet_dir
+
+ logger.info(f"P environment configured: P={info.p_compiler_path}, dotnet={info.dotnet_path}")
+ return True
+
+
+def ensure_environment() -> EnvironmentInfo:
+ """
+ Ensure the P development environment is properly set up.
+ Returns environment info with any issues.
+ """
+ info = EnvironmentDetector.detect()
+
+ if info.is_valid:
+ EnvironmentDetector.setup_environment()
+
+ return info
+
+
+def get_compile_command(project_path: str) -> tuple:
+ """
+ Get the command and environment for compiling a P project.
+ Returns (command_list, environment_dict).
+ """
+ info = EnvironmentDetector.detect()
+ env = EnvironmentDetector.get_environment_vars()
+
+ if info.p_compiler_path:
+ cmd = [info.p_compiler_path, "compile"]
+ else:
+ cmd = ["p", "compile"] # Hope it's in PATH
+
+ return cmd, env
+
+
+def get_check_command(project_path: str, test_case: str = None, schedules: int = 100) -> tuple:
+ """
+ Get the command and environment for running PChecker.
+ Returns (command_list, environment_dict).
+ """
+ info = EnvironmentDetector.detect()
+ env = EnvironmentDetector.get_environment_vars()
+
+ if info.p_compiler_path:
+ cmd = [info.p_compiler_path, "check"]
+ else:
+ cmd = ["p", "check"]
+
+ cmd.extend(["-s", str(schedules)])
+
+ if test_case:
+ cmd.extend(["-tc", test_case])
+
+ return cmd, env
diff --git a/Src/PeasyAI/src/core/compilation/error_fixers.py b/Src/PeasyAI/src/core/compilation/error_fixers.py
new file mode 100644
index 0000000000..426d4403ab
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/error_fixers.py
@@ -0,0 +1,601 @@
+"""
+Specialized Error Fixers for P Code
+
+Implements automatic fixes for common P compiler errors.
+"""
+
+import re
+import logging
+from typing import Optional, Tuple, List, Callable
+from dataclasses import dataclass
+
+from .error_parser import PCompilerError, ErrorCategory
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class CodeFix:
+ """A fix to apply to code."""
+ file_path: str
+ original_code: str
+ fixed_code: str
+ description: str
+ line_changes: List[Tuple[int, str, str]] = None # [(line, old, new), ...]
+
+ @property
+ def diff_summary(self) -> str:
+ if self.line_changes:
+ changes = []
+ for line, old, new in self.line_changes:
+ changes.append(f" Line {line}: '{old.strip()}' → '{new.strip()}'")
+ return "\n".join(changes)
+ return f" {self.description}"
+
+
+class PErrorFixer:
+ """Fixes common P compiler errors automatically."""
+
+ def __init__(self):
+ self._fixers = {
+ ErrorCategory.VAR_DECLARATION_ORDER: self._fix_var_declaration_order,
+ ErrorCategory.FOREACH_ITERATOR: self._fix_foreach_iterator,
+ ErrorCategory.INVALID_CHARACTER: self._fix_invalid_characters,
+ ErrorCategory.UNHANDLED_EVENT: self._fix_unhandled_event,
+ ErrorCategory.MISSING_SEMICOLON: self._fix_missing_semicolon,
+ }
+ # Additional pattern-based fixers for issues without categories
+ self._pattern_fixers = [
+ # Single-field named tuples missing trailing comma — covers all variants:
+ # "no viable alternative at input 'fieldName=value)'"
+ # "missing Iden at ')'"
+ # "no viable alternative at input 'funFoo(config:(field:type,)'"
+ (r"no viable alternative.*\w+\s*=\s*\w+\)'", self._fix_single_field_tuple),
+ (r"missing Iden at '\)'", self._fix_single_field_tuple),
+ (r"no viable alternative.*'reason=\d+\)'", self._fix_single_field_tuple),
+ (r"no viable alternative.*'reservationId=", self._fix_single_field_tuple),
+ (r"no viable alternative.*testTest", self._fix_test_declaration),
+ (r"could not find.*type.*'(\w+)'", self._fix_undefined_type),
+ (r"extraneous input 'var'", self._fix_var_declaration_order_from_message),
+ ]
+
+ def can_fix(self, error: PCompilerError) -> bool:
+ """Check if we can automatically fix this error."""
+ if error.category in self._fixers:
+ return True
+ # Check pattern-based fixers
+ for pattern, _ in self._pattern_fixers:
+ if re.search(pattern, error.message, re.IGNORECASE):
+ return True
+ return False
+
+ def fix(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """
+ Attempt to fix an error in the code.
+ Returns CodeFix if successful, None otherwise.
+ """
+ # Try category-based fixer first
+ fixer = self._fixers.get(error.category)
+ if fixer:
+ try:
+ result = fixer(error, code)
+ if result:
+ return result
+ except Exception as e:
+ logger.error(f"Error in fixer for {error.category}: {e}")
+
+ # Try pattern-based fixers
+ for pattern, pattern_fixer in self._pattern_fixers:
+ if re.search(pattern, error.message, re.IGNORECASE):
+ try:
+ result = pattern_fixer(error, code)
+ if result:
+ return result
+ except Exception as e:
+ logger.error(f"Error in pattern fixer for {pattern}: {e}")
+
+ logger.debug(f"No fixer available for error: {error.message[:100]}")
+ return None
+
+ def _fix_var_declaration_order(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Fix variable declarations that appear after statements."""
+ lines = code.split('\n')
+ error_line_idx = error.line - 1
+
+ if error_line_idx >= len(lines):
+ return None
+
+ problem_line = lines[error_line_idx]
+
+ # Check if it's a var declaration
+ if not problem_line.strip().startswith('var '):
+ return None
+
+ # Find the start of the current block (function or entry)
+ block_start = None
+ brace_count = 0
+
+ for i in range(error_line_idx - 1, -1, -1):
+ line = lines[i]
+ brace_count += line.count('}') - line.count('{')
+
+ # Check for function or entry start
+ if re.match(r'\s*(fun\s+\w+|entry)\s*', line) and '{' in line:
+ block_start = i
+ break
+ elif '{' in line and brace_count <= 0:
+ block_start = i
+ break
+
+ if block_start is None:
+ return None
+
+ # Find where var declarations should end
+ var_end = block_start + 1
+ for i in range(block_start + 1, error_line_idx):
+ if lines[i].strip().startswith('var '):
+ var_end = i + 1
+ elif lines[i].strip() and not lines[i].strip().startswith('//'):
+ break
+
+ # Move the problematic var declaration
+ var_decl = lines.pop(error_line_idx)
+ lines.insert(var_end, var_decl)
+
+ fixed_code = '\n'.join(lines)
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code=fixed_code,
+ description=f"Moved var declaration from line {error.line} to line {var_end + 1}",
+ line_changes=[(error.line, problem_line, f"(moved to line {var_end + 1})")]
+ )
+
+ def _fix_foreach_iterator(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Fix missing foreach iterator variable declaration."""
+ lines = code.split('\n')
+ error_line_idx = error.line - 1
+
+ if error_line_idx >= len(lines):
+ return None
+
+ problem_line = lines[error_line_idx]
+
+ # Extract iterator variable name from foreach
+ match = re.search(r'foreach\s*\(\s*(\w+)\s+in', problem_line)
+ if not match:
+ # Try to extract from error message
+ match = re.search(r"variable '(\w+)'", error.message)
+
+ if not match:
+ return None
+
+ iterator_name = match.group(1)
+
+ # Find the function start to add var declaration
+ func_start = None
+ for i in range(error_line_idx - 1, -1, -1):
+ line = lines[i]
+ if re.match(r'\s*fun\s+\w+', line):
+ func_start = i
+ break
+
+ if func_start is None:
+ return None
+
+ # Find where to insert var declaration (after opening brace)
+ insert_line = func_start + 1
+ for i in range(func_start, error_line_idx):
+ if '{' in lines[i]:
+ insert_line = i + 1
+ break
+
+ # Determine the type (try to infer from context)
+ # Default to 'machine' as that's common for iterating over sets
+ iterator_type = "machine"
+
+ # Check if we can infer type from the collection
+ collection_match = re.search(r'foreach\s*\(\s*\w+\s+in\s+(\w+)', problem_line)
+ if collection_match:
+ collection_name = collection_match.group(1)
+ # Search for collection type in the code
+ type_match = re.search(rf'var\s+{collection_name}\s*:\s*(set|seq|map)\s*\[\s*(\w+)', code)
+ if type_match:
+ iterator_type = type_match.group(2)
+
+ # Get indentation
+ indent = len(problem_line) - len(problem_line.lstrip())
+ base_indent = " " * (indent // 4)
+
+ # Insert var declaration
+ var_decl = f"{base_indent}var {iterator_name}: {iterator_type};"
+ lines.insert(insert_line, var_decl)
+
+ fixed_code = '\n'.join(lines)
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code=fixed_code,
+ description=f"Added declaration 'var {iterator_name}: {iterator_type};' at line {insert_line + 1}",
+ line_changes=[(insert_line + 1, "", var_decl)]
+ )
+
+ def _fix_invalid_characters(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Fix invalid characters like markdown code fences."""
+ original = code
+
+ # Remove markdown artifacts
+ code = re.sub(r'^```\w*\s*\n?', '', code)
+ code = re.sub(r'\n?```\s*$', '', code)
+ code = code.replace('```', '')
+
+ if code == original:
+ return None
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=original,
+ fixed_code=code,
+ description="Removed markdown code fence artifacts (```)"
+ )
+
+ def _fix_unhandled_event(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Fix unhandled event by adding ignore statement."""
+ # Extract event name from error
+ match = re.search(r"event '([^']+)'", error.message)
+ if not match:
+ match = re.search(r"(e\w+)", error.message)
+
+ if not match:
+ return None
+
+ event_name = match.group(1)
+
+ # Find the state where this needs to be added
+ lines = code.split('\n')
+
+ # Look for the state definition
+ state_match = re.search(r"state\s+'([^']+)'", error.message)
+ if not state_match:
+ return None
+
+ state_name = state_match.group(1)
+ # Extract just the state name without the full path
+ if '.' in state_name:
+ state_name = state_name.split('.')[-1]
+
+ # Find the state in the code
+ state_line = None
+ for i, line in enumerate(lines):
+ if f'state {state_name}' in line:
+ state_line = i
+ break
+
+ if state_line is None:
+ return None
+
+ # Find where to insert ignore statement (before closing brace of state)
+ insert_line = None
+ brace_count = 0
+ for i in range(state_line, len(lines)):
+ brace_count += lines[i].count('{') - lines[i].count('}')
+ if brace_count == 0 and '}' in lines[i]:
+ insert_line = i
+ break
+
+ if insert_line is None:
+ return None
+
+ # Get indentation
+ state_indent = len(lines[state_line]) - len(lines[state_line].lstrip())
+ inner_indent = " " * ((state_indent // 4) + 1)
+
+ # Insert ignore statement
+ ignore_stmt = f"{inner_indent}ignore {event_name};"
+ lines.insert(insert_line, ignore_stmt)
+
+ fixed_code = '\n'.join(lines)
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code=fixed_code,
+ description=f"Added 'ignore {event_name};' to state {state_name}",
+ line_changes=[(insert_line + 1, "", ignore_stmt)]
+ )
+
+ def _fix_missing_semicolon(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Fix missing semicolon at end of statement."""
+ lines = code.split('\n')
+ error_line_idx = error.line - 1
+
+ if error_line_idx >= len(lines):
+ return None
+
+ problem_line = lines[error_line_idx]
+
+ # Check if line needs semicolon
+ stripped = problem_line.rstrip()
+ if stripped.endswith(';') or stripped.endswith('{') or stripped.endswith('}'):
+ return None
+
+ # Add semicolon
+ lines[error_line_idx] = stripped + ';'
+
+ fixed_code = '\n'.join(lines)
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code=fixed_code,
+ description=f"Added missing semicolon at line {error.line}",
+ line_changes=[(error.line, problem_line, stripped + ';')]
+ )
+
+ def _fix_named_field_tuple(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """
+ Fix named-field tuple construction on the error line.
+
+ The P compiler rejects ``(field = value, ...)`` in value position.
+ This converts every named-field assignment on the error line to
+ positional form: ``(value, ...)``.
+ """
+ lines = code.split('\n')
+ error_line_idx = error.line - 1 if error.line else 0
+ if error_line_idx >= len(lines):
+ return None
+
+ problem_line = lines[error_line_idx]
+
+ # Quick check: does the line even contain ``identifier = `` inside parens?
+ if not re.search(r'\(\s*\w+\s*=\s*', problem_line):
+ return None
+
+ def _strip_fields_in_parens(line: str) -> str:
+ """Replace all ``(field = val, ...)`` groups on a line."""
+ result = []
+ i = 0
+ while i < len(line):
+ # Look for opening paren followed by identifier =
+ m = re.search(r'\(\s*([a-zA-Z_]\w*)\s*=\s*', line[i:])
+ if not m:
+ result.append(line[i:])
+ break
+
+ open_pos = i + m.start()
+ result.append(line[i:open_pos])
+
+ # Find balanced close-paren
+ depth = 0
+ close_pos = -1
+ for j in range(open_pos, len(line)):
+ if line[j] == '(':
+ depth += 1
+ elif line[j] == ')':
+ depth -= 1
+ if depth == 0:
+ close_pos = j
+ break
+
+ if close_pos == -1:
+ result.append(line[open_pos:])
+ break
+
+ inner = line[open_pos + 1:close_pos]
+ # Skip complex nested expressions
+ if '(' in inner or ')' in inner:
+ result.append(line[open_pos:close_pos + 1])
+ i = close_pos + 1
+ continue
+
+ parts = [p.strip() for p in inner.split(',') if p.strip()]
+ named_count = sum(
+ 1 for p in parts if re.match(r'^[a-zA-Z_]\w*\s*=\s*', p)
+ )
+ if named_count >= 1 and named_count == len(parts):
+ values = []
+ for p in parts:
+ fm = re.match(r'^[a-zA-Z_]\w*\s*=\s*(.+)$', p)
+ values.append(fm.group(1).strip() if fm else p)
+ trailing = ',' if inner.rstrip().endswith(',') else ''
+ result.append(f"({', '.join(values)}{trailing})")
+ else:
+ result.append(line[open_pos:close_pos + 1])
+
+ i = close_pos + 1
+
+ return ''.join(result)
+
+ fixed_line = _strip_fields_in_parens(problem_line)
+ if fixed_line == problem_line:
+ return None
+
+ lines[error_line_idx] = fixed_line
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code='\n'.join(lines),
+ description=f"Converted named-field tuple to positional form at line {error.line}",
+ line_changes=[(error.line, problem_line, fixed_line)],
+ )
+
+ def _fix_single_field_tuple(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """
+ Fix single-field tuple missing trailing comma on the error line.
+
+ Handles all contexts:
+ (field = value) → (field = value,) value construction
+ (field: type) → (field: type,) type annotation
+ ((field = value)) → ((field = value,)) nested in new Machine((...))
+ """
+ lines = code.split('\n')
+ error_line_idx = error.line - 1
+
+ if error_line_idx >= len(lines):
+ return None
+
+ problem_line = lines[error_line_idx]
+
+ # Value context: (identifier = expr) without trailing comma
+ value_pat = r'\((\s*[a-zA-Z_]\w*\s*=\s*[^,()]+[^,\s])\s*\)'
+ # Type annotation context: (identifier: type) without trailing comma
+ type_pat = r'\((\s*[a-zA-Z_]\w*\s*:\s*[^,()]+[^,\s])\s*\)'
+
+ fixed_line = problem_line
+ changed = False
+
+ for pat in [value_pat, type_pat]:
+ m = re.search(pat, fixed_line)
+ if m:
+ inner = m.group(1)
+ if not inner.rstrip().endswith(',') and inner.count('=') <= 1 and inner.count(':') <= 1:
+ fixed_line = re.sub(pat, r'(\1,)', fixed_line, count=1)
+ changed = True
+
+ if not changed or fixed_line == problem_line:
+ return None
+
+ lines[error_line_idx] = fixed_line
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code='\n'.join(lines),
+ description=f"Added trailing comma to single-field tuple at line {error.line}",
+ line_changes=[(error.line, problem_line, fixed_line)],
+ )
+
+ def _fix_test_declaration(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Fix test declaration syntax."""
+ # Common issues:
+ # - test Name [main=X]: assert Y in (union ...) -> test Name [main=X]: assert Y in (union ...)
+ # - Missing module declaration
+
+ lines = code.split('\n')
+
+ # Find test declarations
+ fixed_lines = []
+ changed = False
+
+ for i, line in enumerate(lines):
+ # Fix: test X [main=Y]: assert Z in (union {A, B})
+ # To: module SystemModule = { A, B }; test X [main=Y]: assert Z in (union SystemModule, { Y })
+ if 'test ' in line and '[main=' in line:
+ # Check for malformed union syntax
+ if 'union {' in line and 'union ' not in line.replace('union {', ''):
+ # Extract machines from union
+ union_match = re.search(r'union\s*\{([^}]+)\}', line)
+ if union_match:
+ machines = union_match.group(1)
+ # This is already valid syntax, skip
+ fixed_lines.append(line)
+ continue
+
+ fixed_lines.append(line)
+
+ if not changed:
+ return None
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code='\n'.join(fixed_lines),
+ description="Fixed test declaration syntax"
+ )
+
+ def _fix_undefined_type(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Add placeholder for undefined type."""
+ # Extract type name from error
+ match = re.search(r"could not find.*type.*'(\w+)'", error.message, re.IGNORECASE)
+ if not match:
+ return None
+
+ type_name = match.group(1)
+
+ # Add placeholder type at the beginning of the file
+ placeholder = f"// TODO: Define type properly\ntype {type_name} = (placeholder: int);\n\n"
+ fixed_code = placeholder + code
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code=fixed_code,
+ description=f"Added placeholder for undefined type '{type_name}'"
+ )
+
+ def _fix_var_declaration_order_from_message(self, error: PCompilerError, code: str) -> Optional[CodeFix]:
+ """Fix var declaration order based on 'extraneous input var' message."""
+ # This handles the case when category isn't set but message indicates var order issue
+ lines = code.split('\n')
+ error_line_idx = error.line - 1
+
+ if error_line_idx >= len(lines):
+ return None
+
+ problem_line = lines[error_line_idx]
+
+ # Verify it's a var declaration
+ if not problem_line.strip().startswith('var '):
+ return None
+
+ # Find function/entry block start
+ block_start = None
+ for i in range(error_line_idx - 1, -1, -1):
+ line = lines[i]
+ if re.match(r'\s*(fun\s+\w+|entry)', line):
+ block_start = i
+ break
+
+ if block_start is None:
+ return None
+
+ # Find where vars should go (after opening brace)
+ insert_pos = block_start + 1
+ for i in range(block_start, error_line_idx):
+ if '{' in lines[i]:
+ insert_pos = i + 1
+ break
+
+ # Move the var declaration
+ var_decl = lines.pop(error_line_idx)
+
+ # Find end of existing var declarations
+ for i in range(insert_pos, len(lines)):
+ if not lines[i].strip().startswith('var ') and lines[i].strip():
+ insert_pos = i
+ break
+
+ lines.insert(insert_pos, var_decl)
+
+ return CodeFix(
+ file_path=error.file,
+ original_code=code,
+ fixed_code='\n'.join(lines),
+ description=f"Moved var declaration to start of function/block"
+ )
+
+
+def apply_fix(code: str, fix: CodeFix) -> str:
+ """Apply a fix to code."""
+ return fix.fixed_code
+
+
+def fix_all_errors(code: str, errors: List[PCompilerError]) -> Tuple[str, List[CodeFix]]:
+ """
+ Attempt to fix all errors in the code.
+ Returns (fixed_code, list_of_applied_fixes).
+ """
+ fixer = PErrorFixer()
+ applied_fixes = []
+ current_code = code
+
+ for error in errors:
+ if fixer.can_fix(error):
+ fix = fixer.fix(error, current_code)
+ if fix:
+ current_code = fix.fixed_code
+ applied_fixes.append(fix)
+ logger.info(f"Applied fix: {fix.description}")
+
+ return current_code, applied_fixes
diff --git a/Src/PeasyAI/src/core/compilation/error_parser.py b/Src/PeasyAI/src/core/compilation/error_parser.py
new file mode 100644
index 0000000000..4f3cd31aca
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/error_parser.py
@@ -0,0 +1,318 @@
+"""
+P Compiler Error Parser
+
+Parses P compiler output into structured error objects for easier handling.
+"""
+
+import re
+import logging
+from typing import List, Optional, Dict, Any
+from dataclasses import dataclass, field
+from enum import Enum
+
+logger = logging.getLogger(__name__)
+
+
+class ErrorType(Enum):
+ """Types of P compiler errors"""
+ PARSE = "parse"
+ TYPE = "type"
+ SEMANTIC = "semantic"
+ INTERNAL = "internal"
+ UNKNOWN = "unknown"
+
+
+class ErrorCategory(Enum):
+ """Common error categories for pattern matching"""
+ VAR_DECLARATION_ORDER = "var_declaration_order"
+ MISSING_SEMICOLON = "missing_semicolon"
+ UNDEFINED_EVENT = "undefined_event"
+ UNDEFINED_TYPE = "undefined_type"
+ UNDEFINED_VARIABLE = "undefined_variable"
+ FOREACH_ITERATOR = "foreach_iterator"
+ TYPE_MISMATCH = "type_mismatch"
+ DUPLICATE_DECLARATION = "duplicate_declaration"
+ UNHANDLED_EVENT = "unhandled_event"
+ NULL_TARGET = "null_target"
+ INVALID_CHARACTER = "invalid_character"
+ UNBALANCED_BRACES = "unbalanced_braces"
+ UNKNOWN = "unknown"
+
+
+@dataclass
+class PCompilerError:
+ """Structured representation of a P compiler error"""
+ file: str
+ line: int
+ column: int
+ error_type: ErrorType
+ category: ErrorCategory
+ message: str
+ raw_message: str
+ suggestion: Optional[str] = None
+ context_lines: List[str] = field(default_factory=list)
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "file": self.file,
+ "line": self.line,
+ "column": self.column,
+ "error_type": self.error_type.value,
+ "category": self.category.value,
+ "message": self.message,
+ "suggestion": self.suggestion,
+ }
+
+
+class PCompilerErrorParser:
+ """Parses P compiler output into structured errors."""
+
+ # Patterns for different error formats
+ PATTERNS = [
+ # [filename.p] parse error: line X:Y message
+ (r'\[([^\]]+\.p)\]\s*parse error:\s*line\s*(\d+):(\d+)\s*(.+)', ErrorType.PARSE),
+ # [filename.p] error: line X:Y message
+ (r'\[([^\]]+\.p)\]\s*error:\s*line\s*(\d+):(\d+)\s*(.+)', ErrorType.TYPE),
+ # [Error:] [filename.p:line:col] message
+ (r'\[Error:\]\s*\[([^\]:]+\.p):(\d+):(\d+)\]\s*(.+)', ErrorType.SEMANTIC),
+ # [Parser Error:] [filename.p] parse error: line X:Y message
+ (r'\[Parser Error:\]\s*\[([^\]]+\.p)\]\s*parse error:\s*line\s*(\d+):(\d+)\s*(.+)', ErrorType.PARSE),
+ # Generic error pattern
+ (r'\[([^\]]+\.p)\].*?line\s*(\d+):(\d+)\s*(.+)', ErrorType.UNKNOWN),
+ ]
+
+ # Category detection patterns
+ CATEGORY_PATTERNS = {
+ ErrorCategory.VAR_DECLARATION_ORDER: [
+ r"extraneous input 'var'",
+ r"var.*expecting.*announce",
+ ],
+ ErrorCategory.FOREACH_ITERATOR: [
+ r"could not find foreach iterator",
+ r"foreach.*iterator.*variable",
+ ],
+ ErrorCategory.TYPE_MISMATCH: [
+ r"got type:.*expected:",
+ r"type mismatch",
+ r"cannot convert",
+ ],
+ ErrorCategory.UNDEFINED_EVENT: [
+ r"event.*not found",
+ r"undefined event",
+ r"could not find event",
+ ],
+ ErrorCategory.UNDEFINED_TYPE: [
+ r"type.*not found",
+ r"undefined type",
+ r"could not find type",
+ ],
+ ErrorCategory.UNDEFINED_VARIABLE: [
+ r"variable.*not found",
+ r"undefined variable",
+ r"could not find.*variable",
+ ],
+ ErrorCategory.DUPLICATE_DECLARATION: [
+ r"duplicates declaration",
+ r"already declared",
+ r"duplicate definition",
+ ],
+ ErrorCategory.UNHANDLED_EVENT: [
+ r"cannot be handled",
+ r"unhandled event",
+ r"no handler for",
+ ],
+ ErrorCategory.NULL_TARGET: [
+ r"target.*cannot be null",
+ r"null target",
+ r"send.*null",
+ ],
+ ErrorCategory.INVALID_CHARACTER: [
+ r"token recognition error",
+ r"invalid character",
+ r"unexpected character",
+ ],
+ ErrorCategory.MISSING_SEMICOLON: [
+ r"missing.*semicolon",
+ r"expecting.*';'",
+ ],
+ ErrorCategory.UNBALANCED_BRACES: [
+ r"missing.*'}'",
+ r"unmatched.*brace",
+ r"expecting.*'}'",
+ ],
+ }
+
+ # Suggestions for each category
+ SUGGESTIONS = {
+ ErrorCategory.VAR_DECLARATION_ORDER:
+ "Move variable declarations to the start of the function/block, before any statements.",
+ ErrorCategory.FOREACH_ITERATOR:
+ "Declare the foreach iterator variable before the loop: 'var iterName: Type;'",
+ ErrorCategory.TYPE_MISMATCH:
+ "Check that the types match. You may need to cast or convert the value.",
+ ErrorCategory.UNDEFINED_EVENT:
+ "Ensure the event is declared in the types/events file with 'event eEventName: PayloadType;'",
+ ErrorCategory.UNDEFINED_TYPE:
+ "Ensure the type is declared with 'type TypeName = ...;'",
+ ErrorCategory.UNDEFINED_VARIABLE:
+ "Declare the variable before using it with 'var varName: Type;'",
+ ErrorCategory.DUPLICATE_DECLARATION:
+ "Remove the duplicate declaration or rename one of them.",
+ ErrorCategory.UNHANDLED_EVENT:
+ "Add a handler for the event with 'on eEvent do Handler;' or 'ignore eEvent;'",
+ ErrorCategory.NULL_TARGET:
+ "Ensure the target machine is initialized before sending. Add configuration events to wire machines together.",
+ ErrorCategory.INVALID_CHARACTER:
+ "Remove invalid characters (like markdown code fences ```) from the file.",
+ ErrorCategory.MISSING_SEMICOLON:
+ "Add a semicolon at the end of the statement.",
+ ErrorCategory.UNBALANCED_BRACES:
+ "Check for missing or extra braces. Each '{' needs a matching '}'.",
+ }
+
+ @classmethod
+ def parse(cls, compiler_output: str) -> List[PCompilerError]:
+ """Parse compiler output into structured errors."""
+ errors = []
+
+ for pattern, error_type in cls.PATTERNS:
+ for match in re.finditer(pattern, compiler_output, re.IGNORECASE | re.MULTILINE):
+ file_name = match.group(1)
+ line = int(match.group(2))
+ column = int(match.group(3))
+ message = match.group(4).strip()
+
+ # Determine category
+ category = cls._categorize_error(message)
+
+ # Get suggestion
+ suggestion = cls.SUGGESTIONS.get(category)
+
+ error = PCompilerError(
+ file=file_name,
+ line=line,
+ column=column,
+ error_type=error_type,
+ category=category,
+ message=message,
+ raw_message=match.group(0),
+ suggestion=suggestion,
+ )
+
+ errors.append(error)
+
+ # Deduplicate errors
+ seen = set()
+ unique_errors = []
+ for e in errors:
+ key = (e.file, e.line, e.column, e.message)
+ if key not in seen:
+ seen.add(key)
+ unique_errors.append(e)
+
+ return unique_errors
+
+ @classmethod
+ def _categorize_error(cls, message: str) -> ErrorCategory:
+ """Categorize an error message."""
+ message_lower = message.lower()
+
+ for category, patterns in cls.CATEGORY_PATTERNS.items():
+ for pattern in patterns:
+ if re.search(pattern, message_lower):
+ return category
+
+ return ErrorCategory.UNKNOWN
+
+ @classmethod
+ def parse_checker_trace(cls, trace: str) -> Dict[str, Any]:
+ """Parse a PChecker trace log to extract error information."""
+ result = {
+ "error_type": None,
+ "error_message": None,
+ "failing_machine": None,
+ "failing_state": None,
+ "event_involved": None,
+ "trace_summary": [],
+ }
+
+ lines = trace.strip().split('\n')
+
+ for line in lines:
+ # Extract error log
+ if '' in line:
+ result["error_message"] = line.split('')[-1].strip()
+
+ # Check for specific error types
+ if 'cannot be handled' in line:
+ result["error_type"] = "unhandled_event"
+ # Extract event name
+ match = re.search(r"event '([^']+)'", line)
+ if match:
+ result["event_involved"] = match.group(1)
+ elif 'cannot be null' in line:
+ result["error_type"] = "null_target"
+ elif 'assertion' in line.lower():
+ result["error_type"] = "assertion_failure"
+
+ # Extract state transitions
+ if '' in line:
+ result["trace_summary"].append(line)
+ # Get current machine and state
+ match = re.search(r"(\w+)\((\d+)\)\s+enters state\s+'([^']+)'", line)
+ if match:
+ result["failing_machine"] = f"{match.group(1)}({match.group(2)})"
+ result["failing_state"] = match.group(3)
+
+ # Extract send events
+ if '' in line:
+ result["trace_summary"].append(line)
+
+ return result
+
+
+@dataclass
+class CompilationResult:
+ """Result of a P compilation attempt."""
+ success: bool
+ errors: List[PCompilerError] = field(default_factory=list)
+ warnings: List[str] = field(default_factory=list)
+ output: str = ""
+
+ def get_first_error(self) -> Optional[PCompilerError]:
+ return self.errors[0] if self.errors else None
+
+ def get_errors_by_file(self) -> Dict[str, List[PCompilerError]]:
+ by_file = {}
+ for error in self.errors:
+ if error.file not in by_file:
+ by_file[error.file] = []
+ by_file[error.file].append(error)
+ return by_file
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "success": self.success,
+ "error_count": len(self.errors),
+ "errors": [e.to_dict() for e in self.errors],
+ "warnings": self.warnings,
+ }
+
+
+def parse_compilation_output(output: str) -> CompilationResult:
+ """Parse full compilation output into a result object."""
+ success = "Compilation succeeded" in output or "Build succeeded" in output
+ errors = PCompilerErrorParser.parse(output)
+
+ # Extract warnings
+ warnings = []
+ for line in output.split('\n'):
+ if 'warning' in line.lower() and 'error' not in line.lower():
+ warnings.append(line.strip())
+
+ return CompilationResult(
+ success=success and len(errors) == 0,
+ errors=errors,
+ warnings=warnings,
+ output=output,
+ )
diff --git a/Src/PeasyAI/src/core/compilation/p_code_utils.py b/Src/PeasyAI/src/core/compilation/p_code_utils.py
new file mode 100644
index 0000000000..91bed0cf02
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/p_code_utils.py
@@ -0,0 +1,277 @@
+"""
+Shared utilities for robust P code parsing.
+
+Provides brace-balanced extraction and LLM response code extraction
+that replaces fragile regex-based approaches.
+"""
+
+import re
+import logging
+from typing import Optional, Tuple, List, Dict
+
+logger = logging.getLogger(__name__)
+
+
+def find_balanced_brace(text: str, open_pos: int) -> int:
+ """
+ Find the position of the matching close-brace for the open-brace
+ at *open_pos*. Returns -1 if no match is found.
+
+ Handles arbitrary nesting depth.
+ """
+ depth = 0
+ for i in range(open_pos, len(text)):
+ if text[i] == '{':
+ depth += 1
+ elif text[i] == '}':
+ depth -= 1
+ if depth == 0:
+ return i
+ return -1
+
+
+def extract_block_body(text: str, open_pos: int) -> Tuple[str, int]:
+ """
+ Extract the body between ``{`` at *open_pos* and its matching ``}``.
+
+ Returns ``(body_text, close_pos)`` or ``("", -1)`` if unbalanced.
+ The body_text does NOT include the outer braces.
+ """
+ close = find_balanced_brace(text, open_pos)
+ if close == -1:
+ return "", -1
+ return text[open_pos + 1:close], close
+
+
+def extract_function_body(code: str, func_name: str) -> Optional[str]:
+ """
+ Extract the full body of ``fun (...) { ... }`` using
+ brace-balanced parsing.
+
+ Handles nested parens in the parameter list (e.g., tuple types).
+
+ Returns the body text (between the braces) or None if not found.
+ """
+ # Find "fun Name" then skip to the opening { of the body
+ # (can't use [^)]* for params since they may contain nested parens)
+ pattern = rf'\bfun\s+{re.escape(func_name)}\s*\('
+ match = re.search(pattern, code)
+ if not match:
+ return None
+ # Skip past the balanced parameter parens
+ paren_close = find_balanced_brace.__wrapped__(code, match.end() - 1) if hasattr(find_balanced_brace, '__wrapped__') else _find_balanced_char(code, match.end() - 1, '(', ')')
+ if paren_close == -1:
+ return None
+ # Now find the opening { after the param list (may have : ReturnType)
+ rest = code[paren_close + 1:]
+ brace_match = re.search(r'\s*(?::\s*\w+)?\s*\{', rest)
+ if not brace_match:
+ return None
+ open_pos = paren_close + 1 + brace_match.end() - 1
+ body, close = extract_block_body(code, open_pos)
+ return body if body or close != -1 else None
+
+
+def _find_balanced_char(text: str, open_pos: int, open_ch: str, close_ch: str) -> int:
+ """Find matching close character for balanced open/close pairs."""
+ depth = 0
+ for i in range(open_pos, len(text)):
+ if text[i] == open_ch:
+ depth += 1
+ elif text[i] == close_ch:
+ depth -= 1
+ if depth == 0:
+ return i
+ return -1
+
+
+def extract_state_body(code: str, state_name: str, is_start: bool = False) -> Optional[str]:
+ """
+ Extract the full body of a state declaration using brace-balanced parsing.
+
+ Handles ``start state Name { ... }`` and ``state Name { ... }``.
+
+ Returns the body text (between the braces) or None if not found.
+ """
+ prefix = r'start\s+' if is_start else r'(?:start\s+)?'
+ pattern = rf'\b{prefix}state\s+{re.escape(state_name)}\s*\{{'
+ match = re.search(pattern, code)
+ if not match:
+ return None
+ open_pos = match.end() - 1
+ body, _ = extract_block_body(code, open_pos)
+ return body if body or _ != -1 else None
+
+
+def extract_start_state(code: str) -> Tuple[Optional[str], Optional[str]]:
+ """
+ Find the start state in a P machine and return ``(name, body)``.
+
+ Returns ``(None, None)`` if no start state is found.
+ """
+ match = re.search(r'\bstart\s+state\s+(\w+)\s*\{', code)
+ if not match:
+ return None, None
+ state_name = match.group(1)
+ open_pos = match.end() - 1
+ body, _ = extract_block_body(code, open_pos)
+ if _ == -1:
+ return state_name, None
+ return state_name, body
+
+
+def iter_function_bodies(code: str):
+ """
+ Yield ``(func_name, header, body, start_pos, end_pos)`` for every
+ ``fun`` definition in *code*, using brace-balanced extraction.
+
+ Handles nested parens in parameter lists (e.g., tuple types).
+ """
+ for match in re.finditer(r'fun\s+(\w+)\s*\(', code):
+ func_name = match.group(1)
+ func_start = match.start()
+ # Skip past balanced parameter parens
+ paren_close = _find_balanced_char(code, match.end() - 1, '(', ')')
+ if paren_close == -1:
+ continue
+ # Find the opening { after params (may have : ReturnType)
+ rest = code[paren_close + 1:]
+ brace_match = re.search(r'\s*(?::\s*\w+)?\s*\{', rest)
+ if not brace_match:
+ continue
+ open_pos = paren_close + 1 + brace_match.end() - 1
+ header = code[func_start:open_pos]
+ body, close_pos = extract_block_body(code, open_pos)
+ if close_pos != -1:
+ yield func_name, header, body, func_start, close_pos
+
+
+def iter_all_code_blocks(code: str):
+ """
+ Yield ``(block_name, header, body, start_pos, end_pos)`` for every
+ code block where variable declarations must appear first:
+
+ - ``fun Name(...) { ... }``
+ - ``entry { ... }`` and ``entry (param: Type) { ... }``
+ - ``on eEvent do { ... }`` and ``on eEvent do (param: Type) { ... }``
+
+ This is a superset of :func:`iter_function_bodies`.
+ """
+ yield from iter_function_bodies(code)
+
+ # entry { ... } or entry (param: Type) { ... }
+ for match in re.finditer(r'\bentry\s*(?:\([^)]*\)\s*)?\{', code):
+ block_start = match.start()
+ open_pos = match.end() - 1
+ header = code[block_start:open_pos]
+ body, close_pos = extract_block_body(code, open_pos)
+ if close_pos != -1:
+ yield "entry", header, body, block_start, close_pos
+
+ # on eEvent do { ... } or on eEvent do (param: Type) { ... }
+ for match in re.finditer(r'\bon\s+(\w+)\s+do\s*(?:\([^)]*\)\s*)?\{', code):
+ handler_name = f"on_{match.group(1)}"
+ block_start = match.start()
+ open_pos = match.end() - 1
+ header = code[block_start:open_pos]
+ body, close_pos = extract_block_body(code, open_pos)
+ if close_pos != -1:
+ yield handler_name, header, body, block_start, close_pos
+
+
+# ── LLM response code extraction ────────────────────────────────────
+
+def extract_p_code_from_response(
+ response: str,
+ expected_filename: Optional[str] = None,
+) -> Tuple[Optional[str], Optional[str]]:
+ """
+ Extract P code and filename from an LLM response.
+
+ Tries multiple strategies in order of specificity:
+ 1. XML-style tags: ``...``
+ 2. Markdown code block with filename comment
+ 3. Markdown code block (any)
+ 4. Raw P code (machine/spec/test block detection)
+
+ If no filename can be determined from the response, falls back to
+ *expected_filename* (converted to a safe .p filename).
+
+ Returns ``(filename, code)`` or ``(None, None)``.
+ """
+ filename: Optional[str] = None
+ code: Optional[str] = None
+
+ # Strategy 1: XML tags ... (case-insensitive tag)
+ xml_match = re.search(r'<(\w+\.p)>(.*?)\1>', response, re.DOTALL)
+ if xml_match:
+ filename = xml_match.group(1)
+ code = xml_match.group(2).strip()
+
+ # Strategy 2: Markdown with filename comment ```p\n// Foo.p\n...```
+ if not code:
+ md_match = re.search(
+ r'```(?:p|P)?\s*\n\s*//\s*(\w+\.p)\s*\n(.*?)```',
+ response, re.DOTALL,
+ )
+ if md_match:
+ filename = md_match.group(1)
+ code = md_match.group(2).strip()
+
+ # Strategy 3: Any markdown code block
+ if not code:
+ md_bare = re.search(r'```(?:p|P)?\s*\n(.*?)```', response, re.DOTALL)
+ if md_bare:
+ candidate = md_bare.group(1).strip()
+ # Derive filename from first machine/spec/test keyword
+ for kw_pattern, suffix in [
+ (r'\bmachine\s+(\w+)', ''),
+ (r'\bspec\s+(\w+)', ''),
+ (r'\btest\s+(\w+)', ''),
+ ]:
+ nm = re.search(kw_pattern, candidate)
+ if nm:
+ filename = f"{nm.group(1)}.p"
+ code = candidate
+ break
+ if not code and candidate:
+ code = candidate
+
+ # Strategy 4: Raw P code — find the first top-level P construct
+ if not code:
+ for kw in [r'machine\s+\w+\s*\{', r'spec\s+\w+\s+observes',
+ r'test\s+\w+\s*\[', r'type\s+\w+\s*=', r'event\s+\w+']:
+ m = re.search(kw, response)
+ if m:
+ # Take from the start of the match to the end of the response,
+ # but strip trailing prose after the last closing brace.
+ raw = response[m.start():]
+ # Find the last balanced close-brace
+ last_close = raw.rfind('}')
+ if last_close != -1:
+ raw = raw[:last_close + 1]
+ # Try to find a trailing semicolon-terminated section
+ # (for type/event declarations that don't use braces)
+ trailing = response[m.start() + last_close + 1:]
+ extra = re.search(r'^[^{}]*?;', trailing)
+ if extra:
+ raw += extra.group(0)
+ code = raw.strip()
+ nm = re.search(r'\b(?:machine|spec)\s+(\w+)', code)
+ if nm:
+ filename = f"{nm.group(1)}.p"
+ break
+
+ # Fallback filename from expected_name
+ if code and not filename and expected_filename:
+ safe = re.sub(r'[^\w]', '', expected_filename)
+ filename = f"{safe}.p" if safe else None
+
+ if not filename or not code:
+ return None, None
+
+ # Strip leftover markdown fences
+ code = re.sub(r'^```\w*\s*', '', code)
+ code = re.sub(r'\s*```\s*$', '', code)
+
+ return filename, code
diff --git a/Src/PeasyAI/src/core/compilation/p_post_processor.py b/Src/PeasyAI/src/core/compilation/p_post_processor.py
new file mode 100644
index 0000000000..8aa246046b
--- /dev/null
+++ b/Src/PeasyAI/src/core/compilation/p_post_processor.py
@@ -0,0 +1,631 @@
+"""
+P Code Post-Processor.
+
+Automatically fixes common LLM-generated P code issues before compilation.
+This reduces the need for iterative fixing and improves code generation quality.
+"""
+
+import re
+import logging
+from typing import List, Optional
+from dataclasses import dataclass
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class PostProcessResult:
+ """Result of post-processing."""
+ code: str
+ fixes_applied: List[str]
+ warnings: List[str]
+
+
+class PCodePostProcessor:
+ """
+ Auto-fix common P syntax issues in generated code.
+
+ Handles:
+ 1. Variable declaration order (must be at start of functions)
+ 2. Single-field tuple syntax (needs trailing comma)
+ 3. Enum access syntax (EnumName.VALUE -> VALUE)
+ 4. Test declaration syntax fixes
+ 5. Missing semicolons
+ """
+
+ def __init__(self):
+ self.fixes_applied: List[str] = []
+ self.warnings: List[str] = []
+
+ def process(
+ self,
+ code: str,
+ filename: str = "",
+ is_test_file: bool = False,
+ ) -> PostProcessResult:
+ """
+ Process P code and fix common issues.
+
+ Args:
+ code: The P code to process
+ filename: Optional filename for better error messages
+ is_test_file: If True, this file belongs to PTst and must
+ contain test declarations.
+
+ Returns:
+ PostProcessResult with fixed code and list of fixes applied
+ """
+ self.fixes_applied = []
+ self.warnings = []
+
+ original_code = code
+
+ # Apply fixes in order
+ code = self._fix_trailing_comma_in_params(code)
+ code = self._fix_variable_declaration_order(code)
+ code = self._fix_single_field_tuples(code)
+ code = self._fix_named_field_tuple_construction(code)
+ code = self._fix_enum_access_syntax(code)
+ code = self._fix_test_declaration_syntax(code)
+ code = self._fix_test_declaration_union_syntax(code)
+ code = self._fix_missing_semicolons(code)
+ code = self._fix_entry_function_syntax(code)
+ code = self._fix_bare_halt(code)
+ code = self._fix_forbidden_in_monitors(code)
+
+ # For PTst files, check for common wiring bugs and ensure test declarations
+ if is_test_file:
+ code = self._warn_timer_wired_to_this(code, filename)
+ code = self._ensure_test_declarations(code, filename)
+
+ if code != original_code:
+ logger.info(f"Post-processing applied {len(self.fixes_applied)} fix(es) to {filename or 'code'}")
+
+ return PostProcessResult(
+ code=code,
+ fixes_applied=self.fixes_applied,
+ warnings=self.warnings
+ )
+
+ # ── Syntax fixes ──────────────────────────────────────────────────
+
+ def _fix_trailing_comma_in_params(self, code: str) -> str:
+ """
+ Remove trailing commas from function, entry, and handler parameter lists.
+
+ LLMs frequently generate ``fun Foo(param: Type,)`` but P requires
+ ``fun Foo(param: Type)``. Same for ``entry (param: Type,)`` and
+ ``on eEvent do (param: Type,)``.
+
+ Only targets single-parameter lists (the most common case). Multi-param
+ trailing commas are also fixed.
+ """
+ # fun Name(... ,) → fun Name(...)
+ # entry (... ,) → entry (...)
+ # on eX do (... ,) → on eX do (...)
+ pattern = re.compile(
+ r'(\b(?:fun\s+\w+|entry|on\s+\w+\s+do)\s*\([^)]*?)' # up to last content
+ r',(\s*\))' # trailing comma before )
+ )
+ fix_count = 0
+ while pattern.search(code):
+ code = pattern.sub(r'\1\2', code)
+ fix_count += 1
+ if fix_count > 50:
+ break
+
+ if fix_count > 0:
+ self.fixes_applied.append(
+ f"Removed trailing comma from {fix_count} parameter list(s)"
+ )
+ return code
+
+ def _fix_named_field_tuple_construction(self, code: str) -> str:
+ """
+ Convert named-field tuple construction to positional form.
+
+ LLMs frequently generate ``(field1 = val1, field2 = val2)`` when
+ creating a value of a user-defined named-tuple type. P requires
+ the *positional* form ``(val1, val2)`` — the field names are part
+ of the type definition, not the value constructor.
+
+ This fix targets the most common occurrences:
+ - send target, event, (field = value, ...);
+ - new Machine((field = value, ...));
+ - raise event, (field = value, ...);
+ - variable assignment: x = (field = value, ...);
+ - function call argument: fun(..., (field = value, ...));
+
+ Single-field named tuples ``(field = value,)`` → ``(value,)``
+ are handled too.
+ """
+ original = code
+
+ def _strip_field_names(tuple_inner: str) -> str:
+ """Strip ``field =`` prefixes from a comma-separated tuple body."""
+ parts = []
+ for part in tuple_inner.split(','):
+ part = part.strip()
+ if not part:
+ continue
+ # Match fieldName = expression
+ m = re.match(r'^([a-zA-Z_]\w*)\s*=\s*(.+)$', part)
+ if m:
+ parts.append(m.group(2).strip())
+ else:
+ parts.append(part)
+ return ', '.join(parts)
+
+ def _has_named_fields(inner: str) -> bool:
+ """Return True if the inner text looks like named-field assignments."""
+ parts = [p.strip() for p in inner.split(',') if p.strip()]
+ named = sum(1 for p in parts if re.match(r'^[a-zA-Z_]\w*\s*=\s*', p))
+ return named >= 1 and named == len(parts)
+
+ def _find_balanced_paren(text: str, open_pos: int) -> int:
+ """Return index of matching close-paren, or -1."""
+ depth = 0
+ for i in range(open_pos, len(text)):
+ if text[i] == '(':
+ depth += 1
+ elif text[i] == ')':
+ depth -= 1
+ if depth == 0:
+ return i
+ return -1
+
+ # We iterate through occurrences of ``(identifier = `` which is the
+ # telltale opening of a named-field construction. We then extract the
+ # balanced content up to the matching ``)`` and convert.
+ result_parts: List[str] = []
+ pos = 0
+ fix_count = 0
+
+ named_field_open = re.compile(r'\(\s*[a-zA-Z_]\w*\s*=\s*')
+
+ while pos < len(code):
+ m = named_field_open.search(code, pos)
+ if not m:
+ result_parts.append(code[pos:])
+ break
+
+ open_pos = m.start()
+ close_pos = _find_balanced_paren(code, open_pos)
+ if close_pos == -1:
+ result_parts.append(code[pos:])
+ break
+
+ inner = code[open_pos + 1:close_pos]
+
+ # Skip if the inner text contains nested parens (complex expressions)
+ # that would make naive splitting unreliable.
+ if '(' in inner or ')' in inner:
+ result_parts.append(code[pos:close_pos + 1])
+ pos = close_pos + 1
+ continue
+
+ if _has_named_fields(inner):
+ stripped = _strip_field_names(inner)
+ trailing_comma = ',' if inner.rstrip().endswith(',') else ''
+ replacement = f"({stripped}{trailing_comma})"
+ result_parts.append(code[pos:open_pos])
+ result_parts.append(replacement)
+ fix_count += 1
+ pos = close_pos + 1
+ else:
+ result_parts.append(code[pos:close_pos + 1])
+ pos = close_pos + 1
+
+ code = ''.join(result_parts)
+
+ if fix_count > 0:
+ self.fixes_applied.append(
+ f"Converted {fix_count} named-field tuple construction(s) to positional form"
+ )
+
+ return code
+
+ def _fix_variable_declaration_order(self, code: str) -> str:
+ """
+ Move variable declarations to the start of functions.
+
+ In P, all var declarations must come before any statements in a function.
+ Uses brace-balanced extraction to handle nested blocks correctly.
+ """
+ from .p_code_utils import iter_function_bodies
+
+ # Process functions from last to first so position offsets stay valid
+ replacements = []
+ for func_name, header, body, start_pos, close_pos in iter_function_bodies(code):
+ lines = body.split('\n')
+ var_lines = []
+ other_lines = []
+
+ for line in lines:
+ stripped = line.strip()
+ if stripped.startswith('var ') and ';' in stripped:
+ var_lines.append(line)
+ else:
+ other_lines.append(line)
+
+ if not var_lines:
+ continue
+
+ first_non_var_idx = -1
+ first_var_idx = -1
+ last_var_idx = -1
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ if not stripped or stripped.startswith('//'):
+ continue
+ if stripped.startswith('var ') and ';' in stripped:
+ if first_var_idx == -1:
+ first_var_idx = i
+ last_var_idx = i
+ else:
+ if first_non_var_idx == -1:
+ first_non_var_idx = i
+
+ needs_reorder = (
+ first_var_idx != -1
+ and first_non_var_idx != -1
+ and (first_var_idx > first_non_var_idx or last_var_idx > first_non_var_idx)
+ )
+ if needs_reorder:
+ new_body = '\n'.join(var_lines + other_lines)
+ # Reconstruct: header + { + new_body + }
+ replacement = header + '{' + new_body + '}'
+ replacements.append((start_pos, close_pos + 1, replacement))
+
+ if replacements:
+ self.fixes_applied.append("Moved variable declarations to start of function")
+ # Apply from end to start to preserve positions
+ for start, end, repl in reversed(replacements):
+ code = code[:start] + repl + code[end:]
+
+ return code
+
+ def _fix_single_field_tuples(self, code: str) -> str:
+ """
+ Add trailing comma to single-field named tuples in VALUE contexts only.
+
+ P requires trailing comma for single-field tuples in value expressions:
+ (field = value,) not (field = value)
+
+ But NOT in type definitions or function parameter type annotations:
+ type tFoo = (field: int); // CORRECT — no trailing comma
+ fun Bar(x: (field: machine)) {...} // CORRECT — no trailing comma
+
+ Handles: send payloads, new Machine(...), raise, assignments.
+ """
+ fix_count = 0
+
+ # --- Value context only: (identifier = expression) without trailing comma ---
+ # Matches (word = stuff) where "stuff" has no comma and no nested parens.
+ value_pattern = r'\((\s*[a-zA-Z_]\w*\s*=\s*[^,()=]+[^,\s()=])\s*\)'
+
+ def _add_comma_value(m):
+ inner = m.group(1)
+ if inner.rstrip().endswith(','):
+ return m.group(0)
+ if inner.count('=') > 1:
+ return m.group(0)
+ nonlocal fix_count
+ fix_count += 1
+ return f'({inner},)'
+
+ code = re.sub(value_pattern, _add_comma_value, code)
+
+ # NOTE: We intentionally do NOT add trailing commas to type annotation
+ # contexts like (field: type). P type definitions and function parameter
+ # type annotations do NOT require trailing commas for single-field tuples.
+ # Evidence: Tutorial/Advanced/4_Paxos and other examples all use
+ # type tFoo = (field: type); without trailing commas.
+
+ if fix_count > 0:
+ self.fixes_applied.append(
+ f"Added trailing comma to {fix_count} single-field named tuple value(s)"
+ )
+
+ return code
+
+ def _fix_enum_access_syntax(self, code: str) -> str:
+ """
+ Fix enum access from EnumName.VALUE to just VALUE.
+
+ In P, enums are accessed directly without the type prefix.
+ """
+ # Common enum patterns to fix
+ # Pattern: EnumName.VALUE where EnumName starts with 't' (P convention)
+ pattern = r'\bt([A-Z]\w*)\.([\w]+)\b'
+
+ def fix_enum(match):
+ enum_name = 't' + match.group(1)
+ value = match.group(2)
+ self.fixes_applied.append(f"Fixed enum access: {enum_name}.{value} -> {value}")
+ return value
+
+ return re.sub(pattern, fix_enum, code)
+
+ def _fix_test_declaration_syntax(self, code: str) -> str:
+ """
+ Fix test declaration syntax issues.
+
+ Valid P test declaration forms:
+ test Name [main=Driver]: assert SpecA in { Machine1, Machine2, Driver };
+ test Name [main=Driver]: assert SpecA, SpecB in { Machine1, Driver };
+ test Name [main=Driver]: { Machine1, Machine2, Driver };
+
+ NOTE: 'assert SpecName in' is CORRECT P syntax — do NOT remove it.
+ PChecker needs it to know which spec monitors to verify.
+ """
+ # We intentionally do NOT strip 'assert X in' — it's valid and required.
+ # Only fix actual syntax errors if any are detected.
+ return code
+
+ def _fix_test_declaration_union_syntax(self, code: str) -> str:
+ """
+ Fix ``(union { ... })`` in test declarations to ``{ ... }``.
+
+ LLMs frequently generate:
+ test tc [main=M]: assert S in (union { A, B, C });
+ But P requires:
+ test tc [main=M]: assert S in { A, B, C };
+
+ Also handles the variant without ``assert ... in``:
+ test tc [main=M]: (union { A, B }); → test tc [main=M]: { A, B };
+ """
+ # Pattern: (union { ... }) → { ... }
+ # This can appear anywhere in a test declaration line
+ pattern = re.compile(r'\(\s*union\s*(\{[^}]*\})\s*\)')
+ matches = pattern.findall(code)
+ if matches:
+ code = pattern.sub(r'\1', code)
+ self.fixes_applied.append(
+ f"Fixed {len(matches)} test declaration(s): (union {{...}}) → {{...}}"
+ )
+
+ # Also fix missing semicolons at end of test declarations.
+ # test tc [main=M]: assert S in { ... } (no semicolon)
+ # → test tc [main=M]: assert S in { ... };
+ # Uses DOTALL so \s and [^}] can span newlines (multi-line decls).
+ test_no_semi = re.compile(
+ r'(test\s+\w+\s*\[main=\w+\]\s*:\s*' # test header
+ r'(?:assert\s+[\w,\s]+\s+in\s*)?' # optional assert ... in
+ r'\{[^}]*\})' # { machine list }
+ r'(?!\s*;)' # NOT followed by ;
+ r'(\s*(?:\n|$))', # end of line
+ re.DOTALL,
+ )
+ semi_count = 0
+ def _add_semi(m):
+ nonlocal semi_count
+ semi_count += 1
+ return m.group(1) + ';' + m.group(2)
+ code = test_no_semi.sub(_add_semi, code)
+ if semi_count > 0:
+ self.fixes_applied.append(
+ f"Added missing semicolon to {semi_count} test declaration(s)"
+ )
+
+ return code
+
+ def _fix_missing_semicolons(self, code: str) -> str:
+ """
+ Add missing semicolons after statements.
+ """
+ # Pattern for statements that should end with semicolon but don't
+ # This is conservative to avoid breaking valid code
+
+ # Fix: return statement without semicolon
+ pattern = r'(return\s+[^;{}\n]+)(\n)'
+
+ def add_semicolon(match):
+ stmt = match.group(1).rstrip()
+ newline = match.group(2)
+ if not stmt.endswith(';') and not stmt.endswith('{') and not stmt.endswith('}'):
+ self.fixes_applied.append("Added missing semicolon after return")
+ return stmt + ';' + newline
+ return match.group(0)
+
+ code = re.sub(pattern, add_semicolon, code)
+
+ return code
+
+ def _fix_entry_function_syntax(self, code: str) -> str:
+ """
+ Fix entry function references.
+
+ entry { ... } is valid
+ entry FunctionName; is valid
+ entry FunctionName() is WRONG - should be entry FunctionName;
+ """
+ # Fix entry FunctionName() -> entry FunctionName;
+ pattern = r'entry\s+(\w+)\s*\(\s*\)\s*;'
+ if re.search(pattern, code):
+ code = re.sub(pattern, r'entry \1;', code)
+ self.fixes_applied.append("Fixed entry function syntax: removed ()")
+
+ return code
+
+ def _fix_bare_halt(self, code: str) -> str:
+ """
+ Fix bare `halt;` → `raise halt;`.
+ In P, `halt` is an event and must be raised, not used as a statement.
+ """
+ pattern = r'(? str:
+ """
+ Detect and remove forbidden keywords inside spec monitor bodies.
+ P spec monitors cannot use: this, new, send, announce, receive, $, $$, pop.
+ """
+ # Find all spec blocks
+ spec_pattern = r'\bspec\s+(\w+)\s+observes\s+[^{]+\{'
+ for match in re.finditer(spec_pattern, code):
+ spec_name = match.group(1)
+ start = match.end() - 1
+ depth = 0
+ body_end = start
+ for ci in range(start, len(code)):
+ if code[ci] == '{':
+ depth += 1
+ elif code[ci] == '}':
+ depth -= 1
+ if depth == 0:
+ body_end = ci
+ break
+ body = code[start:body_end + 1]
+
+ # Check for forbidden keywords (only standalone uses, not in strings/comments)
+ forbidden = {
+ 'this': r'\bthis\b',
+ 'new': r'\bnew\s+\w+',
+ 'send': r'\bsend\s+',
+ 'announce': r'\bannounce\s+',
+ 'receive': r'\breceive\s*\{',
+ }
+ for kw, pattern_kw in forbidden.items():
+ if re.search(pattern_kw, body):
+ self.warnings.append(
+ f"Spec monitor '{spec_name}' uses forbidden keyword '{kw}'. "
+ "Monitors cannot use this/new/send/announce/receive/$/$$/pop."
+ )
+ logger.warning(self.warnings[-1])
+
+ # Auto-fix `this as machine` → remove the line (common pattern)
+ if re.search(r'\bthis\b', body):
+ # Try to remove `var = this as machine;` assignments
+ fixed_body = re.sub(
+ r'\n\s*\w+\s*=\s*this\s+as\s+machine\s*;\s*\n',
+ '\n',
+ body,
+ )
+ if fixed_body != body:
+ code = code[:start] + fixed_body + code[body_end + 1:]
+ self.fixes_applied.append(
+ f"Removed 'this as machine' from spec monitor '{spec_name}'"
+ )
+
+ return code
+
+ def _warn_timer_wired_to_this(self, code: str, filename: str) -> str:
+ """
+ Detect when a Timer is created with `this` as client inside a
+ scenario/test machine. Timer(this) in a scenario machine is almost
+ always wrong — the timer should fire to the Coordinator or the
+ machine that actually handles eTimeOut.
+ """
+ # Find `new Timer(this)` or `new Timer((client = this, ...))` patterns
+ pattern = r'\bnew\s+Timer\s*\(\s*this\s*\)'
+ if re.search(pattern, code):
+ self.warnings.append(
+ f"[{filename}] Timer created with 'this' as client. "
+ "In a scenario machine this means eTimeOut will be sent "
+ "to the scenario machine which likely doesn't handle it. "
+ "Pass the Coordinator or the machine that handles eTimeOut instead."
+ )
+ logger.warning(self.warnings[-1])
+ # Also check named-tuple form: (client = this, ...) passed to Timer
+ pattern2 = r'\bnew\s+Timer\s*\(\s*\([^)]*client\s*=\s*this[^)]*\)\s*\)'
+ if re.search(pattern2, code):
+ self.warnings.append(
+ f"[{filename}] Timer created with 'client = this'. "
+ "See above — pass the actual handler machine instead."
+ )
+ logger.warning(self.warnings[-1])
+ return code
+
+ def _ensure_test_declarations(self, code: str, filename: str) -> str:
+ """
+ Check that a test file contains `test` declarations.
+ If machines exist but no test declarations, generate stub
+ declarations so PChecker can discover and run the tests.
+ """
+ has_test_decl = bool(re.search(r'^\s*test\s+\w+\s*\[', code, re.MULTILINE))
+ if has_test_decl:
+ return code # Already has test declarations
+
+ # Find all machine names in the file
+ machine_names = re.findall(r'\bmachine\s+(\w+)', code)
+ if not machine_names:
+ return code
+
+ # In PTst files, machines that set up the system are test entry points.
+ # A scenario machine typically: creates other machines (``new``),
+ # sends config events (``send``), or is named with common scenario
+ # prefixes (Scenario*, TestSetup*, Driver*).
+ candidate_machines: List[str] = []
+
+ for name in machine_names:
+ pattern = rf'\bmachine\s+{re.escape(name)}\s*\{{'
+ match = re.search(pattern, code)
+ if not match:
+ continue
+ start = match.end() - 1
+ depth = 0
+ body_end = start
+ for ci in range(start, len(code)):
+ if code[ci] == '{':
+ depth += 1
+ elif code[ci] == '}':
+ depth -= 1
+ if depth == 0:
+ body_end = ci
+ break
+ body = code[start:body_end]
+
+ is_scenario = (
+ 'new ' in body
+ or 'send ' in body
+ or re.match(r'(?i)(Scenario|TestSetup|Driver|Setup|Test)', name)
+ )
+ if is_scenario:
+ candidate_machines.append(name)
+
+ # Fallback: if no candidates found via heuristic, treat ALL machines
+ # in the test file as potential scenario machines.
+ if not candidate_machines:
+ candidate_machines = list(machine_names)
+
+ if not candidate_machines:
+ self.warnings.append(
+ f"[{filename}] No test declarations found and no scenario machines detected. "
+ "PChecker will not discover any tests. "
+ "Add 'test tcName [main=Machine]: ...;' declarations."
+ )
+ logger.warning(self.warnings[-1])
+ return code
+
+ # Collect all machine names for the test scope
+ all_machines = ', '.join(machine_names)
+
+ # Generate test declarations
+ test_lines = ['\n// Auto-generated test declarations (post-processor)']
+ for sc in candidate_machines:
+ tc_name = 'tc' + sc.replace('Scenario', '').replace('_', '')
+ test_lines.append(
+ f'test {tc_name} [main={sc}]:\n'
+ f' {{{all_machines}}};'
+ )
+
+ appended = '\n'.join(test_lines) + '\n'
+ self.fixes_applied.append(
+ f"Added {len(candidate_machines)} test declaration(s) for scenario machines: "
+ + ', '.join(candidate_machines)
+ )
+ logger.info(
+ f"[{filename}] Auto-generated {len(candidate_machines)} test declaration(s): "
+ + ', '.join(candidate_machines)
+ )
+ return code + appended
+
+
+def post_process_file(code: str, filename: str = "") -> PostProcessResult:
+ """Convenience function to post-process a single file."""
+ processor = PCodePostProcessor()
+ return processor.process(code, filename)
diff --git a/Src/PeasyAI/src/core/config.py b/Src/PeasyAI/src/core/config.py
new file mode 100644
index 0000000000..8ff4537b8a
--- /dev/null
+++ b/Src/PeasyAI/src/core/config.py
@@ -0,0 +1,298 @@
+"""
+PeasyAI Configuration Loader.
+
+Loads configuration from ~/.peasyai/settings.json (like ~/.claude/settings.json).
+
+Resolution order (highest priority first):
+ 1. Environment variables (override anything in the file)
+ 2. ~/.peasyai/settings.json
+
+The config file is the single source of truth for LLM provider credentials,
+model selection, compiler paths, and generation defaults.
+"""
+
+import json
+import logging
+import os
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+logger = logging.getLogger(__name__)
+
+# ---------------------------------------------------------------------------
+# Paths
+# ---------------------------------------------------------------------------
+
+PEASYAI_HOME = Path.home() / ".peasyai"
+SETTINGS_FILE = PEASYAI_HOME / "settings.json"
+
+# ---------------------------------------------------------------------------
+# Data classes
+# ---------------------------------------------------------------------------
+
+
+@dataclass
+class ProviderConfig:
+ """Configuration for a single LLM provider."""
+ api_key: Optional[str] = None
+ base_url: Optional[str] = None
+ model: Optional[str] = None
+ model_id: Optional[str] = None
+ region: Optional[str] = None
+ timeout: float = 600
+
+
+@dataclass
+class LLMConfig:
+ """Top-level LLM configuration."""
+ provider: str = "bedrock"
+ model: Optional[str] = None
+ timeout: float = 600
+ providers: Dict[str, ProviderConfig] = field(default_factory=dict)
+
+
+@dataclass
+class PCompilerConfig:
+ """P compiler configuration."""
+ path: Optional[str] = None
+ dotnet_path: Optional[str] = None
+
+
+@dataclass
+class GenerationConfig:
+ """Code generation defaults."""
+ ensemble_size: int = 3
+ output_dir: str = "./PGenerated"
+
+
+@dataclass
+class PeasyAISettings:
+ """Root settings object loaded from ~/.peasyai/settings.json."""
+ llm: LLMConfig = field(default_factory=LLMConfig)
+ p_compiler: PCompilerConfig = field(default_factory=PCompilerConfig)
+ generation: GenerationConfig = field(default_factory=GenerationConfig)
+
+ # ── convenience helpers ──────────────────────────────────────────
+
+ def active_provider_name(self) -> str:
+ """Return the provider name that should be used."""
+ # Env var override
+ explicit = os.environ.get("LLM_PROVIDER", "").lower()
+ if explicit:
+ return explicit
+ return self.llm.provider
+
+ def active_provider_config(self) -> ProviderConfig:
+ """Return the ProviderConfig for the active provider."""
+ name = self.active_provider_name()
+ # Normalise aliases
+ canonical = {
+ "snowflake_cortex": "snowflake",
+ "aws_bedrock": "bedrock",
+ "anthropic_direct": "anthropic",
+ }.get(name, name)
+ return self.llm.providers.get(canonical, ProviderConfig())
+
+ def to_env_vars(self) -> Dict[str, str]:
+ """
+ Export the current config as environment variables.
+
+ This bridges the gap: existing code reads env vars, so we populate
+ them from the settings file. Environment vars already set by the
+ user take precedence (they are NOT overwritten).
+ """
+ env: Dict[str, str] = {}
+ name = self.active_provider_name()
+ pc = self.active_provider_config()
+
+ if name in ("snowflake", "snowflake_cortex"):
+ if pc.api_key:
+ env["OPENAI_API_KEY"] = pc.api_key
+ if pc.base_url:
+ env["OPENAI_BASE_URL"] = pc.base_url
+ model = self.llm.model or pc.model or "claude-opus-4-6"
+ env["OPENAI_MODEL_NAME"] = model
+
+ elif name in ("anthropic", "anthropic_direct"):
+ if pc.api_key:
+ env["ANTHROPIC_API_KEY"] = pc.api_key
+ if pc.base_url:
+ env["ANTHROPIC_BASE_URL"] = pc.base_url
+ model = self.llm.model or pc.model or "claude-3-5-sonnet-20241022"
+ env["ANTHROPIC_MODEL_NAME"] = model
+
+ elif name in ("bedrock", "aws_bedrock"):
+ if pc.region:
+ env["AWS_REGION"] = pc.region
+ model_id = pc.model_id or pc.model or "anthropic.claude-3-5-sonnet-20241022-v2:0"
+ env["BEDROCK_MODEL_ID"] = model_id
+
+ env["LLM_PROVIDER"] = name
+ env["LLM_TIMEOUT"] = str(self.llm.timeout or pc.timeout or 600)
+
+ return env
+
+
+# ---------------------------------------------------------------------------
+# Loader
+# ---------------------------------------------------------------------------
+
+def _parse_provider(raw: Dict[str, Any]) -> ProviderConfig:
+ return ProviderConfig(
+ api_key=raw.get("api_key"),
+ base_url=raw.get("base_url"),
+ model=raw.get("model"),
+ model_id=raw.get("model_id"),
+ region=raw.get("region"),
+ timeout=float(raw.get("timeout", 600)),
+ )
+
+
+def load_settings(path: Optional[Path] = None) -> PeasyAISettings:
+ """
+ Load settings from ``~/.peasyai/settings.json``.
+
+ If the file does not exist, returns defaults and logs a warning.
+ """
+ path = path or SETTINGS_FILE
+
+ if not path.exists():
+ logger.warning(
+ "No settings file found at %s — using defaults / env vars. "
+ "Run peasyai-mcp init or create the file manually.",
+ path,
+ )
+ return PeasyAISettings()
+
+ # Warn if the file is world-readable (contains API keys)
+ try:
+ mode = path.stat().st_mode & 0o777
+ if mode & 0o044:
+ logger.warning(
+ "Settings file %s is readable by group/others (mode %o). "
+ "Consider running: chmod 600 %s",
+ path, mode, path,
+ )
+ except OSError:
+ pass
+
+ try:
+ raw = json.loads(path.read_text(encoding="utf-8"))
+ except (json.JSONDecodeError, OSError) as exc:
+ logger.error("Failed to parse %s: %s — falling back to defaults", path, exc)
+ return PeasyAISettings()
+
+ logger.info("Loaded PeasyAI settings from %s", path)
+
+ # ── LLM section ──────────────────────────────────────────────────
+ llm_raw = raw.get("llm", {})
+ providers: Dict[str, ProviderConfig] = {}
+ for pname, pdata in llm_raw.get("providers", {}).items():
+ providers[pname] = _parse_provider(pdata)
+
+ llm = LLMConfig(
+ provider=llm_raw.get("provider", "bedrock"),
+ model=llm_raw.get("model"),
+ timeout=float(llm_raw.get("timeout", 600)),
+ providers=providers,
+ )
+
+ # ── P compiler section ───────────────────────────────────────────
+ pc_raw = raw.get("p_compiler", {})
+ p_compiler = PCompilerConfig(
+ path=pc_raw.get("path"),
+ dotnet_path=pc_raw.get("dotnet_path"),
+ )
+
+ # ── Generation section ───────────────────────────────────────────
+ gen_raw = raw.get("generation", {})
+ generation = GenerationConfig(
+ ensemble_size=int(gen_raw.get("ensemble_size", 3)),
+ output_dir=gen_raw.get("output_dir", "./PGenerated"),
+ )
+
+ return PeasyAISettings(llm=llm, p_compiler=p_compiler, generation=generation)
+
+
+def apply_settings_to_env(settings: Optional[PeasyAISettings] = None) -> PeasyAISettings:
+ """
+ Load settings and export them as environment variables.
+
+ Already-set env vars are NOT overwritten, so users can still
+ override individual values via the shell environment.
+ """
+ settings = settings or load_settings()
+
+ for key, value in settings.to_env_vars().items():
+ if key not in os.environ or not os.environ[key]:
+ os.environ[key] = value
+ logger.debug("Set %s from ~/.peasyai/settings.json", key)
+ else:
+ logger.debug("Keeping existing env var %s", key)
+
+ return settings
+
+
+# ---------------------------------------------------------------------------
+# CLI helpers
+# ---------------------------------------------------------------------------
+
+def init_settings() -> Path:
+ """
+ Create ``~/.peasyai/settings.json`` with a starter template.
+
+ Returns the path to the created file.
+ """
+ PEASYAI_HOME.mkdir(parents=True, exist_ok=True)
+ try:
+ os.chmod(str(PEASYAI_HOME), 0o700)
+ except OSError:
+ pass
+
+ template = {
+ "$schema": "https://raw.githubusercontent.com/p-org/P/main/Src/PeasyAI/.peasyai-schema.json",
+ "llm": {
+ "provider": "snowflake",
+ "model": "claude-sonnet-4-5",
+ "timeout": 600,
+ "providers": {
+ "snowflake": {
+ "api_key": "",
+ "base_url": "https://your-account.snowflakecomputing.com/api/v2/cortex/openai",
+ },
+ "anthropic": {
+ "api_key": "",
+ "model": "claude-3-5-sonnet-20241022",
+ },
+ "bedrock": {
+ "region": "us-west-2",
+ "model_id": "anthropic.claude-3-5-sonnet-20241022-v2:0",
+ },
+ },
+ },
+ "p_compiler": {
+ "path": None,
+ "dotnet_path": None,
+ },
+ "generation": {
+ "ensemble_size": 3,
+ "output_dir": "./PGenerated",
+ },
+ }
+
+ if SETTINGS_FILE.exists():
+ logger.info("Settings file already exists at %s", SETTINGS_FILE)
+ return SETTINGS_FILE
+
+ SETTINGS_FILE.write_text(
+ json.dumps(template, indent=2) + "\n",
+ encoding="utf-8",
+ )
+ try:
+ os.chmod(str(SETTINGS_FILE), 0o600)
+ except OSError:
+ pass
+ logger.info("Created starter settings at %s", SETTINGS_FILE)
+ return SETTINGS_FILE
+
diff --git a/Src/PeasyAI/src/core/llm/__init__.py b/Src/PeasyAI/src/core/llm/__init__.py
new file mode 100644
index 0000000000..5cf383d1f2
--- /dev/null
+++ b/Src/PeasyAI/src/core/llm/__init__.py
@@ -0,0 +1,30 @@
+"""
+LLM Provider Abstraction Layer
+
+This module provides a clean abstraction over different LLM providers,
+allowing PeasyAI to work with AWS Bedrock, Snowflake Cortex, Anthropic,
+and OpenAI with a unified interface.
+"""
+
+from .base import (
+ LLMProvider,
+ LLMConfig,
+ LLMResponse,
+ Message,
+ MessageRole,
+ Document,
+)
+from .factory import LLMProviderFactory, get_default_provider
+
+__all__ = [
+ "LLMProvider",
+ "LLMConfig",
+ "LLMResponse",
+ "Message",
+ "MessageRole",
+ "Document",
+ "LLMProviderFactory",
+ "get_default_provider",
+]
+
+
diff --git a/Src/PeasyAI/src/core/llm/anthropic_direct.py b/Src/PeasyAI/src/core/llm/anthropic_direct.py
new file mode 100644
index 0000000000..15852c5cfe
--- /dev/null
+++ b/Src/PeasyAI/src/core/llm/anthropic_direct.py
@@ -0,0 +1,178 @@
+"""
+Direct Anthropic API Provider
+
+Uses the Anthropic Python SDK to access Claude models directly.
+"""
+
+import time
+import logging
+from typing import List, Dict, Any, Optional
+
+from .base import (
+ LLMProvider,
+ LLMConfig,
+ LLMResponse,
+ Message,
+ TokenUsage,
+ ProviderError,
+ AuthenticationError,
+ TimeoutError as ProviderTimeoutError,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class AnthropicProvider(LLMProvider):
+ """
+ Direct Anthropic API provider using the official SDK.
+
+ Configuration:
+ api_key: Anthropic API key
+ base_url: Optional custom base URL
+ model: Default model name (e.g., 'claude-3-5-sonnet-20241022')
+ timeout: Request timeout in seconds (default: 600)
+ """
+
+ AVAILABLE_MODELS = [
+ "claude-3-5-sonnet-20241022",
+ "claude-3-5-haiku-20241022",
+ "claude-3-opus-20240229",
+ "claude-3-sonnet-20240229",
+ "claude-3-haiku-20240307",
+ ]
+
+ DEFAULT_MODEL = "claude-3-5-sonnet-20241022"
+
+ def __init__(self, config: Dict[str, Any]):
+ super().__init__(config)
+
+ if not config.get("api_key"):
+ raise ValueError("Anthropic provider requires 'api_key' in config")
+
+ self._api_key = config["api_key"]
+ self._base_url = config.get("base_url")
+ self._timeout = config.get("timeout", 600.0)
+ self._default_model = config.get("model", self.DEFAULT_MODEL)
+
+ # Initialize client lazily
+ self._client = None
+
+ def _get_client(self):
+ """Get or create the Anthropic client"""
+ if self._client is None:
+ try:
+ import anthropic
+ import httpx
+
+ client_kwargs = {
+ "api_key": self._api_key,
+ "timeout": httpx.Timeout(self._timeout, connect=60.0),
+ }
+
+ if self._base_url:
+ client_kwargs["base_url"] = self._base_url
+
+ self._client = anthropic.Anthropic(**client_kwargs)
+ except ImportError:
+ raise ProviderError(
+ self.name,
+ "anthropic package not installed. Run: pip install anthropic"
+ )
+ return self._client
+
+ def complete(
+ self,
+ messages: List[Message],
+ config: Optional[LLMConfig] = None,
+ system_prompt: Optional[str] = None
+ ) -> LLMResponse:
+ """Send messages to Anthropic API and get completion"""
+
+ cfg = self._get_config(config)
+ model = self._get_model(config)
+ client = self._get_client()
+
+ # Build message list
+ formatted_messages = []
+ for msg in messages:
+ formatted_messages.append({
+ "role": msg.role.value,
+ "content": msg.get_full_content()
+ })
+
+ # Cap max tokens to avoid streaming requirement
+ max_tokens = min(cfg.max_tokens, 8192)
+
+ logger.info(f"Anthropic request: model={model}, messages={len(formatted_messages)}")
+ start_time = time.time()
+
+ try:
+ # Build request kwargs
+ request_kwargs = {
+ "model": model,
+ "max_tokens": max_tokens,
+ "messages": formatted_messages,
+ }
+
+ if system_prompt:
+ request_kwargs["system"] = system_prompt
+
+ response = client.messages.create(**request_kwargs)
+
+ latency_ms = self._measure_latency(start_time)
+ logger.info(f"Anthropic response: latency={latency_ms}ms")
+
+ # Extract content
+ content = response.content[0].text if response.content else ""
+
+ # Extract usage
+ usage = TokenUsage(
+ input_tokens=response.usage.input_tokens,
+ output_tokens=response.usage.output_tokens,
+ total_tokens=response.usage.input_tokens + response.usage.output_tokens,
+ )
+
+ return LLMResponse(
+ content=content,
+ usage=usage,
+ finish_reason=response.stop_reason or "stop",
+ latency_ms=latency_ms,
+ model=model,
+ provider=self.name,
+ raw_response=response,
+ )
+
+ except Exception as e:
+ latency_ms = self._measure_latency(start_time)
+ error_str = str(e).lower()
+
+ logger.error(f"Anthropic error after {latency_ms}ms: {e}")
+
+ if "401" in error_str or "unauthorized" in error_str or "authentication" in error_str:
+ raise AuthenticationError(
+ self.name,
+ f"Authentication failed. Check your API key. Error: {e}",
+ original_error=e
+ )
+ elif "timeout" in error_str:
+ raise ProviderTimeoutError(
+ self.name,
+ f"Request timed out after {cfg.timeout}s",
+ original_error=e
+ )
+ else:
+ raise ProviderError(
+ self.name,
+ f"Request failed: {e}",
+ original_error=e
+ )
+
+ def available_models(self) -> List[str]:
+ """List available models"""
+ return self.AVAILABLE_MODELS.copy()
+
+ @property
+ def name(self) -> str:
+ return "anthropic"
+
+
diff --git a/Src/PeasyAI/src/core/llm/base.py b/Src/PeasyAI/src/core/llm/base.py
new file mode 100644
index 0000000000..2c3a7996bd
--- /dev/null
+++ b/Src/PeasyAI/src/core/llm/base.py
@@ -0,0 +1,226 @@
+"""
+Base classes and data models for LLM providers.
+
+This module defines the abstract interface that all LLM providers must implement,
+along with the data structures for messages, responses, and configuration.
+"""
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from typing import List, Dict, Any, Optional
+from enum import Enum
+import time
+
+
+class MessageRole(Enum):
+ """Role of a message in a conversation"""
+ SYSTEM = "system"
+ USER = "user"
+ ASSISTANT = "assistant"
+
+
+@dataclass
+class Document:
+ """A document attachment for a message"""
+ name: str
+ content: str
+ format: str = "txt"
+
+ def to_xml(self) -> str:
+ """Convert document to XML format for inclusion in prompts"""
+ return f"<{self.name}>\n{self.content}\n{self.name}>"
+
+
+@dataclass
+class Message:
+ """A single message in a conversation"""
+ role: MessageRole
+ content: str
+ documents: Optional[List[Document]] = None
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert to dictionary format for API calls"""
+ return {
+ "role": self.role.value,
+ "content": self.content
+ }
+
+ def get_full_content(self) -> str:
+ """Get content including any embedded documents"""
+ parts = [self.content]
+ if self.documents:
+ for doc in self.documents:
+ parts.append(doc.to_xml())
+ return "\n\n".join(parts)
+
+
+@dataclass
+class TokenUsage:
+ """Token usage statistics for an LLM call"""
+ input_tokens: int = 0
+ output_tokens: int = 0
+ total_tokens: int = 0
+ cache_read_tokens: int = 0
+ cache_write_tokens: int = 0
+
+ def to_dict(self) -> Dict[str, int]:
+ return {
+ "inputTokens": self.input_tokens,
+ "outputTokens": self.output_tokens,
+ "totalTokens": self.total_tokens,
+ "cacheReadInputTokens": self.cache_read_tokens,
+ "cacheWriteInputTokens": self.cache_write_tokens
+ }
+
+
+@dataclass
+class LLMResponse:
+ """Response from an LLM provider"""
+ content: str
+ usage: TokenUsage
+ finish_reason: str
+ latency_ms: int
+ model: str
+ provider: str
+ raw_response: Optional[Any] = None
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert to dictionary format (compatible with existing code)"""
+ return {
+ "output": {
+ "message": {
+ "role": "assistant",
+ "content": [{"text": self.content}]
+ }
+ },
+ "stopReason": self.finish_reason,
+ "usage": self.usage.to_dict(),
+ "metrics": {"latencyMs": self.latency_ms}
+ }
+
+
+@dataclass
+class LLMConfig:
+ """Configuration for an LLM call"""
+ model: Optional[str] = None # Use provider default if None
+ max_tokens: int = 4096
+ temperature: float = 1.0
+ top_p: float = 0.999
+ timeout: float = 600.0
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert to inference config format (compatible with existing code)"""
+ return {
+ "maxTokens": self.max_tokens,
+ "temperature": self.temperature,
+ "topP": self.top_p
+ }
+
+
+class LLMProvider(ABC):
+ """
+ Abstract base class for LLM providers.
+
+ All LLM providers (Bedrock, Snowflake, Anthropic, OpenAI) must implement
+ this interface to be usable by PeasyAI.
+ """
+
+ def __init__(self, config: Dict[str, Any]):
+ """
+ Initialize the provider with configuration.
+
+ Args:
+ config: Provider-specific configuration dictionary
+ """
+ self._config = config
+ self._default_model = config.get("model") or config.get("default_model")
+
+ @abstractmethod
+ def complete(
+ self,
+ messages: List[Message],
+ config: Optional[LLMConfig] = None,
+ system_prompt: Optional[str] = None
+ ) -> LLMResponse:
+ """
+ Send messages to the LLM and get a completion.
+
+ Args:
+ messages: List of conversation messages
+ config: Optional configuration overrides
+ system_prompt: Optional system prompt
+
+ Returns:
+ LLMResponse with the completion
+ """
+ pass
+
+ @abstractmethod
+ def available_models(self) -> List[str]:
+ """
+ List available models for this provider.
+
+ Returns:
+ List of model identifiers
+ """
+ pass
+
+ @property
+ @abstractmethod
+ def name(self) -> str:
+ """
+ Get the provider name for logging/identification.
+
+ Returns:
+ Provider name string
+ """
+ pass
+
+ @property
+ def default_model(self) -> str:
+ """Get the default model for this provider"""
+ return self._default_model
+
+ def _measure_latency(self, start_time: float) -> int:
+ """Calculate latency in milliseconds"""
+ return int((time.time() - start_time) * 1000)
+
+ def _get_model(self, config: Optional[LLMConfig]) -> str:
+ """Get the model to use, from config or default"""
+ if config and config.model:
+ return config.model
+ return self.default_model
+
+ def _get_config(self, config: Optional[LLMConfig]) -> LLMConfig:
+ """Get config with defaults applied"""
+ return config or LLMConfig()
+
+
+class ProviderError(Exception):
+ """Base exception for provider errors"""
+ def __init__(self, provider: str, message: str, original_error: Optional[Exception] = None):
+ self.provider = provider
+ self.original_error = original_error
+ super().__init__(f"[{provider}] {message}")
+
+
+class AuthenticationError(ProviderError):
+ """Authentication failed with the provider"""
+ pass
+
+
+class RateLimitError(ProviderError):
+ """Rate limit exceeded"""
+ pass
+
+
+class ModelNotFoundError(ProviderError):
+ """Requested model not available"""
+ pass
+
+
+class TimeoutError(ProviderError):
+ """Request timed out"""
+ pass
+
+
diff --git a/Src/PeasyAI/src/core/llm/bedrock.py b/Src/PeasyAI/src/core/llm/bedrock.py
new file mode 100644
index 0000000000..b553e5b684
--- /dev/null
+++ b/Src/PeasyAI/src/core/llm/bedrock.py
@@ -0,0 +1,208 @@
+"""
+AWS Bedrock LLM Provider
+
+Uses the AWS Bedrock Converse API to access Claude models.
+"""
+
+import time
+import json
+import logging
+from typing import List, Dict, Any, Optional
+
+from .base import (
+ LLMProvider,
+ LLMConfig,
+ LLMResponse,
+ Message,
+ TokenUsage,
+ ProviderError,
+ AuthenticationError,
+ TimeoutError as ProviderTimeoutError,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class BedrockProvider(LLMProvider):
+ """
+ AWS Bedrock provider using the Converse API.
+
+ Configuration:
+ region: AWS region (default: 'us-west-2')
+ model: Default model ID (e.g., 'anthropic.claude-3-5-sonnet-20241022-v2:0')
+ timeout: Request timeout in seconds (default: 1000)
+ """
+
+ AVAILABLE_MODELS = [
+ "anthropic.claude-3-5-sonnet-20241022-v2:0",
+ "anthropic.claude-3-haiku-20240307-v1:0",
+ "anthropic.claude-3-opus-20240229-v1:0",
+ ]
+
+ DEFAULT_MODEL = "anthropic.claude-3-5-sonnet-20241022-v2:0"
+
+ def __init__(self, config: Dict[str, Any]):
+ super().__init__(config)
+
+ self._region = config.get("region", "us-west-2")
+ self._timeout = config.get("timeout", 1000)
+ self._default_model = config.get("model", self.DEFAULT_MODEL)
+
+ # Initialize client lazily
+ self._client = None
+
+ def _get_client(self):
+ """Get or create the Bedrock client"""
+ if self._client is None:
+ try:
+ import boto3
+ from botocore.config import Config
+
+ bedrock_config = Config(read_timeout=self._timeout)
+ self._client = boto3.client(
+ service_name='bedrock-runtime',
+ region_name=self._region,
+ config=bedrock_config
+ )
+ except ImportError:
+ raise ProviderError(
+ self.name,
+ "boto3 package not installed. Run: pip install boto3"
+ )
+ return self._client
+
+ def _convert_messages_to_bedrock_format(
+ self,
+ messages: List[Message]
+ ) -> List[Dict[str, Any]]:
+ """Convert messages to Bedrock Converse API format"""
+ bedrock_messages = []
+
+ for msg in messages:
+ content_parts = []
+
+ # Add main text content
+ full_content = msg.get_full_content()
+ if full_content:
+ content_parts.append({"text": full_content})
+
+ bedrock_messages.append({
+ "role": msg.role.value,
+ "content": content_parts
+ })
+
+ return bedrock_messages
+
+ def complete(
+ self,
+ messages: List[Message],
+ config: Optional[LLMConfig] = None,
+ system_prompt: Optional[str] = None
+ ) -> LLMResponse:
+ """Send messages to AWS Bedrock and get completion"""
+
+ cfg = self._get_config(config)
+ model = self._get_model(config)
+ client = self._get_client()
+
+ # Convert messages to Bedrock format
+ bedrock_messages = self._convert_messages_to_bedrock_format(messages)
+
+ # Build system prompt
+ system_prompt_content = None
+ if system_prompt:
+ system_prompt_content = [{"text": system_prompt}]
+
+ # Build inference config
+ inference_config = {
+ "maxTokens": cfg.max_tokens,
+ "temperature": cfg.temperature,
+ "topP": cfg.top_p,
+ }
+
+ logger.info(f"Bedrock request: model={model}, messages={len(bedrock_messages)}")
+ start_time = time.time()
+
+ try:
+ # Build request kwargs
+ request_kwargs = {
+ "modelId": model,
+ "messages": bedrock_messages,
+ "inferenceConfig": inference_config,
+ }
+
+ if system_prompt_content:
+ request_kwargs["system"] = system_prompt_content
+
+ response = client.converse(**request_kwargs)
+
+ latency_ms = self._measure_latency(start_time)
+ logger.info(f"Bedrock response: latency={latency_ms}ms")
+
+ # Extract content
+ content = ""
+ output_message = response.get("output", {}).get("message", {})
+ content_parts = output_message.get("content", [])
+ if content_parts and "text" in content_parts[0]:
+ content = content_parts[0]["text"]
+
+ # Extract usage
+ usage_data = response.get("usage", {})
+ usage = TokenUsage(
+ input_tokens=usage_data.get("inputTokens", 0),
+ output_tokens=usage_data.get("outputTokens", 0),
+ total_tokens=usage_data.get("totalTokens", 0),
+ cache_read_tokens=usage_data.get("cacheReadInputTokens", 0),
+ cache_write_tokens=usage_data.get("cacheWriteInputTokens", 0),
+ )
+
+ # Get actual latency from metrics if available
+ metrics = response.get("metrics", {})
+ if "latencyMs" in metrics:
+ latency_ms = metrics["latencyMs"]
+
+ return LLMResponse(
+ content=content,
+ usage=usage,
+ finish_reason=response.get("stopReason", "end_turn"),
+ latency_ms=latency_ms,
+ model=model,
+ provider=self.name,
+ raw_response=response,
+ )
+
+ except Exception as e:
+ latency_ms = self._measure_latency(start_time)
+ error_str = str(e).lower()
+
+ # Log the conversation for debugging
+ logger.error(f"Bedrock error after {latency_ms}ms: {e}")
+
+ if "credential" in error_str or "unauthorized" in error_str or "access denied" in error_str:
+ raise AuthenticationError(
+ self.name,
+ f"AWS authentication failed. Check your credentials. Error: {e}",
+ original_error=e
+ )
+ elif "timeout" in error_str or "timed out" in error_str:
+ raise ProviderTimeoutError(
+ self.name,
+ f"Request timed out after {self._timeout}s",
+ original_error=e
+ )
+ else:
+ raise ProviderError(
+ self.name,
+ f"Request failed: {e}",
+ original_error=e
+ )
+
+ def available_models(self) -> List[str]:
+ """List available models"""
+ return self.AVAILABLE_MODELS.copy()
+
+ @property
+ def name(self) -> str:
+ return "bedrock"
+
+
diff --git a/Src/PeasyAI/src/core/llm/factory.py b/Src/PeasyAI/src/core/llm/factory.py
new file mode 100644
index 0000000000..a12b37b978
--- /dev/null
+++ b/Src/PeasyAI/src/core/llm/factory.py
@@ -0,0 +1,218 @@
+"""
+LLM Provider Factory
+
+Creates and manages LLM provider instances based on configuration
+or environment variables.
+"""
+
+import os
+import logging
+from typing import Dict, Any, Optional, Type
+
+from .base import LLMProvider, ProviderError
+from .snowflake import SnowflakeCortexProvider
+from .bedrock import BedrockProvider
+from .anthropic_direct import AnthropicProvider
+
+logger = logging.getLogger(__name__)
+
+
+class LLMProviderFactory:
+ """
+ Factory for creating LLM provider instances.
+
+ Supports:
+ - Explicit provider creation by name
+ - Auto-detection from environment variables
+ - Provider configuration via dictionaries
+ """
+
+ # Registry of available providers
+ _providers: Dict[str, Type[LLMProvider]] = {
+ "snowflake": SnowflakeCortexProvider,
+ "snowflake_cortex": SnowflakeCortexProvider,
+ "bedrock": BedrockProvider,
+ "aws_bedrock": BedrockProvider,
+ "anthropic": AnthropicProvider,
+ "anthropic_direct": AnthropicProvider,
+ }
+
+ @classmethod
+ def register_provider(cls, name: str, provider_class: Type[LLMProvider]):
+ """
+ Register a new provider type.
+
+ Args:
+ name: Name to register the provider under
+ provider_class: The provider class to register
+ """
+ cls._providers[name.lower()] = provider_class
+ logger.info(f"Registered LLM provider: {name}")
+
+ @classmethod
+ def available_providers(cls) -> list:
+ """Get list of available provider names"""
+ return list(set(cls._providers.values()))
+
+ @classmethod
+ def create(cls, provider_name: str, config: Dict[str, Any]) -> LLMProvider:
+ """
+ Create a provider instance by name.
+
+ Args:
+ provider_name: Name of the provider (e.g., 'snowflake', 'bedrock')
+ config: Provider configuration dictionary
+
+ Returns:
+ Configured LLMProvider instance
+
+ Raises:
+ ValueError: If provider name is unknown
+ """
+ provider_name = provider_name.lower()
+
+ if provider_name not in cls._providers:
+ available = list(set(cls._providers.keys()))
+ raise ValueError(
+ f"Unknown provider: {provider_name}. "
+ f"Available providers: {available}"
+ )
+
+ provider_class = cls._providers[provider_name]
+ logger.info(f"Creating LLM provider: {provider_name}")
+
+ return provider_class(config)
+
+ @classmethod
+ def from_env(cls) -> LLMProvider:
+ """
+ Create a provider based on environment variables.
+
+ Detection order:
+ 1. Snowflake Cortex (if OPENAI_BASE_URL contains 'snowflake')
+ 2. Direct Anthropic (if ANTHROPIC_API_KEY is set)
+ 3. OpenAI-compatible (if OPENAI_API_KEY is set without snowflake URL)
+ 4. AWS Bedrock (default fallback)
+
+ Returns:
+ Configured LLMProvider instance
+ """
+ # Check for explicit provider selection
+ explicit_provider = os.environ.get("LLM_PROVIDER", "").lower()
+
+ if explicit_provider:
+ logger.info(f"Using explicitly configured provider: {explicit_provider}")
+ return cls._create_from_explicit_env(explicit_provider)
+
+ # Auto-detect based on available credentials
+ return cls._auto_detect_provider()
+
+ @classmethod
+ def _auto_detect_provider(cls) -> LLMProvider:
+ """Auto-detect and create provider based on available env vars"""
+
+ openai_base_url = os.environ.get("OPENAI_BASE_URL", "")
+ openai_api_key = os.environ.get("OPENAI_API_KEY", "")
+ anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY", "")
+
+ # 1. Check for Snowflake Cortex
+ if openai_base_url and "snowflake" in openai_base_url.lower():
+ logger.info("Auto-detected: Snowflake Cortex (OpenAI-compatible)")
+ return cls.create("snowflake", {
+ "api_key": openai_api_key,
+ "base_url": openai_base_url,
+ "model": os.environ.get("OPENAI_MODEL_NAME", "claude-sonnet-4-5"),
+ "timeout": float(os.environ.get("LLM_TIMEOUT", "600")),
+ })
+
+ # 2. Check for direct Anthropic
+ if anthropic_api_key:
+ logger.info("Auto-detected: Direct Anthropic API")
+ config = {
+ "api_key": anthropic_api_key,
+ "model": os.environ.get("ANTHROPIC_MODEL_NAME", "claude-3-5-sonnet-20241022"),
+ "timeout": float(os.environ.get("LLM_TIMEOUT", "600")),
+ }
+
+ anthropic_base_url = os.environ.get("ANTHROPIC_BASE_URL")
+ if anthropic_base_url:
+ config["base_url"] = anthropic_base_url
+
+ return cls.create("anthropic", config)
+
+ # 3. Default to AWS Bedrock
+ logger.info("Auto-detected: AWS Bedrock (default)")
+ return cls.create("bedrock", {
+ "region": os.environ.get("AWS_REGION", "us-west-2"),
+ "model": os.environ.get(
+ "BEDROCK_MODEL_ID",
+ "anthropic.claude-3-5-sonnet-20241022-v2:0"
+ ),
+ "timeout": float(os.environ.get("LLM_TIMEOUT", "1000")),
+ })
+
+ @classmethod
+ def _create_from_explicit_env(cls, provider_name: str) -> LLMProvider:
+ """Create provider from explicit LLM_PROVIDER env var"""
+
+ if provider_name in ("snowflake", "snowflake_cortex"):
+ return cls.create("snowflake", {
+ "api_key": os.environ.get("OPENAI_API_KEY"),
+ "base_url": os.environ.get("OPENAI_BASE_URL"),
+ "model": os.environ.get("OPENAI_MODEL_NAME", "claude-sonnet-4-5"),
+ "timeout": float(os.environ.get("LLM_TIMEOUT", "600")),
+ })
+
+ elif provider_name in ("anthropic", "anthropic_direct"):
+ config = {
+ "api_key": os.environ.get("ANTHROPIC_API_KEY"),
+ "model": os.environ.get("ANTHROPIC_MODEL_NAME", "claude-3-5-sonnet-20241022"),
+ "timeout": float(os.environ.get("LLM_TIMEOUT", "600")),
+ }
+
+ anthropic_base_url = os.environ.get("ANTHROPIC_BASE_URL")
+ if anthropic_base_url:
+ config["base_url"] = anthropic_base_url
+
+ return cls.create("anthropic", config)
+
+ elif provider_name in ("bedrock", "aws_bedrock"):
+ return cls.create("bedrock", {
+ "region": os.environ.get("AWS_REGION", "us-west-2"),
+ "model": os.environ.get(
+ "BEDROCK_MODEL_ID",
+ "anthropic.claude-3-5-sonnet-20241022-v2:0"
+ ),
+ "timeout": float(os.environ.get("LLM_TIMEOUT", "1000")),
+ })
+
+ else:
+ raise ValueError(f"Unknown provider in LLM_PROVIDER: {provider_name}")
+
+
+# Singleton instance for convenience
+_default_provider: Optional[LLMProvider] = None
+
+
+def get_default_provider() -> LLMProvider:
+ """
+ Get the default LLM provider (singleton).
+
+ Creates a provider from environment variables on first call,
+ then returns the same instance on subsequent calls.
+
+ Returns:
+ The default LLMProvider instance
+ """
+ global _default_provider
+
+ if _default_provider is None:
+ _default_provider = LLMProviderFactory.from_env()
+
+ return _default_provider
+
+
+def reset_default_provider():
+ """Reset the default provider (for testing or reconfiguration)"""
+ global _default_provider
+ _default_provider = None
diff --git a/Src/PeasyAI/src/core/llm/snowflake.py b/Src/PeasyAI/src/core/llm/snowflake.py
new file mode 100644
index 0000000000..28387376d6
--- /dev/null
+++ b/Src/PeasyAI/src/core/llm/snowflake.py
@@ -0,0 +1,171 @@
+"""
+Snowflake Cortex LLM Provider
+
+Uses the OpenAI-compatible API endpoint provided by Snowflake Cortex
+to access Claude models.
+"""
+
+import time
+import logging
+from typing import List, Dict, Any, Optional
+
+from .base import (
+ LLMProvider,
+ LLMConfig,
+ LLMResponse,
+ Message,
+ TokenUsage,
+ ProviderError,
+ AuthenticationError,
+ TimeoutError as ProviderTimeoutError,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class SnowflakeCortexProvider(LLMProvider):
+ """
+ Snowflake Cortex provider using OpenAI-compatible API.
+
+ Configuration:
+ api_key: Snowflake programmatic access token
+ base_url: Cortex OpenAI endpoint URL
+ model: Default model name (default: 'claude-opus-4-6')
+ timeout: Request timeout in seconds (default: 600)
+ """
+
+ AVAILABLE_MODELS = [
+ "claude-opus-4-6",
+ "claude-sonnet-4-6",
+ "claude-opus-4-5",
+ "claude-sonnet-4-5",
+ "claude-haiku-4-5",
+ ]
+
+ def __init__(self, config: Dict[str, Any]):
+ super().__init__(config)
+
+ # Validate required config
+ if not config.get("api_key"):
+ raise ValueError("Snowflake Cortex requires 'api_key' in config")
+ if not config.get("base_url"):
+ raise ValueError("Snowflake Cortex requires 'base_url' in config")
+
+ self._api_key = config["api_key"]
+ self._base_url = config["base_url"].rstrip("/")
+ self._timeout = config.get("timeout", 600.0)
+ self._default_model = config.get("model", "claude-opus-4-6")
+
+ # Initialize OpenAI client lazily
+ self._client = None
+
+ def _get_client(self):
+ """Get or create the OpenAI client"""
+ if self._client is None:
+ try:
+ from openai import OpenAI
+ self._client = OpenAI(
+ api_key=self._api_key,
+ base_url=self._base_url,
+ timeout=self._timeout,
+ )
+ except ImportError:
+ raise ProviderError(
+ self.name,
+ "OpenAI package not installed. Run: pip install openai"
+ )
+ return self._client
+
+ def complete(
+ self,
+ messages: List[Message],
+ config: Optional[LLMConfig] = None,
+ system_prompt: Optional[str] = None
+ ) -> LLMResponse:
+ """Send messages to Snowflake Cortex and get completion"""
+
+ cfg = self._get_config(config)
+ model = self._get_model(config)
+ client = self._get_client()
+
+ # Build message list
+ formatted_messages = []
+
+ # Add system prompt if provided
+ if system_prompt:
+ formatted_messages.append({
+ "role": "system",
+ "content": system_prompt
+ })
+
+ # Add conversation messages
+ for msg in messages:
+ formatted_messages.append({
+ "role": msg.role.value,
+ "content": msg.get_full_content()
+ })
+
+ max_tokens = min(cfg.max_tokens, 20000)
+
+ logger.info(f"Snowflake Cortex request: model={model}, messages={len(formatted_messages)}")
+ start_time = time.time()
+
+ try:
+ response = client.chat.completions.create(
+ model=model,
+ messages=formatted_messages,
+ max_completion_tokens=max_tokens,
+ temperature=cfg.temperature,
+ top_p=cfg.top_p,
+ )
+
+ latency_ms = self._measure_latency(start_time)
+ logger.info(f"Snowflake Cortex response: latency={latency_ms}ms")
+
+ # Extract usage
+ usage = TokenUsage(
+ input_tokens=response.usage.prompt_tokens if response.usage else 0,
+ output_tokens=response.usage.completion_tokens if response.usage else 0,
+ total_tokens=response.usage.total_tokens if response.usage else 0,
+ )
+
+ return LLMResponse(
+ content=response.choices[0].message.content,
+ usage=usage,
+ finish_reason=response.choices[0].finish_reason or "stop",
+ latency_ms=latency_ms,
+ model=model,
+ provider=self.name,
+ raw_response=response,
+ )
+
+ except Exception as e:
+ latency_ms = self._measure_latency(start_time)
+ error_str = str(e).lower()
+
+ if "401" in error_str or "unauthorized" in error_str or "authentication" in error_str:
+ raise AuthenticationError(
+ self.name,
+ f"Authentication failed. Check your Snowflake access token. Error: {e}",
+ original_error=e
+ )
+ elif "timeout" in error_str:
+ raise ProviderTimeoutError(
+ self.name,
+ f"Request timed out after {cfg.timeout}s",
+ original_error=e
+ )
+ else:
+ raise ProviderError(
+ self.name,
+ f"Request failed: {e}",
+ original_error=e
+ )
+
+ def available_models(self) -> List[str]:
+ """List available models"""
+ return self.AVAILABLE_MODELS.copy()
+
+ @property
+ def name(self) -> str:
+ return "snowflake_cortex"
diff --git a/Src/PeasyAI/src/core/modes/DesignDocInputModeV2.py b/Src/PeasyAI/src/core/modes/DesignDocInputModeV2.py
new file mode 100644
index 0000000000..c70ce27a03
--- /dev/null
+++ b/Src/PeasyAI/src/core/modes/DesignDocInputModeV2.py
@@ -0,0 +1,200 @@
+"""
+Design Document Input Mode (V2) - Refactored to use Service Layer.
+
+This module provides the Streamlit UI for generating P code from design documents,
+using the new service layer and workflow engine.
+"""
+
+import streamlit as st
+from io import StringIO
+from typing import Optional
+import os
+from pathlib import Path
+
+# Import the adapter
+from ui.stlit.adapters import get_adapter
+
+
+class DesignDocInputModeV2:
+ """
+ Streamlit mode for generating P code from design documents.
+
+ Uses the workflow engine via StreamlitWorkflowAdapter for all
+ generation operations.
+ """
+
+ def __init__(self):
+ self.status_container = st.container()
+ self.messages = st.container()
+ self.bottom_path = st.container()
+
+ # Initialize session state
+ if "design_doc_v2_state" not in st.session_state:
+ st.session_state.design_doc_v2_state = {
+ "generated_files": {},
+ "project_path": None,
+ "success": None,
+ "metrics": {}
+ }
+
+ self.display_page()
+
+ def generate_from_design_doc(self):
+ """Generate P code from the uploaded design document."""
+ try:
+ # Validate uploaded file
+ uploaded_design_doc = st.session_state.get("design_doc")
+ if not uploaded_design_doc:
+ st.warning("Please upload a design document.", icon="⚠️")
+ return
+
+ # Read design doc content
+ design_doc_content_io = StringIO(
+ uploaded_design_doc.getvalue().decode("utf-8")
+ )
+ design_doc_content = design_doc_content_io.read()
+
+ # Get destination path
+ destination_path = st.session_state.get("destination_path", "").strip()
+ if not destination_path:
+ # Default to a generated_code directory
+ project_root = Path(__file__).parent.parent.parent.parent
+ destination_path = str(project_root / "generated_code" / "new_project")
+
+ # Ensure directory exists
+ os.makedirs(destination_path, exist_ok=True)
+
+ # Show user message
+ user_msg = f"📄 Uploaded Design Document: {uploaded_design_doc.name}"
+ self.messages.chat_message("user").write(user_msg)
+
+ # Get adapter and generate
+ adapter = get_adapter()
+
+ with self.status_container:
+ result = adapter.generate_project(
+ design_doc=design_doc_content,
+ project_path=destination_path,
+ status_container=st
+ )
+
+ # Store results in session state
+ state = st.session_state.design_doc_v2_state
+ state["success"] = result.get("success", False)
+ state["generated_files"] = result.get("files", {})
+ state["project_path"] = result.get("project_path")
+ state["metrics"] = result.get("metrics", {})
+
+ # Display results
+ if result.get("success"):
+ self._display_success_results(result)
+ else:
+ self._display_error_results(result)
+
+ except Exception as e:
+ st.error(f"Error generating P code: {str(e)}", icon="⚠️")
+ import traceback
+ st.code(traceback.format_exc())
+
+ def _display_success_results(self, result: dict):
+ """Display successful generation results."""
+ files = result.get("files", {})
+
+ for filename, code in files.items():
+ with self.messages.chat_message("assistant"):
+ st.subheader(f"📄 {filename}")
+ st.code(code, language="kotlin")
+
+ # Show metrics
+ metrics = result.get("metrics", {})
+ with self.messages.expander("📊 Generation Metrics"):
+ st.write(f"Steps Completed: {metrics.get('steps_completed', 'N/A')}")
+ if result.get("completed_steps"):
+ st.write("Completed Steps:")
+ for step in result.get("completed_steps", []):
+ st.write(f" ✅ {step}")
+
+ # Show project path
+ project_path = result.get("project_path")
+ if project_path:
+ self.bottom_path.success(f"📁 Project saved to: {project_path}")
+
+ def _display_error_results(self, result: dict):
+ """Display error results."""
+ errors = result.get("errors", [])
+
+ with self.messages.chat_message("assistant"):
+ st.error("Generation completed with errors")
+
+ if errors:
+ st.subheader("Errors:")
+ for error in errors:
+ st.error(error)
+
+ # Still show any generated files
+ files = result.get("files", {})
+ if files:
+ st.subheader("Partial Results:")
+ for filename, code in files.items():
+ with st.expander(f"📄 {filename}"):
+ st.code(code, language="kotlin")
+
+ def display_page(self):
+ """Display the design document input page."""
+ # Welcome message
+ self.messages.chat_message("assistant").write(
+ "👋 Hello! I'm an AI assistant for generating P code.\n\n"
+ "Upload a design document and I'll generate the corresponding P project."
+ )
+
+ # Input form
+ with st.form("design_doc_form_v2", clear_on_submit=True):
+ st.file_uploader(
+ "📄 Upload a design document (.txt)",
+ key="design_doc",
+ type=["txt", "md"],
+ help="Upload a design document (.txt or .md) for your P system."
+ )
+
+ st.text_input(
+ "📁 Destination Path (optional)",
+ key="destination_path",
+ placeholder="/path/to/output/project",
+ help="Leave empty to use default location."
+ )
+
+ col1, col2 = st.columns([1, 5])
+ with col1:
+ submitted = st.form_submit_button("🚀 Generate", type="primary")
+
+ if submitted:
+ self.generate_from_design_doc()
+
+ # Show previous results if available
+ state = st.session_state.design_doc_v2_state
+ if state.get("success") is not None:
+ st.divider()
+ st.subheader("Previous Generation Results")
+
+ if state["success"]:
+ st.success("✅ Generation successful!")
+ if state.get("project_path"):
+ st.info(f"📁 Project: {state['project_path']}")
+ else:
+ st.warning("⚠️ Generation had errors")
+
+ # Button to clear
+ if st.button("🗑️ Clear Results"):
+ st.session_state.design_doc_v2_state = {
+ "generated_files": {},
+ "project_path": None,
+ "success": None,
+ "metrics": {}
+ }
+ st.rerun()
+
+
+# For backward compatibility, alias as DesignDocInputMode
+def DesignDocInputModeV2Wrapper():
+ """Wrapper function for use in main app."""
+ DesignDocInputModeV2()
diff --git a/Src/PeasyAI/src/core/modes/pchecker_mode.py b/Src/PeasyAI/src/core/modes/pchecker_mode.py
new file mode 100644
index 0000000000..6c9d8d5d62
--- /dev/null
+++ b/Src/PeasyAI/src/core/modes/pchecker_mode.py
@@ -0,0 +1,862 @@
+import streamlit as st
+from dataclasses import dataclass, field
+from typing import Dict, List, Optional, Tuple
+import os
+import shutil
+import re
+import time
+from utils import file_utils, checker_utils, compile_utils
+from core.modes import pipelines
+from datetime import datetime
+from enum import Enum
+from core.pipelining.prompting_pipeline import PromptingPipeline
+from utils import string_utils
+from st_diff_viewer import diff_viewer
+import streamlit_scrollable_textbox as stx
+from pathlib import Path
+
+from core.llm import get_default_provider
+from core.services import CompilationService, FixerService
+from core.services.base import ResourceLoader
+
+PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
+
+class Stages(Enum):
+ INITIAL = 0
+ RUNNING_FILE_ANALYSIS = 1
+ FILE_ANALYSIS_COMPLETE = 2
+ RUNNING_ERROR_ANALYSIS = 3
+ ERROR_ANALYSIS_COMPLETE = 4
+ RUNNING_GET_FIX = 5
+ GET_FIX_COMPLETE = 6
+ RUNNING_APPLY_FIX = 7
+ APPLY_FIX_COMPLETE = 8
+ COMPILE_FIX_FAILED = 9
+ COMPILE_FIX_PASSED = 10
+ PCHECKER_FAILED = 11
+ PCHECKER_PASSED = 12
+
+@dataclass
+class CheckerConfig:
+ schedules: int = 100
+ timeout_seconds: int = 20
+ seed: str = "default-seed"
+
+@dataclass
+class InteractiveModeState:
+ current_stage: Stages = Stages.INITIAL
+ current_test_name: str = ""
+ previous_test_name: str = ""
+ current_project_state: Dict[str, str] = field(default_factory=dict)
+ new_files_dict: Dict[str, str] = field(default_factory=dict)
+ patches: Dict[str, str] = ""
+ new_project_state: Dict[str, str] = field(default_factory=dict)
+ current_error_analysis: str = ""
+ current_error_category: str = ""
+ previous_error_category: str = ""
+ current_trace_log: str = ""
+ tests_to_fix: List[str] = field(default_factory=list)
+ selected_files: List[str] = field(default_factory=list)
+ additional_user_guidance: str = ""
+ current_pipeline: PromptingPipeline = pipelines.create_base_pipeline_fewshot()
+ debug_str: str = ""
+ recent_compile_output: str = ""
+ patch_debug_info: Dict[str, tuple] = field(default_factory=dict)
+ patch_results_dict: Dict[str, tuple] = field(default_factory=dict)
+ remaining_faulty_patches_to_fix: Dict[str, tuple] = field(default_factory=dict)
+ tmp_project_dir: str = ""
+
+@dataclass
+class PCheckerState:
+ config: CheckerConfig = field(default_factory=CheckerConfig)
+ project_path: str = ""
+ latest_project_path: str = ""
+ results: Optional[Dict[str, bool]] = field(default=None)
+ trace_dicts: Optional[Dict] = field(default=None)
+ trace_logs: Optional[Dict] = field(default=None)
+ interactive_mode_active = False
+ current_interactive_mode_state: InteractiveModeState = field(default_factory=InteractiveModeState)
+ fix_progress: Dict[str, float] = field(default_factory=dict)
+ usage_stats: Dict[str, Dict[str, int]] = field(default_factory=dict)
+
+if 'pchecker_state' not in st.session_state:
+ st.session_state.pchecker_state = PCheckerState()
+
+def update_test_statuses(status_dict):
+ state = st.session_state.pchecker_state
+
+ for test_name in status_dict:
+ state.results[test_name] = status_dict[test_name]
+ # st.rerun()
+
+def update_trace_logs(trace_logs):
+ state = st.session_state.pchecker_state
+
+ for test_name in trace_logs:
+ state.trace_logs[test_name] = trace_logs[test_name]
+
+class PCheckerMode:
+ def __init__(self):
+ self.display_page()
+
+ def _get_services(self):
+ if not hasattr(self, "_services"):
+ provider = get_default_provider()
+ resource_loader = ResourceLoader(PROJECT_ROOT / "resources")
+ compilation = CompilationService(
+ llm_provider=provider,
+ resource_loader=resource_loader
+ )
+ fixer = FixerService(
+ llm_provider=provider,
+ resource_loader=resource_loader,
+ compilation_service=compilation
+ )
+ self._services = {
+ "compilation": compilation,
+ "fixer": fixer
+ }
+ return self._services
+
+ def _run_checker(self, project_path: str, schedules: int, timeout: int):
+ services = self._get_services()
+ return services["compilation"].run_checker(
+ project_path=project_path,
+ schedules=schedules,
+ timeout=timeout
+ )
+
+ def handle_auto_fix(self, test_name: str, project_path: str, trace_log: str, progress_bar=None, spinner_column=None):
+ print("[handle_auto_fix]")
+ """Handle auto fix for a single test"""
+ state = st.session_state.pchecker_state
+ services = self._get_services()
+ error_category = pipelines.identify_error_category(trace_log)
+
+ if progress_bar:
+ progress_bar.progress(0.1)
+
+ with spinner_column:
+ with st.spinner("Fixing...", show_time=False):
+ fix_result = services["fixer"].fix_checker_error(
+ project_path=project_path,
+ trace_log=trace_log,
+ error_category=str(error_category) if error_category else None
+ )
+
+ if fix_result.needs_guidance:
+ state.user_guidance_request = fix_result.guidance_request
+
+ if progress_bar:
+ progress_bar.progress(0.7)
+
+ checker_result = self._run_checker(
+ project_path=project_path,
+ schedules=state.config.schedules,
+ timeout=state.config.timeout_seconds
+ )
+ state.results = checker_result.test_results
+ state.trace_logs = checker_result.trace_logs
+
+ if progress_bar:
+ progress_bar.progress(1.0)
+
+ def fix_tests(self, project_path: str, failed_tests: List[str], trace_logs: Dict, ui_elements=None):
+ print("[fix_tests]")
+ """Handle auto fix for all failed tests"""
+ state = st.session_state.pchecker_state
+
+ def update_progress(name, new_progress_value):
+ ui_elements[name]["progress_bar"].progress(new_progress_value)
+ # st.rerun()
+
+ with st.spinner("Attempting to auto fix all tests...", show_time=True):
+ for test_name in failed_tests:
+ trace_log = trace_logs.get(test_name, "")
+ if not trace_log:
+ continue
+ self.handle_auto_fix(
+ test_name=test_name,
+ project_path=project_path,
+ trace_log=trace_log,
+ progress_bar=ui_elements[test_name]["progress_bar"],
+ spinner_column=ui_elements[test_name]["spinner"]
+ )
+
+ def display_project_path_box(self, state):
+ print("[display_project_path_box]")
+ # Project path input
+ state.project_path = st.text_input(
+ "Enter the path to your P project:",
+ value=state.project_path,
+ placeholder="/path/to/your/project"
+ )
+
+ def display_configuration_section(self, state):
+ print("[display_configuration_section]")
+ # Configuration settings
+ with st.expander("Configuration Settings"):
+ state.config.schedules = st.number_input(
+ "Number of schedules",
+ min_value=1,
+ value=state.config.schedules
+ )
+ state.config.timeout_seconds = st.number_input(
+ "Timeout (seconds)",
+ min_value=1,
+ value=state.config.timeout_seconds
+ )
+ state.config.seed = st.text_input(
+ "Seed",
+ value=state.config.seed
+ )
+
+ def _handle_project_submit(self, state):
+ print("[_handle_project_submit]")
+ if not state.project_path:
+ st.error("Please enter a project path")
+ return
+
+ if not os.path.exists(state.project_path):
+ st.error("Project path does not exist")
+ return
+
+ state.latest_project_path = state.project_path
+ state.usage_stats = {"cumulative": {"inputTokens": 0, "outputTokens":0}, "last_action": {"inputTokens": 0, "outputTokens":0}}
+
+ with st.spinner("Getting Project State...."):
+ checker_result = self._run_checker(
+ state.latest_project_path,
+ schedules=state.config.schedules,
+ timeout=state.config.timeout_seconds
+ )
+ state.results = checker_result.test_results
+ state.trace_logs = checker_result.trace_logs
+ state.trace_dicts = {}
+
+ def display_submit_button(self, state):
+ print("[display_submit_button]")
+ st.button("Run Checker", on_click=lambda: self._handle_project_submit(state))
+
+ def display_test_status(self, state, passed):
+ print("[display_test_status]")
+ symbol = "✅ [Passing]" if passed else "❌ [Failing]"
+ st.write(f"{symbol}")
+
+
+ def display_button_autofix_test(self, state, test_name, progress_bar, spinner_column):
+ print("[display_button_autofix_test]")
+ st.button("🔧 Auto Fix", key=f"fix_{test_name}", on_click=lambda: self.handle_auto_fix(
+ test_name=test_name,
+ project_path=state.latest_project_path,
+ trace_log=state.trace_logs[test_name],
+ progress_bar=progress_bar,
+ spinner_column = spinner_column
+ ))
+
+ def display_checker_summary_row(self, state, test_name, passed):
+ print("[display_checker_summary_row]")
+ cols = st.columns([1, 3, 2, 3, 2])
+ progress_bar = None
+ spinner_column = cols[2]
+
+ with cols[0]:
+ self.display_test_status(state, passed)
+ with cols[1]:
+ st.write(f"{test_name}")
+ if not passed:
+ with cols[3]:
+ progress = state.fix_progress.get(test_name, 0)
+ progress_bar = st.progress(progress)
+ with cols[4]:
+ self.display_button_autofix_test(state, test_name, progress_bar, cols[2])
+
+ return {"progress_bar": progress_bar, "spinner":spinner_column}
+
+ def initialize_interactive_mode(self, state, failed_tests, ui_elements):
+ print("[initialize_interactive_mode]")
+ first_test_name = failed_tests[0]
+ state.interactive_mode_active = True
+ state.current_interactive_mode_state = InteractiveModeState()
+ state.current_interactive_mode_state.tests_to_fix = failed_tests
+ state.current_interactive_mode_state.current_test_name = first_test_name
+ state.current_interactive_mode_state.current_stage = Stages.RUNNING_FILE_ANALYSIS
+ state.current_interactive_mode_state.current_trace_log = state.trace_logs[first_test_name]
+ state.current_interactive_mode_state.current_project_state = file_utils.capture_project_state(state.project_path)
+ state.current_interactive_mode_state.current_error_category = pipelines.identify_error_category(state.trace_logs[first_test_name])
+
+
+ def _handle_interactive_fix_all(self, state, failed_tests, ui_elements):
+ print("[_handle_interactive_fix_all]")
+ self.initialize_interactive_mode(state, failed_tests, ui_elements)
+ # st.rerun()
+
+ def display_button_interactive_fix_all(self, state, failed_tests, ui_elements):
+ print("[display_button_interactive_fix_all]")
+ st.button("🔧 Interactive Fix All",
+ key="fix_all_interactive",
+ on_click=lambda: self._handle_interactive_fix_all(state, failed_tests, ui_elements),
+ disabled=state.interactive_mode_active
+ )
+
+ def _handle_autofix_all(self, state, failed_tests, ui_elements):
+ print("[_handle_autofix_all]")
+ self.fix_tests(
+ project_path=state.latest_project_path,
+ failed_tests=failed_tests,
+ trace_logs=state.trace_logs,
+ ui_elements=ui_elements
+ )
+
+ def display_button_autofix_all(self, state, failed_tests, ui_elements):
+ print("[display_button_autofix_all]")
+ st.button("🔧 Auto Fix All", key="fix_all", on_click=lambda: self._handle_autofix_all(state, failed_tests, ui_elements))
+
+ def display_checker_summary(self, state):
+ print("[display_checker_summary]")
+ st.subheader("Project Checker Summary")
+
+ ui_elements = {t:{} for t in state.results}
+ failed_tests = sorted([t for t in state.results.keys() if not state.results[t]])
+
+ for test_name, passed in sorted(state.results.items()):
+ ui_elements[test_name] = {**self.display_checker_summary_row(state, test_name, passed)}
+
+ if failed_tests:
+ cols = st.columns([1, 2, 2, 1])
+ with cols[1]:
+ self.display_button_autofix_all(state, failed_tests, ui_elements)
+ with cols[2]:
+ self.display_button_interactive_fix_all(state, failed_tests, ui_elements)
+
+ @st.dialog("Successfully Fixed!")
+ def display_fix_success_dialog(self, state, im_state):
+ print("[display_fix_success_dialog]")
+ st.text(f"Test case {im_state.previous_test_name} was successfully fixed!")
+ if st.button("Continue"):
+ im_state.current_stage = Stages.RUNNING_FILE_ANALYSIS
+ st.rerun()
+
+ @st.dialog("Saved!")
+ def display_save_success_dialog(self, state, im_state):
+ st.text(f"Project was sucessfully saved!")
+ if st.button("Close"):
+ st.rerun()
+
+ @st.dialog(f"Test case still Failing!")
+ def display_fix_failed_dialog(self, state, im_state):
+ print("[display_fix_failed_dialog]")
+ st.text("Test case was not fixed!")
+ st.markdown(f"**Previous Error Category:** {im_state.previous_error_category}")
+ st.markdown(f"**New Error Category:** {im_state.current_error_category}")
+ if st.button("Continue"):
+ im_state.current_stage = Stages.RUNNING_FILE_ANALYSIS
+ st.rerun()
+
+ def display_file_selection_page(self, state, im_state):
+ print("[display_file_selection_page]")
+ st.subheader("Select Files for Analysis")
+
+ # Categorize files
+ project_files = []
+ psrc_files = []
+ pspec_files = []
+ ptst_files = []
+
+ for file_path in im_state.current_project_state.keys():
+ if file_path.endswith('.pproj') or file_path.endswith('.ddoc'):
+ project_files.append(file_path)
+ elif file_path.startswith('PSrc/'):
+ psrc_files.append(file_path)
+ elif file_path.startswith('PSpec/'):
+ pspec_files.append(file_path)
+ elif file_path.startswith('PTst/'):
+ ptst_files.append(file_path)
+
+ # Display files in columns
+ cols = st.columns(4)
+ selected_files = []
+
+ with cols[0]:
+ st.markdown("##### Project")
+ for file_path in project_files:
+ is_checked = st.checkbox(
+ file_path,
+ value=file_path in im_state.selected_files,
+ key=f"file_checkbox_{file_path}"
+ )
+ if is_checked:
+ selected_files.append(file_path)
+
+ with cols[1]:
+ st.markdown("##### PSrc")
+ for file_path in psrc_files:
+ is_checked = st.checkbox(
+ file_path,
+ value=file_path in im_state.selected_files,
+ key=f"file_checkbox_{file_path}"
+ )
+ if is_checked:
+ selected_files.append(file_path)
+
+ with cols[2]:
+ st.markdown("##### PSpec")
+ for file_path in pspec_files:
+ is_checked = st.checkbox(
+ file_path,
+ value=file_path in im_state.selected_files,
+ key=f"file_checkbox_{file_path}"
+ )
+ if is_checked:
+ selected_files.append(file_path)
+
+ with cols[3]:
+ st.markdown("##### PTst")
+ for file_path in ptst_files:
+ is_checked = st.checkbox(
+ file_path,
+ value=file_path in im_state.selected_files,
+ key=f"file_checkbox_{file_path}"
+ )
+ if is_checked:
+ selected_files.append(file_path)
+
+ im_state.selected_files = selected_files
+ print(f"SELECTED FILES:")
+ print('\n\t'.join(im_state.selected_files))
+
+ if st.button("⌯⌲ Submit Selected Files"):
+ im_state.current_stage = Stages.RUNNING_ERROR_ANALYSIS
+ st.rerun()
+
+ def display_error_analysis_page(self, state, im_state):
+ st.subheader("LLM's Error Analysis")
+ st.write(string_utils.tags_to_md(im_state.current_error_analysis))
+ st.markdown("#### Do you agree with this analysis?")
+ im_state.additional_user_guidance = st.text_area("Additional guidance:",placeholder="Optional")
+ cols = st.columns([1,1,5])
+ with cols[0]:
+ if st.button("Agree ✅"):
+ im_state.current_stage = Stages.RUNNING_GET_FIX
+ st.rerun()
+ with cols[1]:
+ if st.button("Disagree ❌"):
+ im_state.current_stage = Stages.RUNNING_ERROR_ANALYSIS
+ st.rerun()
+
+
+ def _handle_save_current_project(self, state, im_state, save_path):
+ try:
+ # Convert to Path objects for easier handling
+ source_dir = Path(im_state.tmp_project_dir)
+ target_dir = Path(save_path)
+
+ # Check if source directory exists
+ if not source_dir.exists():
+ st.error(f"Source directory does not exist: {source_dir}")
+ return
+
+ # Create target directory if it doesn't exist
+ target_dir.mkdir(parents=True, exist_ok=True)
+
+ # Create a unique project folder name to avoid overwriting
+ project_name = source_dir.name or "project"
+ final_target = target_dir / project_name
+
+ # If the target already exists, add a number suffix
+ counter = 1
+ while final_target.exists():
+ final_target = target_dir / f"{project_name}_{counter}"
+ counter += 1
+
+ # Copy the entire directory tree
+ shutil.copytree(source_dir, final_target)
+
+ self.display_save_success_dialog(state, im_state)
+
+ except PermissionError:
+ st.error("Permission denied. Please check that you have write access to the selected folder.")
+ except Exception as e:
+ st.error(f"An error occurred while saving the project: {str(e)}")
+
+ def display_current_goal(self, state, im_state):
+ st.markdown(f"#### Current Goal")
+ st.write(f"Fixing `{im_state.current_error_category}` for `{im_state.current_test_name}`")
+ stx.scrollableTextbox(state.trace_logs[im_state.current_test_name], height=300)
+
+ def display_save_recent_project(self, state, im_state):
+ st.markdown("#### Save Most Recent Project")
+ save_path = st.text_input("Enter the full path where you want to save the project", disabled=not im_state.tmp_project_dir, placeholder="e.g. /home/user/Desktop")
+ st.button("Save", on_click=lambda : self._handle_save_current_project(state, im_state, save_path), disabled=not im_state.tmp_project_dir)
+ if not im_state.tmp_project_dir:
+ st.markdown("_There is no recent project yet_")
+
+ def display_interactive_mode_control_center_header(self, state, im_state):
+ print("[display_interactive_mode_control_center_header]")
+ st.subheader("Interactive Mode Control Center")
+ self.display_current_goal(state, im_state)
+ self.display_save_recent_project(state, im_state)
+ st.write("---")
+
+ def update_usage_stats(self, state, usage_stats):
+ state.usage_stats['cumulative']['inputTokens'] += usage_stats['cumulative']['inputTokens']
+ state.usage_stats['cumulative']['outputTokens'] += usage_stats['cumulative']['outputTokens']
+ state.usage_stats['last_action']['inputTokens'] = usage_stats['cumulative']['inputTokens']
+ state.usage_stats['cumulative']['outputTokens'] = usage_stats['cumulative']['outputTokens']
+
+
+ def llm_call_get_selected_files(self, state, im_state, current_test_name):
+ im_state.current_pipeline = pipelines.create_base_pipeline_fewshot()
+ file_list_str = pipelines.ask_llm_which_files_it_needs(
+ im_state.current_pipeline,
+ im_state.current_project_state,
+ current_test_name,
+ im_state.current_trace_log,
+ im_state.current_error_category
+ )
+ self.update_usage_stats(state, im_state.current_pipeline.get_token_usage())
+ return file_list_str.split("\n")
+
+ def run_analysis_files_needed(self, state, im_state):
+ print("[run_analysis_files_needed]")
+
+ current_test_name = im_state.current_test_name # always get the first test, will be removed as the checker goes on
+
+ with st.spinner(f"Analyzing {current_test_name}..."):
+ selected_files = self.llm_call_get_selected_files(state, im_state, current_test_name)
+ im_state.selected_files = selected_files
+
+ im_state.current_stage = Stages.FILE_ANALYSIS_COMPLETE
+ st.rerun()
+
+ def run_error_analysis(self, state, im_state):
+ selected_files_dict = { f:im_state.current_project_state[f] for f in im_state.selected_files }
+
+ im_state.current_pipeline = pipelines.create_base_pipeline_fewshot()
+ with st.spinner("Analyzing error based on selected files ..."):
+ im_state.current_error_analysis = pipelines.get_error_analysis(
+ im_state.current_pipeline,
+ selected_files_dict,
+ im_state.current_test_name,
+ im_state.current_trace_log,
+ im_state.current_error_category,
+ im_state.additional_user_guidance
+ )
+ im_state.current_stage = Stages.ERROR_ANALYSIS_COMPLETE
+ im_state.additional_user_guidance = ""
+
+ im_state.files_to_fix = [f.strip() for f in string_utils.extract_tag_contents(im_state.current_error_analysis, "files_to_fix").split(",")]
+
+ self.update_usage_stats(state, im_state.current_pipeline.get_token_usage())
+ # Can't use self.update_usage stats since this is a special case the pipeline is being reused between this and the previous steps
+ # state.usage_stats['cumulative']['inputTokens'] = im_state.current_pipeline.get_token_usage()["cumulative"]["inputTokens"]
+ # state.usage_stats['cumulative']['outputTokens'] = im_state.current_pipeline.get_token_usage()["cumulative"]["outputTokens"]
+ # state.usage_stats['last_action']['inputTokens'] = im_state.current_pipeline.get_token_usage()["sequential"][-1]["inputTokens"]
+ # state.usage_stats['last_action']['outputTokens'] = im_state.current_pipeline.get_token_usage()["sequential"][-1]["outputTokens"]
+
+ st.rerun()
+
+ def run_get_fix(self, state, im_state):
+ print("[run_get_fix]")
+ selected_files_dict_numbered = { f:string_utils.add_line_numbers(im_state.current_project_state[f]) for f in im_state.selected_files }
+ im_state.current_pipeline = pipelines.create_base_pipeline_fewshot()
+ files_str = ",".join(im_state.selected_files)
+ with st.spinner(f"Getting fix from LLM. Sent: {files_str}..."):
+ patches = pipelines.attempt_fix_error_patches(
+ im_state.current_pipeline,
+ selected_files_dict_numbered,
+ im_state.current_error_analysis,
+ im_state.current_error_category,
+ im_state.additional_user_guidance
+ )
+
+ im_state.patches = patches
+
+ selected_files_dict = { f:im_state.current_project_state[f] for f in im_state.patches }
+ im_state.patch_results_dict = string_utils.apply_patch_whatthepatch_per_file(im_state.patches, selected_files_dict)
+ im_state.remaining_faulty_patches_to_fix = { k:(c,e) for k,(c,e) in im_state.patch_results_dict.items() if e }
+
+ im_state.current_stage = Stages.GET_FIX_COMPLETE
+ self.update_usage_stats(state, im_state.current_pipeline.get_token_usage())
+ st.rerun()
+
+ def display_debug_info_attempted_patches(self, state, im_state):
+ for (filename, (fixed, attempted_patches)) in im_state.patch_debug_info.items():
+ status = "FIXED" if fixed else "FAILED"
+ with st.expander(f"[{status}] {filename}"):
+ for i, ap in enumerate(attempted_patches):
+ with st.expander(f"Patch Attempt {i}"):
+ st.code(ap)
+
+ def display_patch_debug_info(self, state, im_state):
+ numbered_files_dict = { f:string_utils.add_line_numbers(im_state.current_project_state[f]) for f in im_state.selected_files }
+
+ with st.expander("[DevTool] Debug info"):
+ for filename in im_state.patches:
+ with st.expander(f"Patch: {filename}"):
+ st.code(im_state.patches[filename])
+
+ with st.expander("Numbered Files Dict"):
+ for f in numbered_files_dict:
+ with st.expander(f):
+ st.code(numbered_files_dict[f])
+
+ self.display_debug_info_attempted_patches(state, im_state)
+
+
+ def run_faulty_patch_adjustment(self, state, im_state, filename, contents, err_msg):
+ fixed, attempted_patches, patched_content, token_usage = pipelines.apply_patch_correction(
+ filename,
+ contents,
+ im_state.patches[filename],
+ err_msg,
+ max_attempts=5
+ )
+
+ im_state.patch_results_dict[filename] = (patched_content, err_msg if not fixed else "")
+ im_state.patch_debug_info[filename] = (fixed, attempted_patches)
+ self.update_usage_stats(state, token_usage)
+ del im_state.remaining_faulty_patches_to_fix[filename]
+
+ def display_fix_diff(self, state, im_state):
+ print("[display_fix_diff]")
+
+ self.display_patch_debug_info(state, im_state)
+
+ good_patch_files = [f for f in im_state.patch_results_dict if f not in im_state.remaining_faulty_patches_to_fix]
+ faulty_patch_files = [f for f in im_state.remaining_faulty_patches_to_fix]
+ print(f"faulty_patch_files = {faulty_patch_files}")
+
+ ordered_files = good_patch_files + faulty_patch_files
+
+ for filename in ordered_files:
+ (contents, err_msg) = im_state.patch_results_dict[filename]
+ st.markdown(f"###### 📄 {filename}")
+ if filename in faulty_patch_files:
+ with st.spinner(f"Fixing faulty patch for {filename}"):
+ self.run_faulty_patch_adjustment(state, im_state, filename, contents, err_msg)
+ st.rerun()
+ else:
+ st.write(f"Orig {len(im_state.current_project_state[filename])} : {(len(contents))} New")
+ with st.expander("Full Code"):
+ with st.expander(f"Original - {len(im_state.current_project_state[filename])} chars"):
+ st.code(im_state.current_project_state[filename])
+ with st.expander(f"New - {len(contents)} chars"):
+ st.code(contents)
+
+ diff_viewer(
+ im_state.current_project_state[filename],
+ contents,
+ split_view=True,
+ disabled_word_diff=True
+ )
+ if err_msg:
+ st.warning(f"{filename}: {err_msg}")
+
+ cols = st.columns([1,1,5])
+ with cols[0]:
+ if st.button("Approve ✅"):
+ im_state.new_files_dict = {k:c for (k, (c, _)) in im_state.patch_results_dict.items()}
+ im_state.current_stage = Stages.RUNNING_APPLY_FIX
+ st.rerun()
+
+ with cols[1]:
+ if st.button("Retry ❌"):
+ im_state.current_stage = Stages.RUNNING_GET_FIX
+ st.rerun()
+
+ def run_apply_fix(self, state, im_state):
+ print("[run_apply_fix]")
+ with st.spinner("Applying fix..."):
+ im_state.new_project_state = {**im_state.current_project_state, **im_state.new_files_dict}
+ # checker_utils.try_pchecker_on_dict(new_project_state)
+
+ im_state.current_stage = Stages.APPLY_FIX_COMPLETE
+ st.rerun()
+
+ def run_compile_fix(self, state, im_state):
+ print("[run_compile_fix]")
+ passed = False
+ with st.spinner("Compiling fixed code.."):
+ passed, tmp_dir, stdout = compile_utils.try_compile_project_state(im_state.new_project_state)
+ im_state.tmp_project_dir = tmp_dir
+ im_state.recent_compile_output = stdout
+
+ im_state.current_stage = Stages.COMPILE_FIX_PASSED if passed else Stages.COMPILE_FIX_FAILED
+ st.rerun()
+
+ def display_compile_failed_page(self, state, im_state):
+ print("[display_compile_failed_page]")
+ st.subheader("Compilation Error")
+ stx.scrollableTextbox(im_state.recent_compile_output, height = 300)
+
+ with st.expander("[DevTools] Patches"):
+ for filename, (patch, _) in im_state.patch_results_dict.items():
+ with st.expander(f"{filename}"):
+ st.code(patch)
+
+ if st.button("Regenerate Fix"):
+ im_state.current_stage = Stages.RUNNING_GET_FIX
+ st.rerun()
+
+ if st.button("[DevTool] Assume fixed"):
+ im_state.tmp_project_dir = state.latest_project_path
+ im_state.current_stage = Stages.COMPILE_FIX_PASSED
+ st.rerun()
+
+ def move_to_next_test_case(self, state, im_state):
+ failed_tests = sorted([t for t in state.results.keys() if not state.results[t]])
+ next_test_name = failed_tests[0]
+
+ im_state.current_project_state = {**im_state.new_project_state}
+ im_state.current_test_name = next_test_name
+ im_state.current_trace_log = state.trace_logs[next_test_name]
+ im_state.new_files_dict = {}
+ im_state.new_project_state = {}
+ im_state.current_error_analysis = ""
+ im_state.current_error_category = ""
+ im_state.tests_to_fix = failed_tests
+ im_state.selected_files = []
+ im_state.additional_user_guidance = ""
+ im_state.current_pipeline = pipelines.create_base_pipeline_fewshot()
+ im_state.debug_str = ""
+ im_state.recent_compile_output = ""
+
+ def run_pchecker_on_fix(self, state, im_state):
+ print("[run_pchecker_on_fix]")
+ with st.spinner("Running PChecker on code..."):
+ results, trace_dicts, trace_logs = checker_utils.try_pchecker(
+ im_state.tmp_project_dir,
+ schedules=state.config.schedules,
+ timeout=state.config.timeout_seconds,
+ seed=state.config.seed
+ )
+
+ current_test = im_state.current_test_name
+
+
+ im_state.previous_error_category = im_state.current_error_category
+ im_state.current_error_category = pipelines.identify_error_category(trace_logs[current_test])
+ im_state.current_trace_log = trace_logs[current_test]
+
+
+ state.results = results
+ state.trace_dicts = trace_dicts
+ state.trace_logs = trace_logs
+
+ if results[current_test]:
+ im_state.previous_test_name = current_test
+ self.move_to_next_test_case(state, im_state)
+ im_state.current_stage = Stages.PCHECKER_PASSED
+ else:
+ im_state.current_stage = Stages.PCHECKER_FAILED
+
+ st.rerun()
+
+
+ def display_interactve_mode_control_center(self, state):
+ print("[display_interactve_mode_control_center]")
+
+ im_state = state.current_interactive_mode_state
+ self.display_interactive_mode_control_center_header(state, im_state)
+
+ stage = im_state.current_stage
+ print(f"STAGE = {stage}")
+
+ if stage == Stages.RUNNING_FILE_ANALYSIS:
+ self.run_analysis_files_needed(state, im_state)
+ if stage == Stages.FILE_ANALYSIS_COMPLETE:
+ self.display_file_selection_page(state, im_state)
+ if stage == Stages.RUNNING_ERROR_ANALYSIS:
+ self.run_error_analysis(state, im_state)
+ if stage == Stages.ERROR_ANALYSIS_COMPLETE:
+ self.display_error_analysis_page(state, im_state)
+ if stage == Stages.RUNNING_GET_FIX:
+ self.run_get_fix(state, im_state)
+ if stage == Stages.GET_FIX_COMPLETE:
+ self.display_fix_diff(state, im_state)
+ if im_state.remaining_faulty_patches_to_fix:
+ self.run_faulty_patch_adjustment(state, im_state)
+ if stage == Stages.RUNNING_APPLY_FIX:
+ self.run_apply_fix(state, im_state)
+ if stage == Stages.APPLY_FIX_COMPLETE:
+ self.run_compile_fix(state, im_state)
+ if stage == Stages.COMPILE_FIX_FAILED:
+ self.display_compile_failed_page(state, im_state)
+ if stage == Stages.COMPILE_FIX_PASSED:
+ self.run_pchecker_on_fix(state, im_state)
+ if stage == Stages.PCHECKER_FAILED:
+ self.display_fix_failed_dialog(state, im_state)
+ if stage == Stages.PCHECKER_PASSED:
+ self.display_fix_success_dialog(state, im_state)
+ else:
+ pass
+
+ def append_to_log(self, state, line):
+ state.log_lines.append(line)
+
+ def display_token_metric(self, label: str, value: int):
+ """Display a single token metric using Streamlit's metric component."""
+ st.metric(
+ label=label,
+ value=f"{value:,}",
+ delta=None
+ )
+
+ def display_cumulative_stats(self, stats: dict):
+ """Display cumulative token usage statistics."""
+ st.markdown("#### Cumulative Usage")
+ metrics_col1, metrics_col2 = st.columns(2)
+ with metrics_col1:
+ self.display_token_metric("Input Tokens", stats['inputTokens'])
+ with metrics_col2:
+ self.display_token_metric("Output Tokens", stats['outputTokens'])
+
+ def display_last_action_stats(self, stats: dict):
+ """Display token usage statistics for the last action."""
+ st.markdown("#### Last Action")
+ metrics_col1, metrics_col2 = st.columns(2)
+ with metrics_col1:
+ self.display_token_metric("Input Tokens", stats['inputTokens'])
+ with metrics_col2:
+ self.display_token_metric("Output Tokens", stats['outputTokens'])
+
+ def display_statistics_hud(self, state):
+ if not state.usage_stats:
+ return
+ st.markdown("### 📊 Token Usage Statistics")
+ col1, col2 = st.columns(2)
+ with col1:
+ self.display_cumulative_stats(state.usage_stats['cumulative'])
+ with col2:
+ self.display_last_action_stats(state.usage_stats['last_action'])
+
+ def display_title_bar(self, state):
+ st.set_page_config(page_title='Project Analysis Mode', layout='wide', page_icon = "ui/assets/p_icon.ico")
+ cols = st.columns([5,1,4])
+ st.title('Project Analysis Mode')
+ if state.results:
+ if st.button("Reset"):
+ st.session_state.pchecker_state = PCheckerState()
+ st.rerun()
+
+
+ def display_page(self):
+ state = st.session_state.pchecker_state
+
+ self.display_title_bar(state)
+
+ if not state.results:
+ self.display_project_path_box(state)
+
+ self.display_configuration_section(state)
+
+ self.display_submit_button(state)
+
+ if state.results:
+ self.display_checker_summary(state)
+ st.markdown("---")
+ if state.interactive_mode_active:
+ self.display_statistics_hud(state)
+ st.write("---")
+ self.display_interactve_mode_control_center(state)
diff --git a/Src/PeasyAI/src/core/modes/pipelines.py b/Src/PeasyAI/src/core/modes/pipelines.py
new file mode 100644
index 0000000000..69c7a5cdaa
--- /dev/null
+++ b/Src/PeasyAI/src/core/modes/pipelines.py
@@ -0,0 +1,673 @@
+from utils.module_utils import save_module_state, restore_module_state
+from utils import file_utils, global_state, log_utils, string_utils, regex_utils, checker_utils, compile_utils
+from utils.project_structure_utils import setup_project_structure
+from core.services.compilation import CompilationService
+from core.pipelining.prompting_pipeline import PromptingPipeline
+from utils.constants import *
+from utils.generate_p_code import extract_filenames, extract_validate_and_log_Pcode
+import re, os, json
+
+
+def _safe_format(template: str, **kwargs) -> str:
+ """Format a template, falling back to manual replacement on unescaped braces."""
+ try:
+ return template.format(**kwargs)
+ except (KeyError, ValueError, IndexError):
+ result = template
+ for key, value in kwargs.items():
+ result = result.replace("{" + key + "}", str(value))
+ return result
+
+def compiler_analysis(model_id, pipeline, all_responses, num_of_iterations, ctx_pruning=None):
+ max_iterations = num_of_iterations
+ recent_project_path = file_utils.get_recent_project_path()
+
+ compilation_service = CompilationService()
+ compile_result = compilation_service.compile(recent_project_path)
+ compilation_success = compile_result.success
+ compilation_output = compile_result.stdout or compile_result.stderr or ""
+
+ if compilation_success:
+ print(f":white_check_mark: :green[Compilation succeeded in {max_iterations - num_of_iterations} iterations.]")
+ print(f"Compilation succeeded in {max_iterations - num_of_iterations} iterations.")
+ return
+
+ P_filenames_dict = regex_utils.get_all_P_files(compilation_output)
+ while (not compilation_success and num_of_iterations > 0):
+ # Parse errors using CompilationService
+ errors = compilation_service.get_all_errors(compilation_output)
+ if not errors:
+ break
+ error = errors[0]
+ file_name = os.path.basename(error.file_path)
+ line_number = error.line_number
+ column_number = error.column_number
+
+ print(f". . . :red[[Iteration #{(max_iterations - num_of_iterations)}] Compilation failed in {file_name} at line {line_number}:{column_number}. Fixing the error...]")
+
+ # Get file path and contents
+ file_path = P_filenames_dict.get(file_name, error.file_path)
+ file_contents = file_utils.read_file(file_path) if os.path.exists(file_path) else ""
+
+ # Build correction instruction
+ custom_msg = f"Fix the following compilation error in {file_name} at line {line_number}, column {column_number}:\n{error.message}"
+
+ # Continue the conversation to fix compiler errors
+ pipeline.add_user_msg(custom_msg)
+ response = pipeline.invoke_llm(model_id, candidates=1, heuristic='random')
+ print(f". . . . . . Compiling the fixed code...")
+ log_filename, Pcode = extract_validate_and_log_Pcode(response, "", "", logging_enabled=False)
+ if log_filename is not None and Pcode is not None:
+ log_utils.log_Pcode_to_file(Pcode, file_path)
+ all_responses[log_filename] = Pcode
+ num_of_iterations -= 1
+
+ # Log the diff
+ log_utils.log_code_diff(file_contents, response, "After fixing the P code")
+ compile_result = compilation_service.compile(recent_project_path)
+ compilation_success = compile_result.success
+ compilation_output = compile_result.stdout or compile_result.stderr or ""
+ if compilation_success:
+ print(f":green[Compilation succeeded in {max_iterations - num_of_iterations} iterations.]")
+ print(f"Compilation succeeded in {max_iterations - num_of_iterations} iterations.")
+
+ global_state.compile_iterations += (max_iterations - num_of_iterations)
+
+ global_state.compile_success = compilation_success
+
+def create_proj_files(project_root):
+
+ # Create project structure with folders and pproj file
+ # parent_abs_path = global_state.custom_dir_path or os.path.join(os.getcwd(), global_state.recent_dir_path)
+ setup_project_structure(project_root, global_state.project_name)
+
+
+def generate_machine_code(model, pipeline, instructions, filename, dirname):
+ """Generate machine code using either two-stage or single-stage process."""
+ # Stage 1: Generate structure
+ pipeline.add_user_msg(_safe_format(instructions['MACHINE_STRUCTURE'], machineName=filename))
+ pipeline.add_documents_inline(get_context_files()["MACHINE_STRUCTURE"], string_utils.tag_surround)
+
+ stage1_response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ structure_pattern = r'(.*?)'
+ match = re.search(structure_pattern, stage1_response, re.DOTALL)
+
+ if match:
+ # Two-stage generation
+ machine_structure = match.group(1).strip()
+ print(f" . . . Stage 2: Implementing function bodies for {filename}.p")
+
+ pipeline.add_user_msg(_safe_format(instructions[MACHINE], machineName=filename)+ "\n\nHere is the starting structure:\n\n" + machine_structure)
+ pipeline.add_documents_inline(get_context_files()["MACHINE"], string_utils.tag_surround)
+
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ print(response)
+ else:
+ # Fallback to single-stage
+ print(f" . . . :red[Failed to extract structure for {filename}.p. Falling back to single-stage generation.]")
+ pipeline.add_user_msg(_safe_format(instructions[MACHINE], machineName=filename))
+ pipeline.add_documents_inline(get_context_files()["MACHINE"], string_utils.tag_surround)
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ print(response)
+
+ # token_usage = response["current_tokens"]
+ # log_token_usage(token_usage, backend_status)
+
+ return extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, dirname)
+
+def generate_generic_file(model_id, pipeline, instructions, filename, dirname):
+
+ pipeline.add_user_msg(instructions[dirname].format(filename=filename))
+ pipeline.add_documents_inline(get_context_files()[dirname], string_utils.tag_surround)
+ response = pipeline.invoke_llm(model_id, candidates=1, heuristic='random')
+
+ return extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, dirname)
+
+def old_pipeline_replicated(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, out_dir=".tmp"):
+
+ SAVED_GLOBAL_STATE = save_module_state(global_state)
+
+ all_responses = {}
+
+ dd_path = task
+ dd_content = file_utils.read_file(dd_path)
+
+ project_name_pattern = r'^#\s+(.+?)\s*$'
+
+ match = re.search(project_name_pattern, dd_content, re.MULTILINE | re.IGNORECASE)
+ if match:
+ global_state.project_name = match.group(1).strip().replace(" ", "_")
+
+ timestamp = global_state.current_time.strftime('%Y_%m_%d_%H_%M_%S')
+ global_state.project_name_with_timestamp = f"{global_state.project_name}_{timestamp}"
+ log_utils.move_recent_to_archive()
+ file_utils.empty_file(global_state.full_log_path)
+ file_utils.empty_file(global_state.code_diff_log_path)
+
+ destination_path = out_dir
+ if destination_path and destination_path.strip() != "" and file_utils.check_directory(destination_path.strip()):
+ global_state.custom_dir_path = destination_path.strip()
+
+ parent_abs_path = global_state.custom_dir_path or os.path.join(os.getcwd(), global_state.recent_dir_path)
+
+ project_root = os.path.join(parent_abs_path, global_state.project_name_with_timestamp)
+
+ create_proj_files(project_root)
+
+ pipeline = PromptingPipeline()
+ system_prompt = file_utils.read_file(global_state.system_prompt_path)
+ pipeline.add_system_prompt(system_prompt)
+
+ # Get initial machine list
+ text = instructions[LIST_OF_MACHINE_NAMES].format(userText=dd_content)
+ pipeline.add_user_msg(text, [global_state.P_basics_path])
+ pipeline.add_user_msg("These are the example P Programs ",[global_state.P_program_example_path])
+ machines_list = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ # Generate filenames
+ pipeline.add_user_msg(instructions[LIST_OF_FILE_NAMES])
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ global_state.filenames_map = extract_filenames(response)
+ # Generate enums, types, and events
+ pipeline.add_user_msg(instructions[ENUMS_TYPES_EVENTS])
+ pipeline.add_documents_inline(get_context_files()["ENUMS_TYPES_EVENTS"], string_utils.tag_surround)
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ log_filename, Pcode = extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, PSRC)
+
+ file_abs_path = os.path.join(project_root, PSRC, log_filename)
+ print(f":blue[. . . filepath: {file_abs_path}]")
+
+ if log_filename is not None and Pcode is not None:
+ all_responses[log_filename] = Pcode
+
+ step_no = 2
+
+ for dirname, filenames in global_state.filenames_map.items():
+ if dirname != PSRC:
+ print(f"Step {step_no}: Generating {dirname}")
+ step_no += 1
+
+ for filename in filenames:
+ print(f"Generating file {filename}.p")
+ if dirname == PSRC and filename in machines_list:
+ log_filename, Pcode = generate_machine_code(model, pipeline, instructions, filename, dirname)
+ else:
+ log_filename, Pcode = generate_generic_file(model, pipeline, instructions, filename, dirname)
+
+ log_file_full_path = os.path.join(project_root, dirname, log_filename)
+ print(f":blue[. . . filepath: {log_file_full_path}]")
+ if log_filename is not None and Pcode is not None:
+ all_responses[log_filename] = Pcode
+
+ print(f"Running the P compiler and analyzer on {dirname}...")
+ num_iterations = 20 if dirname == PTST else 15
+ compiler_analysis(model, pipeline, all_responses, num_iterations)
+
+ final_compile_status = True if global_state.compile_success else False
+ # return all_responses
+ restore_module_state(global_state, SAVED_GLOBAL_STATE)
+ return (pipeline, project_root, final_compile_status)
+
+
+def reduce_trace_size(trace_log):
+ lines = trace_log.splitlines()
+ reduced = "\n".join(lines[-50:])
+ return reduced
+
+def generate_generic_analysis_prompt(p_code, trace_log, additional_user_guidance=""):
+ with open(global_state.generate_files_to_fix_for_pchecker, "r") as file:
+ template = file.read()
+ llm_query = template.format(
+ error_trace=reduce_trace_size(trace_log),
+ p_code=p_code,
+ tool="PChecker",
+ additional_error_info=additional_user_guidance
+ )
+ return llm_query
+
+def generate_hot_state_analysis_prompt(test_case, p_code, trace_log):
+ with open(global_state.template_path_hot_state_bug_analysis, "r") as file:
+ template = file.read()
+ llm_query = template.format(
+ error_trace=reduce_trace_size(trace_log),
+ p_code=p_code,
+ tool="PChecker",
+ additional_error_info="The line that starts with \"\" has the error message. You are only given the last 50 lines."
+ )
+ return llm_query
+
+
+# def generate_analysis_prompt(p_code, test_case, trace_log, error_category):
+# """Generate the prompt for analyzing P checker errors."""
+# llm_query = ""
+# if error_category == ErrorCategories.ENDED_IN_HOT_STATE:
+# llm_query = generate_hot_state_analysis_prompt(test_case, p_code, trace_log)
+# else:
+# llm_query = generate_generic_analysis_prompt(p_code, trace_log)
+# return llm_query
+
+def generate_analysis_prompt(p_code, test_case, trace_log, error_category, additional_user_guidance=""):
+ """Generate the prompt for analyzing P checker errors."""
+ llm_query = generate_generic_analysis_prompt(p_code, trace_log, additional_user_guidance=additional_user_guidance)
+ return llm_query
+
+def request_and_save_analysis(pipeline, prompt, out_dir, error_category):
+ """Request error analysis from LLM and save the response."""
+ pipeline.add_user_msg(prompt)
+ pipeline.invoke_llm()
+ analysis = pipeline.get_last_response()
+
+ with open(f"{out_dir}/llm_analysis.txt", "w") as f:
+ f.write(analysis)
+
+ return analysis
+
+def process_llm_response_with_tags(llm_analysis):
+ pattern = r'<([^>]+)>(.*?)\1>'
+ matches = re.findall(pattern, llm_analysis, re.DOTALL)
+ return {tag: content.strip() for tag, content in matches}
+
+def generate_fix_prompt(p_code, analysis, error_category, additional_user_guidance=""):
+ with open(global_state.generate_fixed_file_for_pchecker, "r") as f:
+ template = f.read()
+ additional_analysis = f'\n{additional_user_guidance}\n' if additional_user_guidance else ''
+ full_analysis = f"{analysis}\n{additional_analysis}"
+ params = {"p_code": p_code, "fix_description": full_analysis}
+ llm_query = template.format(**params)
+ return llm_query
+
+def generate_fix_patches_prompt(p_code, analysis, error_category, additional_user_guidance=""):
+ with open(global_state.generate_fix_patches_for_file, "r") as f:
+ template = f.read()
+ additional_analysis = f'\n{additional_user_guidance}\n' if additional_user_guidance else ''
+ full_analysis = f"{analysis}\n{additional_analysis}"
+ params = {"p_code": p_code, "fix_description": full_analysis}
+ llm_query = template.format(**params)
+ return llm_query
+
+def request_and_save_fix(pipeline, prompt, out_dir, error_category):
+ pipeline.add_user_msg(f"{prompt}\n\nApply the recommended fixes")
+ pipeline.invoke_llm()
+ response = pipeline.get_last_response()
+ with open(f"{out_dir}/llm_fix.txt", "w") as f:
+ f.write(response)
+
+ return process_llm_response_with_tags(response)
+
+def parse_fix_response(response):
+ """Parse the fix response and extract updated files."""
+ new_project_state = {}
+ content = []
+ current_filename = None
+ inside_tags = False
+
+ for line in response.split('\n'):
+ if line.startswith('<') and not line.startswith('') and line.endswith('>'):
+ # Found start tag
+ current_filename = line[1:-1] # Remove < and >
+ inside_tags = True
+ content = []
+ elif line.startswith(''):
+ # Found end tag, save the content
+ if content and current_filename:
+ new_project_state[current_filename] = '\n'.join(content)
+ else:
+ # Collecting content lines
+ content.append(line)
+
+ return new_project_state
+
+def attempt_fix_pchecker_errors(pipeline: PromptingPipeline, current_project_state, test_case, trace_dict, trace_log, error_category, out_dir):
+ """Attempt to fix P checker errors by getting analysis and fixes from LLM."""
+ # Generate prompt and get analysis
+ p_code = string_utils.file_dict_to_prompt(current_project_state)
+ prompt = generate_analysis_prompt(p_code, test_case, trace_log, error_category)
+ analysis = request_and_save_analysis(pipeline, prompt, out_dir, error_category)
+
+ # Get and save fix
+
+ fix_prompt = generate_fix_prompt(p_code, analysis, error_category)
+ fix_response = request_and_save_fix(pipeline, fix_prompt, out_dir, error_category)
+
+ return fix_response
+
+def request_analysis(pipeline, prompt):
+ """Request error analysis from LLM and save the response."""
+ pipeline.add_user_msg(prompt)
+ pipeline.invoke_llm()
+ analysis = pipeline.get_last_response()
+ return analysis
+
+def get_error_analysis(pipeline, current_project_state, test_case, trace_log, error_category, additional_user_guidance=""):
+ p_code = string_utils.file_dict_to_prompt(current_project_state)
+ prompt = generate_analysis_prompt(p_code, test_case, trace_log, error_category, additional_user_guidance=additional_user_guidance)
+ return request_analysis(pipeline, prompt)
+
+
+def ask_llm_which_files_it_needs(pipeline: PromptingPipeline, current_project_state, test_case, trace_log, error_category):
+ file_list = "\n".join(list(current_project_state.keys()))
+ template = file_utils.read_file(global_state.template_ask_llm_which_files_it_needs)
+ query = template.format(file_list=file_list, error_trace=trace_log)
+ pipeline.add_user_msg(query)
+ pipeline.invoke_llm()
+ return pipeline.get_last_response()
+
+# def get_error_analysis_snappy(pipeline, current_project_state, test_case, trace_log, error_category):
+# # p_code = file_dict_to_prompt(current_project_state)
+# file_list = "\n".join(list(current_project_state.keys()))
+# prompt = generate_filenames_prompt(file_names, test_case, trace_log, error_category)
+# return request_analysis(pipeline, prompt)
+
+def attempt_fix_error(pipeline, current_project_state, analysis, error_category, additional_user_guidance=""):
+ p_code = string_utils.file_dict_to_prompt(current_project_state)
+ fix_prompt = generate_fix_prompt(p_code, analysis, error_category, additional_user_guidance)
+ fix_response = request_fix(pipeline, fix_prompt, error_category)
+ return process_llm_response_with_tags(fix_response)
+
+def attempt_fix_error_patches(pipeline, current_project_state, analysis, error_category, additional_user_guidance=""):
+ p_code = string_utils.file_dict_to_prompt(current_project_state)
+ fix_prompt = generate_fix_patches_prompt(p_code, analysis, error_category, additional_user_guidance)
+ fix_response = request_fix(pipeline, fix_prompt, error_category)
+ return string_utils.parse_patches_by_file(fix_response)
+
+def request_fix(pipeline, prompt, error_category):
+ pipeline.add_user_msg(f"{prompt}\n\nApply the recommended fixes")
+ pipeline.invoke_llm()
+ response = pipeline.get_last_response()
+ return response
+
+def apply_patch_correction(filename, contents, patches, err_msg, max_attempts=5):
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt("You respond in unified diff format for a given task. No talking.")
+ pipeline.add_user_msg(
+ f"""I got the following error
+{err_msg}
+from whatthepatch library while trying to apply the patch for
+{filename}
+in the following full patch summary.
+{patches}
+
+Here are the contents for {filename}:
+{string_utils.add_line_numbers(contents)}
+
+Please fix the issue with the patch and give it back.
+\n""")
+ attempts = 0
+ attempted_patches = []
+ while True:
+ if attempts < max_attempts:
+ pipeline.invoke_llm()
+ new_patch = pipeline.get_last_response()
+ if new_patch.startswith("`"):
+ new_patch = "\n".join(new_patch.splitlines()[1:-1])
+ attempted_patches.append(new_patch)
+
+ print("---- new patch ----")
+ print(new_patch)
+
+ new_dict = string_utils.apply_patch_whatthepatch_per_file({filename:new_patch}, {filename:contents})
+ (new_content, err_msg) = new_dict[filename]
+
+ if not err_msg:
+ print("FIXED PATCH!!!")
+ return True, attempted_patches, new_content, pipeline.get_token_usage()
+ else:
+ pipeline.add_user_msg(f"Still failing with error {err_msg}. Try again.")
+ else:
+ break
+ attempts += 1
+
+ return False, attempted_patches, contents, pipeline.get_token_usage()
+
+
+def create_base_pipeline_fewshot():
+ p_few_shot = 'resources/context_files/modular-fewshot/p-fewshot-formatted.txt'
+ p_nuances = 'resources/context_files/p_nuances.txt'
+
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary."
+
+ initial_msg = 'Here are some information relevant to P.'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg, documents = [p_nuances])
+ pipeline.add_documents_inline([p_few_shot], preprocessing_function=lambda _, c: f"\n\n\n{c}\n\n\n")
+ return pipeline
+
+def setup_fix_pipeline():
+ """Create and setup the pipeline for fixing P checker errors."""
+ pipeline = create_base_pipeline_fewshot()
+ # pipeline = create_base_old_pipeline()
+ return pipeline
+
+def setup_project_state(project_root, out_dir):
+ """Setup initial project state."""
+ new_project_root = file_utils.make_copy(project_root, out_dir, new_name="original_project")
+ current_project_state = file_utils.capture_project_state(new_project_root)
+ return new_project_root, current_project_state
+
+def attempt_fix_iteration(pipeline, current_project_state, test_case, trace_dict, trace_log, error_category, attempt_dir):
+ """Run one iteration of the fix attempt."""
+ new_state = attempt_fix_pchecker_errors(
+ pipeline, current_project_state, test_case, trace_dict, trace_log, error_category=error_category, out_dir=attempt_dir
+ )
+
+ return {**current_project_state, **new_state}
+
+
+def get_failing_test_names(result_dict):
+ failing_tests = []
+ for test_name in result_dict:
+ if not result_dict[test_name]:
+ failing_tests.append(test_name)
+
+ return failing_tests
+
+from enum import Enum
+
+class ErrorCategories(Enum):
+ DEADLOCK = 1
+ UNHANDLED_EVENT = 2
+ ENDED_IN_HOT_STATE = 3
+ FAILED_ASSERTION = 4
+ EXCEPTION = 5
+ UNKNOWN = 6
+
+def categorize_error(log):
+ """Categorize an error log based on its content."""
+ log_lower = log.lower()
+ if "deadlock detected" in log_lower:
+ return ErrorCategories.DEADLOCK
+ elif "received event" in log_lower and "cannot be handled" in log_lower:
+ return ErrorCategories.UNHANDLED_EVENT
+ elif "in hot state" in log_lower and "at the end of program" in log_lower:
+ return ErrorCategories.ENDED_IN_HOT_STATE
+ elif "assertion failed" in log_lower:
+ return ErrorCategories.FAILED_ASSERTION
+ elif "exception" in log_lower:
+ return ErrorCategories.EXCEPTION
+ else:
+ return ErrorCategories.UNKNOWN
+
+def identify_error_category(trace_str):
+ error_lines = re.findall(r' (.*?)$', trace_str, re.MULTILINE)
+ return categorize_error(error_lines[0])
+
+
+def compute_progress_percentage(i, total):
+ return f"{i/total*100:.2f}"
+
+# def dummy_test(**kwargs):
+# test_status_changed_callback = kwargs["cb_test_status_changed"]
+# print(kwargs["tests"])
+# test_status_changed_callback({k:True for k in kwargs["tests"]})
+
+
+def test_fix_pchecker_errors(task, filter_tests=[], model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ """Test fixing P checker errors with multiple attempts."""
+ user_quit = False
+ test_name, project_root = task
+ test_name_dir = f"{kwargs['out_dir']}/{test_name}"
+ out_dir = f"{test_name_dir}/fix_attempts"
+ progress_callback = kwargs["cb_progress"]
+ test_status_changed_callback = kwargs["cb_test_status_changed"]
+ cb_update_trace_logs = kwargs["cb_update_trace_logs"]
+ cb_request_user_feedback = kwargs["cb_request_user_feedback"]
+
+ # Setup
+ pipeline = setup_fix_pipeline()
+ prev_project_root = project_root
+ new_project_root, current_project_state = setup_project_state(project_root, test_name_dir)
+ prev_project_state = current_project_state
+
+
+ orig_results, _, orig_trace_logs = checker_utils.try_pchecker(project_root, "/tmp")
+ test_cases_to_fix = sorted([t for t in get_failing_test_names(orig_results) if t in filter_tests])
+ subprocess.run(['cp', '-r', f'{project_root}/../std_streams', f"{test_name_dir}/original_std_streams"])
+ all_tests_fixed = False
+ latest_results = {}
+ latest_trace_logs = {}
+ total_tokens_input = 0
+ total_tokens_output = 0
+
+ for i, test_case in enumerate(test_cases_to_fix):
+ max_attempts = 3
+ grace_points = 1.0
+ attempt = 0
+ prev_error_category = identify_error_category(orig_trace_logs[test_case])
+ error_category = prev_error_category
+
+
+ if all_tests_fixed:
+ break
+
+ while attempt < max_attempts and not user_quit:
+ # print(f"==== ({total_progress}%) TASK SUB-PROGRESS {compute_progress_percentage(i+(attempt/(max_attempts+0.1)), len(test_cases_to_fix))}% ====")
+ print(f"FIXING TEST: {test_case}\nATTEMPT {attempt}")
+ progress_callback(test_case, float(compute_progress_percentage(attempt+1, max_attempts))/100)
+ attempt_dir = f"{out_dir}/{test_case}/attempt_{attempt}"
+
+ current_results, trace_dicts, trace_logs = checker_utils.try_pchecker(new_project_root, f"{attempt_dir}/std_streams")
+ cb_update_trace_logs(trace_logs)
+ latest_results = current_results
+ latest_trace_logs = trace_logs
+ prev_project_root = new_project_root
+
+ if not trace_dicts.keys():
+ all_tests_fixed = True
+ print("ALL TESTS FIXED!")
+ test_status_changed_callback(latest_results)
+ break # No more errors to fix
+
+ if test_case not in trace_dicts:
+ print(f"TEST CASE FIXED: {test_case}")
+ test_status_changed_callback(latest_results)
+ break # Current error has been fixed
+
+
+ # Setup for next fix attempt
+ new_project_root = f"{attempt_dir}/modified"
+ # test_case = list(trace_dicts.keys())[0]
+
+ prev_error_category = error_category
+ error_category = identify_error_category(trace_logs[test_case])
+
+ if error_category != prev_error_category:
+ # Give the model a few more chances if it made some progress
+ awarded = round(grace_points)
+ max_attempts += awarded
+ grace_points -= 0.1
+ print(f"Awarded {awarded} grace point for changed error.")
+ print(f"Remaning grace points {(grace_points-0.5)/0.1}")
+ print(f"New max_attempts = {max_attempts}, attempt = {attempt}")
+ else:
+ # Reset the context
+ pipeline = setup_fix_pipeline()
+
+ try:
+ # Attempt fix
+ prev_project_state = current_project_state
+ current_project_state = attempt_fix_iteration(
+ pipeline, current_project_state, test_case,
+ trace_dicts[test_case], trace_logs[test_case],
+ error_category, attempt_dir
+ )
+ total_tokens_input += pipeline.get_total_input_tokens()
+ total_tokens_output += pipeline.get_total_output_tokens()
+ except Exception as e:
+ print(f"EXCEPTION WHILE FIXING:\n\t{e}")
+ file_utils.write_file(f"{new_project_root}/exception.txt", f"{e}")
+ new_project_root = prev_project_root
+ current_project_state = prev_project_state
+ attempt += 1
+ continue
+
+ # Write updated state
+ file_utils.write_project_state(current_project_state, new_project_root)
+
+ # Try compilation
+ print(f"COMPILING: {new_project_root}")
+ compilable = compile_utils.try_compile(new_project_root, f"{attempt_dir}/std_streams")
+
+
+ if not compilable:
+ # Ideally we should add sanity check here to see if that fixes the issue.
+ print("COMPILATION FAILED...")
+ error_msg = file_utils.read_file(f"{attempt_dir}/std_streams/compile/stdout.txt").splitlines()
+ truncated_msg = "\n".join(error_msg[-10:])
+ print("----- COMPILE ERROR --------")
+ print(truncated_msg)
+ print("----------------------------")
+ print(f"Reverting {new_project_root} -> {prev_project_root}")
+ print(f"Next attempt: {attempt+1}")
+ print("----------------------------")
+ new_project_root = prev_project_root # handle_compilation_failure(new_project_root, prev_project_root)
+ current_project_state = prev_project_state
+ attempt += 1
+ continue
+
+ attempt += 1
+
+ write_fix_diff_log(orig_results, orig_trace_logs, latest_results, latest_trace_logs, f"{test_name_dir}/fix_diff.json")
+
+ with open(f"{test_name_dir}/token_usage_for_fixer.json", "w") as f:
+ json.dump({"input":total_tokens_input, "output":total_tokens_output}, f, indent=4)
+
+ return (pipeline, new_project_root, latest_results)
+
+
+def write_fix_diff_log(orig_results, orig_trace_logs, new_results, new_trace_logs, out_file):
+
+ diff_dict = {}
+ for test_name in orig_results:
+
+
+ if test_name not in new_results:
+ diff_dict[test_name] = {"changed":True, "new":"DOES NOT EXIST!", "original":orig_results[test_name]}
+ continue
+
+ orig_error_category = f"{identify_error_category(orig_trace_logs[test_name]) if test_name in orig_trace_logs else None}"
+ new_error_category = f"{identify_error_category(new_trace_logs[test_name]) if test_name in new_trace_logs else None}"
+
+ old_result = orig_results[test_name]
+ new_result = new_results[test_name]
+
+ changed = old_result != new_result or (old_result == new_result and orig_error_category != new_error_category)
+ diff_dict[test_name] = {
+ "changed": changed,
+ "new": {
+ "result":new_result,
+ "category":new_error_category
+ },
+ "original": {
+ "result": old_result,
+ "category": orig_error_category
+ }
+ }
+
+ with open(out_file, "w") as f:
+ json.dump(diff_dict, f, indent=4)
\ No newline at end of file
diff --git a/Src/PeasyAI/src/core/pipelining/__init__.py b/Src/PeasyAI/src/core/pipelining/__init__.py
new file mode 100644
index 0000000000..8b05ccf144
--- /dev/null
+++ b/Src/PeasyAI/src/core/pipelining/__init__.py
@@ -0,0 +1,22 @@
+"""
+Pipelining Module
+
+Provides conversation management for LLM interactions.
+"""
+
+# Import from new module
+from .pipeline import Pipeline, PromptingPipeline
+
+# Also export from legacy module for backward compatibility
+try:
+ from .prompting_pipeline import PromptingPipeline as LegacyPromptingPipeline
+except ImportError:
+ LegacyPromptingPipeline = PromptingPipeline
+
+__all__ = [
+ "Pipeline",
+ "PromptingPipeline",
+ "LegacyPromptingPipeline",
+]
+
+
diff --git a/Src/PeasyAI/src/core/pipelining/pipeline.py b/Src/PeasyAI/src/core/pipelining/pipeline.py
new file mode 100644
index 0000000000..7a57cd111b
--- /dev/null
+++ b/Src/PeasyAI/src/core/pipelining/pipeline.py
@@ -0,0 +1,361 @@
+"""
+Refactored Prompting Pipeline
+
+This module provides stateful conversation management using the
+new LLM provider abstraction. It maintains backward compatibility
+with existing code while enabling the use of multiple providers.
+"""
+
+import os
+import json
+import logging
+from pathlib import Path
+from typing import List, Dict, Any, Optional, Callable
+
+from ..llm import (
+ LLMProvider,
+ LLMConfig,
+ LLMResponse,
+ Message,
+ MessageRole,
+ Document,
+ get_default_provider,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class Pipeline:
+ """
+ Stateful conversation pipeline for LLM interactions.
+
+ This is a refactored version of PromptingPipeline that uses
+ the new LLM provider abstraction while maintaining the same
+ interface for backward compatibility.
+
+ Features:
+ - Multi-turn conversation management
+ - System prompt handling
+ - Document attachment support
+ - Token usage tracking
+ - Provider-agnostic operation
+ """
+
+ def __init__(self, llm_provider: Optional[LLMProvider] = None):
+ """
+ Initialize the pipeline.
+
+ Args:
+ llm_provider: Optional LLM provider instance.
+ If not provided, auto-detects from environment.
+ """
+ self._provider = llm_provider
+ self._messages: List[Message] = []
+ self._system_prompt: Optional[str] = None
+ self._usage_stats = {
+ 'cumulative': {
+ 'inputTokens': 0,
+ 'outputTokens': 0,
+ 'totalTokens': 0,
+ },
+ 'sequential': []
+ }
+ self._removed_messages: List[List[Message]] = []
+
+ @property
+ def provider(self) -> LLMProvider:
+ """Get the LLM provider (lazy initialization)"""
+ if self._provider is None:
+ self._provider = get_default_provider()
+ return self._provider
+
+ # =========================================================================
+ # Message Management
+ # =========================================================================
+
+ def add_system_prompt(self, prompt: str):
+ """
+ Set the system prompt for the conversation.
+
+ Args:
+ prompt: The system prompt text
+ """
+ self._system_prompt = prompt
+
+ def add_user_msg(self, text: str, document_paths: Optional[List[str]] = None):
+ """
+ Add a user message to the conversation.
+
+ Args:
+ text: The message text
+ document_paths: Optional list of file paths to attach
+ """
+ documents = None
+ if document_paths:
+ documents = []
+ for path in document_paths:
+ try:
+ content = Path(path).read_text(encoding='utf-8')
+ name = Path(path).stem
+ documents.append(Document(name=name, content=content))
+ except Exception as e:
+ logger.warning(f"Could not read document {path}: {e}")
+
+ self._messages.append(Message(
+ role=MessageRole.USER,
+ content=text,
+ documents=documents
+ ))
+
+ def add_assistant_msg(self, text: str):
+ """
+ Add an assistant message to the conversation.
+
+ Args:
+ text: The message text
+ """
+ self._messages.append(Message(
+ role=MessageRole.ASSISTANT,
+ content=text
+ ))
+
+ def add_text(self, text: str):
+ """
+ Add a user message with just text (legacy compatibility).
+
+ Args:
+ text: The message text
+ """
+ self.add_user_msg(text)
+
+ def add_document(self, doc_path: str):
+ """
+ Add a user message with a document attachment.
+
+ Args:
+ doc_path: Path to the document file
+ """
+ try:
+ content = Path(doc_path).read_text(encoding='utf-8')
+ name = Path(doc_path).stem
+
+ self._messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Please refer to the attached document: {name}",
+ documents=[Document(name=name, content=content)]
+ ))
+ except Exception as e:
+ logger.error(f"Could not read document {doc_path}: {e}")
+ raise
+
+ def add_documents_inline(
+ self,
+ doc_paths: List[str],
+ wrapper_func: Optional[Callable[[str, str], str]] = None,
+ ):
+ """
+ Add multiple documents as inline content in a single message.
+
+ Args:
+ doc_paths: List of document file paths
+ wrapper_func: Optional function to wrap each document's content.
+ Receives (filename, content) and returns wrapped content.
+ """
+ if wrapper_func is None:
+ wrapper_func = lambda name, content: f"<{name}>\n{content}\n{name}>"
+
+ parts = []
+ for path in doc_paths:
+ try:
+ content = Path(path).read_text(encoding='utf-8')
+ name = Path(path).name
+ parts.append(wrapper_func(name, content))
+ except Exception as e:
+ logger.warning(f"Could not read document {path}: {e}")
+
+ if parts:
+ self._messages.append(Message(
+ role=MessageRole.USER,
+ content="\n\n".join(parts)
+ ))
+
+ def remove_last_messages(self, n: int = 1):
+ """
+ Remove the last N messages from the conversation.
+
+ Args:
+ n: Number of messages to remove
+ """
+ if n > 0 and len(self._messages) >= n:
+ removed = self._messages[-n:]
+ self._messages = self._messages[:-n]
+ self._removed_messages.append(removed)
+
+ # =========================================================================
+ # LLM Invocation
+ # =========================================================================
+
+ def invoke_llm(
+ self,
+ model: Optional[str] = None,
+ candidates: int = 1,
+ heuristic: str = 'random',
+ inference_config: Optional[Dict[str, Any]] = None,
+ **kwargs
+ ) -> str:
+ """
+ Invoke the LLM and get a response.
+
+ Args:
+ model: Optional model override (uses provider default if not specified)
+ candidates: Number of response candidates to generate
+ heuristic: Selection heuristic if candidates > 1 ('random', 'first')
+ inference_config: Optional dict with 'maxTokens', 'temperature', 'topP'
+ **kwargs: Additional arguments (for backward compatibility)
+
+ Returns:
+ The LLM response text
+ """
+ # Build LLM config
+ config = self._build_config(model, inference_config)
+
+ # Generate candidates
+ responses = []
+ for _ in range(candidates):
+ try:
+ response = self.provider.complete(
+ messages=self._messages,
+ config=config,
+ system_prompt=self._system_prompt
+ )
+
+ # Track usage
+ self._update_usage_stats(response)
+
+ responses.append(response.content)
+
+ except Exception as e:
+ logger.error(f"LLM invocation failed: {e}")
+ raise
+
+ # Select response
+ selected = self._select_response(responses, heuristic)
+
+ # Add to conversation
+ self._messages.append(Message(
+ role=MessageRole.ASSISTANT,
+ content=selected
+ ))
+
+ return selected
+
+ def _build_config(
+ self,
+ model: Optional[str],
+ inference_config: Optional[Dict[str, Any]]
+ ) -> LLMConfig:
+ """Build LLMConfig from parameters"""
+ config = LLMConfig()
+
+ if model:
+ config.model = model
+
+ if inference_config:
+ if 'maxTokens' in inference_config:
+ config.max_tokens = inference_config['maxTokens']
+ if 'temperature' in inference_config:
+ config.temperature = inference_config['temperature']
+ if 'topP' in inference_config:
+ config.top_p = inference_config['topP']
+
+ return config
+
+ def _select_response(self, responses: List[str], heuristic: str) -> str:
+ """Select a response from candidates"""
+ if not responses:
+ raise ValueError("No responses to select from")
+
+ if len(responses) == 1:
+ return responses[0]
+
+ if heuristic == 'random':
+ import random
+ return random.choice(responses)
+
+ # Default to first
+ return responses[0]
+
+ def _update_usage_stats(self, response: LLMResponse):
+ """Update token usage statistics"""
+ usage = response.usage.to_dict()
+
+ # Update cumulative
+ for key in ['inputTokens', 'outputTokens', 'totalTokens']:
+ if key in usage:
+ self._usage_stats['cumulative'][key] += usage[key]
+
+ # Add to sequential
+ self._usage_stats['sequential'].append(usage)
+
+ # =========================================================================
+ # State Access
+ # =========================================================================
+
+ def get_last_response(self) -> Optional[str]:
+ """Get the last assistant response"""
+ for msg in reversed(self._messages):
+ if msg.role == MessageRole.ASSISTANT:
+ return msg.content
+ return None
+
+ def get_conversation(self) -> List[Message]:
+ """Get the full conversation"""
+ return self._messages.copy()
+
+ def get_context(self) -> List[Message]:
+ """Alias for get_conversation (legacy compatibility)"""
+ return self.get_conversation()
+
+ def get_system_prompt(self) -> Optional[str]:
+ """Get the system prompt"""
+ return self._system_prompt
+
+ def get_token_usage(self) -> Dict[str, Any]:
+ """Get token usage statistics"""
+ return self._usage_stats
+
+ def get_total_input_tokens(self) -> int:
+ """Get total input tokens used"""
+ return self._usage_stats['cumulative'].get('inputTokens', 0)
+
+ def get_total_output_tokens(self) -> int:
+ """Get total output tokens used"""
+ return self._usage_stats['cumulative'].get('outputTokens', 0)
+
+ def prune_context(self, pruner_func: Callable[[List[Message]], List[Message]]):
+ """
+ Prune the conversation using a custom function.
+
+ Args:
+ pruner_func: Function that takes message list and returns pruned list
+ """
+ self._messages = pruner_func(self._messages)
+
+ def clear(self):
+ """Clear the conversation"""
+ self._messages = []
+ self._system_prompt = None
+ self._usage_stats = {
+ 'cumulative': {
+ 'inputTokens': 0,
+ 'outputTokens': 0,
+ 'totalTokens': 0,
+ },
+ 'sequential': []
+ }
+
+
+# Backward compatibility alias
+PromptingPipeline = Pipeline
+
+
diff --git a/Src/PeasyAI/src/core/pipelining/prompting_pipeline.py b/Src/PeasyAI/src/core/pipelining/prompting_pipeline.py
new file mode 100644
index 0000000000..64423736cd
--- /dev/null
+++ b/Src/PeasyAI/src/core/pipelining/prompting_pipeline.py
@@ -0,0 +1,491 @@
+import boto3
+import json
+from botocore.exceptions import ClientError
+from botocore.config import Config
+from pathlib import Path
+from utils import file_utils, global_state
+import os
+import utils.constants as constants
+
+
+DEFAULT_TOOL = {
+ "toolSpec": {
+ "name": "NOP",
+ "description": "This tool does nothing.",
+ "inputSchema": {
+ "json": {
+ "type": "object",
+ "properties": {
+ "x": {
+ "type": "number",
+ "description": "The number to pass to the function."
+ }
+ },
+ "required": ["x"]
+ }
+ }
+ }
+ }
+
+# LLM Provider configuration - set via environment variables
+# ANTHROPIC_API_KEY: API key for Anthropic API
+# ANTHROPIC_BASE_URL: Base URL for Anthropic API (optional, uses default if not set)
+# LLM_PROVIDER: "anthropic" (default) or "bedrock"
+
+def get_llm_provider():
+ """Get the LLM provider from environment variable, default to anthropic"""
+ return os.environ.get("LLM_PROVIDER", "anthropic").lower()
+
+class PromptingPipeline:
+
+ def __init__(self):
+ # Initialize with empty context list in the required format
+ self.conversation = []
+ self.system_prompt = None
+ self.usage_stats = {'cumulative': {}, 'sequential': []}
+ self.removed_messages = []
+
+ def add_system_prompt(self, prompt):
+ # Add system prompt as assistant role
+ self.system_prompt = [{"text": prompt}]
+
+ def add_text(self, text):
+ # Add text as user role
+ self.conversation.append({
+ "role": "user",
+ "content": [
+ {
+ "text": text
+ }
+ ]
+ })
+
+ def remove_last_messages(self, n=1):
+ self.removed_messages.append(self.conversation[-n:])
+ self.conversation = self.conversation[:-n]
+
+ def add_document(self, doc_path):
+ # Add document as user role with document content
+ path = Path(doc_path)
+
+ self.conversation.append({
+ "role": "user",
+ "content": [
+ self._create_document_entry(path)
+ ]
+ })
+
+ def _create_document_entry(self, path):
+ content = file_utils.read_file(path.resolve())
+ return {
+ "document": {
+ "name": path.stem,
+ "format": path.suffix.lstrip('.'),
+ "source": {
+ "bytes": content.encode("utf-8")
+ }
+ }
+ }
+
+ def add_documents_inline(self, doc_list, pre="", post="", preprocessing_function=lambda _, c:c):
+ # Process all documents into a single string
+ combined_text = file_utils.combine_files(doc_list, pre, post, preprocessing_function)
+ # Add the combined text as a single text item
+ self.conversation.append({
+ "role": "user",
+ "content": [
+ {
+ "text": combined_text.strip()
+ }
+ ]
+ })
+
+ def _create_bedrock_client(self):
+ """Create and return a configured Bedrock client"""
+ config = Config(read_timeout=1000)
+ return boto3.client(service_name='bedrock-runtime', region_name='us-west-2', config=config)
+
+ def _create_anthropic_client(self):
+ """Create and return an Anthropic client"""
+ import anthropic
+ import httpx
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
+ base_url = os.environ.get("ANTHROPIC_BASE_URL")
+
+ if not api_key:
+ raise ValueError("ANTHROPIC_API_KEY environment variable is required for Anthropic provider")
+
+ client_kwargs = {"api_key": api_key}
+ if base_url:
+ client_kwargs["base_url"] = base_url
+ # For Snowflake Cortex, add Authorization header with Bearer token
+ client_kwargs["default_headers"] = {
+ "Authorization": f"Bearer {api_key}"
+ }
+
+ return anthropic.Anthropic(**client_kwargs)
+
+ def _convert_conversation_to_anthropic_format(self):
+ """Convert Bedrock conversation format to Anthropic format"""
+ messages = []
+ system_text = ""
+
+ # Extract system prompt
+ if self.system_prompt:
+ system_text = self.system_prompt[0].get("text", "")
+
+ # Convert each message
+ for msg in self.conversation:
+ role = msg["role"]
+ content_parts = msg.get("content", [])
+
+ # Extract text content, handle documents by reading their content
+ text_content = []
+ for part in content_parts:
+ if "text" in part:
+ text_content.append(part["text"])
+ elif "document" in part:
+ # Extract document content from bytes
+ doc = part["document"]
+ doc_bytes = doc.get("source", {}).get("bytes", b"")
+ if isinstance(doc_bytes, bytes):
+ doc_text = doc_bytes.decode("utf-8")
+ else:
+ doc_text = str(doc_bytes)
+ doc_name = doc.get("name", "document")
+ text_content.append(f"<{doc_name}>\n{doc_text}\n{doc_name}>")
+
+ combined_content = "\n\n".join(text_content)
+ messages.append({"role": role, "content": combined_content})
+
+ return messages, system_text
+
+ def _get_single_response_anthropic(self, client, model, inference_config):
+ """Get response from Anthropic API or Snowflake Cortex"""
+ import httpx
+
+ # Check if using Snowflake Cortex (OpenAI-compatible endpoint)
+ openai_base_url = os.environ.get("OPENAI_BASE_URL")
+ if openai_base_url and "snowflakecomputing.com" in openai_base_url:
+ return self._get_single_response_snowflake_cortex(inference_config)
+
+ # Otherwise use the standard Anthropic SDK
+ messages, system_text = self._convert_conversation_to_anthropic_format()
+
+ # Map model names for Anthropic API
+ anthropic_model = os.environ.get("ANTHROPIC_MODEL_NAME", "claude-3-5-sonnet")
+
+ # Cap max_tokens to avoid streaming requirement
+ max_tokens = min(inference_config.get("maxTokens", 4096), 8192)
+
+ try:
+ response = client.messages.create(
+ model=anthropic_model,
+ max_tokens=max_tokens,
+ system=system_text,
+ messages=messages,
+ timeout=httpx.Timeout(600.0, connect=60.0)
+ )
+
+ return {
+ "output": {
+ "message": {
+ "role": "assistant",
+ "content": [{"text": response.content[0].text}]
+ }
+ },
+ "stopReason": response.stop_reason,
+ "usage": {
+ "inputTokens": response.usage.input_tokens,
+ "outputTokens": response.usage.output_tokens,
+ "totalTokens": response.usage.input_tokens + response.usage.output_tokens,
+ "cacheReadInputTokens": 0,
+ "cacheWriteInputTokens": 0
+ },
+ "metrics": {"latencyMs": 0}
+ }
+ except Exception as e:
+ print("======= EXCEPTION WHILE CALLING ANTHROPIC API ======== ")
+ print(f"Error: {e}")
+ raise e
+
+ def _get_single_response_snowflake_cortex(self, inference_config):
+ """Get response from Snowflake Cortex using OpenAI SDK (Cortex is OpenAI-compatible)"""
+ from openai import OpenAI
+
+ messages, system_text = self._convert_conversation_to_anthropic_format()
+
+ base_url = os.environ.get("OPENAI_BASE_URL", "").rstrip("/")
+ api_key = os.environ.get("OPENAI_API_KEY")
+ model_name = os.environ.get("OPENAI_MODEL_NAME", "claude-3-5-sonnet")
+
+ max_tokens = min(inference_config.get("maxTokens", 4096), 8192)
+
+ # Prepare messages with system message
+ formatted_messages = []
+ if system_text:
+ formatted_messages.append({"role": "system", "content": system_text})
+ formatted_messages.extend(messages)
+
+ try:
+ print(f"======= SNOWFLAKE CORTEX REQUEST ========")
+ print(f"Base URL: {base_url}")
+ print(f"Model: {model_name}")
+
+ client = OpenAI(
+ api_key=api_key,
+ base_url=base_url,
+ timeout=600.0,
+ )
+
+ response = client.chat.completions.create(
+ model=model_name,
+ messages=formatted_messages,
+ max_completion_tokens=max_tokens,
+ temperature=inference_config.get("temperature", 1.0),
+ top_p=inference_config.get("topP", 0.999),
+ )
+
+ print(f"Response received successfully")
+
+ return {
+ "output": {
+ "message": {
+ "role": "assistant",
+ "content": [{"text": response.choices[0].message.content}]
+ }
+ },
+ "stopReason": response.choices[0].finish_reason,
+ "usage": {
+ "inputTokens": response.usage.prompt_tokens if response.usage else 0,
+ "outputTokens": response.usage.completion_tokens if response.usage else 0,
+ "totalTokens": response.usage.total_tokens if response.usage else 0,
+ "cacheReadInputTokens": 0,
+ "cacheWriteInputTokens": 0
+ },
+ "metrics": {"latencyMs": 0}
+ }
+ except Exception as e:
+ print("======= EXCEPTION WHILE CALLING SNOWFLAKE CORTEX ======== ")
+ print(f"Error: {e}")
+ raise e
+
+ def _get_single_response(self, bedrock_client, model, inference_config, tool_config={"tools": [DEFAULT_TOOL]}):
+
+ try:
+ response = bedrock_client.converse(
+ modelId = model,
+ messages = self.conversation,
+ system = self.system_prompt,
+ inferenceConfig = inference_config,
+ # toolConfig=tool_config
+ )
+ except Exception as e:
+ class BytesEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, bytes):
+ return f"{obj}"
+
+ return super().default(obj)
+ print("======= VALIDATION EXCEPTION WHILE CALLING CONVERSE ======== ")
+ # print("------ CONVERSATION ------")
+ # print(self.conversation)
+ os.makedirs(".err", exist_ok=True)
+ conv_dict = {"conv": self.conversation}
+ with open(".err/conversation.json", 'w') as f:
+ json.dump(conv_dict, f, indent=4, cls=BytesEncoder)
+ print("------ USAGE STATS ------")
+ print(self.usage_stats)
+ with open(".err/usage_stats.json", 'w') as f:
+ json.dump(self.usage_stats, f, indent=4, cls=BytesEncoder)
+ print("="*30)
+ raise e
+
+ return response
+
+ def _select_response(self, responses, heuristic='random'):
+ """Select a response using the specified heuristic"""
+ if heuristic == 'random':
+ import random
+ return random.choice(responses)
+ # TODO: Add other heuristics here as needed
+
+ return responses[0] # Default to first response
+
+ def invoke_llm(
+ self,
+ model=constants.CLAUDE_3_7,
+ candidates=1,
+ heuristic='random',
+ inference_config=None,
+ tool_config = {"tools": [DEFAULT_TOOL]}):
+ if inference_config is None:
+ inference_config = {
+ "maxTokens": global_state.maxTokens,
+ "temperature": global_state.temperature,
+ "topP": global_state.topP
+ }
+
+ provider = get_llm_provider()
+
+ # Check if using Snowflake Cortex (OpenAI-compatible endpoint)
+ openai_base_url = os.environ.get("OPENAI_BASE_URL", "")
+ is_snowflake_cortex = openai_base_url and "snowflakecomputing.com" in openai_base_url
+
+ responses = []
+ for _ in range(candidates):
+ if provider == "bedrock":
+ bedrock_client = self._create_bedrock_client()
+ response_dict = self._get_single_response(bedrock_client, model, inference_config, tool_config=tool_config)
+ elif is_snowflake_cortex:
+ # Use OpenAI SDK for Snowflake Cortex
+ response_dict = self._get_single_response_snowflake_cortex(inference_config)
+ else: # default to anthropic
+ client = self._create_anthropic_client()
+ response_dict = self._get_single_response_anthropic(client, model, inference_config)
+
+ # print(response_dict)
+ if response_dict['stopReason'] == "tool_use":
+ print(response_dict)
+ input("tool called")
+
+ self.update_usage_stats(response_dict)
+ response = response_dict["output"]["message"]["content"][0]["text"]
+ responses.append(response)
+
+ selected_response = self._select_response(responses, heuristic)
+
+ self.conversation.append(self._create_assistant_msg(selected_response))
+ return selected_response
+
+ def _create_assistant_msg(self, msg):
+ return {
+ "role": "assistant",
+ "content": [
+ {
+ "text": msg
+ }
+ ]
+ }
+
+ def get_last_response(self):
+ # Find the last assistant message in context
+ for message in reversed(self.conversation):
+ if message["role"] == "assistant":
+ return message["content"][0]["text"]
+ return None
+
+ def prune_context(self, pruner_function):
+ self.conversation = pruner_function(self.conversation)
+
+ def get_context(self):
+ return self.conversation
+
+ def add_user_msg(self, msg, documents=[]):
+ document_entries = list(map(lambda p: self._create_document_entry(Path(p)), documents))
+ self.conversation.append({
+ "role": "user",
+ "content": [
+ {
+ "text": msg
+ },
+ *document_entries
+ ]
+ })
+
+ def add_assistant_msg(self, msg):
+ self.conversation.append(self._create_assistant_msg(msg))
+
+ def get_conversation(self):
+ return self.conversation
+
+ def get_system_prompt(self):
+ return self.system_prompt
+
+ def update_cumulative_stats(self, usage_dict):
+ for k, v in usage_dict.items():
+ cumu_dict = self.usage_stats['cumulative']
+ if k not in cumu_dict:
+ cumu_dict[k] = 0
+ self.usage_stats['cumulative'][k] += v
+
+ def update_usage_stats(self, response):
+ usage_dict = response['usage']
+ self.update_cumulative_stats(usage_dict)
+ self.usage_stats['sequential'].append(usage_dict)
+
+ def get_token_usage(self):
+ return self.usage_stats
+
+ def get_total_input_tokens(self):
+ return self.usage_stats['cumulative']['inputTokens']
+
+ def get_total_output_tokens(self):
+ return self.usage_stats['cumulative']['outputTokens']
+
+
+# =================== SAMPLE CONVERSATION FROM ORIGINAL IMPLEMENTATION ============================
+# """
+# [
+# {
+# "role": "user",
+# "content": [
+# {
+# "text": "Read the attached P language basics guide for reference. You can refer to this document to understand P syntax and answer accordingly. Additional specific syntax guides will be provided as needed for each task."
+# },
+# {
+# "document": {
+# "name": "P_basics_guide",
+# "format": "txt",
+# "source": {
+# "bytes": "48657265206973207468652062617369632050206c616e67756167652067756964653a0a3c67756964653e0a4120502070726f6772616d20636f6e7369737473206f66206120636f6c6c656374696f6e206f6620666f6c6c6f77696e6720746f702d6c6576656c206465636c61726174696f6e733a0a312e20456e756d730a322e205573657220446566696e65642054797065730a332e204576656e74730a342e205374617465204d616368696e65730a352e2053706563696669636174696f6e204d6f6e69746f72730a362e20476c6f62616c2046756e6374696f6e730a372e204d6f64756c652053797374656d0a0a4865726520697320746865206c697374206f6620616c6c20776f726473207265736572766564206279207468652050206c616e67756167652e20546865736520776f72647320686176652061207370656369616c206d65616e696e6720616e6420707572706f73652c20616e6420746865792063616e6e6f742062652075736564206173206964656e7469666965727320666f72207661726961626c65732c20656e756d732c2074797065732c206576656e74732c206d616368696e65732c2066756e6374696f6e20706172616d65746572732c206574632e3a0a3c72657365727665645f6b6579776f7264733e0a7661722c20747970652c20656e756d2c206576656e742c206f6e2c20646f2c20676f746f2c20646174612c2073656e642c20616e6e6f756e63652c20726563656976652c20636173652c2072616973652c206d616368696e652c2073746174652c20686f742c20636f6c642c2073746172742c20737065632c206d6f64756c652c20746573742c206d61696e2c2066756e2c206f627365727665732c20656e7472792c20657869742c20776974682c20756e696f6e2c20666f72656163682c20656c73652c207768696c652c2072657475726e2c20627265616b2c20636f6e74696e75652c2069676e6f72652c2064656665722c206173736572742c207072696e742c206e65772c2073697a656f662c206b6579732c2076616c7565732c2063686f6f73652c20666f726d61742c2069662c2068616c742c20746869732c2061732c20746f2c20696e2c2064656661756c742c20496e746572666163652c20747275652c2066616c73652c20696e742c20626f6f6c2c20666c6f61742c20737472696e672c207365712c206d61702c207365742c20616e790a3c2f72657365727665645f6b6579776f7264733e0a3c2f67756964653e0a"
+# }
+# }
+# }
+# ]
+# },
+# {
+# "role": "assistant",
+# "content": [
+# {
+# "text": "I understand. I will refer to the P language guides to provide accurate information about P syntax when answering questions."
+# }
+# ]
+# },
+# {
+# "role": "user",
+# "content": [
+# {
+# "text": "hi\n\nReference P Language Guide:\nHere is the basic P language guide:\n\nA P program consists of a collection of following top-level declarations:\n1. Enums\n2. User Defined Types\n3. Events\n4. State Machines\n5. Specification Monitors\n6. Global Functions\n7. Module System\n\nHere is the list of all words reserved by the P language. These words have a special meaning and purpose, and they cannot be used as identifiers for variables, enums, types, events, machines, function parameters, etc.:\n\nvar, type, enum, event, on, do, goto, data, send, announce, receive, case, raise, machine, state, hot, cold, start, spec, module, test, main, fun, observes, entry, exit, with, union, foreach, else, while, return, break, continue, ignore, defer, assert, print, new, sizeof, keys, values, choose, format, if, halt, this, as, to, in, default, Interface, true, false, int, bool, float, string, seq, map, set, any\n\n\n"
+# }
+# ]
+# }
+# ]
+# """
+
+# =================== SAMPLE BEDROCK RESPONSE DICT ============================
+# {
+# 'ResponseMetadata': {
+# 'RequestId': 'c4a81505-b723-4f67-a816-3abbd1f52739',
+# 'HTTPStatusCode': 200,
+# 'HTTPHeaders': {
+# 'date': 'Tue, 24 Jun 2025 21:05:43 GMT', 'content-type': 'application/json', 'content-length': '1975', 'connection': 'keep-alive', 'x-amzn-requestid': 'c4a81505-b723-4f67-a816-3abbd1f52739'
+# },
+# 'RetryAttempts': 0
+# },
+# 'output': {
+# 'message': {
+# 'role': 'assistant',
+# 'content': [{'text': '```\ntype tToggleReq = (source: Switch, switchId: int);\ntype tStatusResp = (switchId: int, isOn: bool);\nevent eToggleReq : tToggleReq;\nevent eStatusResp: tStatusResp;\n\nmachine Switch {\n\n var light: Light;\n var switchId: int;\n var expectedState: bool;\n var statusStr: string;\n\n start state Init {\n entry (lightParam: Light, idParam: int, initStateParam: bool) {\n light = lightParam;\n switchId = idParam;\n expectedState = initStateParam;\n statusStr = "";\n goto ReadyToToggle;\n }\n }\n\n state ReadyToToggle {\n entry {\n send light, eToggleReq, (source = this, switchId = switchId);\n expectedState = !expectedState;\n }\n\n on eStatusResp do (resp: tStatusResp) {\n\n if(resp.isOn) {\n statusStr = "ON";\n } else {\n statusStr = "OFF";\n }\n \n print format("Switch {0}: Light is now {1}", switchId, statusStr);\n\n }\n }\n}\n\nmachine Light {\n var isOn: bool;\n \n start state Off {\n entry {\n isOn = false;\n }\n\n on eToggleReq do (req: tToggleReq) {\n isOn = true;\n send req.source, eStatusResp, (switchId = req.switchId, isOn = isOn);\n goto On;\n }\n }\n\n state On {\n entry {\n isOn = true;\n }\n\n on eToggleReq do (req: tToggleReq) {\n isOn = false;\n send req.source, eStatusResp, (switchId = req.switchId, isOn = isOn);\n goto Off;\n }\n }\n}\n```'}]
+# }
+# },
+# 'stopReason': 'end_turn',
+# 'usage': {
+# 'inputTokens': 1448,
+# 'outputTokens': 487,
+# 'totalTokens': 1935,
+# 'cacheReadInputTokens': 0,
+# 'cacheWriteInputTokens': 0
+# },
+# 'metrics': {'latencyMs': 6610}
+# }
\ No newline at end of file
diff --git a/Src/PeasyAI/src/core/rag/__init__.py b/Src/PeasyAI/src/core/rag/__init__.py
new file mode 100644
index 0000000000..3cb3fdb15d
--- /dev/null
+++ b/Src/PeasyAI/src/core/rag/__init__.py
@@ -0,0 +1,23 @@
+"""
+RAG (Retrieval-Augmented Generation) Module for P Programs.
+
+Provides access to a database of P programs and documentation
+to enhance code generation with real examples.
+"""
+
+from .embeddings import EmbeddingProvider, get_embedding_provider
+from .vector_store import VectorStore, Document, SearchResult
+from .p_corpus import PCorpus, PExample
+from .rag_service import RAGService, get_rag_service
+
+__all__ = [
+ "EmbeddingProvider",
+ "get_embedding_provider",
+ "VectorStore",
+ "Document",
+ "SearchResult",
+ "PCorpus",
+ "PExample",
+ "RAGService",
+ "get_rag_service",
+]
diff --git a/Src/PeasyAI/src/core/rag/embeddings.py b/Src/PeasyAI/src/core/rag/embeddings.py
new file mode 100644
index 0000000000..08bcbf4673
--- /dev/null
+++ b/Src/PeasyAI/src/core/rag/embeddings.py
@@ -0,0 +1,271 @@
+"""
+Embedding Providers for RAG.
+
+Supports multiple embedding backends:
+- OpenAI embeddings (via Snowflake Cortex or direct)
+- Local sentence-transformers (for offline use)
+"""
+
+import os
+import logging
+from abc import ABC, abstractmethod
+from typing import List, Optional
+import hashlib
+import json
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddingProvider(ABC):
+ """Abstract base class for embedding providers."""
+
+ @abstractmethod
+ def embed(self, text: str) -> List[float]:
+ """Generate embedding for a single text."""
+ pass
+
+ @abstractmethod
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
+ """Generate embeddings for multiple texts."""
+ pass
+
+ @property
+ @abstractmethod
+ def dimension(self) -> int:
+ """Return embedding dimension."""
+ pass
+
+
+class OpenAIEmbeddings(EmbeddingProvider):
+ """OpenAI-compatible embeddings (works with Snowflake Cortex)."""
+
+ def __init__(self, model: str = "text-embedding-3-small"):
+ self.model = model
+ self._dimension = 1536 # Default for text-embedding-3-small
+ self._client = None
+
+ def _get_client(self):
+ if self._client is None:
+ try:
+ import httpx
+ except ImportError:
+ logger.warning("httpx not installed, using hash-based fallback embeddings")
+ return None
+
+ api_key = os.environ.get("OPENAI_API_KEY", "")
+ base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
+
+ # Adjust for embeddings endpoint
+ if "cortex" in base_url.lower():
+ # Snowflake Cortex doesn't support embeddings via OpenAI API
+ # Fall back to simple hash-based embeddings
+ logger.warning("Cortex doesn't support embeddings, using local fallback")
+ return None
+
+ self._client = httpx.Client(
+ base_url=base_url,
+ headers={"Authorization": f"Bearer {api_key}"},
+ timeout=30.0
+ )
+ return self._client
+
+ def embed(self, text: str) -> List[float]:
+ client = self._get_client()
+ if client is None:
+ return self._fallback_embed(text)
+
+ try:
+ response = client.post(
+ "/embeddings",
+ json={"input": text, "model": self.model}
+ )
+ response.raise_for_status()
+ return response.json()["data"][0]["embedding"]
+ except Exception as e:
+ logger.warning(f"OpenAI embedding failed: {e}, using fallback")
+ return self._fallback_embed(text)
+
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
+ client = self._get_client()
+ if client is None:
+ return [self._fallback_embed(t) for t in texts]
+
+ try:
+ response = client.post(
+ "/embeddings",
+ json={"input": texts, "model": self.model}
+ )
+ response.raise_for_status()
+ data = response.json()["data"]
+ return [d["embedding"] for d in sorted(data, key=lambda x: x["index"])]
+ except Exception as e:
+ logger.warning(f"OpenAI batch embedding failed: {e}, using fallback")
+ return [self._fallback_embed(t) for t in texts]
+
+ def _fallback_embed(self, text: str) -> List[float]:
+ """Simple hash-based embedding fallback."""
+ # Create a deterministic pseudo-embedding from text hash
+ import hashlib
+ import struct
+
+ # Hash the text
+ h = hashlib.sha256(text.encode()).digest()
+
+ # Extend to full dimension using repeated hashing
+ embedding = []
+ seed = h
+ while len(embedding) < self._dimension:
+ seed = hashlib.sha256(seed).digest()
+ # Unpack as floats, normalize to [-1, 1]
+ for i in range(0, len(seed), 4):
+ if len(embedding) >= self._dimension:
+ break
+ val = struct.unpack('f', seed[i:i+4])[0]
+ # Normalize
+ val = max(-1.0, min(1.0, val / 1e38))
+ embedding.append(val)
+
+ return embedding[:self._dimension]
+
+ @property
+ def dimension(self) -> int:
+ return self._dimension
+
+
+class LocalEmbeddings(EmbeddingProvider):
+ """Local sentence-transformers embeddings."""
+
+ def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
+ self.model_name = model_name
+ self._model = None
+ self._dimension = 384 # Default for MiniLM
+
+ def _get_model(self):
+ if self._model is None:
+ try:
+ from sentence_transformers import SentenceTransformer
+ self._model = SentenceTransformer(self.model_name)
+ self._dimension = self._model.get_sentence_embedding_dimension()
+ except ImportError:
+ logger.warning("sentence-transformers not installed, using hash fallback")
+ return None
+ return self._model
+
+ def embed(self, text: str) -> List[float]:
+ model = self._get_model()
+ if model is None:
+ return self._fallback_embed(text)
+ return model.encode(text).tolist()
+
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
+ model = self._get_model()
+ if model is None:
+ return [self._fallback_embed(t) for t in texts]
+ return model.encode(texts).tolist()
+
+ def _fallback_embed(self, text: str) -> List[float]:
+ """Hash-based fallback."""
+ import hashlib
+ import struct
+
+ h = hashlib.sha256(text.encode()).digest()
+ embedding = []
+ seed = h
+ while len(embedding) < self._dimension:
+ seed = hashlib.sha256(seed).digest()
+ for i in range(0, len(seed), 4):
+ if len(embedding) >= self._dimension:
+ break
+ val = struct.unpack('f', seed[i:i+4])[0]
+ val = max(-1.0, min(1.0, val / 1e38))
+ embedding.append(val)
+ return embedding[:self._dimension]
+
+ @property
+ def dimension(self) -> int:
+ return self._dimension
+
+
+class CachedEmbeddings(EmbeddingProvider):
+ """Wrapper that caches embeddings to disk."""
+
+ def __init__(self, provider: EmbeddingProvider, cache_dir: str = ".embedding_cache"):
+ self.provider = provider
+ self.cache_dir = Path(cache_dir)
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
+
+ def _cache_key(self, text: str) -> str:
+ return hashlib.md5(text.encode()).hexdigest()
+
+ def _cache_path(self, key: str) -> Path:
+ return self.cache_dir / f"{key}.json"
+
+ def embed(self, text: str) -> List[float]:
+ key = self._cache_key(text)
+ cache_path = self._cache_path(key)
+
+ if cache_path.exists():
+ with open(cache_path, 'r') as f:
+ return json.load(f)
+
+ embedding = self.provider.embed(text)
+
+ with open(cache_path, 'w') as f:
+ json.dump(embedding, f)
+
+ return embedding
+
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
+ results = []
+ uncached_texts = []
+ uncached_indices = []
+
+ for i, text in enumerate(texts):
+ key = self._cache_key(text)
+ cache_path = self._cache_path(key)
+
+ if cache_path.exists():
+ with open(cache_path, 'r') as f:
+ results.append(json.load(f))
+ else:
+ results.append(None)
+ uncached_texts.append(text)
+ uncached_indices.append(i)
+
+ if uncached_texts:
+ new_embeddings = self.provider.embed_batch(uncached_texts)
+
+ for idx, embedding in zip(uncached_indices, new_embeddings):
+ results[idx] = embedding
+ key = self._cache_key(texts[idx])
+ cache_path = self._cache_path(key)
+ with open(cache_path, 'w') as f:
+ json.dump(embedding, f)
+
+ return results
+
+ @property
+ def dimension(self) -> int:
+ return self.provider.dimension
+
+
+_default_provider: Optional[EmbeddingProvider] = None
+
+
+def get_embedding_provider() -> EmbeddingProvider:
+ """Get the default embedding provider."""
+ global _default_provider
+
+ if _default_provider is None:
+ # Try to use local embeddings first (faster, no API calls)
+ try:
+ from sentence_transformers import SentenceTransformer
+ _default_provider = CachedEmbeddings(LocalEmbeddings())
+ logger.info("Using local sentence-transformers for embeddings")
+ except ImportError:
+ # Fall back to OpenAI-compatible
+ _default_provider = CachedEmbeddings(OpenAIEmbeddings())
+ logger.info("Using OpenAI-compatible embeddings")
+
+ return _default_provider
diff --git a/Src/PeasyAI/src/core/rag/p_corpus.py b/Src/PeasyAI/src/core/rag/p_corpus.py
new file mode 100644
index 0000000000..48d99ca4f8
--- /dev/null
+++ b/Src/PeasyAI/src/core/rag/p_corpus.py
@@ -0,0 +1,1049 @@
+"""
+P Program Corpus Manager.
+
+Handles indexing and searching of P program examples,
+documentation, and tutorials.
+
+All indexed content is sourced from the bundled resources/ directory
+within the PeasyAI package — no external repo paths required.
+"""
+
+import os
+import re
+import logging
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import List, Dict, Any, Optional, Set
+import hashlib
+
+from .embeddings import EmbeddingProvider, get_embedding_provider
+from .vector_store import VectorStore, Document, SearchResult
+
+logger = logging.getLogger(__name__)
+
+# ── Locate the resources directory bundled with this package ──────────
+_PACKAGE_ROOT = Path(__file__).parent.parent.parent.parent # PeasyAI/
+RESOURCES_DIR = _PACKAGE_ROOT / "resources"
+RAG_EXAMPLES_DIR = RESOURCES_DIR / "rag_examples"
+CONTEXT_FILES_DIR = RESOURCES_DIR / "context_files"
+
+
+@dataclass
+class PExample:
+ """A P code example with metadata."""
+ id: str
+ name: str
+ description: str
+ code: str
+ category: str # "machine", "spec", "test", "types", "documentation", "full_project"
+ tags: List[str] = field(default_factory=list)
+ source_file: Optional[str] = None
+ project_name: Optional[str] = None
+
+ def to_document_content(self) -> str:
+ """Convert to searchable content."""
+ parts = [
+ f"Name: {self.name}",
+ f"Category: {self.category}",
+ f"Description: {self.description}",
+ f"Tags: {', '.join(self.tags)}",
+ "",
+ "Code:",
+ self.code,
+ ]
+ return "\n".join(parts)
+
+
+class PCorpus:
+ """
+ Manager for the P program corpus.
+
+ All content is sourced from the bundled ``resources/`` directory so that
+ PeasyAI works as a standalone MCP installation without needing the full
+ P repository.
+
+ Provides:
+ - Indexing of P programs, examples, and documentation from resources/
+ - Semantic search for similar examples
+ - Category-based filtering
+ """
+
+ def __init__(
+ self,
+ store_path: str = ".p_corpus",
+ embedding_provider: Optional[EmbeddingProvider] = None,
+ ):
+ self.store_path = Path(store_path)
+ self.store_path.mkdir(parents=True, exist_ok=True)
+
+ self.embeddings = embedding_provider or get_embedding_provider()
+ self.vector_store = VectorStore(
+ persist_path=str(self.store_path / "vectors.json")
+ )
+
+ # Track indexed files to avoid re-indexing
+ self._indexed_files: Set[str] = set()
+ self._load_indexed_files()
+
+ # ── Public API ─────────────────────────────────────────────────────
+
+ def index_p_file(
+ self, file_path: str, project_name: Optional[str] = None
+ ) -> int:
+ """Index a single P file. Returns number of examples indexed."""
+ file_path = Path(file_path)
+ if not file_path.exists():
+ logger.warning(f"File not found: {file_path}")
+ return 0
+
+ file_hash = self._file_hash(file_path)
+ if file_hash in self._indexed_files:
+ logger.debug(f"File already indexed: {file_path}")
+ return 0
+
+ try:
+ content = file_path.read_text()
+ except Exception as e:
+ logger.error(f"Failed to read {file_path}: {e}")
+ return 0
+
+ examples = self._extract_examples(content, str(file_path), project_name)
+ for example in examples:
+ self._index_example(example)
+
+ self._indexed_files.add(file_hash)
+ self._save_indexed_files()
+ logger.info(f"Indexed {len(examples)} examples from {file_path}")
+ return len(examples)
+
+ def index_directory(
+ self, dir_path: str, project_name: Optional[str] = None
+ ) -> int:
+ """Index all *.p files in a directory recursively."""
+ dir_path = Path(dir_path)
+ if not dir_path.exists():
+ logger.warning(f"Directory not found: {dir_path}")
+ return 0
+
+ total = 0
+ for p_file in dir_path.rglob("*.p"):
+ if "PGenerated" in str(p_file) or "PCheckerOutput" in str(p_file):
+ continue
+ proj = project_name
+ if proj is None:
+ for parent in p_file.parents:
+ pproj_files = list(parent.glob("*.pproj"))
+ if pproj_files:
+ proj = pproj_files[0].stem
+ break
+ total += self.index_p_file(str(p_file), proj)
+ return total
+
+ def index_bundled_resources(self) -> int:
+ """
+ **Primary indexing entry-point.**
+
+ Index everything shipped in the ``resources/`` directory:
+ 1. rag_examples/ – curated P tutorial & protocol code
+ 2. context_files/ – language guides as documentation entries
+ 3. context_files/p_documentation_reference.txt – patterns & idioms
+
+ This method is self-contained and does NOT require the broader
+ P repository to be present on disk.
+ """
+ total = 0
+
+ # ── 1. Curated P code examples ─────────────────────────────
+ if RAG_EXAMPLES_DIR.exists():
+ for project_dir in sorted(RAG_EXAMPLES_DIR.iterdir()):
+ if project_dir.is_dir():
+ total += self.index_directory(
+ str(project_dir), project_name=project_dir.name
+ )
+
+ # ── 2. Documentation reference file ────────────────────────
+ doc_ref = CONTEXT_FILES_DIR / "p_documentation_reference.txt"
+ if doc_ref.exists():
+ total += self._index_documentation_file(doc_ref)
+
+ # ── 3. Modular language guides as documentation ────────────
+ modular_dir = CONTEXT_FILES_DIR / "modular"
+ if modular_dir.exists():
+ for guide_file in sorted(modular_dir.glob("*.txt")):
+ total += self._index_guide_file(guide_file)
+
+ # ── 4. P nuances / pitfalls ────────────────────────────────
+ nuances = CONTEXT_FILES_DIR / "p_nuances.txt"
+ if nuances.exists():
+ total += self._index_guide_file(nuances)
+
+ logger.info(f"Indexed {total} entries from bundled resources")
+ return total
+
+ # Keep backward compat — callers may still call index_p_repo, but
+ # now it simply delegates to the bundled resources.
+ def index_p_repo(self, repo_path: str) -> int: # noqa: ARG002
+ """
+ Index P examples.
+
+ For standalone installs this indexes the bundled resources/.
+ If additional P files exist at *repo_path* they are indexed too,
+ but the bundled resources are always the primary source.
+ """
+ total = self.index_bundled_resources()
+
+ # Optionally index extra P files if a repo_path is given and valid
+ repo = Path(repo_path)
+ extra_dirs = [
+ repo / "Tutorial",
+ repo / "Tst" / "PortfolioTests",
+ repo / "Tst" / "RegressionTests",
+ ]
+ for d in extra_dirs:
+ if d.exists():
+ total += self.index_directory(str(d))
+
+ return total
+
+ def add_example(self, example: PExample) -> None:
+ """Add a manually created example."""
+ self._index_example(example)
+
+ def search(
+ self,
+ query: str,
+ top_k: int = 5,
+ category: Optional[str] = None,
+ ) -> List[SearchResult]:
+ """Semantic search for similar P examples."""
+ query_embedding = self.embeddings.embed(query)
+ filter_metadata = {}
+ if category:
+ filter_metadata["category"] = category
+ return self.vector_store.search(
+ query_embedding,
+ top_k=top_k,
+ filter_metadata=filter_metadata if filter_metadata else None,
+ )
+
+ def search_faceted(
+ self,
+ query: str,
+ top_k: int = 5,
+ categories: Optional[List[str]] = None,
+ protocols: Optional[List[str]] = None,
+ constructs: Optional[List[str]] = None,
+ patterns: Optional[List[str]] = None,
+ ) -> List[SearchResult]:
+ """
+ Faceted retrieval with lightweight metadata-aware reranking.
+
+ This complements semantic similarity with explicit protocol/construct/pattern
+ matches so RAG can generalize beyond protocol-only retrieval.
+ """
+ # Pull a wider candidate set first, then rerank/filter.
+ base_k = max(top_k * 6, 30)
+ candidates = self.search(query, top_k=base_k)
+
+ protocol_set = {p.lower() for p in (protocols or [])}
+ construct_set = {c.lower() for c in (constructs or [])}
+ pattern_set = {p.lower() for p in (patterns or [])}
+ category_set = {c.lower() for c in (categories or [])}
+
+ reranked: List[tuple[SearchResult, float]] = []
+ for result in candidates:
+ meta = result.document.metadata
+ category = str(meta.get("category", "")).lower()
+ if category_set and category not in category_set:
+ continue
+
+ score = result.score
+ doc_protocols = {
+ str(x).lower() for x in (meta.get("protocol_tags") or [])
+ }
+ doc_constructs = {
+ str(x).lower() for x in (meta.get("construct_tags") or [])
+ }
+ doc_patterns = {
+ str(x).lower() for x in (meta.get("pattern_tags") or [])
+ }
+ doc_intents = {
+ str(x).lower() for x in (meta.get("intent_tags") or [])
+ }
+
+ # Explicit facet matches boost ranking.
+ if protocol_set:
+ hit = len(protocol_set.intersection(doc_protocols))
+ if hit > 0:
+ score += 0.15 + (0.03 * min(hit, 3))
+ elif category not in ("documentation", "manual", "tutorial", "advanced", "getting_started"):
+ score -= 0.08
+
+ if construct_set:
+ hit = len(construct_set.intersection(doc_constructs))
+ if hit > 0:
+ score += 0.10 + (0.02 * min(hit, 4))
+
+ if pattern_set:
+ hit = len(pattern_set.intersection(doc_patterns))
+ if hit > 0:
+ score += 0.10 + (0.02 * min(hit, 4))
+
+ # Prefer executable code over prose-only docs in most cases.
+ has_code = bool(str(meta.get("code") or "").strip())
+ if has_code:
+ score += 0.04
+ elif category in ("documentation", "manual", "tutorial", "advanced", "getting_started"):
+ score -= 0.06
+
+ # Small bonus for test/spec intents when explicitly asked.
+ if construct_set and ("monitor" in construct_set or "hot-state" in construct_set):
+ if "liveness" in doc_intents or "safety" in doc_intents:
+ score += 0.03
+
+ reranked.append((result, score))
+
+ reranked.sort(key=lambda x: x[1], reverse=True)
+ return [r for r, _ in reranked[:top_k]]
+
+ def search_by_tags(self, tags: List[str], limit: int = 10) -> List[Document]:
+ results = []
+ for doc in self.vector_store.list_all(limit=1000):
+ doc_tags = doc.metadata.get("tags", [])
+ if any(tag in doc_tags for tag in tags):
+ results.append(doc)
+ if len(results) >= limit:
+ break
+ return results
+
+ def get_examples_by_category(
+ self, category: str, limit: int = 10
+ ) -> List[Document]:
+ return self.vector_store.search_by_metadata(
+ {"category": category}, limit=limit
+ )
+
+ def get_similar_machines(
+ self, description: str, top_k: int = 3
+ ) -> List[SearchResult]:
+ return self.search(description, top_k=top_k, category="machine")
+
+ def get_similar_specs(
+ self, description: str, top_k: int = 3
+ ) -> List[SearchResult]:
+ return self.search(description, top_k=top_k, category="spec")
+
+ def get_protocol_examples(
+ self, protocol_name: str, top_k: int = 5
+ ) -> List[SearchResult]:
+ query = f"protocol {protocol_name} distributed system P language"
+ return self.search(query, top_k=top_k)
+
+ def count(self) -> int:
+ return self.vector_store.count()
+
+ def count_by_category(self) -> Dict[str, int]:
+ counts: Dict[str, int] = {}
+ for doc in self.vector_store.list_all(limit=10000):
+ cat = doc.metadata.get("category", "unknown")
+ counts[cat] = counts.get(cat, 0) + 1
+ return counts
+
+ def has_facet_schema(self, sample_size: int = 200) -> bool:
+ """
+ Check whether indexed documents contain required facet metadata fields.
+
+ Returns True only when sampled non-documentation entries include all
+ expected facet keys.
+ """
+ docs = self.vector_store.list_all(limit=sample_size)
+ if not docs:
+ return False
+
+ required = {"protocol_tags", "construct_tags", "pattern_tags", "intent_tags"}
+ checked = 0
+ for doc in docs:
+ meta = doc.metadata or {}
+ category = str(meta.get("category", "")).lower()
+ if category in ("documentation", "manual", "tutorial", "advanced", "getting_started"):
+ continue
+ checked += 1
+ if not required.issubset(set(meta.keys())):
+ return False
+ return checked > 0
+
+ def rebuild_bundled_index(self) -> int:
+ """
+ Clear persisted corpus and rebuild exclusively from bundled resources.
+ """
+ self.vector_store.clear()
+ self._indexed_files.clear()
+ self._save_indexed_files()
+ return self.index_bundled_resources()
+
+ # ── Guide / documentation indexing ─────────────────────────────────
+
+ def _index_guide_file(self, guide_path: Path) -> int:
+ """Index a modular language guide file as a documentation entry."""
+ file_hash = self._file_hash(guide_path)
+ if file_hash in self._indexed_files:
+ return 0
+
+ try:
+ content = guide_path.read_text()
+ except Exception as e:
+ logger.error(f"Failed to read guide {guide_path}: {e}")
+ return 0
+
+ name = guide_path.stem
+ example = PExample(
+ id=f"guide_{name}_{file_hash[:8]}",
+ name=name,
+ description=f"P language guide: {name}",
+ code=content[:4000] if len(content) > 4000 else content,
+ category="documentation",
+ tags=["documentation", "guide", name.replace("p_", "").replace("_guide", "")],
+ source_file=str(guide_path),
+ project_name="PeasyAI_Guides",
+ )
+ self._index_example(example)
+ self._indexed_files.add(file_hash)
+ self._save_indexed_files()
+ return 1
+
+ def _index_documentation_file(self, doc_path: Path) -> int:
+ """Index the bundled documentation reference by section."""
+ file_hash = self._file_hash(doc_path)
+ if file_hash in self._indexed_files:
+ return 0
+
+ try:
+ content = doc_path.read_text()
+ except Exception as e:
+ logger.error(f"Failed to read doc {doc_path}: {e}")
+ return 0
+
+ count = 0
+ # Split on ... sections
+ section_pattern = r'<(\w+)>(.*?)\1>'
+ for match in re.finditer(section_pattern, content, re.DOTALL):
+ tag_name = match.group(1)
+ section_text = match.group(2).strip()
+ if not section_text or len(section_text) < 50:
+ continue
+
+ tags = ["documentation", "pattern", "reference"]
+ # Infer additional tags from section tag name
+ if "pattern" in tag_name or "pitfall" in tag_name:
+ tags.append("best-practice")
+ if any(kw in tag_name for kw in ["paxos", "raft", "commit", "leader", "timer"]):
+ tags.append("protocol")
+ tags.append(tag_name.replace("_", "-"))
+
+ example = PExample(
+ id=f"docref_{tag_name}_{hashlib.md5(section_text.encode()).hexdigest()[:8]}",
+ name=tag_name.replace("_", " ").title(),
+ description=f"Documentation reference: {tag_name}",
+ code=section_text,
+ category="documentation",
+ tags=tags,
+ source_file=str(doc_path),
+ project_name="PeasyAI_Docs",
+ )
+ self._index_example(example)
+ count += 1
+
+ self._indexed_files.add(file_hash)
+ self._save_indexed_files()
+ return count
+
+ # ── Extraction Methods ─────────────────────────────────────────────
+
+ def _extract_examples(
+ self,
+ content: str,
+ source_file: str,
+ project_name: Optional[str],
+ ) -> List[PExample]:
+ """Extract examples from P file content."""
+ examples: List[PExample] = []
+
+ # ── Machines (brace-balanced) ─────────────────────────────
+ for machine_name, machine_code in self._extract_top_level_blocks(
+ content, "machine"
+ ):
+ desc = self._extract_description(content, content.find(machine_code))
+ tags = self._infer_tags(machine_code)
+ examples.append(
+ PExample(
+ id=f"machine_{machine_name}_{hashlib.md5(machine_code.encode()).hexdigest()[:8]}",
+ name=machine_name,
+ description=desc or f"Machine {machine_name}",
+ code=machine_code,
+ category="machine",
+ tags=tags,
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Specs (brace-balanced) ────────────────────────────────
+ for spec_name, spec_code in self._extract_top_level_blocks(
+ content, "spec"
+ ):
+ desc = self._extract_description(content, content.find(spec_code))
+ tags = ["safety", "specification", "monitor"]
+ if "hot state" in spec_code or "hot " in spec_code:
+ tags.append("liveness")
+ if "cold state" in spec_code or "cold " in spec_code:
+ tags.append("cold-state")
+ tags.extend(self._infer_tags(spec_code))
+ examples.append(
+ PExample(
+ id=f"spec_{spec_name}_{hashlib.md5(spec_code.encode()).hexdigest()[:8]}",
+ name=spec_name,
+ description=desc or f"Safety specification {spec_name}",
+ code=spec_code,
+ category="spec",
+ tags=list(set(tags)),
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Test declarations ─────────────────────────────────────
+ test_pattern = r'(test\s+(?:param\s+\(.*?\)\s+(?:assume\s+\(.*?\)\s+)?(?:\d+\s+wise\s+)?)?\s*\w+\s*\[.*?\].*?;)'
+ for match in re.finditer(test_pattern, content, re.DOTALL):
+ test_code = match.group(0).strip()
+ test_name_match = re.search(
+ r'test\s+(?:param\s+\(.*?\)\s+(?:assume\s+\(.*?\)\s+)?(?:\d+\s+wise\s+)?)?(\w+)',
+ test_code,
+ re.DOTALL,
+ )
+ test_name = test_name_match.group(1) if test_name_match else "Unknown"
+ examples.append(
+ PExample(
+ id=f"test_{test_name}_{hashlib.md5(test_code.encode()).hexdigest()[:8]}",
+ name=test_name,
+ description=f"Test case {test_name}",
+ code=test_code,
+ category="test",
+ tags=["test", "verification"],
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Module definitions ────────────────────────────────────
+ module_pattern = r'(module\s+\w+\s*=\s*[^;]+;)'
+ for match in re.finditer(module_pattern, content, re.DOTALL):
+ mod_code = match.group(1).strip()
+ mod_name_match = re.search(r'module\s+(\w+)', mod_code)
+ mod_name = mod_name_match.group(1) if mod_name_match else "Unknown"
+ examples.append(
+ PExample(
+ id=f"module_{mod_name}_{hashlib.md5(mod_code.encode()).hexdigest()[:8]}",
+ name=mod_name,
+ description=f"Module definition {mod_name}",
+ code=mod_code,
+ category="types",
+ tags=["module", "module-system", "composition"],
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Global functions ──────────────────────────────────────
+ for func_name, func_code in self._extract_global_functions(content):
+ desc = self._extract_description(content, content.find(func_code))
+ tags = self._infer_tags(func_code)
+ tags.append("global-function")
+ if "receive" in func_code:
+ tags.append("blocking-receive")
+ examples.append(
+ PExample(
+ id=f"globalfun_{func_name}_{hashlib.md5(func_code.encode()).hexdigest()[:8]}",
+ name=func_name,
+ description=desc or f"Global function {func_name}",
+ code=func_code,
+ category="machine",
+ tags=tags,
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Types / events / enums ────────────────────────────────
+ if "event " in content or "type " in content:
+ file_name = Path(source_file).stem
+ types_events = self._extract_types_events(content)
+ if types_events.strip():
+ examples.append(
+ PExample(
+ id=f"types_{file_name}_{hashlib.md5(types_events.encode()).hexdigest()[:8]}",
+ name=f"{file_name} Types and Events",
+ description=f"Type and event definitions from {file_name}",
+ code=types_events,
+ category="types",
+ tags=["types", "events", "definitions"],
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Full test driver files ────────────────────────────────
+ is_test_file = (
+ "PTst/" in source_file
+ or "/PTst/" in source_file
+ or "TestDriver" in Path(source_file).stem
+ or "Test" in Path(source_file).stem
+ )
+ if is_test_file and "machine " in content:
+ file_name = Path(source_file).stem
+ tags = self._infer_tags(content)
+ tags.extend(["test-driver-full", "machine-wiring"])
+ desc = self._extract_file_description(content)
+ examples.append(
+ PExample(
+ id=f"testdriver_{file_name}_{hashlib.md5(content.encode()).hexdigest()[:8]}",
+ name=f"{file_name} (Full Test Driver)",
+ description=desc
+ or f"Complete test driver with machine initialization and wiring from {file_name}",
+ code=content,
+ category="test",
+ tags=tags,
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Best-practice annotated files ─────────────────────────
+ if "BEST PRACTICE" in content:
+ file_name = Path(source_file).stem
+ tags = self._infer_tags(content)
+ tags.append("best-practice-guide")
+ examples.append(
+ PExample(
+ id=f"bestpractice_{file_name}_{hashlib.md5(content.encode()).hexdigest()[:8]}",
+ name=f"{file_name} (Best Practices)",
+ description=f"Annotated best practices from {file_name}",
+ code=content,
+ category="machine",
+ tags=tags,
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ # ── Full multi-machine files ──────────────────────────────
+ machine_count = len(list(re.finditer(r'\bmachine\s+\w+', content)))
+ if machine_count >= 2 and not is_test_file:
+ file_name = Path(source_file).stem
+ tags = self._infer_tags(content)
+ tags.extend(["multi-machine", "full-file"])
+ desc = self._extract_file_description(content)
+ examples.append(
+ PExample(
+ id=f"fullfile_{file_name}_{hashlib.md5(content.encode()).hexdigest()[:8]}",
+ name=f"{file_name} (Complete File)",
+ description=desc
+ or f"Complete P file with multiple machines from {file_name}",
+ code=content,
+ category="full_project",
+ tags=tags,
+ source_file=source_file,
+ project_name=project_name,
+ )
+ )
+
+ return examples
+
+ # ── Brace-balanced block extraction ────────────────────────────────
+
+ def _extract_top_level_blocks(
+ self, content: str, keyword: str
+ ) -> List[tuple]:
+ """Extract top-level machine/spec blocks using balanced brace matching."""
+ results = []
+ if keyword == "spec":
+ pattern = re.compile(r"\bspec\s+(\w+)\s+observes\b")
+ else:
+ pattern = re.compile(rf"\b{keyword}\s+(\w+)\s*\{{")
+
+ for match in pattern.finditer(content):
+ name = match.group(1)
+ brace_start = content.find("{", match.start())
+ if brace_start == -1:
+ continue
+ depth = 0
+ end = brace_start
+ for i in range(brace_start, len(content)):
+ if content[i] == "{":
+ depth += 1
+ elif content[i] == "}":
+ depth -= 1
+ if depth == 0:
+ end = i + 1
+ break
+ if depth != 0:
+ continue
+ results.append((name, content[match.start() : end]))
+ return results
+
+ def _extract_global_functions(self, content: str) -> List[tuple]:
+ """Extract global functions (outside any machine/spec block)."""
+ results = []
+ occupied = []
+ for kw in ("machine", "spec"):
+ for _, block in self._extract_top_level_blocks(content, kw):
+ start = content.find(block)
+ if start >= 0:
+ occupied.append((start, start + len(block)))
+
+ def _inside(pos: int) -> bool:
+ return any(s <= pos < e for s, e in occupied)
+
+ for match in re.finditer(r"\bfun\s+(\w+)\s*\(", content):
+ if _inside(match.start()):
+ continue
+ brace_start = content.find("{", match.start())
+ if brace_start == -1:
+ continue
+ depth = 0
+ end = brace_start
+ for i in range(brace_start, len(content)):
+ if content[i] == "{":
+ depth += 1
+ elif content[i] == "}":
+ depth -= 1
+ if depth == 0:
+ end = i + 1
+ break
+ if depth != 0:
+ continue
+ results.append((match.group(1), content[match.start() : end]))
+ return results
+
+ # ── Description extraction ─────────────────────────────────────────
+
+ def _extract_description(
+ self, content: str, position: int
+ ) -> Optional[str]:
+ if position < 0:
+ return None
+ before = content[:position]
+ lines = before.split("\n")[-10:]
+ comments: list[str] = []
+ in_block = False
+ for line in reversed(lines):
+ stripped = line.strip()
+ if stripped.endswith("*/"):
+ in_block = True
+ text = stripped.rstrip("*/").strip()
+ if text and not text.startswith("/*"):
+ comments.insert(0, text)
+ elif text.startswith("/*"):
+ text = text[2:].strip()
+ if text:
+ comments.insert(0, text)
+ in_block = False
+ continue
+ if in_block:
+ text = stripped.lstrip("* ").strip()
+ if stripped.startswith("/*"):
+ text = stripped[2:].lstrip("* ").strip()
+ in_block = False
+ if text and not all(c in "=-*/" for c in text):
+ comments.insert(0, text)
+ continue
+ if stripped.startswith("//"):
+ text = stripped[2:].strip()
+ if text and not all(c in "=-*/" for c in text):
+ comments.insert(0, text)
+ elif stripped.startswith("/*"):
+ text = stripped[2:].rstrip("*/").strip()
+ if text:
+ comments.insert(0, text)
+ elif stripped:
+ break
+ return " ".join(comments) if comments else None
+
+ def _extract_file_description(self, content: str) -> Optional[str]:
+ lines = content.split("\n")
+ comments: list[str] = []
+ in_block = False
+ for line in lines[:30]:
+ stripped = line.strip()
+ if in_block:
+ if "*/" in stripped:
+ text = stripped.split("*/")[0].lstrip("* ").strip()
+ if text and not all(c in "=-*/" for c in text):
+ comments.append(text)
+ in_block = False
+ else:
+ text = stripped.lstrip("* ").strip()
+ if text and not all(c in "=-*/" for c in text):
+ comments.append(text)
+ continue
+ if stripped.startswith("/*"):
+ in_block = True
+ text = stripped[2:].rstrip("*/").strip()
+ if text:
+ comments.append(text)
+ if "*/" in stripped:
+ in_block = False
+ elif stripped.startswith("//"):
+ text = stripped[2:].strip()
+ if text and not all(c in "=-*/" for c in text):
+ comments.append(text)
+ elif stripped and not stripped.startswith("*"):
+ break
+ return " ".join(comments) if comments else None
+
+ # ── Tag inference ──────────────────────────────────────────────────
+
+ def _infer_tags(self, code: str) -> List[str]:
+ tags: list[str] = []
+ if "send " in code:
+ tags.append("message-passing")
+ if "goto " in code:
+ tags.append("state-machine")
+ if "defer " in code:
+ tags.append("deferred-events")
+ if "raise " in code:
+ tags.append("internal-events")
+ if "new " in code:
+ tags.append("machine-creation")
+ if "foreach" in code:
+ tags.append("iteration")
+ if "map[" in code or "seq[" in code:
+ tags.append("collections")
+ if "$" in code or "choose(" in code:
+ tags.append("nondeterminism")
+ if "assert " in code:
+ tags.append("assertions")
+ if "receive" in code and "case " in code:
+ tags.append("blocking-receive")
+ if "announce " in code:
+ tags.append("announce-event")
+ if "hot state" in code or "hot " in code:
+ tags.append("liveness")
+ if "cold state" in code or "cold " in code:
+ tags.append("cold-state")
+ if "ignore " in code:
+ tags.append("ignore-pattern")
+ if re.search(r"while\s*\(.*sizeof.*\)\s*\{.*send\b", code, re.DOTALL):
+ tags.append("broadcast-pattern")
+ if re.search(r"eSetup\w+|eConfig\w+|eInform\w+|eInit\w+", code):
+ tags.append("setup-event")
+ if re.search(r"BEST\s+PRACTICE", code):
+ tags.append("best-practice")
+ if re.search(r"ANTI.?PATTERN", code):
+ tags.append("anti-pattern")
+ if re.search(r"machine\s+\w*(?:Test|Scenario|Driver)\w*", code):
+ tags.append("test-driver")
+ if "fun SetUp" in code or "fun Setup" in code:
+ tags.append("test-setup")
+ if "majority" in code.lower() or "quorum" in code.lower():
+ tags.append("quorum-pattern")
+ if re.search(r"seq\[machine\]|set\[machine\]", code):
+ tags.append("machine-collection")
+
+ # Protocol-specific
+ lower = code.lower()
+ if "paxos" in lower or "proposer" in lower or "acceptor" in lower:
+ tags.append("paxos")
+ if "commit" in lower and ("coordinator" in lower or "participant" in lower):
+ tags.append("two-phase-commit")
+ if "raft" in lower or ("leader" in lower and "follower" in lower):
+ tags.append("raft")
+ if "lock" in lower and ("acquire" in lower or "release" in lower):
+ tags.append("distributed-lock")
+ if "timer" in lower or "timeout" in lower:
+ tags.append("timer-pattern")
+ if "failure" in lower and ("detector" in lower or "inject" in lower):
+ tags.append("failure-detection")
+ return tags
+
+ def _infer_facets(
+ self,
+ code: str,
+ category: str,
+ tags: List[str],
+ description: Optional[str] = None,
+ project_name: Optional[str] = None,
+ ) -> Dict[str, List[str]]:
+ """
+ Infer structured facet metadata from code/tags/description.
+
+ Facets are used by multi-lane retrieval:
+ - protocol_tags
+ - construct_tags
+ - pattern_tags
+ - intent_tags
+ """
+ text = f"{code}\n{description or ''}\n{project_name or ''}".lower()
+ tagset = {t.lower() for t in tags}
+
+ protocol_tags: List[str] = []
+ construct_tags: List[str] = []
+ pattern_tags: List[str] = []
+ intent_tags: List[str] = []
+
+ # Protocol facets
+ if "paxos" in text or {"proposer", "acceptor", "learner"}.intersection(tagset):
+ protocol_tags.append("paxos")
+ if "raft" in text or ("leader" in text and "follower" in text):
+ protocol_tags.append("raft")
+ if "two-phase-commit" in tagset or ("coordinator" in text and "participant" in text and "commit" in text):
+ protocol_tags.append("two-phase-commit")
+ if "distributed-lock" in tagset or ("lock" in text and ("acquire" in text or "release" in text)):
+ protocol_tags.append("distributed-lock")
+ if "failure-detection" in tagset or ("failure" in text and "detector" in text):
+ protocol_tags.append("failure-detector")
+
+ # Construct facets (language-level)
+ construct_map = {
+ "send": "send",
+ "receive": "receive",
+ "goto": "goto",
+ "defer": "defer",
+ "ignore": "ignore",
+ "announce": "announce",
+ "new ": "new-machine",
+ "hot state": "hot-state",
+ "cold state": "cold-state",
+ "spec ": "spec-machine",
+ "module ": "module-system",
+ "test ": "test-case",
+ "event ": "events",
+ "type ": "types",
+ "enum ": "enums",
+ "foreach": "foreach",
+ "while ": "while-loop",
+ "assert ": "assertions",
+ "choose(": "nondeterminism",
+ "$": "nondeterminism",
+ }
+ for needle, facet in construct_map.items():
+ if needle in text:
+ construct_tags.append(facet)
+
+ # Pattern facets
+ if "broadcast-pattern" in tagset or ("while" in text and "send" in text):
+ pattern_tags.append("broadcast")
+ if "quorum-pattern" in tagset or "majority" in text or "quorum" in text:
+ pattern_tags.append("quorum")
+ if "test-driver" in tagset or "test-driver-full" in tagset:
+ pattern_tags.append("test-driver")
+ if "setup-event" in tagset:
+ pattern_tags.append("setup-event")
+ if "blocking-receive" in tagset:
+ pattern_tags.append("request-response")
+ if "timer-pattern" in tagset:
+ pattern_tags.append("timer-timeout")
+ if "machine-creation" in tagset:
+ pattern_tags.append("machine-wiring")
+
+ # Intent facets
+ if category == "spec" or "monitor" in tagset:
+ intent_tags.append("safety")
+ if "liveness" in tagset or "hot-state" in construct_tags:
+ intent_tags.append("liveness")
+ if category == "test":
+ intent_tags.append("testing")
+ if category == "machine":
+ intent_tags.append("implementation")
+ if category == "types":
+ intent_tags.append("declarations")
+ if category == "documentation":
+ intent_tags.append("reference")
+
+ # Deduplicate while preserving order.
+ def _unique(seq: List[str]) -> List[str]:
+ seen: Set[str] = set()
+ out: List[str] = []
+ for item in seq:
+ if item not in seen:
+ seen.add(item)
+ out.append(item)
+ return out
+
+ return {
+ "protocol_tags": _unique(protocol_tags),
+ "construct_tags": _unique(construct_tags),
+ "pattern_tags": _unique(pattern_tags),
+ "intent_tags": _unique(intent_tags),
+ }
+
+ # ── Types / events extraction ──────────────────────────────────────
+
+ def _extract_types_events(self, content: str) -> str:
+ lines: list[str] = []
+ in_declaration = False
+ for line in content.split("\n"):
+ stripped = line.strip()
+ if in_declaration:
+ lines.append(line)
+ if ";" in stripped or "}" in stripped:
+ in_declaration = False
+ continue
+ if stripped.startswith(("type ", "event ", "enum ")):
+ lines.append(line)
+ if ";" not in stripped and "{" not in stripped:
+ in_declaration = True
+ elif "{" in stripped and "}" not in stripped:
+ in_declaration = True
+ elif stripped.startswith("//"):
+ lines.append(line)
+ return "\n".join(lines)
+
+ # ── Internal persistence helpers ───────────────────────────────────
+
+ def _index_example(self, example: PExample) -> None:
+ content = example.to_document_content()
+ embedding = self.embeddings.embed(content)
+ facets = self._infer_facets(
+ code=example.code,
+ category=example.category,
+ tags=example.tags,
+ description=example.description,
+ project_name=example.project_name,
+ )
+ doc = Document(
+ id=example.id,
+ content=content,
+ embedding=embedding,
+ metadata={
+ "name": example.name,
+ "category": example.category,
+ "tags": example.tags,
+ "source_file": example.source_file,
+ "project_name": example.project_name,
+ "code": example.code,
+ "description": example.description,
+ "protocol_tags": facets["protocol_tags"],
+ "construct_tags": facets["construct_tags"],
+ "pattern_tags": facets["pattern_tags"],
+ "intent_tags": facets["intent_tags"],
+ },
+ )
+ self.vector_store.add(doc)
+
+ def _file_hash(self, file_path: Path) -> str:
+ return hashlib.md5(file_path.read_bytes()).hexdigest()
+
+ def _load_indexed_files(self) -> None:
+ index_file = self.store_path / "indexed_files.json"
+ if index_file.exists():
+ import json
+
+ with open(index_file, "r") as f:
+ self._indexed_files = set(json.load(f))
+
+ def _save_indexed_files(self) -> None:
+ import json
+
+ index_file = self.store_path / "indexed_files.json"
+ with open(index_file, "w") as f:
+ json.dump(list(self._indexed_files), f)
diff --git a/Src/PeasyAI/src/core/rag/rag_service.py b/Src/PeasyAI/src/core/rag/rag_service.py
new file mode 100644
index 0000000000..0feaaafc1a
--- /dev/null
+++ b/Src/PeasyAI/src/core/rag/rag_service.py
@@ -0,0 +1,962 @@
+"""
+RAG Service for P Code Generation.
+
+Provides context-enriched prompts using similar examples
+from the P program corpus.
+"""
+
+import os
+import logging
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import List, Dict, Any, Optional
+
+from .p_corpus import PCorpus, PExample
+from .vector_store import SearchResult
+
+logger = logging.getLogger(__name__)
+
+
+# ── Comprehensive syntax hints per category ───────────────────────────
+# These are drawn from the official P docs and common generation pitfalls.
+
+MACHINE_SYNTAX_HINTS = [
+ "Variables must be declared at the very start of functions, before any executable statements.",
+ "Variable declaration and assignment must be separate: `var x: int;` then `x = 5;` — NOT `var x: int = 5;`",
+ "Single-field named tuples require a trailing comma: `(field = value,)`",
+ "Use `this` to reference the current machine (NOT `self`).",
+ "State machines must have exactly one `start state`.",
+ "Entry functions for non-start states take at most 1 parameter (the goto/transition payload).",
+ "Exit functions cannot have parameters.",
+ "Do NOT access functions contained inside other machines.",
+ "Send syntax: `send target, eventName, payload;` — target must be a machine-typed variable.",
+ "Collections are empty by default on declaration — do NOT redundantly re-initialize them.",
+ "Sequence insert: `seq += (index, value)` — NOT `seq += (value)`.",
+ "Set insert: `set += (element)` — parentheses required around element.",
+ "The `foreach` iteration variable must be declared at the top of the function body.",
+ "Switch/case is NOT supported — use if/else chains.",
+ "`const` keyword is NOT supported in P.",
+ "Compound assignment operators `+=`, `-=` are only for collections, NOT for `int`/`float`.",
+ "The `!` operator with `in` requires parentheses: `!(x in collection)` — NOT `!x in collection`.",
+ "`!in` and `not in` are NOT supported.",
+ "Formatted strings: `format(\"text {0} {1}\", arg0, arg1)`",
+]
+
+SPEC_SYNTAX_HINTS = [
+ "Spec machines observe events, they do NOT send/receive/create machines.",
+ "Syntax: `spec Name observes event1, event2 { ... }`",
+ "Entry functions in spec machines CANNOT take parameters.",
+ "`$`, `$$`, `this`, `new`, `send`, `announce`, `receive`, and `pop` are NOT allowed in monitors.",
+ "Use `hot state` for liveness properties — the system must eventually leave hot states.",
+ "Use `cold state` for states where the system may remain indefinitely.",
+ "Specs are synchronously composed: events are delivered to monitors before the target machine.",
+ "Safety specs observe events and assert invariants using local state tracking.",
+ "Liveness specs mark intermediate states as `hot` and check eventual convergence.",
+ "Never generate empty functions or functions with only comments in spec machines.",
+ "Spec observes list must include ALL events the spec handles (on/do/goto handlers).",
+]
+
+TEST_SYNTAX_HINTS = [
+ "Test syntax: `test TestName [main=MainMachine]: assert Spec1, Spec2 in (union Module1, Module2, { TestMachine });`",
+ "The main machine must be included in the module expression (typically via `{ TestMachine }`).",
+ "Test driver machines set up the system by creating machines and sending configuration events.",
+ "Use `announce` to initialize spec monitors before machines start communicating.",
+ "Use `choose(N)` for nondeterministic choices the P checker will explore.",
+ "Use `$` for nondeterministic boolean choices.",
+ "NEVER declare events in both test driver files and source files — this causes duplicate declaration errors.",
+ "Test case and module declarations go in a separate TestScript.p file from the TestDriver.p machine code.",
+ "Parameterized tests: `test param (nClients in [2, 3, 4]) tcTest [main=TestDriver]: ...;`",
+]
+
+TYPES_SYNTAX_HINTS = [
+ "Named tuple types: `type tRequest = (client: machine, value: int);`",
+ "Events must use a separately declared type for payloads — NOT inline named tuples.",
+ "Correct: `type tPayload = (x: int);` then `event eMsg: tPayload;`",
+ "WRONG: `event eMsg: (x: int);` — inline payload types are not allowed.",
+ "Events without payloads: `event ePing;`",
+ "Enums: `enum Status { SUCCESS, FAILURE, TIMEOUT }`",
+ "Enums with values: `enum Code { OK = 200, ERROR = 500 }`",
+ "Enum values are global constants and must have unique names across the project.",
+ "`type` names, `event` names, and `enum` names must all be unique and not clash with reserved keywords.",
+ "P supports: `int`, `bool`, `float`, `string`, `machine`, `event`, `any`, `data`.",
+ "Collections: `seq[T]`, `set[T]`, `map[K, V]`.",
+ "Default values: `default(type)` — e.g., `default(int)` is `0`, `default(seq[int])` is empty sequence.",
+]
+
+
+@dataclass
+class RAGContext:
+ """Context retrieved from the corpus."""
+ examples: List[Dict[str, Any]] = field(default_factory=list)
+ documentation: List[str] = field(default_factory=list)
+ syntax_hints: List[str] = field(default_factory=list)
+
+ def to_prompt_section(self) -> str:
+ """Convert to a prompt section."""
+ sections = []
+
+ if self.examples:
+ sections.append("## Relevant P Code Examples")
+ for i, ex in enumerate(self.examples, 1):
+ sections.append(f"\n### Example {i}: {ex.get('name', 'Unknown')}")
+ if ex.get('description'):
+ sections.append(f"Description: {ex['description']}")
+ code = ex.get('code', '')
+ if code:
+ sections.append(f"```p\n{code}\n```")
+
+ if self.documentation:
+ sections.append("\n## P Language Documentation")
+ for doc in self.documentation:
+ sections.append(doc)
+
+ if self.syntax_hints:
+ sections.append("\n## Syntax Hints")
+ for hint in self.syntax_hints:
+ sections.append(f"- {hint}")
+
+ return "\n".join(sections)
+
+
+class RAGService:
+ """
+ RAG Service for enriching prompts with P examples.
+
+ Usage:
+ rag = RAGService()
+ rag.index_p_repo("/path/to/P") # Index P repository
+
+ # Get context for machine generation
+ context = rag.get_machine_context("distributed lock manager")
+ prompt = f"{context.to_prompt_section()}\n\nGenerate machine: {description}"
+ """
+
+ def __init__(
+ self,
+ corpus_path: Optional[str] = None,
+ auto_index: bool = True
+ ):
+ # Default corpus path
+ if corpus_path is None:
+ corpus_path = os.environ.get(
+ "P_CORPUS_PATH",
+ str(Path(__file__).parent.parent.parent.parent / ".p_corpus")
+ )
+
+ self.corpus = PCorpus(store_path=corpus_path)
+ self._indexed = False
+
+ # Cache for documentation loaded from modular guide files
+ self._doc_cache: Dict[str, str] = {}
+
+ # Auto-index/migrate corpus so runtime uses current facet schema.
+ if auto_index:
+ if self.corpus.count() == 0:
+ self._auto_index()
+ elif not self.corpus.has_facet_schema():
+ logger.info("Detected legacy corpus schema; rebuilding bundled index")
+ rebuilt = self.corpus.rebuild_bundled_index()
+ if rebuilt > 0:
+ self._indexed = True
+
+ def _auto_index(self) -> None:
+ """
+ Automatically index the bundled resources.
+
+ This is fully self-contained — it indexes the curated examples and
+ documentation shipped inside ``resources/`` so no external repo is
+ needed. If ``P_REPO_PATH`` is set, extra examples from the full
+ repo are indexed as a bonus.
+ """
+ # Always start with the bundled resources (standalone)
+ logger.info("Auto-indexing bundled PeasyAI resources")
+ count = self.corpus.index_bundled_resources()
+ if count > 0:
+ self._indexed = True
+
+ # Optionally index extra files from the full P repo if available
+ p_repo = os.environ.get("P_REPO_PATH")
+ if p_repo and Path(p_repo).exists():
+ logger.info(f"Also indexing extra examples from P repo: {p_repo}")
+ extra_dirs = [
+ Path(p_repo) / "Tutorial",
+ Path(p_repo) / "Tst" / "RegressionTests",
+ ]
+ for d in extra_dirs:
+ if d.exists():
+ self.corpus.index_directory(str(d))
+ self._indexed = True
+
+ def _load_guide(self, guide_name: str) -> str:
+ """
+ Load a modular guide file from the resources/context_files directory.
+ Returns the content or empty string if not found.
+ """
+ if guide_name in self._doc_cache:
+ return self._doc_cache[guide_name]
+
+ # Try to locate the resources directory
+ resources_dir = Path(__file__).parent.parent.parent.parent / "resources" / "context_files"
+ guide_path = resources_dir / guide_name
+
+ if guide_path.exists():
+ try:
+ content = guide_path.read_text()
+ self._doc_cache[guide_name] = content
+ return content
+ except Exception as e:
+ logger.debug(f"Failed to load guide {guide_name}: {e}")
+
+ return ""
+
+ def index_p_repo(self, repo_path: str) -> int:
+ """Manually index P repository."""
+ count = self.corpus.index_p_repo(repo_path)
+ self._indexed = True
+ return count
+
+ def index_directory(self, dir_path: str) -> int:
+ """Index P files in a directory."""
+ return self.corpus.index_directory(dir_path)
+
+ def index_file(self, file_path: str) -> int:
+ """Index a single P file."""
+ return self.corpus.index_p_file(file_path)
+
+ def add_example(self, example: PExample) -> None:
+ """Add a custom example."""
+ self.corpus.add_example(example)
+
+ def _context_categories(self, context_type: str) -> List[str]:
+ if context_type == "machine":
+ return ["machine", "full_project", "test"]
+ if context_type == "spec":
+ return ["spec", "documentation", "full_project"]
+ if context_type == "test":
+ return ["test", "machine", "full_project", "documentation"]
+ if context_type == "types":
+ return ["types", "machine", "documentation"]
+ return ["machine", "spec", "test", "types", "full_project", "documentation"]
+
+ def _derive_facets(
+ self,
+ text: str,
+ context_type: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ) -> Dict[str, List[str]]:
+ """
+ Infer retrieval facets from user description/design text and
+ optionally from already-generated context files (types, machines).
+
+ Facets drive multi-lane retrieval beyond protocol names:
+ protocol + language construct + pattern.
+ """
+ # Combine description text with event/type names from context files
+ # so facets reflect the actual generated code, not just the prose.
+ extra = ""
+ if context_files:
+ for content in context_files.values():
+ extra += " " + content
+ lower = ((text or "") + " " + extra).lower()
+ protocols: List[str] = []
+ constructs: List[str] = []
+ patterns: List[str] = []
+
+ # Protocol facets
+ if any(k in lower for k in ["paxos", "proposer", "acceptor", "learner"]):
+ protocols.append("paxos")
+ if any(k in lower for k in ["raft", "leader election", "appendentries", "follower", "candidate", "leader heartbeat"]):
+ protocols.append("raft")
+ if any(k in lower for k in ["two phase", "2pc", "coordinator", "participant", "atomic commit"]):
+ protocols.append("two-phase-commit")
+ if any(k in lower for k in ["distributed lock", "mutex", "acquire lock", "release lock"]):
+ protocols.append("distributed-lock")
+ if any(k in lower for k in ["failure detector", "suspect", "heartbeat timeout", "crash detector",
+ "heartbeat", "liveness check", "node crash", "node failure"]):
+ protocols.append("failure-detector")
+ if any(k in lower for k in ["ring", "token ring", "chang-roberts", "ring election"]):
+ protocols.append("ring-election")
+ if any(k in lower for k in ["client server", "client-server", "request response", "rpc"]):
+ protocols.append("client-server")
+
+ # Construct facets
+ construct_hints = {
+ "send": ["send", "message"],
+ "receive": ["receive", "request-response", "blocking"],
+ "goto": ["goto", "transition", "state"],
+ "defer": ["defer"],
+ "ignore": ["ignore"],
+ "announce": ["announce", "monitor init"],
+ "new-machine": ["new machine", "spawn", "create machine"],
+ "hot-state": ["hot state", "eventually", "liveness"],
+ "cold-state": ["cold state"],
+ "spec-machine": ["spec", "monitor", "safety"],
+ "module-system": ["module", "compose", "union"],
+ "test-case": ["test", "checker", "schedule"],
+ "events": ["event", "payload"],
+ "types": ["type", "tuple"],
+ "enums": ["enum"],
+ "nondeterminism": ["choose(", "nondeterministic", "$"],
+ "assertions": ["assert", "invariant"],
+ "foreach": ["foreach", "iterate"],
+ "while-loop": ["while"],
+ }
+ for facet, kws in construct_hints.items():
+ if any(kw in lower for kw in kws):
+ constructs.append(facet)
+
+ # Pattern facets
+ if any(k in lower for k in ["broadcast", "fan-out", "multicast"]):
+ patterns.append("broadcast")
+ if any(k in lower for k in ["quorum", "majority"]):
+ patterns.append("quorum")
+ if any(k in lower for k in ["timeout", "timer", "heartbeat", "periodic",
+ "etimeout", "estarttimer", "ecanceltimer"]):
+ patterns.append("timer-timeout")
+ if any(k in lower for k in ["setup", "initialize", "bootstrap", "wiring"]):
+ patterns.append("setup-event")
+ patterns.append("machine-wiring")
+ if any(k in lower for k in ["test driver", "scenario"]):
+ patterns.append("test-driver")
+ if any(k in lower for k in ["request response", "request-response", "rpc", "blocking receive"]):
+ patterns.append("request-response")
+ if any(k in lower for k in ["coffee", "espresso", "vending", "appliance", "dispenser",
+ "grinding", "brewing", "idle"]):
+ patterns.append("sequential-states")
+ if any(k in lower for k in ["election", "leader", "vote", "ballot", "term"]):
+ patterns.append("leader-election")
+
+ # Cross-derive: protocols that inherently use certain patterns
+ if "failure-detector" in protocols and "timer-timeout" not in patterns:
+ patterns.append("timer-timeout")
+ if "raft" in protocols:
+ if "timer-timeout" not in patterns:
+ patterns.append("timer-timeout")
+ if "leader-election" not in patterns:
+ patterns.append("leader-election")
+ if "broadcast" not in patterns:
+ patterns.append("broadcast")
+
+ # Context-sensitive defaults
+ if context_type == "spec":
+ constructs.extend(["spec-machine", "assertions"])
+ elif context_type == "test":
+ constructs.extend(["test-case", "announce", "module-system", "nondeterminism"])
+ patterns.append("test-driver")
+ elif context_type == "types":
+ constructs.extend(["types", "events", "enums"])
+ elif context_type == "machine":
+ constructs.extend(["send", "goto"])
+
+ def _unique(items: List[str]) -> List[str]:
+ seen = set()
+ out = []
+ for item in items:
+ if item not in seen:
+ seen.add(item)
+ out.append(item)
+ return out
+
+ return {
+ "protocols": _unique(protocols),
+ "constructs": _unique(constructs),
+ "patterns": _unique(patterns),
+ }
+
+ def _build_faceted_query(
+ self,
+ description: str,
+ design_doc: Optional[str],
+ context_type: str,
+ facets: Dict[str, List[str]],
+ ) -> str:
+ parts = [context_type, "P language", description]
+ if design_doc:
+ parts.append(self._extract_keywords(design_doc))
+ parts.extend(facets.get("protocols", []))
+ parts.extend(facets.get("constructs", []))
+ parts.extend(facets.get("patterns", []))
+ return " ".join([p for p in parts if p]).strip()
+
+ def _examples_from_results(
+ self,
+ results: List[SearchResult],
+ min_score: float = 0.28,
+ limit: int = 3,
+ ) -> List[Dict[str, Any]]:
+ examples: List[Dict[str, Any]] = []
+ for result in results:
+ if result.score < min_score:
+ continue
+ meta = result.document.metadata
+ examples.append(
+ {
+ "name": meta.get("name"),
+ "description": meta.get("description"),
+ "code": meta.get("code"),
+ "project": meta.get("project_name"),
+ "category": meta.get("category"),
+ "score": result.score,
+ }
+ )
+ if len(examples) >= limit:
+ break
+ return examples
+
+ def get_machine_context(
+ self,
+ description: str,
+ design_doc: Optional[str] = None,
+ num_examples: int = 3,
+ context_files: Optional[Dict[str, str]] = None,
+ ) -> RAGContext:
+ """
+ Get context for generating a machine.
+
+ Args:
+ description: Machine description or name
+ design_doc: Optional design document for more context
+ num_examples: Number of examples to retrieve
+ context_files: Already-generated P files (e.g. types/events)
+ used to enrich facet derivation
+
+ Returns:
+ RAGContext with relevant examples, documentation, and syntax hints
+ """
+ context = RAGContext()
+
+ facets = self._derive_facets(
+ f"{description} {design_doc or ''}",
+ context_type="machine",
+ context_files=context_files,
+ )
+ query = self._build_faceted_query(description, design_doc, "machine", facets)
+
+ results = self.corpus.search_faceted(
+ query=query,
+ top_k=max(4, num_examples + 1),
+ categories=self._context_categories("machine"),
+ protocols=facets["protocols"],
+ constructs=facets["constructs"],
+ patterns=facets["patterns"],
+ )
+ context.examples.extend(
+ self._examples_from_results(results, min_score=0.28, limit=max(3, num_examples))
+ )
+
+ # Load relevant documentation from modular guides
+ machine_doc = self._load_guide("modular/p_machines_guide.txt")
+ if machine_doc:
+ # Extract the most relevant sections rather than dumping the whole guide
+ context.documentation.append(
+ self._extract_doc_section(machine_doc, [
+ "about_event_handler",
+ "about_send_statements",
+ "about_defer_statement",
+ "about_ignore_statement",
+ "about_machine_creation",
+ "about_entry_exit_functions",
+ ])
+ )
+
+ # Add comprehensive syntax hints
+ context.syntax_hints = list(MACHINE_SYNTAX_HINTS)
+
+ # Add protocol-specific hints based on description
+ desc_lower = description.lower()
+ if any(kw in desc_lower for kw in ['lock', 'mutex', 'acquire', 'release']):
+ context.syntax_hints.append("For lock protocols, use `receive { case eLockGranted: ... }` for blocking lock acquisition.")
+ if any(kw in desc_lower for kw in ['consensus', 'paxos', 'raft', 'vote']):
+ context.syntax_hints.append("For consensus protocols, track quorum with `set[machine]` and check `sizeof(votes) > sizeof(all) / 2`.")
+ if any(kw in desc_lower for kw in ['timer', 'timeout', 'heartbeat']):
+ context.syntax_hints.append(
+ "For timer patterns, use the standard Timer module: "
+ "`timer = CreateTimer(this);` to create, "
+ "`StartTimer(timer);` to start, "
+ "`CancelTimer(timer);` to cancel. "
+ "Handle `eTimeOut` in your machine's state. "
+ "Do NOT re-implement the Timer machine — use the existing Timer module."
+ )
+
+ return context
+
+ def get_spec_context(
+ self,
+ description: str,
+ machines: Optional[List[str]] = None,
+ num_examples: int = 3
+ ) -> RAGContext:
+ """Get context for generating a specification."""
+ context = RAGContext()
+
+ full_desc = description
+ if machines:
+ full_desc += " " + " ".join(machines)
+ facets = self._derive_facets(full_desc, context_type="spec")
+ query = self._build_faceted_query(full_desc, None, "specification monitor safety", facets)
+
+ results = self.corpus.search_faceted(
+ query=query,
+ top_k=max(4, num_examples + 1),
+ categories=self._context_categories("spec"),
+ protocols=facets["protocols"],
+ constructs=facets["constructs"],
+ patterns=facets["patterns"],
+ )
+ context.examples.extend(
+ self._examples_from_results(results, min_score=0.26, limit=max(3, num_examples))
+ )
+
+ # Load spec guide documentation
+ spec_doc = self._load_guide("modular/p_spec_monitors_guide.txt")
+ if spec_doc:
+ context.documentation.append(spec_doc)
+
+ context.syntax_hints = list(SPEC_SYNTAX_HINTS)
+
+ return context
+
+ def get_test_context(
+ self,
+ description: str,
+ machines: Optional[List[str]] = None,
+ num_examples: int = 3
+ ) -> RAGContext:
+ """Get context for generating a test."""
+ context = RAGContext()
+
+ full_desc = description
+ if machines:
+ full_desc += " " + " ".join(machines)
+ facets = self._derive_facets(full_desc, context_type="test")
+ query = self._build_faceted_query(full_desc, None, "test driver scenario", facets)
+
+ results = self.corpus.search_faceted(
+ query=query,
+ top_k=max(4, num_examples + 1),
+ categories=self._context_categories("test"),
+ protocols=facets["protocols"],
+ constructs=facets["constructs"],
+ patterns=facets["patterns"],
+ )
+ context.examples.extend(
+ self._examples_from_results(results, min_score=0.26, limit=max(3, num_examples))
+ )
+
+ # Load test guide and module system documentation
+ test_doc = self._load_guide("modular/p_test_cases_guide.txt")
+ if test_doc:
+ context.documentation.append(test_doc)
+
+ module_doc = self._load_guide("modular/p_module_system_guide.txt")
+ if module_doc:
+ context.documentation.append(module_doc)
+
+ context.syntax_hints = list(TEST_SYNTAX_HINTS)
+
+ return context
+
+ def get_types_context(
+ self,
+ description: str,
+ num_examples: int = 3
+ ) -> RAGContext:
+ """Get context for generating types and events."""
+ context = RAGContext()
+
+ facets = self._derive_facets(description, context_type="types")
+ query = self._build_faceted_query(description, None, "type event enum definition", facets)
+ results = self.corpus.search_faceted(
+ query=query,
+ top_k=max(4, num_examples + 1),
+ categories=self._context_categories("types"),
+ protocols=facets["protocols"],
+ constructs=facets["constructs"],
+ patterns=facets["patterns"],
+ )
+ context.examples.extend(
+ self._examples_from_results(results, min_score=0.26, limit=max(3, num_examples))
+ )
+
+ # Load types, events, and enums guides
+ types_doc = self._load_guide("modular/p_types_guide.txt")
+ if types_doc:
+ context.documentation.append(types_doc)
+
+ events_doc = self._load_guide("modular/p_events_guide.txt")
+ if events_doc:
+ context.documentation.append(events_doc)
+
+ enums_doc = self._load_guide("modular/p_enums_guide.txt")
+ if enums_doc:
+ context.documentation.append(enums_doc)
+
+ context.syntax_hints = list(TYPES_SYNTAX_HINTS)
+
+ return context
+
+ def get_protocol_examples(self, protocol_name: str, top_k: int = 5) -> RAGContext:
+ """
+ Get examples for a specific protocol type.
+
+ Retrieval is intentionally code-first:
+ - Prioritize categories with executable code (machine/spec/test/full_project/types)
+ - Penalize pure documentation hits
+ - Boost results with matching protocol tags/keywords in code, description, and project name
+ """
+ context = RAGContext()
+ normalized = protocol_name.strip().lower()
+
+ protocol_queries = {
+ "paxos": [
+ "paxos proposer acceptor learner ballot quorum consensus",
+ "single decree paxos prepare accept phase1 phase2",
+ ],
+ "two-phase commit": [
+ "two phase commit coordinator participant prepare commit abort atomicity",
+ "2pc transaction commit protocol",
+ ],
+ "2pc": [
+ "two phase commit coordinator participant prepare commit abort atomicity",
+ "2pc transaction commit protocol",
+ ],
+ "raft": [
+ "raft leader follower candidate election append entries log replication term",
+ "raft consensus leader election heartbeat commit index",
+ ],
+ "failure detector": [
+ "failure detector heartbeat timeout suspect crash monitor",
+ "distributed failure detection ping timeout",
+ ],
+ "distributed lock": [
+ "distributed lock acquire release lock server mutual exclusion",
+ "lock manager token lock grant revoke",
+ ],
+ }
+
+ protocol_keywords = {
+ "paxos": ["paxos", "proposer", "acceptor", "learner", "ballot", "quorum"],
+ "two-phase commit": ["2pc", "two phase", "coordinator", "participant", "prepare", "commit", "abort"],
+ "2pc": ["2pc", "two phase", "coordinator", "participant", "prepare", "commit", "abort"],
+ "raft": ["raft", "leader", "follower", "candidate", "term", "appendentries", "heartbeat"],
+ "failure detector": ["failure", "detector", "timeout", "heartbeat", "suspect", "crash"],
+ "distributed lock": ["lock", "acquire", "release", "mutex", "critical section"],
+ }
+ strict_protocol_tokens = {
+ "paxos": ["paxos"],
+ "two-phase commit": ["two phase", "2pc"],
+ "2pc": ["two phase", "2pc"],
+ "raft": ["raft"],
+ "failure detector": ["failure detector", "failure-detect", "detector"],
+ "distributed lock": ["distributed lock", "lock server", "lock"],
+ }
+
+ queries = protocol_queries.get(
+ normalized,
+ [f"protocol {normalized} distributed system P language"],
+ )
+ keywords = protocol_keywords.get(normalized, [normalized])
+
+ protocol_facet_map = {
+ "paxos": ["paxos"],
+ "two-phase commit": ["two-phase-commit"],
+ "2pc": ["two-phase-commit"],
+ "raft": ["raft"],
+ "failure detector": ["failure-detector"],
+ "distributed lock": ["distributed-lock"],
+ }
+ pattern_facet_map = {
+ "paxos": ["quorum"],
+ "two-phase commit": ["request-response"],
+ "2pc": ["request-response"],
+ "raft": ["quorum", "timer-timeout"],
+ "failure detector": ["timer-timeout"],
+ "distributed lock": ["request-response"],
+ }
+ construct_facet_map = {
+ "paxos": ["send", "receive", "goto", "events"],
+ "two-phase commit": ["send", "receive", "events"],
+ "2pc": ["send", "receive", "events"],
+ "raft": ["send", "goto", "events", "types"],
+ "failure detector": ["send", "events", "nondeterminism"],
+ "distributed lock": ["send", "receive", "events"],
+ }
+
+ # Category priority order: code-first
+ priority_categories = ["machine", "spec", "test", "full_project", "types"]
+ candidate_results: List[SearchResult] = []
+ per_bucket_k = max(6, top_k * 3)
+ protocol_facets = protocol_facet_map.get(normalized, [normalized])
+ pattern_facets = pattern_facet_map.get(normalized, [])
+ construct_facets = construct_facet_map.get(normalized, [])
+
+ for q in queries:
+ candidate_results.extend(
+ self.corpus.search_faceted(
+ query=q,
+ top_k=per_bucket_k,
+ categories=priority_categories,
+ protocols=protocol_facets,
+ constructs=construct_facets,
+ patterns=pattern_facets,
+ )
+ )
+ candidate_results.extend(self.corpus.search(q, top_k=max(5, top_k)))
+
+ # Deduplicate by document id, keep best reranked score
+ best_by_id: Dict[str, tuple[SearchResult, float, int, int]] = {}
+ for result in candidate_results:
+ meta = result.document.metadata
+ category = (meta.get("category") or "").lower()
+ code = (meta.get("code") or "")
+ description = (meta.get("description") or "").lower()
+ project = (meta.get("project_name") or "").lower()
+ tags = [str(t).lower() for t in (meta.get("tags") or [])]
+ code_lower = code.lower()
+
+ score = result.score
+
+ # Category boost/penalty
+ if category == "machine":
+ score += 0.18
+ elif category == "spec":
+ score += 0.12
+ elif category == "test":
+ score += 0.10
+ elif category == "full_project":
+ score += 0.08
+ elif category == "types":
+ score += 0.04
+ elif category in ("documentation", "manual", "tutorial", "advanced", "getting_started"):
+ score -= 0.20
+
+ # Prefer results that actually include code
+ if code.strip():
+ score += 0.08
+ else:
+ score -= 0.15
+
+ # Keyword and tag boosts
+ keyword_hits = 0
+ for kw in keywords:
+ kw = kw.lower()
+ if kw in code_lower:
+ keyword_hits += 2
+ if kw in description:
+ keyword_hits += 1
+ if kw in project:
+ keyword_hits += 1
+ if any(kw in tag for tag in tags):
+ keyword_hits += 2
+ score += min(0.20, keyword_hits * 0.02)
+ if normalized in protocol_keywords and keyword_hits == 0:
+ # For known protocols, demote unrelated generic results.
+ score -= 0.25
+
+ strict_hits = 0
+ for tok in strict_protocol_tokens.get(normalized, []):
+ tok = tok.lower()
+ if tok in code_lower:
+ strict_hits += 2
+ if tok in description:
+ strict_hits += 1
+ if tok in project:
+ strict_hits += 1
+ if any(tok in tag for tag in tags):
+ strict_hits += 2
+
+ # Protocol-structure heuristics
+ if normalized in ("raft", "paxos", "two-phase commit", "2pc") and "machine " in code_lower:
+ score += 0.04
+ if normalized in ("two-phase commit", "2pc") and ("coordinator" in code_lower and "participant" in code_lower):
+ score += 0.06
+ if normalized == "raft" and ("leader" in code_lower and "follower" in code_lower):
+ score += 0.06
+ if normalized == "paxos" and ("proposer" in code_lower and "acceptor" in code_lower):
+ score += 0.06
+
+ doc_id = result.document.id
+ prev = best_by_id.get(doc_id)
+ if prev is None or score > prev[1]:
+ best_by_id[doc_id] = (result, score, keyword_hits, strict_hits)
+
+ ranked = sorted(best_by_id.values(), key=lambda x: x[1], reverse=True)
+
+ # For known protocols, prefer entries with explicit keyword matches.
+ # If no such entries exist, fall back to the full ranked set.
+ if normalized in protocol_keywords:
+ strict_ranked = [r for r in ranked if r[3] > 0]
+ if strict_ranked:
+ ranked = strict_ranked
+ keyword_ranked = [r for r in ranked if r[2] > 0]
+ if keyword_ranked:
+ ranked = keyword_ranked
+
+ for result, reranked_score, _keyword_hits, _strict_hits in ranked[:top_k]:
+ context.examples.append({
+ "name": result.document.metadata.get("name"),
+ "description": result.document.metadata.get("description"),
+ "code": result.document.metadata.get("code"),
+ "project": result.document.metadata.get("project_name"),
+ "category": result.document.metadata.get("category"),
+ "score": reranked_score,
+ })
+
+ return context
+
+ def search(self, query: str, top_k: int = 5) -> List[SearchResult]:
+ """General search across all examples."""
+ return self.corpus.search(query, top_k=top_k)
+
+ def get_documentation(self, topic: str) -> List[str]:
+ """
+ Get documentation for a P language topic.
+
+ Loads from the modular guide files for comprehensive coverage.
+ Falls back to built-in summaries if guide files aren't available.
+ """
+ topic_lower = topic.lower().replace(" ", "_")
+
+ # Map topics to guide files
+ topic_guides = {
+ "state_machine": "modular/p_machines_guide.txt",
+ "machine": "modular/p_machines_guide.txt",
+ "state": "modular/p_machines_guide.txt",
+ "events": "modular/p_events_guide.txt",
+ "event": "modular/p_events_guide.txt",
+ "types": "modular/p_types_guide.txt",
+ "type": "modular/p_types_guide.txt",
+ "enum": "modular/p_enums_guide.txt",
+ "enums": "modular/p_enums_guide.txt",
+ "statement": "modular/p_statements_guide.txt",
+ "statements": "modular/p_statements_guide.txt",
+ "spec": "modular/p_spec_monitors_guide.txt",
+ "specification": "modular/p_spec_monitors_guide.txt",
+ "monitor": "modular/p_spec_monitors_guide.txt",
+ "test": "modular/p_test_cases_guide.txt",
+ "tests": "modular/p_test_cases_guide.txt",
+ "module": "modular/p_module_system_guide.txt",
+ "modules": "modular/p_module_system_guide.txt",
+ "compiler": "modular/p_compiler_guide.txt",
+ "error": "modular/p_common_compilation_errors.txt",
+ "errors": "modular/p_common_compilation_errors.txt",
+ "basics": "modular/p_basics.txt",
+ "example": "modular/p_program_example.txt",
+ "syntax": "P_syntax_guide.txt",
+ "nuances": "p_nuances.txt",
+ }
+
+ # Find the best matching guide
+ guide_file = None
+ for key, gf in topic_guides.items():
+ if key in topic_lower:
+ guide_file = gf
+ break
+
+ if guide_file:
+ content = self._load_guide(guide_file)
+ if content:
+ return [content]
+
+ # Fallback built-in documentation
+ docs = {
+ "state_machine": [
+ "## P State Machines",
+ "State machines in P are defined with the `machine` keyword.",
+ "Each machine has states, and transitions between states happen via `goto`.",
+ "The `start state` defines the initial state of the machine.",
+ "Sends are asynchronous: `send target, eventName, payload;`",
+ "Event handlers: `on eEvent do HandlerFunction;` or `on eEvent goto StateName;`",
+ ],
+ "events": [
+ "## P Events",
+ "Events are the primary communication mechanism in P.",
+ "Send events with: `send target, eventName, payload;`",
+ "Handle events in states with: `on eventName do HandlerFunction;`",
+ "Events with payloads require a separately declared type.",
+ ],
+ "types": [
+ "## P Types",
+ "P supports: int, bool, float, string, machine, event, any, data",
+ "Named tuples: `type tRequest = (field1: int, field2: string);`",
+ "Collections: `seq[int]`, `set[int]`, `map[int, string]`",
+ "Enums: `enum Status { SUCCESS, FAILURE }`",
+ ],
+ }
+
+ return docs.get(topic_lower, [f"No documentation found for: {topic}"])
+
+ def get_stats(self) -> Dict[str, Any]:
+ """Get statistics about the corpus."""
+ total = self.corpus.count()
+ by_category = self.corpus.count_by_category()
+
+ return {
+ "total_examples": total,
+ "indexed": self._indexed,
+ "by_category": by_category,
+ }
+
+ def _extract_keywords(self, text: str, max_keywords: int = 10) -> str:
+ """Extract keywords from text."""
+ import re
+ # Remove common words and extract meaningful terms
+ words = re.findall(r'\b[A-Za-z]{3,}\b', text.lower())
+
+ # Common words to skip
+ stopwords = {
+ 'the', 'and', 'for', 'that', 'with', 'this', 'from', 'are',
+ 'will', 'can', 'has', 'have', 'should', 'when', 'each', 'all',
+ 'its', 'which', 'other', 'their', 'them', 'then', 'into',
+ 'been', 'more', 'not', 'but', 'also', 'any', 'may', 'some',
+ }
+
+ keywords = [w for w in words if w not in stopwords]
+
+ # Get unique keywords, preserving order
+ seen = set()
+ unique = []
+ for w in keywords:
+ if w not in seen:
+ seen.add(w)
+ unique.append(w)
+
+ return ' '.join(unique[:max_keywords])
+
+ def _extract_doc_section(self, doc_content: str, section_tags: List[str]) -> str:
+ """
+ Extract specific XML-tagged sections from a documentation file.
+
+ Returns concatenated content of matching sections, truncated
+ to a reasonable length for prompt injection.
+ """
+ import re
+ sections = []
+
+ for tag in section_tags:
+ pattern = rf'<{tag}>(.*?){tag}>'
+ matches = re.findall(pattern, doc_content, re.DOTALL)
+ for match in matches:
+ cleaned = match.strip()
+ if cleaned:
+ sections.append(cleaned)
+
+ combined = "\n\n".join(sections)
+
+ # Truncate to ~3000 chars to keep prompt size reasonable
+ if len(combined) > 3000:
+ combined = combined[:3000] + "\n... (truncated)"
+
+ return combined
+
+
+# Singleton instance
+_rag_service: Optional[RAGService] = None
+
+
+def get_rag_service() -> RAGService:
+ """Get the singleton RAG service instance."""
+ global _rag_service
+ if _rag_service is None:
+ _rag_service = RAGService()
+ return _rag_service
diff --git a/Src/PeasyAI/src/core/rag/vector_store.py b/Src/PeasyAI/src/core/rag/vector_store.py
new file mode 100644
index 0000000000..94563878d5
--- /dev/null
+++ b/Src/PeasyAI/src/core/rag/vector_store.py
@@ -0,0 +1,209 @@
+"""
+Vector Store for P Program Examples.
+
+A simple in-memory vector store with persistence.
+Supports similarity search using cosine similarity.
+"""
+
+import json
+import logging
+import math
+from dataclasses import dataclass, asdict
+from pathlib import Path
+from typing import List, Dict, Any, Optional
+import threading
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class Document:
+ """A document in the vector store."""
+ id: str
+ content: str
+ embedding: List[float]
+ metadata: Dict[str, Any]
+
+ def to_dict(self) -> dict:
+ return asdict(self)
+
+ @classmethod
+ def from_dict(cls, data: dict) -> "Document":
+ return cls(**data)
+
+
+@dataclass
+class SearchResult:
+ """A search result with relevance score."""
+ document: Document
+ score: float
+
+ def to_dict(self) -> dict:
+ return {
+ "id": self.document.id,
+ "content": self.document.content,
+ "metadata": self.document.metadata,
+ "score": self.score
+ }
+
+
+class VectorStore:
+ """
+ Simple in-memory vector store with persistence.
+
+ Uses cosine similarity for search.
+ Thread-safe for concurrent access.
+ """
+
+ def __init__(self, persist_path: Optional[str] = None):
+ self.documents: Dict[str, Document] = {}
+ self.persist_path = Path(persist_path) if persist_path else None
+ self._lock = threading.RLock()
+
+ if self.persist_path and self.persist_path.exists():
+ self._load()
+
+ def add(self, doc: Document) -> None:
+ """Add a document to the store."""
+ with self._lock:
+ self.documents[doc.id] = doc
+ self._save()
+
+ def add_batch(self, docs: List[Document]) -> None:
+ """Add multiple documents."""
+ with self._lock:
+ for doc in docs:
+ self.documents[doc.id] = doc
+ self._save()
+
+ def get(self, doc_id: str) -> Optional[Document]:
+ """Get a document by ID."""
+ with self._lock:
+ return self.documents.get(doc_id)
+
+ def delete(self, doc_id: str) -> bool:
+ """Delete a document by ID."""
+ with self._lock:
+ if doc_id in self.documents:
+ del self.documents[doc_id]
+ self._save()
+ return True
+ return False
+
+ def search(
+ self,
+ query_embedding: List[float],
+ top_k: int = 5,
+ filter_metadata: Optional[Dict[str, Any]] = None
+ ) -> List[SearchResult]:
+ """
+ Search for similar documents.
+
+ Args:
+ query_embedding: Query vector
+ top_k: Number of results to return
+ filter_metadata: Optional metadata filter (exact match)
+
+ Returns:
+ List of SearchResult sorted by relevance
+ """
+ with self._lock:
+ results = []
+
+ for doc in self.documents.values():
+ # Apply metadata filter
+ if filter_metadata:
+ match = all(
+ doc.metadata.get(k) == v
+ for k, v in filter_metadata.items()
+ )
+ if not match:
+ continue
+
+ # Calculate cosine similarity
+ score = self._cosine_similarity(query_embedding, doc.embedding)
+ results.append(SearchResult(document=doc, score=score))
+
+ # Sort by score descending
+ results.sort(key=lambda x: x.score, reverse=True)
+
+ return results[:top_k]
+
+ def search_by_metadata(
+ self,
+ metadata: Dict[str, Any],
+ limit: int = 10
+ ) -> List[Document]:
+ """Search documents by metadata only."""
+ with self._lock:
+ results = []
+ for doc in self.documents.values():
+ match = all(
+ doc.metadata.get(k) == v
+ for k, v in metadata.items()
+ )
+ if match:
+ results.append(doc)
+ if len(results) >= limit:
+ break
+ return results
+
+ def list_all(self, limit: int = 100) -> List[Document]:
+ """List all documents."""
+ with self._lock:
+ return list(self.documents.values())[:limit]
+
+ def count(self) -> int:
+ """Return number of documents."""
+ with self._lock:
+ return len(self.documents)
+
+ def clear(self) -> None:
+ """Clear all documents."""
+ with self._lock:
+ self.documents.clear()
+ self._save()
+
+ def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
+ """Calculate cosine similarity between two vectors."""
+ if len(vec1) != len(vec2):
+ return 0.0
+
+ dot_product = sum(a * b for a, b in zip(vec1, vec2))
+ magnitude1 = math.sqrt(sum(a * a for a in vec1))
+ magnitude2 = math.sqrt(sum(b * b for b in vec2))
+
+ if magnitude1 == 0 or magnitude2 == 0:
+ return 0.0
+
+ return dot_product / (magnitude1 * magnitude2)
+
+ def _save(self) -> None:
+ """Persist to disk."""
+ if self.persist_path is None:
+ return
+
+ try:
+ self.persist_path.parent.mkdir(parents=True, exist_ok=True)
+ data = [doc.to_dict() for doc in self.documents.values()]
+ with open(self.persist_path, 'w') as f:
+ json.dump(data, f)
+ except Exception as e:
+ logger.error(f"Failed to save vector store: {e}")
+
+ def _load(self) -> None:
+ """Load from disk."""
+ if self.persist_path is None or not self.persist_path.exists():
+ return
+
+ try:
+ with open(self.persist_path, 'r') as f:
+ data = json.load(f)
+
+ for doc_data in data:
+ doc = Document.from_dict(doc_data)
+ self.documents[doc.id] = doc
+
+ logger.info(f"Loaded {len(self.documents)} documents from {self.persist_path}")
+ except Exception as e:
+ logger.error(f"Failed to load vector store: {e}")
diff --git a/Src/PeasyAI/src/core/security.py b/Src/PeasyAI/src/core/security.py
new file mode 100644
index 0000000000..0dfbe9a7ec
--- /dev/null
+++ b/Src/PeasyAI/src/core/security.py
@@ -0,0 +1,162 @@
+"""
+Security utilities for PeasyAI.
+
+Provides path validation, input sanitization, and error redaction
+to prevent path traversal attacks, arbitrary file access, and
+information leakage through error messages.
+"""
+
+import logging
+import os
+import re
+from pathlib import Path
+from typing import Optional
+
+logger = logging.getLogger(__name__)
+
+# Maximum sizes for inputs that will be passed to the LLM or stored in memory.
+MAX_DESIGN_DOC_BYTES = 500_000 # 500 KB
+MAX_CODE_BYTES = 200_000 # 200 KB
+MAX_TRACE_LOG_BYTES = 1_000_000 # 1 MB
+MAX_ERROR_MESSAGE_BYTES = 10_000 # 10 KB
+MAX_USER_GUIDANCE_BYTES = 50_000 # 50 KB
+
+ALLOWED_P_EXTENSIONS = {".p", ".pproj"}
+
+# Directories inside a P project that may contain writable files.
+P_PROJECT_SUBDIRS = {"PSrc", "PSpec", "PTst"}
+
+
+class PathSecurityError(Exception):
+ """Raised when a path fails security validation."""
+
+
+def validate_project_path(path: str) -> Path:
+ """
+ Validate that *path* looks like a legitimate P project directory.
+
+ Checks:
+ - Path is absolute
+ - Path exists and is a directory
+ - No ``..`` components after resolution
+ - Contains at least one P project marker (.pproj or PSrc/)
+
+ Returns the resolved ``Path`` on success, raises ``PathSecurityError``
+ on failure.
+ """
+ resolved = Path(path).resolve()
+
+ if not resolved.is_absolute():
+ raise PathSecurityError("Project path must be absolute")
+
+ if not resolved.is_dir():
+ raise PathSecurityError("Project path does not exist or is not a directory")
+
+ if ".." in Path(path).parts:
+ raise PathSecurityError("Path traversal ('..') is not allowed")
+
+ has_pproj = any(resolved.glob("*.pproj"))
+ has_psrc = (resolved / "PSrc").is_dir()
+ if not has_pproj and not has_psrc:
+ raise PathSecurityError(
+ "Path does not look like a P project "
+ "(no .pproj file and no PSrc/ directory)"
+ )
+
+ return resolved
+
+
+def validate_file_write_path(
+ file_path: str,
+ project_path: str,
+) -> Path:
+ """
+ Validate that *file_path* is safe to write to.
+
+ Checks:
+ - Path is absolute
+ - Resolved path lives inside *project_path* (no escape via symlinks or ``..``)
+ - File extension is an allowed P extension
+
+ Returns the resolved ``Path`` on success.
+ """
+ resolved_project = Path(project_path).resolve()
+ resolved_file = Path(file_path).resolve()
+
+ if not resolved_file.is_absolute():
+ raise PathSecurityError("File path must be absolute")
+
+ if ".." in Path(file_path).parts:
+ raise PathSecurityError("Path traversal ('..') is not allowed in file paths")
+
+ # Ensure the file lives within the project directory tree.
+ try:
+ resolved_file.relative_to(resolved_project)
+ except ValueError:
+ raise PathSecurityError(
+ f"File path must be inside the project directory "
+ f"({resolved_project})"
+ )
+
+ if resolved_file.suffix not in ALLOWED_P_EXTENSIONS:
+ raise PathSecurityError(
+ f"Only P files ({', '.join(ALLOWED_P_EXTENSIONS)}) can be written; "
+ f"got '{resolved_file.suffix}'"
+ )
+
+ return resolved_file
+
+
+def validate_file_read_path(
+ file_path: str,
+ project_path: Optional[str] = None,
+) -> Path:
+ """
+ Validate that *file_path* is safe to read.
+
+ If *project_path* is provided, the file must reside within it.
+ """
+ resolved = Path(file_path).resolve()
+
+ if ".." in Path(file_path).parts:
+ raise PathSecurityError("Path traversal ('..') is not allowed")
+
+ if project_path:
+ resolved_project = Path(project_path).resolve()
+ try:
+ resolved.relative_to(resolved_project)
+ except ValueError:
+ raise PathSecurityError(
+ f"File path must be inside the project directory "
+ f"({resolved_project})"
+ )
+
+ return resolved
+
+
+def sanitize_error(error: Exception, context: str = "") -> str:
+ """
+ Produce a user-safe error message that does not leak internal paths.
+
+ Replaces absolute paths with ``/basename`` and strips stack traces.
+ """
+ msg = str(error)
+
+ msg = re.sub(
+ r'(/[^\s:,\'"]+)',
+ lambda m: f"/{Path(m.group(1)).name}" if len(m.group(1)) > 20 else m.group(1),
+ msg,
+ )
+
+ prefix = f"[{context}] " if context else ""
+ return f"{prefix}{type(error).__name__}: {msg}"
+
+
+def check_input_size(value: str, name: str, max_bytes: int) -> None:
+ """Raise ``ValueError`` if *value* exceeds *max_bytes*."""
+ size = len(value.encode("utf-8", errors="replace"))
+ if size > max_bytes:
+ raise ValueError(
+ f"{name} too large: {size:,} bytes exceeds the "
+ f"{max_bytes:,} byte limit"
+ )
diff --git a/Src/PeasyAI/src/core/services/__init__.py b/Src/PeasyAI/src/core/services/__init__.py
new file mode 100644
index 0000000000..932135441d
--- /dev/null
+++ b/Src/PeasyAI/src/core/services/__init__.py
@@ -0,0 +1,25 @@
+"""
+Core Services Layer
+
+This module provides UI-agnostic services for P code generation,
+compilation, checking, and error fixing.
+
+These services can be used by any interface (Streamlit, CLI, MCP)
+without any UI-specific dependencies.
+"""
+
+from .generation import GenerationService, GenerationResult
+from .compilation import CompilationService, CompilationResult
+from .fixer import FixerService, FixResult, FixAttemptTracker
+
+__all__ = [
+ "GenerationService",
+ "GenerationResult",
+ "CompilationService",
+ "CompilationResult",
+ "FixerService",
+ "FixResult",
+ "FixAttemptTracker",
+]
+
+
diff --git a/Src/PeasyAI/src/core/services/base.py b/Src/PeasyAI/src/core/services/base.py
new file mode 100644
index 0000000000..fdd48c6cbb
--- /dev/null
+++ b/Src/PeasyAI/src/core/services/base.py
@@ -0,0 +1,199 @@
+"""
+Base classes for services.
+
+Provides common functionality for all services including
+resource loading and LLM provider access.
+"""
+
+import os
+import logging
+from pathlib import Path
+from typing import Optional, Dict, Any, Callable
+from dataclasses import dataclass, field
+
+from ..llm import LLMProvider, get_default_provider
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class ServiceResult:
+ """Base class for service operation results"""
+ success: bool
+ error: Optional[str] = None
+ token_usage: Dict[str, int] = field(default_factory=dict)
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert to dictionary"""
+ return {
+ "success": self.success,
+ "error": self.error,
+ "token_usage": self.token_usage,
+ }
+
+
+class EventCallback:
+ """
+ Callback interface for service events.
+
+ This replaces direct Streamlit calls (backend_status.write)
+ with a callback-based approach that any UI can implement.
+ """
+
+ def __init__(
+ self,
+ on_status: Optional[Callable[[str], None]] = None,
+ on_progress: Optional[Callable[[str, int, int], None]] = None,
+ on_error: Optional[Callable[[str], None]] = None,
+ on_warning: Optional[Callable[[str], None]] = None,
+ ):
+ self._on_status = on_status or (lambda msg: logger.info(msg))
+ self._on_progress = on_progress or (lambda step, current, total: logger.info(f"[{current}/{total}] {step}"))
+ self._on_error = on_error or (lambda msg: logger.error(msg))
+ self._on_warning = on_warning or (lambda msg: logger.warning(msg))
+
+ def status(self, message: str):
+ """Report a status update"""
+ self._on_status(message)
+
+ def progress(self, step: str, current: int, total: int):
+ """Report progress on a multi-step operation"""
+ self._on_progress(step, current, total)
+
+ def error(self, message: str):
+ """Report an error"""
+ self._on_error(message)
+
+ def warning(self, message: str):
+ """Report a warning"""
+ self._on_warning(message)
+
+
+class ResourceLoader:
+ """
+ Loads resources from the resources directory.
+
+ Provides access to:
+ - Context files (P language guides)
+ - Instruction templates
+ - Few-shot examples
+ """
+
+ def __init__(self, resources_path: Optional[Path] = None):
+ if resources_path is None:
+ # 1. Check PEASYAI_RESOURCES_DIR env var (set by pip-installed entry point)
+ env_resources = os.environ.get("PEASYAI_RESOURCES_DIR")
+ if env_resources and Path(env_resources).is_dir():
+ resources_path = Path(env_resources)
+ else:
+ # 2. Fall back to resources/ relative to project root (dev checkout)
+ project_root = Path(__file__).parent.parent.parent.parent
+ resources_path = project_root / "resources"
+
+ # 3. If still missing, try peasyai_resources/ in site-packages
+ if not resources_path.is_dir():
+ import site
+ for sp in (site.getsitepackages() if hasattr(site, "getsitepackages") else []):
+ candidate = Path(sp) / "peasyai_resources"
+ if candidate.is_dir():
+ resources_path = candidate
+ break
+
+ self.resources_path = resources_path
+ self._cache: Dict[str, str] = {}
+
+ def load(self, relative_path: str, use_cache: bool = True) -> str:
+ """
+ Load a resource file.
+
+ Args:
+ relative_path: Path relative to resources directory
+ use_cache: Whether to use cached content
+
+ Returns:
+ File content as string
+ """
+ if use_cache and relative_path in self._cache:
+ return self._cache[relative_path]
+
+ full_path = self.resources_path / relative_path
+
+ if not full_path.exists():
+ raise FileNotFoundError(f"Resource not found: {relative_path}")
+
+ content = full_path.read_text(encoding="utf-8")
+
+ if use_cache:
+ self._cache[relative_path] = content
+
+ return content
+
+ def load_context(self, filename: str) -> str:
+ """Load a context file from context_files/"""
+ return self.load(f"context_files/{filename}")
+
+ def load_modular_context(self, filename: str) -> str:
+ """Load a modular context file from context_files/modular/"""
+ return self.load(f"context_files/modular/{filename}")
+
+ def load_instruction(self, filename: str) -> str:
+ """Load an instruction template from instructions/"""
+ return self.load(f"instructions/{filename}")
+
+ def clear_cache(self):
+ """Clear the resource cache"""
+ self._cache.clear()
+
+
+class BaseService:
+ """
+ Base class for all services.
+
+ Provides:
+ - LLM provider access
+ - Resource loading
+ - Event callbacks
+ """
+
+ def __init__(
+ self,
+ llm_provider: Optional[LLMProvider] = None,
+ resource_loader: Optional[ResourceLoader] = None,
+ callbacks: Optional[EventCallback] = None,
+ ):
+ self._llm_provider = llm_provider
+ self._resource_loader = resource_loader or ResourceLoader()
+ self._callbacks = callbacks or EventCallback()
+
+ @property
+ def llm(self) -> LLMProvider:
+ """Get the LLM provider (lazy initialization)"""
+ if self._llm_provider is None:
+ self._llm_provider = get_default_provider()
+ return self._llm_provider
+
+ @property
+ def resources(self) -> ResourceLoader:
+ """Get the resource loader"""
+ return self._resource_loader
+
+ @property
+ def callbacks(self) -> EventCallback:
+ """Get the event callbacks"""
+ return self._callbacks
+
+ def _status(self, message: str):
+ """Emit a status message"""
+ self._callbacks.status(message)
+
+ def _progress(self, step: str, current: int, total: int):
+ """Emit a progress update"""
+ self._callbacks.progress(step, current, total)
+
+ def _error(self, message: str):
+ """Emit an error message"""
+ self._callbacks.error(message)
+
+ def _warning(self, message: str):
+ """Emit a warning message"""
+ self._callbacks.warning(message)
diff --git a/Src/PeasyAI/src/core/services/compilation.py b/Src/PeasyAI/src/core/services/compilation.py
new file mode 100644
index 0000000000..8b83c9e92a
--- /dev/null
+++ b/Src/PeasyAI/src/core/services/compilation.py
@@ -0,0 +1,365 @@
+"""
+Compilation Service
+
+Handles P project compilation and PChecker execution.
+This service is UI-agnostic.
+"""
+
+import subprocess
+import logging
+import traceback
+from pathlib import Path
+from typing import Dict, Any, Optional, List, Tuple
+from dataclasses import dataclass, field
+
+from .base import BaseService, ServiceResult, EventCallback, ResourceLoader
+from ..llm import LLMProvider
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class CompilationResult(ServiceResult):
+ """Result of a compilation operation"""
+ stdout: str = ""
+ stderr: str = ""
+ return_code: int = -1
+
+
+@dataclass
+class CheckerResult(ServiceResult):
+ """Result of a PChecker run"""
+ test_results: Dict[str, bool] = field(default_factory=dict)
+ passed_tests: List[str] = field(default_factory=list)
+ failed_tests: List[str] = field(default_factory=list)
+ trace_logs: Dict[str, str] = field(default_factory=dict)
+
+
+@dataclass
+class ParsedError:
+ """A parsed compilation error"""
+ file_path: str
+ line_number: int
+ column_number: int
+ message: str
+ error_type: str = "unknown"
+
+
+class CompilationService(BaseService):
+ """
+ Service for compiling P projects and running PChecker.
+
+ Provides:
+ - Project compilation
+ - Error parsing
+ - PChecker execution
+ - Result analysis
+ """
+
+ def __init__(
+ self,
+ llm_provider: Optional[LLMProvider] = None,
+ resource_loader: Optional[ResourceLoader] = None,
+ callbacks: Optional[EventCallback] = None,
+ ):
+ super().__init__(llm_provider, resource_loader, callbacks)
+
+ def compile(self, project_path: str) -> CompilationResult:
+ """
+ Compile a P project.
+
+ Args:
+ project_path: Path to the P project directory
+
+ Returns:
+ CompilationResult with compilation output
+ """
+ self._status(f"Compiling P project at {project_path}...")
+
+ try:
+ # Verify project path exists
+ if not Path(project_path).exists():
+ return CompilationResult(
+ success=False,
+ error=f"Project path does not exist: {project_path}",
+ return_code=-1,
+ )
+
+ # Run P compiler
+ result = subprocess.run(
+ ['p', 'compile'],
+ capture_output=True,
+ cwd=project_path,
+ text=True,
+ timeout=300, # 5 minute timeout
+ )
+
+ # Check for success
+ success = "succeeded" in result.stdout.lower() or result.returncode == 0
+
+ if success:
+ self._status("Compilation succeeded")
+ else:
+ self._warning("Compilation failed")
+
+ return CompilationResult(
+ success=success,
+ stdout=result.stdout,
+ stderr=result.stderr,
+ return_code=result.returncode,
+ )
+
+ except FileNotFoundError:
+ return CompilationResult(
+ success=False,
+ error="P compiler not found. Make sure 'p' is in your PATH.",
+ return_code=-1,
+ )
+ except subprocess.TimeoutExpired:
+ return CompilationResult(
+ success=False,
+ error="Compilation timed out after 5 minutes",
+ return_code=-1,
+ )
+ except Exception as e:
+ err_msg = f"{type(e).__name__}: {e}"
+ logger.error(f"Compilation error: {err_msg}\n{traceback.format_exc()}")
+ return CompilationResult(
+ success=False,
+ error=err_msg,
+ return_code=-1,
+ )
+
+ def parse_error(self, compilation_output: str) -> Optional[ParsedError]:
+ """
+ Parse the first error from compilation output.
+
+ Args:
+ compilation_output: The stdout/stderr from compilation
+
+ Returns:
+ ParsedError if an error was found, None otherwise
+ """
+ errors = self.get_all_errors(compilation_output)
+ return errors[0] if errors else None
+
+ def get_all_errors(self, compilation_output: str) -> List[ParsedError]:
+ """
+ Parse all errors from compilation output.
+
+ Args:
+ compilation_output: The stdout/stderr from compilation
+
+ Returns:
+ List of ParsedError objects
+ """
+ import re
+
+ errors: List[ParsedError] = []
+
+ # --- Pattern 1: Legacy C# style file(line,col): error: message ---
+ legacy_pattern = r'([^(\n]+)\((\d+),\s*(\d+)\):\s*(error|warning):\s*(.+?)(?=\n[^\s]|\Z)'
+ for match in re.finditer(legacy_pattern, compilation_output, re.MULTILINE | re.DOTALL):
+ errors.append(
+ ParsedError(
+ file_path=match.group(1).strip(),
+ line_number=int(match.group(2)),
+ column_number=int(match.group(3)),
+ error_type=match.group(4),
+ message=match.group(5).strip(),
+ )
+ )
+
+ # --- Pattern 2: [File.p] parse error: line X:Y message ---
+ parser_pattern = r'\[([^\]]+\.p)\]\s*(parse error|error):\s*line\s*(\d+):(\d+)\s*(.+)'
+ for match in re.finditer(parser_pattern, compilation_output, re.IGNORECASE | re.MULTILINE):
+ errors.append(
+ ParsedError(
+ file_path=match.group(1).strip(),
+ line_number=int(match.group(3)),
+ column_number=int(match.group(4)),
+ error_type=match.group(2).lower(),
+ message=match.group(5).strip(),
+ )
+ )
+
+ # --- Pattern 3: [Error:] / [Parser Error:] [filepath.p:line:col] message ---
+ # The P compiler sometimes emits a tag on one line and the location
+ # on the next, e.g.:
+ # [Error:]
+ # [generated_code/.../Client.p:29:24] got type: bool, expected: ProposedValue
+ bracket_pattern = (
+ r'\[(?:Error|Parser Error):\]\s*'
+ r'\[([^\]]+\.p):(\d+):(\d+)\]\s*(.+)'
+ )
+ for match in re.finditer(bracket_pattern, compilation_output, re.IGNORECASE | re.DOTALL):
+ errors.append(
+ ParsedError(
+ file_path=match.group(1).strip(),
+ line_number=int(match.group(2)),
+ column_number=int(match.group(3)),
+ error_type="error",
+ message=match.group(4).strip(),
+ )
+ )
+
+ # Clean up error messages: strip P tool trailer
+ for err in errors:
+ err.message = re.sub(r'\s*~~\s*\[PTool\].*$', '', err.message, flags=re.DOTALL).strip()
+
+ # Deduplicate while preserving order.
+ deduped: List[ParsedError] = []
+ seen = set()
+ for err in errors:
+ key = (err.file_path, err.line_number, err.column_number, err.message)
+ if key not in seen:
+ seen.add(key)
+ deduped.append(err)
+
+ return deduped
+
+ def run_checker(
+ self,
+ project_path: str,
+ schedules: int = 100,
+ timeout: Optional[int] = None,
+ test_name: Optional[str] = None,
+ max_steps: int = 10000,
+ ) -> CheckerResult:
+ """
+ Run PChecker on a P project.
+
+ Args:
+ project_path: Path to the P project
+ schedules: Number of schedules to explore
+ timeout: Timeout in seconds per test (None = no timeout, rely on schedule count)
+ test_name: Optional specific test to run
+ max_steps: Maximum steps per schedule before PChecker moves on
+
+ Returns:
+ CheckerResult with test results
+ """
+ self._status(f"Running PChecker with {schedules} schedules...")
+
+ try:
+ # Import checker utils
+ from utils import checker_utils
+
+ results, trace_dicts, trace_logs = checker_utils.try_pchecker(
+ project_path,
+ schedules=schedules,
+ timeout=timeout,
+ max_steps=max_steps,
+ )
+
+ # Build results
+ test_results = {}
+ passed_tests = []
+ failed_tests = []
+
+ for test, passed in results.items():
+ test_results[test] = passed
+ if passed:
+ passed_tests.append(test)
+ else:
+ failed_tests.append(test)
+
+ if not results:
+ # No test cases discovered – compilation succeeded but
+ # the test driver is missing 'test' declarations.
+ self._warning(
+ "No test cases found. Ensure the PTst file declares "
+ "tests, e.g.: test tcFoo [main=TestMachine]: "
+ "assert Safety in { ... };"
+ )
+ return CheckerResult(
+ success=False,
+ error="No test cases found. The test driver may be missing 'test' declarations.",
+ test_results=test_results,
+ passed_tests=passed_tests,
+ failed_tests=failed_tests,
+ trace_logs=trace_logs,
+ )
+
+ all_passed = all(results.values())
+
+ if all_passed:
+ self._status(f"All {len(passed_tests)} tests passed")
+ else:
+ self._warning(f"{len(failed_tests)} test(s) failed")
+
+ return CheckerResult(
+ success=all_passed,
+ test_results=test_results,
+ passed_tests=passed_tests,
+ failed_tests=failed_tests,
+ trace_logs=trace_logs,
+ )
+
+ except Exception as e:
+ err_msg = f"{type(e).__name__}: {e}"
+ logger.error(f"PChecker error: {err_msg}\n{traceback.format_exc()}")
+ return CheckerResult(
+ success=False,
+ error=err_msg,
+ )
+
+ def get_project_files(self, project_path: str) -> Dict[str, str]:
+ """
+ Get all P files in a project.
+
+ Args:
+ project_path: Path to the P project
+
+ Returns:
+ Dictionary mapping relative file paths to contents
+ """
+ files = {}
+ project = Path(project_path)
+
+ for folder in ['PSrc', 'PSpec', 'PTst']:
+ folder_path = project / folder
+ if folder_path.exists():
+ for p_file in folder_path.glob('*.p'):
+ rel_path = f"{folder}/{p_file.name}"
+ try:
+ files[rel_path] = p_file.read_text(encoding='utf-8')
+ except Exception as e:
+ logger.warning(f"Could not read {rel_path}: {e}")
+
+ return files
+
+ def read_file(self, file_path: str) -> Optional[str]:
+ """
+ Read a single file's contents.
+
+ Args:
+ file_path: Path to the file
+
+ Returns:
+ File contents or None if file doesn't exist
+ """
+ try:
+ return Path(file_path).read_text(encoding='utf-8')
+ except Exception as e:
+ logger.warning(f"Could not read {file_path}: {e}")
+ return None
+
+ def write_file(self, file_path: str, content: str) -> bool:
+ """
+ Write content to a file.
+
+ Args:
+ file_path: Path to the file
+ content: Content to write
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ Path(file_path).parent.mkdir(parents=True, exist_ok=True)
+ Path(file_path).write_text(content, encoding='utf-8')
+ return True
+ except Exception as e:
+ logger.error(f"Could not write {file_path}: {e}")
+ return False
diff --git a/Src/PeasyAI/src/core/services/fixer.py b/Src/PeasyAI/src/core/services/fixer.py
new file mode 100644
index 0000000000..42213cbed1
--- /dev/null
+++ b/Src/PeasyAI/src/core/services/fixer.py
@@ -0,0 +1,1339 @@
+"""
+Fixer Service
+
+Handles automatic fixing of compilation and checker errors
+with human-in-the-loop fallback.
+
+Enhanced with:
+- Structured error parsing
+- Specialized fixers for common errors
+- Better feedback in results
+"""
+
+import re
+import json
+import logging
+import traceback
+from pathlib import Path
+from typing import Dict, Any, Optional, List
+from dataclasses import dataclass, field
+
+from .base import BaseService, ServiceResult, EventCallback, ResourceLoader
+from .compilation import CompilationService, ParsedError
+from ..llm import LLMProvider, LLMConfig, Message, MessageRole
+
+# Import new compilation utilities
+try:
+ from ..compilation import (
+ PCompilerErrorParser,
+ PCompilerError,
+ ErrorCategory,
+ PErrorFixer,
+ CodeFix,
+ fix_all_errors,
+ parse_compilation_output,
+ # Checker error handling
+ PCheckerErrorParser,
+ CheckerError,
+ CheckerErrorCategory,
+ TraceAnalysis,
+ PCheckerErrorFixer,
+ CheckerFix,
+ analyze_and_suggest_fix,
+ )
+ HAS_NEW_FIXERS = True
+ HAS_CHECKER_FIXERS = True
+except ImportError:
+ HAS_NEW_FIXERS = False
+ HAS_CHECKER_FIXERS = False
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class FixResult(ServiceResult):
+ """Result of a fix operation"""
+ fixed: bool = False
+ filename: Optional[str] = None
+ file_path: Optional[str] = None
+ original_code: Optional[str] = None
+ fixed_code: Optional[str] = None
+ attempt_number: int = 0
+ needs_guidance: bool = False
+ guidance_request: Optional[Dict[str, Any]] = None
+ # Enhanced analysis fields
+ analysis: Optional[Dict[str, Any]] = None
+ root_cause: Optional[str] = None
+ suggested_fixes: Optional[List[str]] = None
+ confidence: float = 0.0
+
+
+def build_checker_feedback(
+ check_result: Optional[Dict[str, Any]] = None,
+ fix_response: Optional[Dict[str, Any]] = None,
+) -> str:
+ """
+ Build a structured checker-bug report that can be injected into
+ generation context so the LLM avoids the same mistakes when
+ regenerating spec or test files.
+
+ Args:
+ check_result: The result from ``p_check`` (has ``failed_tests``,
+ ``passed_tests``, ``error``).
+ fix_response: The response from ``fix_buggy_program`` (has
+ ``analysis``, ``root_cause``, ``suggested_fixes``).
+
+ Returns:
+ A string wrapped in ```` tags. Empty string
+ if there is no useful information to report.
+ """
+ analysis = (fix_response or {}).get("analysis")
+ check = check_result or {}
+ failed = check.get("failed_tests", [])
+ passed = check.get("passed_tests", [])
+ error = check.get("error", "")
+
+ if not analysis:
+ if not failed and not error:
+ return ""
+ parts = [""]
+ if failed:
+ parts.append(f"Failed tests: {', '.join(failed)}")
+ if error:
+ parts.append(f"Error: {error}")
+ parts.append(
+ "When regenerating, ensure that:\n"
+ "- The spec monitor correctly tracks the protocol invariant.\n"
+ "- The test driver creates machines in the right order and "
+ "passes the correct config payloads.\n"
+ "- Every state handles or ignores all events that can arrive."
+ )
+ parts.append("")
+ return "\n".join(parts)
+
+ parts = [""]
+ parts.append("PChecker found a bug during model checking.")
+
+ cat = analysis.get("error_category", "unknown")
+ msg = analysis.get("error_message", "")
+ machine = analysis.get("machine", "")
+ state = analysis.get("state", "")
+ event = analysis.get("event", "")
+
+ parts.append(f"Category: {cat}")
+ if msg:
+ parts.append(f"Error: {msg}")
+ if machine:
+ parts.append(f"Machine: {machine}, State: {state}")
+ if event:
+ parts.append(f"Event: {event}")
+
+ root_cause = (fix_response or {}).get("root_cause", "")
+ if root_cause:
+ parts.append(f"Root cause: {root_cause}")
+
+ suggested = (fix_response or {}).get("suggested_fixes") or []
+ if suggested:
+ parts.append("Suggested fixes:")
+ for s in suggested[:5]:
+ parts.append(f" - {s}")
+
+ if failed:
+ parts.append(f"Failed tests: {', '.join(failed)}")
+ if passed:
+ parts.append(f"Passed tests: {', '.join(passed)}")
+
+ parts.append(
+ "\nWhen regenerating this file, carefully avoid the bug described above."
+ )
+ parts.append("")
+ return "\n".join(parts)
+
+
+class FixAttemptTracker:
+ """
+ Tracks fix attempts for human-in-the-loop fallback.
+
+ After a configurable number of failed attempts (default 3),
+ the fixer will request human guidance.
+ """
+
+ def __init__(self, max_attempts: int = 3):
+ self.max_attempts = max_attempts
+ self._attempts: Dict[str, List[str]] = {}
+
+ def add_attempt(self, error_key: str, description: str):
+ """Record an attempted fix"""
+ if error_key not in self._attempts:
+ self._attempts[error_key] = []
+ self._attempts[error_key].append(description)
+
+ def get_attempt_count(self, error_key: str) -> int:
+ """Get number of attempts for an error"""
+ return len(self._attempts.get(error_key, []))
+
+ def get_attempts(self, error_key: str) -> List[str]:
+ """Get all attempt descriptions for an error"""
+ return self._attempts.get(error_key, [])
+
+ def should_request_guidance(self, error_key: str) -> bool:
+ """Check if we should request human guidance"""
+ return self.get_attempt_count(error_key) >= self.max_attempts
+
+ def clear(self, error_key: str):
+ """Clear attempts for an error (after successful fix)"""
+ if error_key in self._attempts:
+ del self._attempts[error_key]
+
+ def clear_all(self):
+ """Clear all tracked attempts"""
+ self._attempts.clear()
+
+
+class FixerService(BaseService):
+ """
+ Service for automatically fixing P code errors.
+
+ Features:
+ - Compilation error fixing
+ - Checker error fixing
+ - Human-in-the-loop fallback after max retries
+ - Context-aware fixing using project files
+ """
+
+ def __init__(
+ self,
+ llm_provider: Optional[LLMProvider] = None,
+ resource_loader: Optional[ResourceLoader] = None,
+ callbacks: Optional[EventCallback] = None,
+ compilation_service: Optional[CompilationService] = None,
+ max_attempts: int = 3,
+ ):
+ super().__init__(llm_provider, resource_loader, callbacks)
+ self._compilation = compilation_service or CompilationService(
+ llm_provider, resource_loader, callbacks
+ )
+ self._tracker = FixAttemptTracker(max_attempts)
+
+ @property
+ def tracker(self) -> FixAttemptTracker:
+ """Get the fix attempt tracker"""
+ return self._tracker
+
+ def _try_specialized_fix(
+ self,
+ error: ParsedError,
+ file_content: str,
+ ) -> Optional[CodeFix]:
+ """
+ Try to fix an error using specialized fixers.
+ Returns CodeFix if successful, None otherwise.
+ """
+ if not HAS_NEW_FIXERS:
+ return None
+
+ try:
+ # Convert ParsedError to PCompilerError
+ p_error = PCompilerError(
+ file=Path(error.file_path).name,
+ line=error.line_number,
+ column=error.column_number,
+ error_type=ErrorCategory.UNKNOWN,
+ category=PCompilerErrorParser._categorize_error(error.message),
+ message=error.message,
+ raw_message=error.message,
+ )
+
+ # Try specialized fixer
+ fixer = PErrorFixer()
+ if fixer.can_fix(p_error):
+ fix = fixer.fix(p_error, file_content)
+ if fix:
+ logger.info(f"Specialized fixer applied: {fix.description}")
+ return fix
+ except Exception as e:
+ logger.debug(f"Specialized fixer failed: {e}")
+
+ return None
+
+ def fix_compilation_error(
+ self,
+ project_path: str,
+ error: ParsedError,
+ user_guidance: Optional[str] = None,
+ ) -> FixResult:
+ """
+ Fix a compilation error.
+
+ First tries specialized fixers for common errors, then falls back to LLM.
+
+ Args:
+ project_path: Path to the P project
+ error: Parsed error to fix
+ user_guidance: Optional guidance from user after failed attempts
+
+ Returns:
+ FixResult with fix details
+ """
+ resolved_file_path = self._resolve_error_file_path(project_path, error.file_path)
+ error_key = f"compile:{resolved_file_path}:{hash(error.message)}"
+ attempt = self._tracker.get_attempt_count(error_key) + 1
+
+ self._status(f"Fixing compilation error (attempt {attempt}): {error.message[:50]}...")
+
+ # Check if we should request guidance
+ if self._tracker.should_request_guidance(error_key) and not user_guidance:
+ return self._create_guidance_request(
+ error_key,
+ "compilation",
+ error,
+ project_path,
+ )
+
+ try:
+ # Read the file with the error
+ file_content = self._compilation.read_file(resolved_file_path)
+ if file_content is None:
+ return FixResult(
+ success=False,
+ error=f"Could not read file: {resolved_file_path}",
+ )
+
+ # First try specialized fixers (faster, more reliable)
+ specialized_fix = self._try_specialized_fix(error, file_content)
+ if specialized_fix:
+ # Apply the fix
+ self._compilation.write_file(resolved_file_path, specialized_fix.fixed_code)
+
+ # Verify by recompiling
+ compile_result = self._compilation.compile(project_path)
+
+ if compile_result.success:
+ self._tracker.clear(error_key)
+ self._status(f"Fix successful (specialized): {specialized_fix.description}")
+
+ return FixResult(
+ success=True,
+ fixed=True,
+ filename=Path(resolved_file_path).name,
+ file_path=resolved_file_path,
+ original_code=file_content,
+ fixed_code=specialized_fix.fixed_code,
+ attempt_number=attempt,
+ )
+ else:
+ # Revert the fix and try LLM
+ self._compilation.write_file(resolved_file_path, file_content)
+ logger.info("Specialized fix didn't resolve error, trying LLM")
+
+ # Fall back to LLM-based fixing
+ # Build fix messages — include all project files for cross-file context
+ messages = self._build_compile_fix_messages(
+ error,
+ file_content,
+ project_path,
+ user_guidance,
+ all_project_files=self._compilation.get_project_files(project_path),
+ )
+
+ # Get system prompt
+ system_prompt = self.resources.load_context("about_p.txt")
+
+ # Invoke LLM
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(messages, config, system_prompt)
+
+ # Extract fixed code
+ filename, fixed_code = self._extract_p_code(response.content)
+
+ if filename and fixed_code:
+ # Write the fix
+ self._compilation.write_file(resolved_file_path, fixed_code)
+
+ # Verify by recompiling
+ compile_result = self._compilation.compile(project_path)
+
+ if compile_result.success:
+ # Fix worked!
+ self._tracker.clear(error_key)
+ self._status("Fix successful!")
+
+ return FixResult(
+ success=True,
+ fixed=True,
+ filename=filename,
+ file_path=resolved_file_path,
+ original_code=file_content,
+ fixed_code=fixed_code,
+ attempt_number=attempt,
+ token_usage=response.usage.to_dict(),
+ )
+ else:
+ # Fix didn't work
+ self._tracker.add_attempt(
+ error_key,
+ f"Attempt {attempt}: {compile_result.stdout[:200]}"
+ )
+
+ return FixResult(
+ success=False,
+ fixed=False,
+ filename=filename,
+ file_path=resolved_file_path,
+ original_code=file_content,
+ fixed_code=fixed_code,
+ attempt_number=attempt,
+ error=compile_result.stdout,
+ token_usage=response.usage.to_dict(),
+ )
+ else:
+ return FixResult(
+ success=False,
+ error="Could not extract fixed code from LLM response",
+ attempt_number=attempt,
+ )
+
+ except Exception as e:
+ err_msg = f"{type(e).__name__}: {e}"
+ logger.error(f"Error fixing compilation error: {err_msg}\n{traceback.format_exc()}")
+ return FixResult(
+ success=False,
+ error=err_msg,
+ attempt_number=attempt,
+ )
+
+ def fix_checker_error(
+ self,
+ project_path: str,
+ trace_log: str,
+ error_category: Optional[str] = None,
+ user_guidance: Optional[str] = None,
+ ) -> FixResult:
+ """
+ Fix a PChecker error with enhanced analysis.
+
+ Args:
+ project_path: Path to the P project
+ trace_log: The error trace from PChecker
+ error_category: Optional category (e.g., 'assertion', 'deadlock')
+ user_guidance: Optional guidance from user
+
+ Returns:
+ FixResult with fix details and analysis
+ """
+ error_key = f"checker:{project_path}:{hash(trace_log[:500])}"
+ attempt = self._tracker.get_attempt_count(error_key) + 1
+
+ self._status(f"Fixing checker error (attempt {attempt})...")
+
+ # Read all project files
+ project_files = self._compilation.get_project_files(project_path)
+
+ # Step 1: Analyze the trace using specialized parser
+ analysis_dict = {}
+ root_cause = None
+ suggested_fixes = []
+
+ if HAS_CHECKER_FIXERS:
+ try:
+ trace_analysis, specialized_fix = analyze_and_suggest_fix(
+ trace_log, project_path, project_files
+ )
+
+ # Build analysis dict for response with enhanced context
+ analysis_dict = {
+ "error_category": trace_analysis.error.category.value,
+ "error_message": trace_analysis.error.message,
+ "machine": trace_analysis.error.machine,
+ "state": trace_analysis.error.machine_state,
+ "event": trace_analysis.error.event_name,
+ "execution_steps": trace_analysis.execution_steps,
+ "machines_involved": trace_analysis.machines_involved,
+ "last_actions": trace_analysis.last_actions,
+ }
+
+ # Include enhanced analysis in response
+ if trace_analysis.error.sender_info:
+ sender = trace_analysis.error.sender_info
+ analysis_dict["sender"] = {
+ "machine": sender.machine,
+ "state": sender.state,
+ "is_test_driver": sender.is_test_driver,
+ "is_initialization_pattern": sender.is_initialization_pattern,
+ "semantic_mismatch": sender.semantic_mismatch,
+ }
+ if trace_analysis.error.cascading_impact:
+ cascade = trace_analysis.error.cascading_impact
+ analysis_dict["cascading_impact"] = {
+ "unhandled_in": cascade.unhandled_in,
+ "broadcasters": cascade.broadcasters,
+ "all_receivers": cascade.all_receivers,
+ }
+ analysis_dict["is_test_driver_bug"] = trace_analysis.error.is_test_driver_bug
+ analysis_dict["requires_new_event"] = trace_analysis.error.requires_new_event
+ analysis_dict["requires_multi_file_fix"] = trace_analysis.error.requires_multi_file_fix
+ if specialized_fix:
+ analysis_dict["fix_strategy"] = specialized_fix.fix_strategy
+ analysis_dict["is_multi_file_fix"] = specialized_fix.is_multi_file
+
+ root_cause = trace_analysis.error.root_cause
+ suggested_fixes = trace_analysis.error.suggested_fixes
+
+ self._status(f"Identified error: {trace_analysis.error.category.value}")
+
+ # Step 2: Try specialized fixer first
+ if specialized_fix:
+ self._status(f"Applying specialized fix ({specialized_fix.fix_strategy or 'auto'}): {specialized_fix.description}")
+
+ # Collect all file backups for potential revert
+ backups = {}
+
+ # Apply primary fix
+ backups[specialized_fix.file_path] = specialized_fix.original_code
+ self._compilation.write_file(
+ specialized_fix.file_path,
+ specialized_fix.fixed_code
+ )
+
+ # Apply additional patches for multi-file fixes
+ if specialized_fix.is_multi_file and specialized_fix.additional_patches:
+ for patch in specialized_fix.additional_patches:
+ backups[patch.file_path] = patch.original_code
+ self._compilation.write_file(
+ patch.file_path,
+ patch.fixed_code
+ )
+ self._status(
+ f"Applied {1 + len(specialized_fix.additional_patches)} "
+ f"file patches (strategy: {specialized_fix.fix_strategy})"
+ )
+
+ # Verify by recompiling
+ compile_result = self._compilation.compile(project_path)
+
+ if compile_result.success:
+ # Re-check with enough schedules to catch intermittent bugs
+ checker_result = self._compilation.run_checker(
+ project_path, schedules=50, timeout=60
+ )
+
+ if checker_result.success:
+ # Vacuous pass detection: check if safety specs observed events
+ vacuous_warning = self._check_vacuous_pass(
+ project_path, project_files, trace_analysis
+ )
+
+ self._tracker.clear(error_key)
+ self._status("Fix successful (specialized fixer)!")
+
+ result = FixResult(
+ success=True,
+ fixed=True,
+ filename=Path(specialized_fix.file_path).name,
+ file_path=specialized_fix.file_path,
+ original_code=specialized_fix.original_code,
+ fixed_code=specialized_fix.fixed_code,
+ attempt_number=attempt,
+ analysis=analysis_dict,
+ root_cause=root_cause,
+ suggested_fixes=suggested_fixes,
+ confidence=specialized_fix.confidence,
+ )
+ if vacuous_warning:
+ analysis_dict["vacuous_pass_warning"] = vacuous_warning
+ return result
+
+ # Revert ALL patches if fix didn't work
+ for file_path, original_code in backups.items():
+ self._compilation.write_file(file_path, original_code)
+ logger.info("Specialized fix didn't resolve error, trying LLM")
+
+ except Exception as e:
+ logger.warning(f"Specialized analysis failed: {e}")
+
+ # Step 3: Check if we should request guidance
+ if self._tracker.should_request_guidance(error_key) and not user_guidance:
+ return self._create_enhanced_checker_guidance_request(
+ error_key,
+ trace_log,
+ project_path,
+ analysis_dict,
+ root_cause,
+ suggested_fixes,
+ )
+
+ # Step 4: Fall back to LLM-based fixing
+ try:
+ # Build fix messages with enhanced context (including sender & cascading analysis)
+ messages = self._build_checker_fix_messages(
+ trace_log,
+ project_files,
+ error_category,
+ user_guidance,
+ analysis_dict=analysis_dict,
+ root_cause=root_cause,
+ suggested_fixes=suggested_fixes,
+ )
+
+ # Get system prompt
+ system_prompt = self.resources.load_context("about_p.txt")
+
+ # Invoke LLM
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(messages, config, system_prompt)
+
+ # Extract all fixed files
+ patches = self._extract_all_p_code(response.content)
+
+ if patches:
+ # Apply patches
+ for filename, fixed_code in patches.items():
+ # Determine full path
+ for folder in ['PSrc', 'PSpec', 'PTst']:
+ full_path = Path(project_path) / folder / filename
+ if full_path.exists():
+ self._compilation.write_file(str(full_path), fixed_code)
+ break
+ else:
+ # Default to PSrc for new files
+ full_path = Path(project_path) / 'PSrc' / filename
+ self._compilation.write_file(str(full_path), fixed_code)
+
+ # Verify by recompiling and rechecking
+ compile_result = self._compilation.compile(project_path)
+
+ if compile_result.success:
+ # Re-check with enough schedules to catch intermittent bugs
+ checker_result = self._compilation.run_checker(
+ project_path, schedules=50, timeout=60
+ )
+
+ if checker_result.success:
+ self._tracker.clear(error_key)
+ self._status("Fix successful (LLM)!")
+
+ return FixResult(
+ success=True,
+ fixed=True,
+ attempt_number=attempt,
+ token_usage=response.usage.to_dict(),
+ analysis=analysis_dict,
+ root_cause=root_cause,
+ suggested_fixes=suggested_fixes,
+ )
+
+ # Fix didn't work
+ self._tracker.add_attempt(
+ error_key,
+ f"Attempt {attempt}: Modified {list(patches.keys())}"
+ )
+
+ return FixResult(
+ success=False,
+ fixed=False,
+ attempt_number=attempt,
+ token_usage=response.usage.to_dict(),
+ analysis=analysis_dict,
+ root_cause=root_cause,
+ suggested_fixes=suggested_fixes,
+ error="Fix was applied but error persists",
+ )
+ else:
+ return FixResult(
+ success=False,
+ error="Could not extract fixed code from LLM response",
+ attempt_number=attempt,
+ analysis=analysis_dict,
+ root_cause=root_cause,
+ suggested_fixes=suggested_fixes,
+ )
+
+ except Exception as e:
+ err_msg = f"{type(e).__name__}: {e}"
+ logger.error(f"Error fixing checker error: {err_msg}\n{traceback.format_exc()}")
+ return FixResult(
+ success=False,
+ error=err_msg,
+ attempt_number=attempt,
+ analysis=analysis_dict,
+ root_cause=root_cause,
+ suggested_fixes=suggested_fixes,
+ )
+
+ def _create_enhanced_checker_guidance_request(
+ self,
+ error_key: str,
+ trace_log: str,
+ project_path: str,
+ analysis: Dict[str, Any],
+ root_cause: Optional[str],
+ suggested_fixes: List[str],
+ ) -> FixResult:
+ """Create an enhanced guidance request with analysis."""
+ previous_attempts = self._tracker.get_attempts(error_key)
+ trace_summary = trace_log.splitlines()[-10:]
+
+ return FixResult(
+ success=False,
+ fixed=False,
+ needs_guidance=True,
+ guidance_request={
+ "context": f"Attempting to fix PChecker error in {project_path}",
+ "problem": root_cause or "PChecker found a violation during model checking",
+ "trace_summary": "\n".join(trace_summary),
+ "attempts": previous_attempts,
+ "questions": [
+ "What is the expected behavior when this event occurs?",
+ "Should the state machine handle this event differently?",
+ "Is there a race condition or ordering issue to address?",
+ ],
+ "suggested_actions": suggested_fixes or [
+ "Explain the expected state transition sequence",
+ "Provide invariants that should hold",
+ "Share the correct event handling logic",
+ ],
+ },
+ attempt_number=self._tracker.get_attempt_count(error_key),
+ analysis=analysis,
+ root_cause=root_cause,
+ suggested_fixes=suggested_fixes,
+ )
+
+ @staticmethod
+ def _resolve_error_path(error: 'ParsedError', project_path: str) -> None:
+ """
+ Resolve a potentially-relative file_path inside a ParsedError to an
+ absolute path so downstream read/write calls succeed regardless of cwd.
+ """
+ from pathlib import Path as _Path
+
+ fp = _Path(error.file_path)
+ if fp.is_absolute() and fp.exists():
+ return
+ # Try relative to the project directory first (most common)
+ candidate = _Path(project_path) / error.file_path
+ if candidate.exists():
+ error.file_path = str(candidate)
+ return
+ # Try just the basename inside PSrc / PSpec / PTst
+ basename = fp.name
+ for sub in ('PSrc', 'PSpec', 'PTst'):
+ candidate = _Path(project_path) / sub / basename
+ if candidate.exists():
+ error.file_path = str(candidate)
+ return
+
+ @staticmethod
+ def _normalize_error_for_spiral(msg: str) -> str:
+ """
+ Collapse variations of the same root-cause error into a single key.
+
+ For example, these should all be treated as the same spiral:
+ - "no viable alternative at input 'totalRooms=5)'"
+ - "mismatched input '=' expecting ')'"
+ - "no viable alternative at input '(totalRooms:'"
+ All stem from using named-field tuple construction.
+
+ Similarly, "could not find spec machine 'X'" and
+ "could not find spec machine 'Y'" are the same class.
+ """
+ import re as _re
+
+ m = msg[:120]
+
+ # Named-field tuple errors
+ if _re.search(r"no viable alternative.*\w+\s*=", m):
+ return "NAMED_FIELD_TUPLE"
+ if _re.search(r"mismatched input '=' expecting", m):
+ return "NAMED_FIELD_TUPLE"
+ if _re.search(r"missing Iden at '\)'", m):
+ return "NAMED_FIELD_TUPLE"
+ if _re.search(r"no viable alternative.*\(\w+:", m):
+ return "NAMED_FIELD_TUPLE_PARAM"
+
+ # Undefined-reference errors (same class regardless of the name)
+ if _re.search(r"could not find (spec machine|interface|event|type)", m):
+ return "UNDEFINED_REF"
+
+ # Extraneous var (always var-declaration-order)
+ if "extraneous input 'var'" in m:
+ return "VAR_ORDER"
+
+ # Fallback: first 80 chars (original behaviour)
+ return m[:80]
+
+ def fix_iteratively(
+ self,
+ project_path: str,
+ max_iterations: int = 10,
+ ) -> Dict[str, Any]:
+ """
+ Iteratively fix compilation errors until success or max iterations.
+
+ Args:
+ project_path: Path to the P project
+ max_iterations: Maximum fix iterations
+
+ Returns:
+ Dictionary with final status and iteration details
+ """
+ self._status("Starting iterative compilation fix...")
+
+ results = {
+ "iterations": [],
+ "success": False,
+ "total_iterations": 0,
+ }
+
+ # Track error messages to detect spirals (same error repeating)
+ recent_errors: List[str] = []
+ SPIRAL_THRESHOLD = 3
+
+ for i in range(max_iterations):
+ self._progress("Compilation fix", i + 1, max_iterations)
+
+ # Compile
+ compile_result = self._compilation.compile(project_path)
+
+ if compile_result.success:
+ self._status(f"Compilation succeeded after {i} iterations")
+ results["success"] = True
+ results["total_iterations"] = i
+ break
+
+ # Parse error – try stdout first, then stderr as fallback
+ combined = compile_result.stdout or ""
+ if compile_result.stderr:
+ combined = combined + "\n" + compile_result.stderr
+ error = self._compilation.parse_error(combined)
+
+ if error is None:
+ self._error("Could not parse compilation error")
+ results["iterations"].append({
+ "iteration": i + 1,
+ "error": "Could not parse error",
+ "output": combined[:500],
+ })
+ break
+
+ # Detect spiral: if the same *root-cause* error repeats, try
+ # incremental regeneration once before giving up.
+ # Normalise the message so slight variations of the same underlying
+ # bug (e.g. "no viable alternative at input 'totalRooms=5)'" vs
+ # "mismatched input '=' expecting ')'") are treated as one.
+ core_msg = self._normalize_error_for_spiral(error.message)
+ recent_errors.append(core_msg)
+ if recent_errors.count(core_msg) >= SPIRAL_THRESHOLD:
+ # Attempt incremental regeneration: ask the LLM to rewrite
+ # the failing file from scratch with the error as context.
+ regen_result = self._try_incremental_regeneration(
+ project_path, error
+ )
+ if regen_result:
+ self._status(f"Incremental regeneration applied for {error.file_path}")
+ results["iterations"].append({
+ "iteration": i + 1,
+ "error": error.message,
+ "fixed": False,
+ "needs_guidance": False,
+ "incremental_regen": True,
+ })
+ # Clear spiral counter so the next compile gets a fresh shot
+ recent_errors.clear()
+ continue
+ else:
+ self._warning(
+ f"Spiral detected: same error '{core_msg}' seen "
+ f"{SPIRAL_THRESHOLD} times. Stopping."
+ )
+ results["iterations"].append({
+ "iteration": i + 1,
+ "error": error.message,
+ "fixed": False,
+ "needs_guidance": False,
+ "spiral_detected": True,
+ })
+ break
+
+ # Resolve relative paths so read/write works
+ self._resolve_error_path(error, project_path)
+
+ # Try to fix
+ fix_result = self.fix_compilation_error(project_path, error)
+
+ results["iterations"].append({
+ "iteration": i + 1,
+ "error": error.message,
+ "fixed": fix_result.fixed,
+ "needs_guidance": fix_result.needs_guidance,
+ })
+
+ if fix_result.needs_guidance:
+ self._warning("Human guidance needed")
+ results["needs_guidance"] = True
+ results["guidance_request"] = fix_result.guidance_request
+ break
+ else:
+ self._warning(f"Max iterations ({max_iterations}) reached")
+ results["total_iterations"] = max_iterations
+
+ return results
+
+ def _try_incremental_regeneration(
+ self,
+ project_path: str,
+ error: 'ParsedError',
+ ) -> bool:
+ """
+ Attempt to regenerate just the failing file from scratch.
+
+ Reads all other project files as context, asks the LLM to rewrite
+ the broken file incorporating the error message, and writes it back.
+
+ Returns True if a new version was written, False otherwise.
+ """
+ try:
+ resolved = self._resolve_error_file_path(project_path, error.file_path)
+ old_code = self._compilation.read_file(resolved)
+ if old_code is None:
+ return False
+
+ all_files = self._compilation.get_project_files(project_path)
+ filename = Path(resolved).name
+
+ # Build context from all *other* files
+ context_parts = []
+ for rel_path, content in all_files.items():
+ if Path(rel_path).name != filename:
+ context_parts.append(f"<{Path(rel_path).name}>\n{content}\n{Path(rel_path).name}>")
+
+ system_prompt = self.resources.load_context("about_p.txt")
+ messages = [
+ Message(role=MessageRole.USER, content="\n".join(context_parts)),
+ Message(role=MessageRole.USER, content=(
+ f"The file {filename} has a compilation error that could not be fixed "
+ f"after multiple attempts:\n"
+ f" Error at line {error.line_number}: {error.message}\n\n"
+ f"Current broken code:\n```\n{old_code}\n```\n\n"
+ f"Please rewrite {filename} from scratch so it compiles correctly. "
+ f"Use the types, events, and machines defined in the other project files above. "
+ f"Return the complete file wrapped in <{filename}>...{filename}> tags."
+ )),
+ ]
+
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(messages, config, system_prompt)
+
+ # Extract code using the shared robust extractor
+ from ..compilation.p_code_utils import extract_p_code_from_response
+ _, new_code = extract_p_code_from_response(
+ response.content, expected_filename=filename
+ )
+ if new_code:
+ self._compilation.write_file(resolved, new_code)
+ return True
+
+ except Exception as e:
+ logger.warning(f"Incremental regeneration failed: {e}")
+
+ return False
+
+ # =========================================================================
+ # Vacuous pass detection
+ # =========================================================================
+
+ def _check_vacuous_pass(
+ self,
+ project_path: str,
+ project_files: Dict[str, str],
+ trace_analysis: Any = None,
+ ) -> Optional[str]:
+ """
+ Check if a fix might have caused the test to pass vacuously.
+
+ A vacuous pass occurs when a safety specification monitors events that
+ are never delivered (e.g., because all are ignored), so the spec
+ trivially passes without actually verifying anything.
+
+ Returns a warning string if a vacuous pass is suspected, None otherwise.
+ """
+ try:
+ # Find all spec machines and the events they observe
+ spec_events: Dict[str, List[str]] = {} # spec_name -> [events]
+ for filepath, content in project_files.items():
+ if not (filepath.startswith('PSpec/') or '/PSpec/' in filepath):
+ continue
+
+ # Find spec declarations: "spec SpecName observes event1, event2 {"
+ spec_pattern = r'spec\s+(\w+)\s+observes\s+([^{]+)\{'
+ for match in re.finditer(spec_pattern, content):
+ spec_name = match.group(1)
+ events_str = match.group(2).strip()
+ events = [e.strip() for e in events_str.split(',')]
+ spec_events[spec_name] = events
+
+ if not spec_events:
+ return None
+
+ # Check if any observed event is now universally ignored by all protocol machines
+ # Re-read current project files (after fix was applied)
+ current_files = self._compilation.get_project_files(project_path)
+
+ warnings = []
+ for spec_name, events in spec_events.items():
+ for event in events:
+ # Check if any protocol machine still sends this event
+ has_sender = False
+ for filepath, content in current_files.items():
+ if filepath.startswith('PSpec/') or filepath.startswith('PTst/'):
+ continue
+ send_pattern = rf'send\s+[^,]+\s*,\s*{re.escape(event)}\b'
+ if re.search(send_pattern, content):
+ has_sender = True
+ break
+
+ if not has_sender:
+ warnings.append(
+ f"Spec '{spec_name}' observes event '{event}' but no "
+ f"protocol machine sends it — the spec may pass vacuously."
+ )
+
+ if warnings:
+ return " | ".join(warnings)
+
+ except Exception as e:
+ logger.debug(f"Vacuous pass check failed: {e}")
+
+ return None
+
+ # =========================================================================
+ # Private helper methods
+ # =========================================================================
+
+ def _resolve_error_file_path(self, project_path: str, file_path: str) -> str:
+ """
+ Resolve compiler-reported file names to absolute project paths.
+ P compiler may emit either absolute paths or basename like 'Safety.p'.
+ """
+ candidate = Path(file_path)
+ if candidate.is_absolute() and candidate.exists():
+ return str(candidate)
+ if candidate.exists():
+ return str(candidate.resolve())
+
+ # Try common P project folders.
+ for folder in ("PSrc", "PSpec", "PTst"):
+ full_path = Path(project_path) / folder / file_path
+ if full_path.exists():
+ return str(full_path)
+
+ # Fallback to original string; callers will surface read/write errors.
+ return file_path
+
+ def _build_compile_fix_messages(
+ self,
+ error: ParsedError,
+ file_content: str,
+ project_path: str,
+ user_guidance: Optional[str],
+ all_project_files: Optional[Dict[str, str]] = None,
+ ) -> List[Message]:
+ """Build messages for compilation error fixing, including cross-file context."""
+ messages = []
+
+ # Add guides
+ p_basics = self.resources.load_modular_context("p_basics.txt")
+ compiler_guide = self.resources.load_modular_context("p_compiler_guide.txt")
+
+ try:
+ common_errors = self.resources.load_modular_context("p_common_compilation_errors.txt")
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{common_errors}\n"
+ ))
+ except:
+ pass
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{p_basics}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{compiler_guide}\n"
+ ))
+
+ # Add ALL project files for cross-file context (types, other machines, etc.)
+ error_filename = Path(error.file_path).name
+ if all_project_files:
+ for rel_path, content in all_project_files.items():
+ fname = Path(rel_path).name
+ if fname == error_filename:
+ continue # Will be added separately as the error file
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"<{fname}>\n{content}\n{fname}>"
+ ))
+
+ # Add the error file content
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"File with error ({error_filename}):\n```\n{file_content}\n```"
+ ))
+
+ # Add error details
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"""
+Compilation error at line {error.line_number}, column {error.column_number}:
+{error.message}
+"""
+ ))
+
+ # Add previous attempts
+ error_key = f"compile:{error.file_path}:{hash(error.message)}"
+ previous = self._tracker.get_attempts(error_key)
+ if previous:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content="Previous fix attempts that failed:\n" + "\n".join(f"- {a}" for a in previous)
+ ))
+
+ # Add user guidance
+ if user_guidance:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"User guidance: {user_guidance}"
+ ))
+
+ # Add instruction
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"""
+Please fix the compilation error in this P code.
+The error may be caused by mismatches with types/events defined in other project files shown above.
+Return the complete fixed file content wrapped in XML tags using the filename.
+Example: <{error_filename}>...fixed code...{error_filename}>
+"""
+ ))
+
+ return messages
+
+ def _build_checker_fix_messages(
+ self,
+ trace_log: str,
+ project_files: Dict[str, str],
+ error_category: Optional[str],
+ user_guidance: Optional[str],
+ analysis_dict: Optional[Dict[str, Any]] = None,
+ root_cause: Optional[str] = None,
+ suggested_fixes: Optional[List[str]] = None,
+ ) -> List[Message]:
+ """Build messages for checker error fixing with enhanced context."""
+ messages = []
+
+ # Add guide
+ p_basics = self.resources.load_modular_context("p_basics.txt")
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{p_basics}\n"
+ ))
+
+ # Add all project files, clearly labeled by folder
+ for filepath, content in project_files.items():
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"<{filepath}>\n{content}\n{filepath}>"
+ ))
+
+ # Add trace (last 50 lines)
+ trace_lines = trace_log.splitlines()[-50:]
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"PChecker Error Trace (last 50 lines):\n" + "\n".join(trace_lines)
+ ))
+
+ if error_category:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Error category: {error_category}"
+ ))
+
+ # Add enhanced analysis context
+ if root_cause:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Root cause analysis: {root_cause}"
+ ))
+
+ if analysis_dict:
+ # Include sender analysis
+ sender = analysis_dict.get("sender")
+ if sender:
+ sender_ctx = (
+ f"Sender analysis:\n"
+ f"- Event sent by: {sender.get('machine', 'unknown')} "
+ f"in state '{sender.get('state', 'unknown')}'\n"
+ f"- Is test driver: {sender.get('is_test_driver', False)}\n"
+ f"- Is initialization pattern: {sender.get('is_initialization_pattern', False)}"
+ )
+ if sender.get("semantic_mismatch"):
+ sender_ctx += f"\n- Semantic mismatch: {sender['semantic_mismatch']}"
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=sender_ctx
+ ))
+
+ # Include cascading impact
+ cascade = analysis_dict.get("cascading_impact")
+ if cascade and cascade.get("unhandled_in"):
+ cascade_ctx = "Cascading impact analysis:\n"
+ for machine, states in cascade["unhandled_in"].items():
+ cascade_ctx += f"- {machine} also lacks handler in states: {', '.join(states)}\n"
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=cascade_ctx
+ ))
+
+ # Include fix strategy hints
+ if analysis_dict.get("is_test_driver_bug"):
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=(
+ "IMPORTANT: This bug originates in the TEST DRIVER, not the protocol. "
+ "The test driver is misusing a protocol event for initialization. "
+ "The fix should introduce a new setup event and modify the test driver, "
+ "not just add ignore statements to protocol machines."
+ )
+ ))
+ if analysis_dict.get("requires_multi_file_fix"):
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=(
+ "IMPORTANT: This requires changes to MULTIPLE files. "
+ "Return ALL modified files, including types, machines, and test driver."
+ )
+ ))
+
+ if suggested_fixes:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content="Suggested fix approaches:\n" + "\n".join(
+ f"- {fix}" for fix in suggested_fixes
+ )
+ ))
+
+ # Add user guidance
+ if user_guidance:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"User guidance: {user_guidance}"
+ ))
+
+ # Add instruction
+ messages.append(Message(
+ role=MessageRole.USER,
+ content="""
+Analyze the PChecker trace and fix the error.
+
+CRITICAL RULES:
+1. If the bug is in a test driver (PTst/ file), fix the test driver — don't just mask
+ the symptom by adding ignore to protocol machines.
+2. If a protocol event is being misused for initialization, introduce a NEW dedicated
+ setup event in the types file and add handlers accordingly.
+3. If multiple machines are affected, fix ALL of them. Return every modified file.
+4. Ensure safety specifications can still observe the events they monitor
+ (don't make tests pass vacuously by suppressing all events).
+
+Return any modified files wrapped in XML tags using their filenames.
+Example: ...fixed types code...
+...fixed machine code...
+...fixed test code...
+"""
+ ))
+
+ return messages
+
+ def _create_guidance_request(
+ self,
+ error_key: str,
+ error_type: str,
+ error: ParsedError,
+ project_path: str,
+ ) -> FixResult:
+ """Create a guidance request for human-in-the-loop"""
+ previous_attempts = self._tracker.get_attempts(error_key)
+
+ return FixResult(
+ success=False,
+ fixed=False,
+ needs_guidance=True,
+ guidance_request={
+ "context": f"Attempting to fix {error_type} error in {error.file_path}",
+ "problem": error.message,
+ "location": f"Line {error.line_number}, Column {error.column_number}",
+ "attempts": previous_attempts,
+ "questions": [
+ f"What is the expected behavior at line {error.line_number}?",
+ "Are there any missing type definitions or event declarations?",
+ "Should this code use a different P language construct?",
+ ],
+ "suggested_actions": [
+ "Provide the correct type definition",
+ "Explain the expected state machine behavior",
+ "Share similar working code as a reference",
+ ],
+ },
+ attempt_number=self._tracker.get_attempt_count(error_key),
+ )
+
+ def _create_checker_guidance_request(
+ self,
+ error_key: str,
+ trace_log: str,
+ project_path: str,
+ ) -> FixResult:
+ """Create a guidance request for checker errors"""
+ previous_attempts = self._tracker.get_attempts(error_key)
+
+ # Extract key info from trace
+ trace_summary = trace_log.splitlines()[-10:]
+
+ return FixResult(
+ success=False,
+ fixed=False,
+ needs_guidance=True,
+ guidance_request={
+ "context": f"Attempting to fix PChecker error in {project_path}",
+ "problem": "PChecker found a violation during model checking",
+ "trace_summary": "\n".join(trace_summary),
+ "attempts": previous_attempts,
+ "questions": [
+ "What is the expected behavior when this event occurs?",
+ "Should the state machine handle this event differently?",
+ "Is there a race condition or ordering issue to address?",
+ ],
+ "suggested_actions": [
+ "Explain the expected state transition sequence",
+ "Provide invariants that should hold",
+ "Share the correct event handling logic",
+ ],
+ },
+ attempt_number=self._tracker.get_attempt_count(error_key),
+ )
+
+ def _extract_p_code(self, response: str) -> tuple:
+ """Extract single P code file from response"""
+ pattern = r'<(\w+\.p)>(.*?)\1>'
+ match = re.search(pattern, response, re.DOTALL)
+
+ if match:
+ return match.group(1), match.group(2).strip()
+ return None, None
+
+ def _extract_all_p_code(self, response: str) -> Dict[str, str]:
+ """Extract all P code files from response"""
+ pattern = r'<([^>]+\.p)>(.*?)\1>'
+ matches = re.findall(pattern, response, re.DOTALL)
+
+ return {filename: code.strip() for filename, code in matches}
diff --git a/Src/PeasyAI/src/core/services/generation.py b/Src/PeasyAI/src/core/services/generation.py
new file mode 100644
index 0000000000..02ddca39ce
--- /dev/null
+++ b/Src/PeasyAI/src/core/services/generation.py
@@ -0,0 +1,1987 @@
+"""
+Generation Service
+
+Handles P code generation from design documents.
+This service is UI-agnostic and can be used by Streamlit, CLI, or MCP.
+
+Supports RAG (Retrieval-Augmented Generation) for improved code quality
+by providing relevant examples from the P program corpus.
+"""
+
+import os
+import re
+import logging
+import traceback
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from pathlib import Path
+from typing import Dict, Any, Optional, List
+from dataclasses import dataclass, field
+
+from .base import BaseService, ServiceResult, EventCallback, ResourceLoader
+from ..llm import LLMProvider, LLMConfig, Message, MessageRole
+
+# Try to import RAG service
+try:
+ from ..rag import get_rag_service, RAGService
+ HAS_RAG = True
+except ImportError:
+ HAS_RAG = False
+
+logger = logging.getLogger(__name__)
+
+# Directory constants
+PSRC = 'PSrc'
+PSPEC = 'PSpec'
+PTST = 'PTst'
+
+
+@dataclass
+class GenerationResult(ServiceResult):
+ """Result of a generation operation"""
+ filename: Optional[str] = None
+ file_path: Optional[str] = None
+ code: Optional[str] = None
+ raw_response: Optional[str] = None
+
+
+@dataclass
+class ProjectGenerationResult(ServiceResult):
+ """Result of a full project generation"""
+ project_path: Optional[str] = None
+ project_name: Optional[str] = None
+ files_generated: List[str] = field(default_factory=list)
+ compilation_success: bool = False
+ compilation_output: Optional[str] = None
+
+
+class GenerationService(BaseService):
+ """
+ Service for generating P code.
+
+ Provides methods for:
+ - Generating project structure
+ - Generating types/events files
+ - Generating machine implementations
+ - Generating specs and tests
+ - Running sanity checks
+
+ Optionally uses RAG (Retrieval-Augmented Generation) to provide
+ relevant examples from the P program corpus for improved generation.
+
+ Generation methods automatically retry on extraction failure
+ (up to MAX_GENERATION_RETRIES times) to mitigate LLM non-determinism.
+ """
+
+ MAX_GENERATION_RETRIES = 2
+
+ @staticmethod
+ def _format_error(e: Exception, context: str = "") -> str:
+ """Build a descriptive error string that includes the exception type.
+
+ ``str(e)`` alone can be nearly empty for some exception types (e.g.
+ ``KeyError(' ')`` renders as ``' '``). This helper always includes the
+ class name so the caller and the end-user get a meaningful message.
+ The full traceback is logged at ERROR level for debugging.
+ """
+ type_name = type(e).__name__
+ detail = str(e).strip() or "(no detail)"
+ msg = f"{type_name}: {detail}"
+ if context:
+ msg = f"{context} — {msg}"
+ logger.error(f"{msg}\n{traceback.format_exc()}")
+ return msg
+
+ def __init__(
+ self,
+ llm_provider: Optional[LLMProvider] = None,
+ resource_loader: Optional[ResourceLoader] = None,
+ callbacks: Optional[EventCallback] = None,
+ use_rag: bool = True,
+ ):
+ super().__init__(llm_provider, resource_loader, callbacks)
+
+ # Initialize RAG service if available
+ self._rag: Optional['RAGService'] = None
+ if use_rag and HAS_RAG:
+ try:
+ self._rag = get_rag_service()
+ logger.info(f"RAG enabled with {self._rag.get_stats()['total_examples']} examples")
+ except Exception as e:
+ logger.warning(f"Failed to initialize RAG: {e}")
+ # Cache static modular contexts/instructions to avoid repeated disk reads and
+ # to support compact prompt assembly.
+ self._static_context_cache: Dict[str, str] = {}
+ # Soft caps to keep prompts concise and reduce LLM latency/cost.
+ self._guide_char_limit = 3500
+ self._context_file_char_limit = 9000
+ self._design_doc_char_limit = 7000
+ self._rag_context_char_limit = 7000
+
+ _TIMER_TEMPLATE: Optional[str] = None
+
+ @classmethod
+ def _load_timer_template(cls) -> str:
+ """Load the Common_Timer template on first use."""
+ if cls._TIMER_TEMPLATE is None:
+ # __file__ = src/core/services/generation.py → 4 parents to reach PeasyAI/
+ timer_path = Path(__file__).resolve().parent.parent.parent.parent / "resources" / "rag_examples" / "Common_Timer" / "PSrc" / "Timer.p"
+ try:
+ cls._TIMER_TEMPLATE = timer_path.read_text(encoding="utf-8")
+ except Exception:
+ cls._TIMER_TEMPLATE = ""
+ return cls._TIMER_TEMPLATE
+
+ @staticmethod
+ def _needs_timer(design_doc: str, context_files: Optional[Dict[str, str]] = None) -> bool:
+ """Detect whether the protocol requires a Timer machine."""
+ text = (design_doc or "").lower()
+ if context_files:
+ text += " " + " ".join(c.lower() for c in context_files.values())
+ return any(kw in text for kw in [
+ "timer", "timeout", "heartbeat", "periodic", "etimeout",
+ "estarttimer", "ecanceltimer", "heartbeat interval",
+ ])
+
+ def _inject_timer_context(
+ self,
+ context_files: Optional[Dict[str, str]],
+ design_doc: str,
+ ) -> Dict[str, str]:
+ """
+ If the design doc uses timer/heartbeat patterns, inject the
+ canonical Timer machine as a reference example so the LLM
+ follows the standard ``CreateTimer``/``StartTimer``/``CancelTimer``
+ API when generating the Timer machine.
+
+ The Timer will be generated as a normal machine (like any other)
+ by the LLM. This context just shows the expected pattern.
+ """
+ ctx = dict(context_files) if context_files else {}
+ if not self._needs_timer(design_doc, context_files):
+ return ctx
+ # Skip if a Timer file is already in context (already generated)
+ if any("timer" in k.lower() for k in ctx):
+ return ctx
+ template = self._load_timer_template()
+ if template:
+ ctx["__Timer_Reference_Example__"] = (
+ "// REFERENCE EXAMPLE: This is the standard Timer machine pattern.\n"
+ "// When generating Timer.p, follow this structure and API.\n"
+ "// NOTE: The Timer events (eStartTimer, eCancelTimer, eTimeOut,\n"
+ "// eDelayedTimeOut) should be declared in Enums_Types_Events.p,\n"
+ "// NOT in Timer.p. Timer.p should only contain the machine\n"
+ "// declaration and the helper functions.\n"
+ "// IMPORTANT: The Timer bounds the number of delays (numDelays)\n"
+ "// so it is GUARANTEED to eventually fire. This is required\n"
+ "// for liveness properties. After numDelays >= 3, the timer\n"
+ "// fires unconditionally.\n"
+ "// Helper functions (declared at file scope, outside the machine):\n"
+ "// fun CreateTimer(client: machine) : Timer — creates a new Timer\n"
+ "// fun StartTimer(timer: Timer) — sends eStartTimer\n"
+ "// fun CancelTimer(timer: Timer) — sends eCancelTimer\n"
+ "// The Timer sends eTimeOut to its client when it fires.\n\n"
+ + template
+ )
+ return ctx
+
+ def _get_rag_context(
+ self,
+ context_type: str,
+ description: str,
+ design_doc: Optional[str] = None,
+ context_files: Optional[Dict[str, str]] = None,
+ ) -> str:
+ """Get RAG context for generation, enriched by already-generated files."""
+ if self._rag is None:
+ return ""
+
+ try:
+ if context_type == "machine":
+ context = self._rag.get_machine_context(
+ description, design_doc=design_doc, context_files=context_files
+ )
+ elif context_type == "spec":
+ context = self._rag.get_spec_context(description)
+ elif context_type == "test":
+ context = self._rag.get_test_context(description)
+ elif context_type == "types":
+ context = self._rag.get_types_context(description)
+ else:
+ return ""
+
+ return context.to_prompt_section()
+ except Exception as e:
+ logger.warning(f"Failed to get RAG context: {e}")
+ return ""
+
+ def create_project_structure(
+ self,
+ output_dir: str,
+ project_name: str,
+ ) -> GenerationResult:
+ """
+ Create P project folder structure.
+
+ Args:
+ output_dir: Directory to create project in
+ project_name: Name of the project
+
+ Returns:
+ GenerationResult with project path
+ """
+ from datetime import datetime
+ from utils.project_structure_utils import setup_project_structure
+
+ self._status(f"Creating project structure for {project_name}...")
+
+ try:
+ timestamp = datetime.now().strftime('%Y_%m_%d_%H_%M_%S')
+ project_name_with_timestamp = f"{project_name}_{timestamp}"
+ project_root = os.path.join(output_dir, project_name_with_timestamp)
+
+ os.makedirs(output_dir, exist_ok=True)
+ setup_project_structure(project_root, project_name)
+
+ self._status(f"Created project at {project_root}")
+
+ return GenerationResult(
+ success=True,
+ file_path=project_root,
+ filename=project_name_with_timestamp,
+ )
+ except Exception as e:
+ return GenerationResult(
+ success=False,
+ error=self._format_error(e, "Error creating project structure"),
+ )
+
+ def generate_types_events(
+ self,
+ design_doc: str,
+ project_path: str,
+ save_to_disk: bool = True,
+ ) -> GenerationResult:
+ """
+ Generate Enums_Types_Events.p file.
+
+ Args:
+ design_doc: Design document content
+ project_path: Path to the P project
+ save_to_disk: If True, write to disk; if False, return code only for preview
+
+ Returns:
+ GenerationResult with generated code
+ """
+ self._status("Generating types, enums, and events...")
+
+ try:
+ # Build messages
+ messages = self._build_types_events_messages(design_doc)
+
+ # Get system prompt
+ system_prompt = self.resources.load_context("about_p.txt")
+
+ # Invoke LLM
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(messages, config, system_prompt)
+
+ # Extract code — retry on extraction failure
+ filename, code = self._extract_p_code(response.content)
+
+ if not filename or not code:
+ for retry in range(self.MAX_GENERATION_RETRIES):
+ self._warning(f"Code extraction failed for types/events, retry {retry + 1}")
+ response = self.llm.complete(messages, config, system_prompt)
+ filename, code = self._extract_p_code(response.content)
+ if filename and code:
+ break
+
+ if filename and code:
+ file_path = os.path.join(project_path, PSRC, filename)
+
+ # Only write if save_to_disk is True
+ if save_to_disk:
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
+ with open(file_path, 'w') as f:
+ f.write(code)
+ self._status(f"Generated and saved {filename}")
+ else:
+ self._status(f"Generated {filename} (preview only)")
+
+ return GenerationResult(
+ success=True,
+ filename=filename,
+ file_path=file_path,
+ code=code,
+ raw_response=response.content,
+ token_usage=response.usage.to_dict(),
+ )
+ else:
+ return GenerationResult(
+ success=False,
+ error="Could not extract P code from response after retries",
+ raw_response=response.content,
+ token_usage=response.usage.to_dict(),
+ )
+
+ except Exception as e:
+ return GenerationResult(
+ success=False,
+ error=self._format_error(e, "Error generating types/events"),
+ )
+
+ def generate_machine(
+ self,
+ machine_name: str,
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ two_stage: bool = True,
+ save_to_disk: bool = True,
+ ) -> GenerationResult:
+ """
+ Generate a P state machine.
+
+ Args:
+ machine_name: Name of the machine to generate
+ design_doc: Design document content
+ project_path: Path to the P project
+ context_files: Additional context files (filename -> content)
+ two_stage: Whether to use two-stage generation (structure first)
+ save_to_disk: If True, write to disk; if False, return code only for preview
+
+ Returns:
+ GenerationResult with generated code
+ """
+ self._status(f"Generating machine: {machine_name}")
+
+ try:
+ system_prompt = self.resources.load_context("about_p.txt")
+
+ if two_stage:
+ # Stage 1: Generate structure
+ self._status(f" Stage 1: Generating structure for {machine_name}")
+ structure_messages = self._build_machine_structure_messages(
+ machine_name, design_doc, context_files
+ )
+
+ config = LLMConfig(max_tokens=2048)
+ structure_response = self.llm.complete(structure_messages, config, system_prompt)
+
+ # Extract structure
+ structure_match = re.search(
+ r'(.*?)',
+ structure_response.content,
+ re.DOTALL
+ )
+
+ if structure_match:
+ machine_structure = structure_match.group(1).strip()
+
+ # Stage 2: Implement machine
+ self._status(f" Stage 2: Implementing {machine_name}")
+ impl_messages = self._build_machine_impl_messages(
+ machine_name, design_doc, context_files, machine_structure
+ )
+
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(impl_messages, config, system_prompt)
+ else:
+ # Fallback to single-stage
+ self._warning(f"Could not extract structure, falling back to single-stage")
+ impl_messages = self._build_machine_impl_messages(
+ machine_name, design_doc, context_files
+ )
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(impl_messages, config, system_prompt)
+ else:
+ # Single-stage generation
+ impl_messages = self._build_machine_impl_messages(
+ machine_name, design_doc, context_files
+ )
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(impl_messages, config, system_prompt)
+
+ # Extract code — retry on extraction failure
+ filename, code = self._extract_p_code(
+ response.content, expected_name=machine_name
+ )
+
+ if not filename or not code:
+ for retry in range(self.MAX_GENERATION_RETRIES):
+ self._warning(
+ f"Code extraction failed for {machine_name}, retry {retry + 1}/{self.MAX_GENERATION_RETRIES}"
+ )
+ impl_messages = self._build_machine_impl_messages(
+ machine_name, design_doc, context_files
+ )
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(impl_messages, config, system_prompt)
+ filename, code = self._extract_p_code(
+ response.content, expected_name=machine_name
+ )
+ if filename and code:
+ break
+
+ if filename and code:
+ file_path = os.path.join(project_path, PSRC, filename)
+
+ # Only write if save_to_disk is True
+ if save_to_disk:
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
+ with open(file_path, 'w') as f:
+ f.write(code)
+ self._status(f"Generated and saved {filename}")
+ else:
+ self._status(f"Generated {filename} (preview only)")
+
+ return GenerationResult(
+ success=True,
+ filename=filename,
+ file_path=file_path,
+ code=code,
+ raw_response=response.content,
+ token_usage=response.usage.to_dict(),
+ )
+ else:
+ return GenerationResult(
+ success=False,
+ error="Could not extract P code from response after retries",
+ raw_response=response.content,
+ token_usage=response.usage.to_dict(),
+ )
+
+ except Exception as e:
+ return GenerationResult(
+ success=False,
+ error=self._format_error(e, f"Error generating machine {machine_name}"),
+ )
+
+ def generate_spec(
+ self,
+ spec_name: str,
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ save_to_disk: bool = True,
+ ) -> GenerationResult:
+ """
+ Generate a P specification/monitor file.
+
+ Args:
+ spec_name: Name of the spec file
+ design_doc: Design document content
+ project_path: Path to the P project
+ context_files: Additional context files
+ save_to_disk: If True, write to disk; if False, return code only for preview
+
+ Returns:
+ GenerationResult with generated code
+ """
+ self._status(f"Generating specification: {spec_name}")
+
+ try:
+ messages = self._build_spec_messages(spec_name, design_doc, context_files)
+ system_prompt = self.resources.load_context("about_p.txt")
+
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(messages, config, system_prompt)
+
+ filename, code = self._extract_p_code(
+ response.content, expected_name=spec_name
+ )
+
+ if not filename or not code:
+ for retry in range(self.MAX_GENERATION_RETRIES):
+ self._warning(f"Code extraction failed for spec {spec_name}, retry {retry + 1}")
+ response = self.llm.complete(messages, config, system_prompt)
+ filename, code = self._extract_p_code(
+ response.content, expected_name=spec_name
+ )
+ if filename and code:
+ break
+
+ if filename and code:
+ file_path = os.path.join(project_path, PSPEC, filename)
+
+ # Only write if save_to_disk is True
+ if save_to_disk:
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
+ with open(file_path, 'w') as f:
+ f.write(code)
+ self._status(f"Generated and saved {filename}")
+ else:
+ self._status(f"Generated {filename} (preview only)")
+
+ return GenerationResult(
+ success=True,
+ filename=filename,
+ file_path=file_path,
+ code=code,
+ raw_response=response.content,
+ token_usage=response.usage.to_dict(),
+ )
+ else:
+ return GenerationResult(
+ success=False,
+ error="Could not extract P code from response after retries",
+ raw_response=response.content,
+ )
+
+ except Exception as e:
+ return GenerationResult(
+ success=False,
+ error=self._format_error(e, f"Error generating spec {spec_name}"),
+ )
+
+ def generate_test(
+ self,
+ test_name: str,
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ save_to_disk: bool = True,
+ ) -> GenerationResult:
+ """
+ Generate a P test file.
+
+ Args:
+ test_name: Name of the test file
+ design_doc: Design document content
+ project_path: Path to the P project
+ context_files: Additional context files
+ save_to_disk: If True, write to disk; if False, return code only for preview
+
+ Returns:
+ GenerationResult with generated code
+ """
+ self._status(f"Generating test: {test_name}")
+
+ try:
+ messages = self._build_test_messages(test_name, design_doc, context_files)
+ system_prompt = self.resources.load_context("about_p.txt")
+
+ config = LLMConfig(max_tokens=4096)
+ response = self.llm.complete(messages, config, system_prompt)
+
+ filename, code = self._extract_p_code(
+ response.content, is_test_file=True, expected_name=test_name
+ )
+
+ if not filename or not code:
+ for retry in range(self.MAX_GENERATION_RETRIES):
+ self._warning(f"Code extraction failed for test {test_name}, retry {retry + 1}")
+ response = self.llm.complete(messages, config, system_prompt)
+ filename, code = self._extract_p_code(
+ response.content, is_test_file=True, expected_name=test_name
+ )
+ if filename and code:
+ break
+
+ if filename and code:
+ file_path = os.path.join(project_path, PTST, filename)
+
+ # Only write if save_to_disk is True
+ if save_to_disk:
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
+ with open(file_path, 'w') as f:
+ f.write(code)
+ self._status(f"Generated and saved {filename}")
+ else:
+ self._status(f"Generated {filename} (preview only)")
+
+ return GenerationResult(
+ success=True,
+ filename=filename,
+ file_path=file_path,
+ code=code,
+ raw_response=response.content,
+ token_usage=response.usage.to_dict(),
+ )
+ else:
+ return GenerationResult(
+ success=False,
+ error="Could not extract P code from response after retries",
+ raw_response=response.content,
+ )
+
+ except Exception as e:
+ return GenerationResult(
+ success=False,
+ error=self._format_error(e, f"Error generating test {test_name}"),
+ )
+
+ # ── LLM-based wiring review ─────────────────────────────────────
+
+ def review_test_wiring(
+ self,
+ test_code: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ) -> Dict[str, str]:
+ """
+ Use the LLM to review a generated test driver for initialization
+ correctness: dependency ordering, circular dependency resolution,
+ empty-collection pitfalls, and named-tuple construction.
+
+ Returns a dict of ``filename -> fixed_code`` for every file that
+ the review changed. If the test is already correct the dict
+ contains only the original test file (unchanged).
+ """
+ self._status("Reviewing test wiring with LLM…")
+
+ instruction = self._load_static_instruction("review_test_wiring.txt")
+ messages: List[Message] = []
+
+ # Provide all context files so the reviewer can see machine signatures
+ if context_files:
+ messages.extend(self._compact_context_messages(context_files))
+
+ # Provide the wiring summary as a quick reference
+ wiring_info = self._extract_machine_wiring_info(context_files)
+ if wiring_info:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=(
+ "\n"
+ f"{wiring_info}\n"
+ ""
+ ),
+ ))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_design_doc(design_doc)}\n",
+ ))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{test_code}\n",
+ ))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=instruction,
+ ))
+
+ system_prompt = (
+ "You are an expert P language reviewer. "
+ "Focus exclusively on machine initialization and wiring correctness."
+ )
+ config = LLMConfig(max_tokens=4096)
+ try:
+ response = self.llm.complete(messages, config, system_prompt)
+ except Exception as e:
+ logger.warning(f"Wiring review LLM call failed: {e}")
+ return {}
+
+ return self._parse_wiring_review_response(response.content)
+
+ @staticmethod
+ def _parse_wiring_review_response(content: str) -> Dict[str, str]:
+ """Extract ```` blocks from the wiring-review response."""
+ fixed: Dict[str, str] = {}
+
+ # Look for wrapper
+ ff_match = re.search(
+ r"(.*?)", content, re.DOTALL
+ )
+ if not ff_match:
+ return fixed
+
+ inner = ff_match.group(1)
+
+ # Extract each ... block
+ for m in re.finditer(
+ r"<(\w+\.p)>(.*?)\1>", inner, re.DOTALL
+ ):
+ filename = m.group(1)
+ code = m.group(2).strip()
+ if code:
+ fixed[filename] = code
+
+ return fixed
+
+ # ── LLM-based spec review ───────────────────────────────────────
+
+ def review_spec_correctness(
+ self,
+ spec_code: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ) -> Dict[str, str]:
+ """
+ Use the LLM to review a generated spec monitor for semantic
+ correctness against the design document's safety properties.
+
+ Checks:
+ - observes clause completeness (all relevant events included)
+ - correct event-to-machine tracking via payloads
+ - assertion logic matches the stated safety property
+ - no forbidden keywords (this/new/send/…)
+ - payload type names match Enums_Types_Events.p
+
+ Returns a dict of ``filename -> fixed_code`` for every file the
+ review changed. If the spec is already correct the dict contains
+ only the original spec file (unchanged).
+ """
+ self._status("Reviewing spec correctness with LLM…")
+
+ instruction = self._load_static_instruction("review_spec_correctness.txt")
+ messages: List[Message] = []
+
+ if context_files:
+ messages.extend(self._compact_context_messages(context_files))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_design_doc(design_doc)}\n",
+ ))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{spec_code}\n",
+ ))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=instruction,
+ ))
+
+ system_prompt = (
+ "You are an expert P language reviewer. "
+ "Focus exclusively on spec monitor correctness: "
+ "observes completeness, assertion logic, and payload usage."
+ )
+ config = LLMConfig(max_tokens=4096)
+ try:
+ response = self.llm.complete(messages, config, system_prompt)
+ except Exception as e:
+ logger.warning(f"Spec review LLM call failed: {e}")
+ return {}
+
+ return self._parse_wiring_review_response(response.content)
+
+ # ── LLM-based code documentation review ─────────────────────────
+
+ def review_code_documentation(
+ self,
+ code: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ) -> Dict[str, Any]:
+ """
+ Use the LLM to add insightful documentation comments to generated P code.
+
+ Unlike regex-based approaches that copy text verbatim from the design
+ doc, this asks the LLM to write contextual comments explaining *why*
+ the code is structured the way it is, what invariants are maintained,
+ and what protocol steps are being implemented.
+
+ Returns a dict with:
+ - status: "success" | "llm_error" | "truncated" | "parse_error" | "declarations_dropped"
+ - code: the documented code string (original code if review failed)
+ - reason: human-readable explanation when status != "success"
+ """
+ self._status("Adding documentation comments via LLM…")
+
+ instruction = self._load_static_instruction("review_code_documentation.txt")
+ messages: List[Message] = []
+
+ if context_files:
+ messages.extend(self._compact_context_messages(context_files))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_design_doc(design_doc)}\n",
+ ))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{code}\n",
+ ))
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=instruction,
+ ))
+
+ system_prompt = (
+ "You are an expert P language developer adding documentation "
+ "comments to generated code. Write comments that explain the "
+ "reasoning, invariants, and protocol semantics — not comments "
+ "that merely restate what the code does."
+ )
+
+ max_tokens = 16384
+ max_attempts = 2
+ for attempt in range(1, max_attempts + 1):
+ config = LLMConfig(max_tokens=max_tokens)
+ try:
+ response = self.llm.complete(messages, config, system_prompt)
+ except Exception as e:
+ reason = f"LLM call failed: {e}"
+ logger.warning(f"Documentation review {reason}")
+ return {"status": "llm_error", "code": code, "reason": reason}
+
+ if response.finish_reason == "length" and attempt < max_attempts:
+ logger.warning(
+ f"Documentation review truncated on attempt {attempt} "
+ f"(max_tokens={max_tokens}). Retrying with higher limit."
+ )
+ max_tokens = min(max_tokens * 2, 20000)
+ continue
+
+ result = self._parse_documentation_review_response(
+ response.content, code, response.finish_reason
+ )
+ return result
+
+ return {"status": "truncated", "code": code,
+ "reason": f"Response still truncated after {max_attempts} attempts"}
+
+ @staticmethod
+ def _parse_documentation_review_response(
+ content: str, original_code: str, finish_reason: str = "stop"
+ ) -> Dict[str, Any]:
+ """Extract documented code from the LLM response.
+
+ Returns a dict with status, code, and reason.
+ """
+ if finish_reason == "length":
+ # Check if we got a partial block we can salvage
+ open_tag = content.find("")
+ if open_tag == -1:
+ return {
+ "status": "truncated",
+ "code": original_code,
+ "reason": "Response truncated (finish_reason=length) "
+ "before tag",
+ }
+
+ match = re.search(
+ r"(.*?)", content, re.DOTALL
+ )
+ if not match:
+ reason = (
+ "Response truncated before closing tag"
+ if finish_reason == "length"
+ else "Response missing tags"
+ )
+ logger.warning(f"Documentation review: {reason}")
+ return {"status": "truncated" if finish_reason == "length" else "parse_error",
+ "code": original_code, "reason": reason}
+
+ documented = match.group(1).strip()
+ if not documented:
+ return {"status": "parse_error", "code": original_code,
+ "reason": "Empty block"}
+
+ orig_machines = set(re.findall(r'\b(?:machine|spec)\s+(\w+)', original_code))
+ doc_machines = set(re.findall(r'\b(?:machine|spec)\s+(\w+)', documented))
+ if orig_machines and not orig_machines.issubset(doc_machines):
+ reason = (
+ f"LLM dropped declarations: expected {orig_machines}, "
+ f"got {doc_machines}"
+ )
+ logger.warning(f"Documentation review: {reason}")
+ return {"status": "declarations_dropped", "code": original_code,
+ "reason": reason}
+
+ return {"status": "success", "code": documented, "reason": None}
+
+ def generate_machines_parallel(
+ self,
+ machine_names: List[str],
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ save_to_disk: bool = True,
+ max_workers: int = 3,
+ ) -> Dict[str, GenerationResult]:
+ """
+ Generate multiple machines in parallel.
+
+ All machines receive the same context_files snapshot (types + any
+ previously generated machines). Results are returned keyed by
+ machine name.
+
+ Args:
+ machine_names: List of machines to generate
+ design_doc: Design document content
+ project_path: Path to the P project
+ context_files: Shared context (types file, etc.)
+ save_to_disk: Whether to write files to disk
+ max_workers: Maximum concurrent LLM calls
+
+ Returns:
+ Dictionary mapping machine name → GenerationResult
+ """
+ results: Dict[str, GenerationResult] = {}
+
+ if len(machine_names) <= 1:
+ # No benefit from parallelism
+ for mn in machine_names:
+ results[mn] = self.generate_machine(
+ mn, design_doc, project_path, context_files, save_to_disk=save_to_disk,
+ )
+ return results
+
+ self._status(f"Generating {len(machine_names)} machines in parallel (max_workers={max_workers})")
+ # Snapshot context so all threads see the same state
+ ctx_snapshot = dict(context_files) if context_files else {}
+
+ def _gen(name: str) -> tuple:
+ return name, self.generate_machine(
+ name, design_doc, project_path, ctx_snapshot, save_to_disk=save_to_disk,
+ )
+
+ with ThreadPoolExecutor(max_workers=max_workers) as pool:
+ futures = {pool.submit(_gen, mn): mn for mn in machine_names}
+ for future in as_completed(futures):
+ mn = futures[future]
+ try:
+ _, result = future.result()
+ results[mn] = result
+ except Exception as exc:
+ results[mn] = GenerationResult(
+ success=False,
+ error=self._format_error(exc, f"Parallel generation of {mn} failed"),
+ )
+
+ return results
+
+ def generate_machine_ensemble(
+ self,
+ machine_name: str,
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ensemble_size: int = 3,
+ save_to_disk: bool = True,
+ ) -> GenerationResult:
+ """
+ Generate a P state machine using ensemble: produce N candidates and
+ pick the best one based on static quality scoring.
+
+ This improves generation reliability by mitigating single-shot LLM
+ non-determinism. For example, some candidates will correctly add
+ ``defer`` or ``ignore`` clauses for events that could arrive in
+ unexpected states – a common source of PChecker failures.
+
+ Args:
+ machine_name: Name of the machine to generate
+ design_doc: Design document content
+ project_path: Path to the P project
+ context_files: Additional context files
+ ensemble_size: Number of candidates to generate (default 3)
+ save_to_disk: Whether to write the best candidate to disk
+
+ Returns:
+ GenerationResult for the best candidate
+ """
+ first = self.generate_machine(
+ machine_name, design_doc, project_path, context_files,
+ save_to_disk=save_to_disk,
+ )
+ if ensemble_size <= 1:
+ return first
+
+ first_score = 0.0
+ should_escalate = not first.success or not first.code
+ if first.success and first.code:
+ first_score = self._score_p_candidate(first.code, machine_name, context_files, file_type="machine")
+ should_escalate = self._should_escalate_ensemble(first.code, first_score, "machine")
+
+ if not should_escalate:
+ self._status(f"Using first machine candidate for {machine_name} (score={first_score:.1f})")
+ return first
+
+ self._status(f"Escalating machine ensemble for {machine_name} (n={ensemble_size})")
+ candidates: List[GenerationResult] = []
+ if first.success and first.code:
+ candidates.append(first)
+
+ additional = max(0, ensemble_size - 1)
+
+ def _gen(_idx: int) -> GenerationResult:
+ return self.generate_machine(
+ machine_name, design_doc, project_path, context_files,
+ save_to_disk=False,
+ )
+
+ if additional > 0:
+ with ThreadPoolExecutor(max_workers=min(additional, 4)) as pool:
+ futures = [pool.submit(_gen, i) for i in range(additional)]
+ for future in as_completed(futures):
+ try:
+ result = future.result()
+ if result.success and result.code:
+ candidates.append(result)
+ except Exception as e:
+ logger.warning(f"Ensemble candidate for {machine_name} failed: {type(e).__name__}: {e}\n{traceback.format_exc()}")
+
+ if not candidates:
+ self._warning(f"All ensemble candidates failed for {machine_name}, returning first attempt")
+ return first
+
+ # Score with heuristic + optional compile-check bonus.
+ scored = []
+ for c in candidates:
+ s = self._score_p_candidate(c.code, machine_name, context_files, file_type="machine")
+ scored.append((c, s))
+
+ # Try compile-check for top candidates when we have the compilation
+ # service. We only do this for machines (the most error-prone step)
+ # and limit to the top-3 by heuristic score to bound latency.
+ scored.sort(key=lambda t: t[1], reverse=True)
+ compile_checked = self._compile_check_candidates(
+ scored[:3], project_path, file_type="machine"
+ )
+ if compile_checked:
+ scored = compile_checked
+
+ best, best_score = max(scored, key=lambda t: t[1])
+ self._status(f"Selected best of {len(candidates)} candidates for {machine_name} (score={best_score:.1f})")
+
+ if save_to_disk and best.file_path:
+ os.makedirs(os.path.dirname(best.file_path), exist_ok=True)
+ with open(best.file_path, "w") as f:
+ f.write(best.code)
+
+ return best
+
+ def generate_machines_ensemble(
+ self,
+ machine_names: List[str],
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ensemble_size: int = 3,
+ save_to_disk: bool = True,
+ ) -> Dict[str, GenerationResult]:
+ """
+ Generate multiple machines sequentially, each with ensemble selection.
+
+ Unlike ``generate_machines_parallel`` (which generates all machines
+ concurrently with a shared context snapshot), this method generates
+ machines one-by-one so that each subsequent machine can see the code
+ of previously generated machines, improving cross-machine consistency.
+
+ Args:
+ machine_names: List of machine names to generate
+ design_doc: Design document content
+ project_path: Path to the P project
+ context_files: Shared context (types file, etc.)
+ ensemble_size: Number of candidates per machine
+ save_to_disk: Whether to write files to disk
+
+ Returns:
+ Dictionary mapping machine name → GenerationResult
+ """
+ results: Dict[str, GenerationResult] = {}
+ ctx = dict(context_files) if context_files else {}
+
+ for mn in machine_names:
+ result = self.generate_machine_ensemble(
+ mn, design_doc, project_path, ctx,
+ ensemble_size=ensemble_size, save_to_disk=save_to_disk,
+ )
+ results[mn] = result
+
+ # Feed successful results as context for subsequent machines
+ if result.success and result.code and result.filename:
+ ctx[result.filename] = result.code
+
+ return results
+
+ def generate_spec_ensemble(
+ self,
+ spec_name: str,
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ensemble_size: int = 3,
+ save_to_disk: bool = True,
+ ) -> GenerationResult:
+ """Generate a P specification with ensemble selection."""
+ first = self.generate_spec(
+ spec_name, design_doc, project_path, context_files,
+ save_to_disk=save_to_disk,
+ )
+ if ensemble_size <= 1:
+ return first
+
+ first_score = 0.0
+ should_escalate = not first.success or not first.code
+ if first.success and first.code:
+ first_score = self._score_p_candidate(first.code, spec_name, context_files, file_type="spec")
+ should_escalate = self._should_escalate_ensemble(first.code, first_score, "spec")
+
+ if not should_escalate:
+ self._status(f"Using first spec candidate for {spec_name} (score={first_score:.1f})")
+ return first
+
+ self._status(f"Escalating spec ensemble for {spec_name} (n={ensemble_size})")
+ candidates: List[GenerationResult] = []
+ if first.success and first.code:
+ candidates.append(first)
+
+ additional = max(0, ensemble_size - 1)
+
+ def _gen(_idx: int) -> GenerationResult:
+ return self.generate_spec(
+ spec_name, design_doc, project_path, context_files,
+ save_to_disk=False,
+ )
+
+ if additional > 0:
+ with ThreadPoolExecutor(max_workers=min(additional, 4)) as pool:
+ futures = [pool.submit(_gen, i) for i in range(additional)]
+ for future in as_completed(futures):
+ try:
+ result = future.result()
+ if result.success and result.code:
+ candidates.append(result)
+ except Exception as e:
+ logger.warning(f"Ensemble candidate for spec {spec_name} failed: {type(e).__name__}: {e}\n{traceback.format_exc()}")
+
+ if not candidates:
+ self._warning(f"All ensemble candidates failed for spec {spec_name}, returning first attempt")
+ return first
+
+ best = max(
+ candidates,
+ key=lambda c: self._score_p_candidate(c.code, spec_name, context_files, file_type="spec"),
+ )
+ best_score = self._score_p_candidate(best.code, spec_name, context_files, file_type="spec")
+ self._status(f"Selected best of {len(candidates)} candidates for spec {spec_name} (score={best_score:.1f})")
+
+ if save_to_disk and best.file_path:
+ os.makedirs(os.path.dirname(best.file_path), exist_ok=True)
+ with open(best.file_path, "w") as f:
+ f.write(best.code)
+
+ return best
+
+ def generate_test_ensemble(
+ self,
+ test_name: str,
+ design_doc: str,
+ project_path: str,
+ context_files: Optional[Dict[str, str]] = None,
+ ensemble_size: int = 3,
+ save_to_disk: bool = True,
+ ) -> GenerationResult:
+ """Generate a P test file with ensemble selection."""
+ first = self.generate_test(
+ test_name, design_doc, project_path, context_files,
+ save_to_disk=save_to_disk,
+ )
+ if ensemble_size <= 1:
+ return first
+
+ first_score = 0.0
+ should_escalate = not first.success or not first.code
+ if first.success and first.code:
+ first_score = self._score_p_candidate(first.code, test_name, context_files, file_type="test")
+ should_escalate = self._should_escalate_ensemble(first.code, first_score, "test")
+
+ if not should_escalate:
+ self._status(f"Using first test candidate for {test_name} (score={first_score:.1f})")
+ return first
+
+ self._status(f"Escalating test ensemble for {test_name} (n={ensemble_size})")
+ candidates: List[GenerationResult] = []
+ if first.success and first.code:
+ candidates.append(first)
+
+ additional = max(0, ensemble_size - 1)
+
+ def _gen(_idx: int) -> GenerationResult:
+ return self.generate_test(
+ test_name, design_doc, project_path, context_files,
+ save_to_disk=False,
+ )
+
+ if additional > 0:
+ with ThreadPoolExecutor(max_workers=min(additional, 4)) as pool:
+ futures = [pool.submit(_gen, i) for i in range(additional)]
+ for future in as_completed(futures):
+ try:
+ result = future.result()
+ if result.success and result.code:
+ candidates.append(result)
+ except Exception as e:
+ logger.warning(f"Ensemble candidate for test {test_name} failed: {type(e).__name__}: {e}\n{traceback.format_exc()}")
+
+ if not candidates:
+ self._warning(f"All ensemble candidates failed for test {test_name}, returning first attempt")
+ return first
+
+ best = max(
+ candidates,
+ key=lambda c: self._score_p_candidate(c.code, test_name, context_files, file_type="test"),
+ )
+ best_score = self._score_p_candidate(best.code, test_name, context_files, file_type="test")
+ self._status(f"Selected best of {len(candidates)} candidates for test {test_name} (score={best_score:.1f})")
+
+ if save_to_disk and best.file_path:
+ os.makedirs(os.path.dirname(best.file_path), exist_ok=True)
+ with open(best.file_path, "w") as f:
+ f.write(best.code)
+
+ return best
+
+ def save_p_file(
+ self,
+ file_path: str,
+ code: str,
+ ) -> GenerationResult:
+ """
+ Save P code to a file.
+
+ Args:
+ file_path: Absolute path where to save the file
+ code: The P code content to save
+
+ Returns:
+ GenerationResult indicating success or failure
+ """
+ try:
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
+
+ with open(file_path, 'w') as f:
+ f.write(code)
+
+ filename = os.path.basename(file_path)
+ self._status(f"Saved {filename}")
+
+ return GenerationResult(
+ success=True,
+ filename=filename,
+ file_path=file_path,
+ code=code,
+ )
+ except Exception as e:
+ return GenerationResult(
+ success=False,
+ error=self._format_error(e, f"Error saving file {file_path}"),
+ )
+
+ # =========================================================================
+ # Private helper methods
+ # =========================================================================
+
+ def _score_p_candidate(
+ self,
+ code: str,
+ name: str,
+ context_files: Optional[Dict[str, str]],
+ file_type: str = "machine",
+ ) -> float:
+ """
+ Score a P code candidate for quality using static heuristics.
+
+ Rewards correct structure and penalises common P syntax mistakes
+ that would cause compilation failures.
+ """
+ if not code:
+ return 0.0
+
+ score = 0.0
+
+ # ── Common scoring ────────────────────────────────────────────
+ open_b = code.count("{")
+ close_b = code.count("}")
+ if open_b == close_b:
+ score += 5
+ else:
+ score -= abs(open_b - close_b) * 2
+
+ lines = len(code.strip().split("\n"))
+ score += min(lines * 0.1, 10)
+
+ # Event references from context (capped to avoid runaway bonus)
+ if context_files:
+ event_hits = 0
+ for _fname, content in context_files.items():
+ for ev in re.findall(r"\bevent\s+(\w+)", content):
+ if ev in code:
+ event_hits += 1
+ score += min(event_hits * 2, 16)
+
+ # ── Penalty: common P syntax errors ──────────────────────────
+ # var x: int = 0; (illegal inline init)
+ score -= len(re.findall(r"\bvar\s+\w+\s*:\s*\w+\s*=", code)) * 5
+ # Redeclared events/types in non-types files
+ if file_type != "types":
+ score -= len(re.findall(r"^\s*event\s+\w+", code, re.MULTILINE)) * 8
+ score -= len(re.findall(r"^\s*type\s+\w+\s*=", code, re.MULTILINE)) * 8
+
+ # ── Machine-specific scoring ──────────────────────────────────
+ if file_type == "machine":
+ if re.search(rf"\bmachine\s+{re.escape(name)}\b", code):
+ score += 10
+ if re.search(r"\bstart\s+state\b", code):
+ score += 10
+ state_count = len(re.findall(r"\bstate\s+\w+\s*\{", code))
+ score += min(state_count * 3, 15)
+ score += min(len(re.findall(r"\bentry\b", code)) * 2, 10)
+ on_handlers = len(re.findall(r"\bon\s+\w+\s+(?:do|goto)\b", code))
+ score += min(on_handlers * 2, 10)
+ # defer/ignore — important but capped
+ score += min(len(re.findall(r"\bdefer\b", code)) * 4, 16)
+ score += min(len(re.findall(r"\bignore\b", code)) * 3, 12)
+ # Bonus for send statements (shows the machine actually communicates)
+ score += min(len(re.findall(r"\bsend\b", code)) * 1, 6)
+
+ # ── Spec-specific scoring ─────────────────────────────────────
+ elif file_type == "spec":
+ if re.search(r"\bspec\s+\w+\s+observes\b", code):
+ score += 15
+ score += min(len(re.findall(r"\bassert\b", code)) * 5, 20)
+ spec_count = len(re.findall(r"\bspec\s+\w+\b", code))
+ score += min(spec_count * 5, 20)
+ # Penalty: spec using forbidden keywords
+ for kw in ["this", r"\bnew\s+\w+", r"\bsend\s+"]:
+ if re.search(kw, code):
+ score -= 10
+
+ # ── Test-specific scoring ─────────────────────────────────────
+ elif file_type == "test":
+ test_decl_count = len(re.findall(r"\btest\s+\w+\s*\[", code))
+ score += min(test_decl_count * 10, 30)
+ if re.search(r"\bassert\s+\w+\s+in\b", code):
+ score += 10
+ score += min(len(re.findall(r"\bnew\s+\w+", code)) * 2, 10)
+ score += min(len(re.findall(r"\bmachine\s+Scenario\w*", code)) * 5, 15)
+ # Bonus for proper sequence building: += (idx, value)
+ score += min(len(re.findall(r"\+=\s*\(", code)) * 2, 8)
+
+ return score
+
+ def _should_escalate_ensemble(self, code: str, score: float, file_type: str) -> bool:
+ """Return True when candidate quality suggests running additional ensemble attempts."""
+ if not code:
+ return True
+
+ if file_type == "machine":
+ has_machine_decl = bool(re.search(r"\bmachine\s+\w+\b", code))
+ has_start_state = bool(re.search(r"\bstart\s+state\b", code))
+ return score < 42 or not has_machine_decl or not has_start_state
+
+ if file_type == "spec":
+ has_spec_observes = bool(re.search(r"\bspec\s+\w+\s+observes\b", code))
+ has_assert = bool(re.search(r"\bassert\b", code))
+ return score < 28 or not has_spec_observes or not has_assert
+
+ if file_type == "test":
+ has_test_decl = bool(re.search(r"\btest\s+\w+\s*\[", code))
+ has_new = bool(re.search(r"\bnew\s+", code))
+ return score < 24 or not has_test_decl or not has_new
+
+ return False
+
+ def _compile_check_candidates(
+ self,
+ scored_candidates: List[tuple],
+ project_path: str,
+ file_type: str = "machine",
+ ) -> Optional[List[tuple]]:
+ """
+ Attempt a compile-check on each candidate to add a strong signal.
+
+ Temporarily writes each candidate's code to its target file,
+ runs the P compiler, and adds a +50 bonus for candidates that
+ compile successfully. Restores the original file after each
+ check.
+
+ Returns the re-scored list, or None if the compilation service
+ is unavailable.
+ """
+ try:
+ from ..compilation import CompilationService as _CS
+ except ImportError:
+ return None
+
+ if not scored_candidates:
+ return None
+
+ # We need an existing file path to overwrite temporarily
+ first_result = scored_candidates[0][0]
+ target_path = first_result.file_path
+ if not target_path or not os.path.exists(os.path.dirname(target_path)):
+ return None
+
+ # Save the current file content (if any) for restoration
+ original_content = None
+ if os.path.exists(target_path):
+ try:
+ with open(target_path, "r") as f:
+ original_content = f.read()
+ except Exception:
+ pass
+
+ COMPILE_BONUS = 50
+ re_scored = []
+
+ for candidate, heuristic_score in scored_candidates:
+ if not candidate.code:
+ re_scored.append((candidate, heuristic_score))
+ continue
+
+ try:
+ # Write candidate to disk
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
+ with open(target_path, "w") as f:
+ f.write(candidate.code)
+
+ # Try compiling
+ from ..compilation import ensure_environment
+ env = ensure_environment()
+ if env.is_valid and env.p_compiler_path:
+ import subprocess
+ result = subprocess.run(
+ [env.p_compiler_path, "compile"],
+ cwd=project_path,
+ capture_output=True,
+ text=True,
+ timeout=30,
+ )
+ if result.returncode == 0:
+ logger.info(f" Candidate compiles successfully (+{COMPILE_BONUS} bonus)")
+ re_scored.append((candidate, heuristic_score + COMPILE_BONUS))
+ else:
+ re_scored.append((candidate, heuristic_score))
+ else:
+ re_scored.append((candidate, heuristic_score))
+ except Exception as e:
+ logger.debug(f" Compile check failed for candidate: {e}")
+ re_scored.append((candidate, heuristic_score))
+
+ # Restore original file
+ try:
+ if original_content is not None:
+ with open(target_path, "w") as f:
+ f.write(original_content)
+ elif os.path.exists(target_path):
+ os.remove(target_path)
+ except Exception:
+ pass
+
+ return re_scored
+
+ def _build_types_events_messages(self, design_doc: str) -> List[Message]:
+ """Build messages for types/events generation"""
+ messages = []
+
+ # Add P basics
+ p_basics = self._load_static_modular_context("p_basics.txt")
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Reference P Language Guide:\n{self._compact_text(p_basics, self._guide_char_limit)}"
+ ))
+
+ # Add specific guides
+ types_guide = self._load_static_modular_context("p_types_guide.txt")
+ events_guide = self._load_static_modular_context("p_events_guide.txt")
+ enums_guide = self._load_static_modular_context("p_enums_guide.txt")
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(types_guide, self._guide_char_limit)}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(events_guide, self._guide_char_limit)}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(enums_guide, self._guide_char_limit)}\n"
+ ))
+
+ # Add RAG context (similar type/event examples from corpus)
+ rag_context = self._get_rag_context("types", "types and events", design_doc)
+ if rag_context:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(rag_context, self._rag_context_char_limit)}\n"
+ ))
+
+ # Add design doc
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Design Document:\n{self._compact_design_doc(design_doc)}"
+ ))
+
+ # Add instruction
+ instruction = self._load_static_instruction("generate_enums_types_events.txt")
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=instruction
+ ))
+
+ return messages
+
+ def _build_machine_structure_messages(
+ self,
+ machine_name: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]],
+ ) -> List[Message]:
+ """Build messages for machine structure generation"""
+ # Auto-inject Timer template if the protocol uses timers
+ context_files = self._inject_timer_context(context_files, design_doc)
+
+ messages = []
+
+ # Add guides
+ p_basics = self._load_static_modular_context("p_basics.txt")
+ machines_guide = self._load_static_modular_context("p_machines_guide.txt")
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(p_basics, self._guide_char_limit)}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(machines_guide, self._guide_char_limit)}\n"
+ ))
+
+ # Add RAG context enriched with already-generated files
+ rag_context = self._get_rag_context(
+ "machine", machine_name, design_doc, context_files=context_files
+ )
+ if rag_context:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(rag_context, self._rag_context_char_limit)}\n"
+ ))
+
+ # Add context files
+ if context_files:
+ messages.extend(self._compact_context_messages(context_files))
+
+ # Add design doc
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Design Document:\n{self._compact_design_doc(design_doc)}"
+ ))
+
+ # Add instruction
+ instruction = self._load_static_instruction("generate_machine_structure.txt")
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=self._safe_format(instruction, machineName=machine_name)
+ ))
+
+ return messages
+
+ def _build_machine_impl_messages(
+ self,
+ machine_name: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]],
+ structure: Optional[str] = None,
+ ) -> List[Message]:
+ """Build messages for machine implementation"""
+ context_files = self._inject_timer_context(context_files, design_doc)
+
+ messages = []
+
+ # Add guides
+ p_basics = self._load_static_modular_context("p_basics.txt")
+ machines_guide = self._load_static_modular_context("p_machines_guide.txt")
+ statements_guide = self._load_static_modular_context("p_statements_guide.txt")
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(p_basics, self._guide_char_limit)}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(machines_guide, self._guide_char_limit)}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(statements_guide, self._guide_char_limit)}\n"
+ ))
+
+ # Add context files
+ if context_files:
+ messages.extend(self._compact_context_messages(context_files))
+
+ # Add design doc
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Design Document:\n{self._compact_design_doc(design_doc)}"
+ ))
+
+ # Add instruction with optional structure
+ instruction = self._load_static_instruction("generate_machine.txt")
+ content = self._safe_format(instruction, machineName=machine_name)
+
+ if structure:
+ content += f"\n\nHere is the starting structure:\n\n{structure}"
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=content
+ ))
+
+ return messages
+
+ def _build_spec_messages(
+ self,
+ spec_name: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]],
+ ) -> List[Message]:
+ """Build messages for spec generation"""
+ messages = []
+
+ # Add guides
+ p_basics = self._load_static_modular_context("p_basics.txt")
+ spec_guide = self._load_static_modular_context("p_spec_monitors_guide.txt")
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(p_basics, self._guide_char_limit)}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(spec_guide, self._guide_char_limit)}\n"
+ ))
+
+ # Add RAG context (similar spec examples from corpus)
+ rag_context = self._get_rag_context("spec", spec_name, design_doc)
+ if rag_context:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(rag_context, self._rag_context_char_limit)}\n"
+ ))
+
+ # Add context files
+ if context_files:
+ messages.extend(self._compact_context_messages(context_files))
+
+ # Add design doc
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Design Document:\n{self._compact_design_doc(design_doc)}"
+ ))
+
+ # Add instruction
+ instruction = self._load_static_instruction("generate_spec_files.txt")
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=self._safe_format(instruction, filename=spec_name)
+ ))
+
+ return messages
+
+ def _build_test_messages(
+ self,
+ test_name: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]],
+ ) -> List[Message]:
+ """Build messages for test generation"""
+ messages = []
+
+ # Add guides
+ p_basics = self._load_static_modular_context("p_basics.txt")
+ test_guide = self._load_static_modular_context("p_test_cases_guide.txt")
+
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(p_basics, self._guide_char_limit)}\n"
+ ))
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(test_guide, self._guide_char_limit)}\n"
+ ))
+
+ # Add RAG context (similar test driver examples from corpus)
+ rag_context = self._get_rag_context("test", test_name, design_doc)
+ if rag_context:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"\n{self._compact_text(rag_context, self._rag_context_char_limit)}\n"
+ ))
+
+ # Add context files
+ if context_files:
+ messages.extend(self._compact_context_messages(context_files))
+
+ # Extract and inject machine wiring information so the LLM
+ # knows the exact constructor signature for each machine.
+ wiring_info = self._extract_machine_wiring_info(context_files)
+ if wiring_info:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=(
+ "\n"
+ "Below is the EXACT constructor/initialization signature for each machine. "
+ "You MUST match these when creating machines with `new` or sending config events.\n\n"
+ f"{wiring_info}\n"
+ ""
+ ),
+ ))
+
+ spec_names = self._extract_spec_monitor_names(context_files)
+ if spec_names:
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=(
+ "\n"
+ "The following spec monitors are defined in the PSpec files. "
+ "Use EXACTLY these names in the `assert` clauses of test declarations.\n"
+ + "\n".join(f"- {name}" for name in spec_names) + "\n"
+ ""
+ ),
+ ))
+
+ # Add design doc
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=f"Design Document:\n{self._compact_design_doc(design_doc)}"
+ ))
+
+ # Add instruction
+ instruction = self._load_static_instruction("generate_test_files.txt")
+ messages.append(Message(
+ role=MessageRole.USER,
+ content=self._safe_format(instruction, filename=test_name)
+ ))
+
+ return messages
+
+ def _load_static_modular_context(self, filename: str) -> str:
+ key = f"modular:{filename}"
+ if key not in self._static_context_cache:
+ self._static_context_cache[key] = self.resources.load_modular_context(filename)
+ return self._static_context_cache[key]
+
+ def _load_static_instruction(self, filename: str) -> str:
+ key = f"instruction:{filename}"
+ if key not in self._static_context_cache:
+ self._static_context_cache[key] = self.resources.load_instruction(filename)
+ return self._static_context_cache[key]
+
+ @staticmethod
+ def _safe_format(template: str, **kwargs) -> str:
+ """Format a template string, falling back to manual replacement if
+ ``str.format()`` fails due to unescaped braces in the template.
+
+ Instruction files often contain P code examples with literal ``{`` and
+ ``}`` characters. If those aren't double-escaped (``{{`` / ``}}``),
+ ``str.format()`` raises ``KeyError`` or ``ValueError``. This helper
+ catches that and performs simple keyword substitution instead.
+ """
+ try:
+ return template.format(**kwargs)
+ except (KeyError, ValueError, IndexError):
+ result = template
+ for key, value in kwargs.items():
+ result = result.replace("{" + key + "}", str(value))
+ return result
+
+ def _compact_text(self, text: str, limit: int) -> str:
+ if not text:
+ return ""
+ if len(text) <= limit:
+ return text
+ # Keep head + tail to preserve overview and concrete syntax examples.
+ head = int(limit * 0.75)
+ tail = limit - head
+ return text[:head] + "\n... (truncated for prompt efficiency) ...\n" + text[-tail:]
+
+ def _compact_design_doc(self, design_doc: str) -> str:
+ return self._compact_text(design_doc, self._design_doc_char_limit)
+
+ def _compact_context_messages(self, context_files: Dict[str, str]) -> List[Message]:
+ messages: List[Message] = []
+ # Prioritize critical files for correctness first.
+ priority = []
+ rest = []
+ for filename, content in context_files.items():
+ lname = filename.lower()
+ if "enums" in lname or "types" in lname or "event" in lname:
+ priority.append((filename, content))
+ elif "safety" in lname or "spec" in lname:
+ priority.append((filename, content))
+ else:
+ rest.append((filename, content))
+ ordered = priority + rest
+
+ budget = self._context_file_char_limit
+ for filename, content in ordered:
+ if budget <= 0:
+ break
+ chunk = self._compact_text(content, min(2500, budget))
+ budget -= len(chunk)
+ messages.append(
+ Message(
+ role=MessageRole.USER,
+ content=f"<{filename}>\n{chunk}\n{filename}>",
+ )
+ )
+ return messages
+
+ @staticmethod
+ def _extract_machine_wiring_info(
+ context_files: Optional[Dict[str, str]],
+ ) -> str:
+ """
+ Extract machine initialization signatures from generated machine code.
+
+ Scans context files for patterns like:
+ machine Foo { ... start state Init { entry InitEntry; ... } ... fun InitEntry(cfg: ...) { ... } }
+ and also config-event patterns like:
+ start state WaitForConfig { on eMyConfig goto Ready with ConfigureHandler; }
+
+ Returns a human-readable summary the LLM can use to wire machines
+ correctly in the test driver.
+ """
+ if not context_files:
+ return ""
+
+ lines_out: list = []
+
+ for filename, code in context_files.items():
+ if not filename.endswith(".p"):
+ continue
+ # Skip types/events/spec files — we only want machines
+ if "Enums" in filename or "Types" in filename or "Safety" in filename:
+ continue
+
+ # Find machine name
+ machine_match = re.search(r"\bmachine\s+(\w+)\s*\{", code)
+ if not machine_match:
+ continue
+ machine_name = machine_match.group(1)
+
+ # Find start state (brace-balanced extraction)
+ from ..compilation.p_code_utils import extract_start_state
+ start_state_name, start_body = extract_start_state(code)
+ if not start_state_name or start_body is None:
+ continue
+
+ # Pattern A: entry function with parameter (constructor payload)
+ entry_match = re.search(r"\bentry\s+(\w+)\s*;", start_body)
+ if entry_match:
+ entry_fn_name = entry_match.group(1)
+ # Find the function definition and its parameter
+ fn_pattern = rf"\bfun\s+{re.escape(entry_fn_name)}\s*\(([^)]*)\)"
+ fn_match = re.search(fn_pattern, code)
+ if fn_match:
+ param_text = fn_match.group(1).strip()
+ if param_text:
+ lines_out.append(
+ f"- {machine_name}: created via `new {machine_name}({param_text.split(':',1)[-1].strip()})` "
+ f" (entry function: `fun {entry_fn_name}({param_text})`)"
+ )
+ else:
+ lines_out.append(
+ f"- {machine_name}: created via `new {machine_name}()` — no constructor config needed"
+ )
+ continue
+
+ # Pattern A alt: inline entry block (entry { ... }) — no config
+ if re.search(r"\bentry\s*\{", start_body):
+ lines_out.append(
+ f"- {machine_name}: created via `new {machine_name}()` — inline entry, no constructor config"
+ )
+ continue
+
+ # Pattern B: config event in start state
+ config_event_match = re.search(
+ r"\bon\s+(\w+)\s+goto\s+\w+(?:\s+with\s+(\w+))?\s*;",
+ start_body,
+ )
+ if config_event_match:
+ event_name = config_event_match.group(1)
+ handler_name = config_event_match.group(2)
+ # Find handler parameter to get the payload type
+ payload_info = ""
+ if handler_name:
+ handler_fn = re.search(
+ rf"\bfun\s+{re.escape(handler_name)}\s*\(([^)]*)\)",
+ code,
+ )
+ if handler_fn and handler_fn.group(1).strip():
+ payload_info = f" with payload `{handler_fn.group(1).strip()}`"
+ lines_out.append(
+ f"- {machine_name}: created via `new {machine_name}()`, then send `{event_name}`{payload_info}"
+ )
+ continue
+
+ # Fallback — no config detected
+ lines_out.append(
+ f"- {machine_name}: created via `new {machine_name}()` — no config detected"
+ )
+
+ return "\n".join(lines_out)
+
+ @staticmethod
+ def _extract_spec_monitor_names(
+ context_files: Optional[Dict[str, str]],
+ ) -> List[str]:
+ """
+ Extract spec monitor names from context files (PSpec/*.p).
+
+ Returns a list of spec machine names (e.g. ["Safety", "OnlyOneValueChosen"])
+ that the test driver should reference in ``assert`` clauses.
+ """
+ if not context_files:
+ return []
+
+ names: List[str] = []
+ for filename, code in context_files.items():
+ if not filename.endswith(".p"):
+ continue
+ for m in re.finditer(r"\bspec\s+(\w+)\s+observes\b", code):
+ names.append(m.group(1))
+ return names
+
+ def _extract_p_code(
+ self,
+ response: str,
+ is_test_file: bool = False,
+ expected_name: Optional[str] = None,
+ ) -> tuple:
+ """
+ Extract P code from LLM response.
+
+ Tries multiple extraction strategies in order:
+ 1. XML-style ... tags
+ 2. Markdown code block with filename comment (```p // filename.p ...)
+ 3. Markdown code block (```p ... ```) using first `machine` name
+
+ Note: post-processing and validation are handled by the
+ ValidationPipeline (see core.validation.pipeline), not here.
+ Callers should run the pipeline on the returned code.
+
+ Args:
+ response: Raw LLM response text
+ is_test_file: If True, this is a PTst file (unused here but
+ kept for API compatibility).
+ expected_name: If provided, used as the fallback filename when
+ the LLM response doesn't include one (e.g. the
+ machine_name parameter from generate_machine).
+
+ Returns:
+ Tuple of (filename, code) or (None, None) if not found
+ """
+ from ..compilation.p_code_utils import extract_p_code_from_response
+
+ filename, code = extract_p_code_from_response(
+ response, expected_filename=expected_name
+ )
+
+ if not filename or not code:
+ return None, None
+
+ return filename, code
diff --git a/Src/PeasyAI/src/core/validation/__init__.py b/Src/PeasyAI/src/core/validation/__init__.py
new file mode 100644
index 0000000000..44d2e397ed
--- /dev/null
+++ b/Src/PeasyAI/src/core/validation/__init__.py
@@ -0,0 +1,75 @@
+"""
+Validation Pipeline for PeasyAI.
+
+Two-stage validation:
+ Stage 1 — PCodePostProcessor: deterministic auto-fixes for common LLM mistakes.
+ Stage 2 — Validator chain: structured checks with severity levels and auto-fix support.
+
+Usage::
+
+ from core.validation import validate_p_code
+
+ result = validate_p_code(code, project_path="/path/to/project")
+ print(result.summary())
+"""
+
+from .validators import (
+ ValidationResult,
+ ValidationIssue,
+ IssueSeverity,
+ Validator,
+ SyntaxValidator,
+ InlineInitValidator,
+ VarDeclarationOrderValidator,
+ CollectionOpsValidator,
+ TypeDeclarationValidator,
+ EventDeclarationValidator,
+ MachineStructureValidator,
+ SpecObservesConsistencyValidator,
+ DuplicateDeclarationValidator,
+ SpecForbiddenKeywordValidator,
+ TestFileValidator,
+ PayloadFieldValidator,
+ NamedTupleConstructionValidator,
+)
+from .pipeline import (
+ ValidationPipeline,
+ PipelineResult,
+ validate_p_code,
+ create_default_pipeline,
+)
+from .input_validators import (
+ DesignDocValidator,
+ ProjectPathValidator,
+)
+
+__all__ = [
+ # Results
+ "ValidationResult",
+ "ValidationIssue",
+ "IssueSeverity",
+ "PipelineResult",
+ # Base
+ "Validator",
+ # Code validators
+ "SyntaxValidator",
+ "InlineInitValidator",
+ "VarDeclarationOrderValidator",
+ "CollectionOpsValidator",
+ "TypeDeclarationValidator",
+ "EventDeclarationValidator",
+ "MachineStructureValidator",
+ "SpecObservesConsistencyValidator",
+ "DuplicateDeclarationValidator",
+ "SpecForbiddenKeywordValidator",
+ "TestFileValidator",
+ "PayloadFieldValidator",
+ "NamedTupleConstructionValidator",
+ # Pipeline
+ "ValidationPipeline",
+ "validate_p_code",
+ "create_default_pipeline",
+ # Input validators
+ "DesignDocValidator",
+ "ProjectPathValidator",
+]
diff --git a/Src/PeasyAI/src/core/validation/input_validators.py b/Src/PeasyAI/src/core/validation/input_validators.py
new file mode 100644
index 0000000000..55ff5ba24e
--- /dev/null
+++ b/Src/PeasyAI/src/core/validation/input_validators.py
@@ -0,0 +1,287 @@
+"""
+Input Validators for PeasyAI.
+
+This module provides validators for user inputs:
+- Design document validation
+- Project path validation
+- Configuration validation
+"""
+
+import os
+import re
+from dataclasses import dataclass
+from typing import List, Optional, Tuple
+from pathlib import Path
+
+
+@dataclass
+class InputValidationResult:
+ """Result of input validation."""
+ is_valid: bool
+ errors: List[str]
+ warnings: List[str]
+
+ @classmethod
+ def success(cls) -> "InputValidationResult":
+ return cls(is_valid=True, errors=[], warnings=[])
+
+ @classmethod
+ def failure(cls, error: str) -> "InputValidationResult":
+ return cls(is_valid=False, errors=[error], warnings=[])
+
+
+class DesignDocValidator:
+ """
+ Validates design documents before processing.
+
+ Expects markdown-formatted design docs with headings like
+ ``# Title``, ``## Components``, ``## Interactions``.
+
+ Checks for:
+ - Required sections (title, components, interactions)
+ - Minimum content length
+ - Valid structure
+ """
+
+ # Required markdown heading keywords (case-insensitive)
+ REQUIRED_SECTIONS = [
+ "title",
+ "component",
+ "interaction",
+ ]
+
+ # Minimum document length (characters)
+ MIN_LENGTH = 100
+
+ # Maximum document length (characters)
+ MAX_LENGTH = 100000
+
+ def validate(self, content: str) -> InputValidationResult:
+ """
+ Validate a design document.
+
+ Args:
+ content: The design document content
+
+ Returns:
+ InputValidationResult with any issues
+ """
+ errors = []
+ warnings = []
+
+ # Check length
+ if len(content) < self.MIN_LENGTH:
+ errors.append(
+ f"Design document is too short ({len(content)} chars). "
+ f"Minimum is {self.MIN_LENGTH} characters."
+ )
+
+ if len(content) > self.MAX_LENGTH:
+ errors.append(
+ f"Design document is too long ({len(content)} chars). "
+ f"Maximum is {self.MAX_LENGTH} characters."
+ )
+
+ # Check for required sections via markdown headings
+ content_lower = content.lower()
+ for section in self.REQUIRED_SECTIONS:
+ if section not in content_lower:
+ warnings.append(
+ f"Design document may be missing '{section}' section. "
+ f"Consider adding a '## {section.title()}' heading."
+ )
+
+ # Check for machine/component definitions
+ if not re.search(r"#{1,4}\s+\d*\.?\s*\w|machine|state\s+machine", content, re.IGNORECASE):
+ warnings.append(
+ "No clear component/machine definitions found. "
+ "Consider listing components under a '## Components' heading."
+ )
+
+ # Check for event definitions
+ if not re.search(r"event|message|signal", content, re.IGNORECASE):
+ warnings.append(
+ "No event/message definitions found. "
+ "Consider describing the events/messages exchanged."
+ )
+
+ return InputValidationResult(
+ is_valid=len(errors) == 0,
+ errors=errors,
+ warnings=warnings
+ )
+
+ def extract_metadata(self, content: str) -> dict:
+ """
+ Extract metadata from a markdown design document.
+
+ Args:
+ content: The design document content
+
+ Returns:
+ Dictionary with extracted metadata
+ """
+ metadata = {
+ "title": None,
+ "components": [],
+ "events": [],
+ }
+
+ # Extract title from top-level markdown heading
+ title_match = re.search(r"^#\s+(.+?)\s*$", content, re.MULTILINE)
+ if title_match:
+ metadata["title"] = title_match.group(1).strip()
+
+ # Extract components from numbered lists or sub-headings under ## Components
+ bullet_pattern = r"[-*]\s*(?:\*\*)?(\w[\w\s]*\w)(?:\*\*)?\s*(?:machine|component|:)"
+ for match in re.finditer(bullet_pattern, content, re.IGNORECASE):
+ name = match.group(1).strip()
+ if name not in metadata["components"] and name[0].isupper():
+ metadata["components"].append(name)
+
+ # Also look for #### N. MachineName sub-headings
+ for match in re.finditer(r"^#{3,4}\s+\d+\.\s+(.+?)\s*$", content, re.MULTILINE):
+ name = match.group(1).strip()
+ if name not in metadata["components"] and name[0].isupper():
+ metadata["components"].append(name)
+
+ return metadata
+
+
+class ProjectPathValidator:
+ """
+ Validates project paths.
+
+ Checks for:
+ - Path existence
+ - Required P project structure
+ - Write permissions
+ """
+
+ # Required directories for a P project
+ REQUIRED_DIRS = ["PSrc", "PSpec", "PTst"]
+
+ def validate_existing_project(self, path: str) -> InputValidationResult:
+ """
+ Validate an existing P project path.
+
+ Args:
+ path: Path to the P project
+
+ Returns:
+ InputValidationResult with any issues
+ """
+ errors = []
+ warnings = []
+
+ # Check if path exists
+ if not os.path.exists(path):
+ return InputValidationResult.failure(f"Path does not exist: {path}")
+
+ if not os.path.isdir(path):
+ return InputValidationResult.failure(f"Path is not a directory: {path}")
+
+ # Check for required directories
+ for dir_name in self.REQUIRED_DIRS:
+ dir_path = os.path.join(path, dir_name)
+ if not os.path.exists(dir_path):
+ warnings.append(f"Missing directory: {dir_name}")
+
+ # Check for .pproj file
+ pproj_files = [f for f in os.listdir(path) if f.endswith(".pproj")]
+ if not pproj_files:
+ warnings.append("No .pproj file found in project root")
+
+ # Check for at least one .p file
+ p_files_found = False
+ for dir_name in self.REQUIRED_DIRS:
+ dir_path = os.path.join(path, dir_name)
+ if os.path.exists(dir_path):
+ p_files = [f for f in os.listdir(dir_path) if f.endswith(".p")]
+ if p_files:
+ p_files_found = True
+ break
+
+ if not p_files_found:
+ warnings.append("No .p files found in project")
+
+ return InputValidationResult(
+ is_valid=len(errors) == 0,
+ errors=errors,
+ warnings=warnings
+ )
+
+ def validate_output_path(self, path: str) -> InputValidationResult:
+ """
+ Validate a path for creating a new project.
+
+ Args:
+ path: Path where project will be created
+
+ Returns:
+ InputValidationResult with any issues
+ """
+ errors = []
+ warnings = []
+
+ # Check if parent directory exists
+ parent = os.path.dirname(path)
+ if parent and not os.path.exists(parent):
+ # Try to check if we can create it
+ try:
+ os.makedirs(parent, exist_ok=True)
+ except PermissionError:
+ return InputValidationResult.failure(
+ f"Cannot create directory: {parent} (permission denied)"
+ )
+ except Exception as e:
+ return InputValidationResult.failure(
+ f"Cannot create directory: {parent} ({e})"
+ )
+
+ # Check if path already exists
+ if os.path.exists(path):
+ if os.listdir(path):
+ warnings.append(
+ f"Directory already exists and is not empty: {path}"
+ )
+
+ # Check write permissions
+ test_path = path if os.path.exists(path) else parent or "."
+ if not os.access(test_path, os.W_OK):
+ return InputValidationResult.failure(
+ f"No write permission for: {test_path}"
+ )
+
+ return InputValidationResult(
+ is_valid=len(errors) == 0,
+ errors=errors,
+ warnings=warnings
+ )
+
+ def get_project_files(self, path: str) -> dict:
+ """
+ Get all P files in a project.
+
+ Args:
+ path: Path to the P project
+
+ Returns:
+ Dictionary mapping relative paths to file contents
+ """
+ files = {}
+
+ for dir_name in self.REQUIRED_DIRS:
+ dir_path = os.path.join(path, dir_name)
+ if os.path.exists(dir_path):
+ for filename in os.listdir(dir_path):
+ if filename.endswith(".p"):
+ file_path = os.path.join(dir_path, filename)
+ rel_path = os.path.join(dir_name, filename)
+ try:
+ with open(file_path, "r") as f:
+ files[rel_path] = f.read()
+ except Exception:
+ pass
+
+ return files
diff --git a/Src/PeasyAI/src/core/validation/pipeline.py b/Src/PeasyAI/src/core/validation/pipeline.py
new file mode 100644
index 0000000000..b51e427865
--- /dev/null
+++ b/Src/PeasyAI/src/core/validation/pipeline.py
@@ -0,0 +1,345 @@
+"""
+Unified Validation Pipeline for PeasyAI.
+
+Two-stage approach:
+ Stage 1 — **Auto-fix** (PCodePostProcessor): deterministic regex fixes for
+ the most common LLM mistakes (var ordering, trailing commas, enum
+ syntax, bare halt, forbidden keywords in monitors, etc.).
+ Stage 2 — **Validate** (Validator chain): structured checks that produce
+ typed issues with severity levels. Some validators also carry
+ auto-fix functions; the pipeline applies those too.
+
+The pipeline replaces the ad-hoc orchestration that previously lived in
+``generation.py::_review_generated_code``.
+"""
+
+import logging
+from pathlib import Path
+from typing import Dict, List, Optional, Type
+from dataclasses import dataclass, field
+
+from .validators import (
+ Validator,
+ ValidationIssue,
+ IssueSeverity,
+ SyntaxValidator,
+ InlineInitValidator,
+ VarDeclarationOrderValidator,
+ CollectionOpsValidator,
+ TypeDeclarationValidator,
+ EventDeclarationValidator,
+ MachineStructureValidator,
+ SpecObservesConsistencyValidator,
+ DuplicateDeclarationValidator,
+ SpecForbiddenKeywordValidator,
+ TestFileValidator,
+ PayloadFieldValidator,
+ NamedTupleConstructionValidator,
+)
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class PipelineResult:
+ """Result of running the full validation pipeline."""
+ is_valid: bool
+ original_code: str
+ fixed_code: str
+ issues: List[ValidationIssue] = field(default_factory=list)
+ fixes_applied: List[str] = field(default_factory=list)
+ validators_run: List[str] = field(default_factory=list)
+
+ @property
+ def errors(self) -> List[ValidationIssue]:
+ return [i for i in self.issues if i.severity == IssueSeverity.ERROR]
+
+ @property
+ def warnings(self) -> List[ValidationIssue]:
+ return [i for i in self.issues if i.severity == IssueSeverity.WARNING]
+
+ def summary(self) -> str:
+ lines = [
+ f"Validation {'PASSED' if self.is_valid else 'FAILED'}",
+ f" Validators run: {len(self.validators_run)}",
+ f" Errors: {len(self.errors)}",
+ f" Warnings: {len(self.warnings)}",
+ f" Auto-fixes applied: {len(self.fixes_applied)}",
+ ]
+ return "\n".join(lines)
+
+ def to_review_dict(self) -> Dict:
+ """Backward-compatible dict matching the old _review_generated_code output."""
+ return {
+ "code": self.fixed_code,
+ "fixes_applied": self.fixes_applied,
+ "warnings": [
+ i.message for i in self.issues
+ if i.severity in (IssueSeverity.WARNING, IssueSeverity.INFO)
+ ],
+ "errors": [i.message for i in self.errors],
+ "is_valid": self.is_valid,
+ "validators_run": self.validators_run,
+ }
+
+
+# ── Default validator sets ────────────────────────────────────────────
+
+CORE_VALIDATORS: List[Type[Validator]] = [
+ SyntaxValidator,
+ InlineInitValidator,
+ VarDeclarationOrderValidator,
+ CollectionOpsValidator,
+ TypeDeclarationValidator,
+ EventDeclarationValidator,
+ MachineStructureValidator,
+ SpecObservesConsistencyValidator,
+ DuplicateDeclarationValidator,
+ SpecForbiddenKeywordValidator,
+ PayloadFieldValidator,
+ NamedTupleConstructionValidator,
+]
+
+TEST_FILE_VALIDATORS: List[Type[Validator]] = [
+ TestFileValidator,
+]
+
+
+class ValidationPipeline:
+ """
+ Two-stage pipeline: auto-fix then validate.
+
+ Usage::
+
+ pipeline = ValidationPipeline()
+ result = pipeline.validate(code, project_path="/path/to/project")
+
+ if not result.is_valid:
+ print(result.summary())
+ for issue in result.errors:
+ print(f" ERROR: {issue.message}")
+ """
+
+ def __init__(
+ self,
+ validators: Optional[List[Validator]] = None,
+ include_test_validators: bool = False,
+ ):
+ if validators is not None:
+ self.validators = validators
+ else:
+ classes = list(CORE_VALIDATORS)
+ if include_test_validators:
+ classes.extend(TEST_FILE_VALIDATORS)
+ self.validators = [v() for v in classes]
+
+ def add_validator(self, validator: Validator) -> None:
+ self.validators.append(validator)
+
+ def remove_validator(self, validator_name: str) -> bool:
+ for i, v in enumerate(self.validators):
+ if v.name == validator_name:
+ self.validators.pop(i)
+ return True
+ return False
+
+ # ── Main entry point ──────────────────────────────────────────────
+
+ def validate(
+ self,
+ code: str,
+ context: Optional[Dict[str, str]] = None,
+ filename: str = "",
+ project_path: Optional[str] = None,
+ is_test_file: bool = False,
+ apply_fixes: bool = True,
+ ) -> PipelineResult:
+ """
+ Run the full two-stage pipeline.
+
+ Args:
+ code: The P code to validate.
+ context: Other project files (relative path -> content).
+ If *project_path* is given and *context* is None,
+ project files are loaded automatically.
+ filename: Name of the file being validated (for messages).
+ project_path: Absolute path to the P project root.
+ is_test_file: If True, also run test-file-specific validators.
+ apply_fixes: Whether to apply auto-fixes from both stages.
+ """
+ # Merge on-disk project files with any in-memory context provided
+ # by the caller. In-memory files take precedence (they may be newer
+ # than what's on disk, e.g. during the preview-then-save flow).
+ disk_files: Dict[str, str] = {}
+ if project_path:
+ disk_files = self._load_project_files(project_path)
+ if context is not None:
+ disk_files.update(context)
+ context = disk_files
+
+ # Remove the file being validated from context so cross-file
+ # validators don't see its own declarations as duplicates.
+ # The file may appear under its basename or a relative path.
+ if filename:
+ basename = Path(filename).name
+ context = {
+ k: v for k, v in context.items()
+ if k != filename and Path(k).name != basename
+ }
+
+ fixes_applied: List[str] = []
+ all_issues: List[ValidationIssue] = []
+ validators_run: List[str] = []
+ current_code = code
+
+ # ── Stage 1: deterministic auto-fixes via PCodePostProcessor ──
+ if apply_fixes:
+ try:
+ from ..compilation.p_post_processor import PCodePostProcessor
+ processor = PCodePostProcessor()
+ pp_result = processor.process(
+ current_code, filename,
+ is_test_file=is_test_file,
+ )
+ current_code = pp_result.code
+ fixes_applied.extend(pp_result.fixes_applied)
+
+ for w in pp_result.warnings:
+ all_issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator="PCodePostProcessor",
+ message=w,
+ ))
+ validators_run.append("PCodePostProcessor")
+ except Exception as e:
+ logger.warning(f"Post-processor stage failed: {e}")
+ all_issues.append(ValidationIssue(
+ severity=IssueSeverity.INFO,
+ validator="PCodePostProcessor",
+ message=f"Post-processor skipped: {e}",
+ ))
+
+ # ── Stage 2: structured validators ────────────────────────────
+ for validator in self.validators:
+ try:
+ result = validator.validate(current_code, context)
+ validators_run.append(validator.name)
+ all_issues.extend(result.issues)
+
+ if apply_fixes:
+ for issue in result.issues:
+ if issue.auto_fixable and issue.fix_function:
+ try:
+ new_code = issue.apply_fix(current_code)
+ if new_code != current_code:
+ current_code = new_code
+ fixes_applied.append(
+ f"[{validator.name}] {issue.message}"
+ )
+ except Exception:
+ pass
+ except Exception as e:
+ logger.warning(f"Validator {validator.name} failed: {e}")
+ all_issues.append(ValidationIssue(
+ severity=IssueSeverity.INFO,
+ validator=validator.name,
+ message=f"Validator skipped: {e}",
+ ))
+
+ is_valid = not any(
+ i.severity == IssueSeverity.ERROR for i in all_issues
+ )
+
+ return PipelineResult(
+ is_valid=is_valid,
+ original_code=code,
+ fixed_code=current_code,
+ issues=all_issues,
+ fixes_applied=fixes_applied,
+ validators_run=validators_run,
+ )
+
+ # ── Convenience methods ───────────────────────────────────────────
+
+ def validate_file(
+ self,
+ file_path: str,
+ context: Optional[Dict[str, str]] = None,
+ ) -> PipelineResult:
+ with open(file_path, "r", encoding="utf-8") as f:
+ code = f.read()
+ project_path = self._find_project_root(file_path)
+ filename = Path(file_path).name
+ is_test = "PTst" in file_path
+ return self.validate(
+ code,
+ context=context,
+ filename=filename,
+ project_path=project_path,
+ is_test_file=is_test,
+ )
+
+ def validate_project(
+ self, project_path: str
+ ) -> Dict[str, PipelineResult]:
+ results: Dict[str, PipelineResult] = {}
+ all_files = self._load_project_files(project_path)
+ for rel_path, code in all_files.items():
+ is_test = rel_path.startswith("PTst")
+ results[rel_path] = self.validate(
+ code,
+ context=dict(all_files),
+ filename=rel_path,
+ project_path=project_path,
+ is_test_file=is_test,
+ )
+ return results
+
+ # ── Helpers ────────────────────────────────────────────────────────
+
+ @staticmethod
+ def _load_project_files(project_path: str) -> Dict[str, str]:
+ files: Dict[str, str] = {}
+ pp = Path(project_path)
+ for p_file in pp.rglob("*.p"):
+ try:
+ rel = str(p_file.relative_to(pp))
+ files[rel] = p_file.read_text(encoding="utf-8")
+ except Exception:
+ pass
+ return files
+
+ @staticmethod
+ def _find_project_root(file_path: str) -> Optional[str]:
+ current = Path(file_path).parent
+ for _ in range(10):
+ if any(current.glob("*.pproj")):
+ return str(current)
+ parent = current.parent
+ if parent == current:
+ break
+ current = parent
+ return None
+
+
+# ── Convenience functions ─────────────────────────────────────────────
+
+def create_default_pipeline(is_test_file: bool = False) -> ValidationPipeline:
+ return ValidationPipeline(include_test_validators=is_test_file)
+
+
+def validate_p_code(
+ code: str,
+ context: Optional[Dict[str, str]] = None,
+ filename: str = "",
+ project_path: Optional[str] = None,
+ is_test_file: bool = False,
+) -> PipelineResult:
+ pipeline = create_default_pipeline(is_test_file=is_test_file)
+ return pipeline.validate(
+ code,
+ context=context,
+ filename=filename,
+ project_path=project_path,
+ is_test_file=is_test_file,
+ )
diff --git a/Src/PeasyAI/src/core/validation/validators.py b/Src/PeasyAI/src/core/validation/validators.py
new file mode 100644
index 0000000000..b8bccbac66
--- /dev/null
+++ b/Src/PeasyAI/src/core/validation/validators.py
@@ -0,0 +1,1050 @@
+"""
+P Code Validators.
+
+Structured validators for checking generated P code quality before compilation.
+Each validator targets a specific class of common LLM errors documented in
+resources/context_files/modular/p_common_compilation_errors.txt and
+resources/instructions/p_code_sanity_check.txt.
+
+Validators produce ValidationIssue objects with severity levels and optional
+auto-fix functions. The ValidationPipeline runs them in order and applies
+auto-fixes to produce a cleaned-up code string.
+"""
+
+import re
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, Callable, Dict, List, Optional, Set
+
+from ..compilation.p_code_utils import find_balanced_brace, iter_function_bodies, iter_all_code_blocks
+
+
+class IssueSeverity(Enum):
+ """Severity level of a validation issue."""
+ ERROR = "error"
+ WARNING = "warning"
+ INFO = "info"
+
+
+@dataclass
+class ValidationIssue:
+ """A single validation issue found in code."""
+ severity: IssueSeverity
+ message: str
+ validator: str = ""
+ line_number: Optional[int] = None
+ column: Optional[int] = None
+ code_snippet: Optional[str] = None
+ suggestion: Optional[str] = None
+ auto_fixable: bool = False
+ fix_function: Optional[Callable[[str], str]] = None
+
+ def apply_fix(self, code: str) -> str:
+ if self.fix_function and self.auto_fixable:
+ return self.fix_function(code)
+ return code
+
+
+@dataclass
+class ValidationResult:
+ """Result of running validation on code."""
+ is_valid: bool
+ issues: List[ValidationIssue] = field(default_factory=list)
+ original_code: Optional[str] = None
+ fixed_code: Optional[str] = None
+
+ @property
+ def errors(self) -> List[ValidationIssue]:
+ return [i for i in self.issues if i.severity == IssueSeverity.ERROR]
+
+ @property
+ def warnings(self) -> List[ValidationIssue]:
+ return [i for i in self.issues if i.severity == IssueSeverity.WARNING]
+
+ def merge(self, other: "ValidationResult") -> "ValidationResult":
+ return ValidationResult(
+ is_valid=self.is_valid and other.is_valid,
+ issues=self.issues + other.issues,
+ original_code=self.original_code,
+ fixed_code=other.fixed_code or self.fixed_code,
+ )
+
+
+class Validator(ABC):
+ """Base class for code validators."""
+
+ name: str = "BaseValidator"
+ description: str = "Base validator"
+
+ @abstractmethod
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ pass
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+_BUILTIN_TYPES: Set[str] = {
+ "int", "bool", "string", "float", "machine", "event", "any",
+ "seq", "set", "map",
+}
+
+_P_KEYWORDS: Set[str] = {"halt", "null", "default"}
+
+
+def _line_of(code: str, pos: int) -> int:
+ """1-based line number for character position *pos*."""
+ return code[:pos].count("\n") + 1
+
+
+def _extract_body(code: str, open_pos: int) -> str:
+ """Return the text between braces starting at *open_pos* (exclusive)."""
+ close = find_balanced_brace(code, open_pos)
+ if close == -1:
+ return ""
+ return code[open_pos + 1 : close]
+
+
+# ---------------------------------------------------------------------------
+# 1. SyntaxValidator — balanced delimiters + common syntax mistakes
+# ---------------------------------------------------------------------------
+
+class SyntaxValidator(Validator):
+ """Checks balanced braces/parens and common syntax mistakes."""
+
+ name = "SyntaxValidator"
+ description = "Balanced delimiters and common syntax mistakes"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ issues: List[ValidationIssue] = []
+
+ open_b = code.count("{")
+ close_b = code.count("}")
+ if open_b != close_b:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Unbalanced braces: {open_b} open, {close_b} close",
+ ))
+
+ open_p = code.count("(")
+ close_p = code.count(")")
+ if open_p != close_p:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Unbalanced parentheses: {open_p} open, {close_p} close",
+ ))
+
+ for m in re.finditer(r"if\s*\([^)]*[^=!<>]=(?!=)[^)]*\)", code):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message="Possible assignment in condition (use == for comparison)",
+ line_number=_line_of(code, m.start()),
+ code_snippet=m.group(0)[:60],
+ ))
+
+ # Extraneous semicolons after closing braces: `};` is invalid in P.
+ # Matches `}` followed by `;` (with optional whitespace), but NOT
+ # inside string literals. Common LLM mistake.
+ _BRACE_SEMI = re.compile(r"\}\s*;")
+ brace_semi_matches = list(_BRACE_SEMI.finditer(code))
+ if brace_semi_matches:
+ def _fix_brace_semi(c: str) -> str:
+ return _BRACE_SEMI.sub("}", c)
+
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Extraneous semicolon after closing brace ({{}}; → {{}}) — {len(brace_semi_matches)} occurrence(s)",
+ auto_fixable=True,
+ fix_function=_fix_brace_semi,
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 2. InlineInitValidator — var x: int = 0; is illegal in P
+# ---------------------------------------------------------------------------
+
+class InlineInitValidator(Validator):
+ """
+ Detects and auto-fixes ``var x: T = expr;`` which P does not support.
+ Must be ``var x: T;`` then ``x = expr;`` on a separate line.
+ """
+
+ name = "InlineInitValidator"
+ description = "Inline variable initialization (var x: T = val)"
+
+ _PATTERN = re.compile(
+ r"^(\s*)(var\s+(\w+)\s*:\s*[^;=]+?)\s*=\s*([^;]+);",
+ re.MULTILINE,
+ )
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ issues: List[ValidationIssue] = []
+ for m in self._PATTERN.finditer(code):
+ indent = m.group(1)
+ decl = m.group(2).rstrip()
+ var_name = m.group(3)
+ init_expr = m.group(4).strip()
+
+ def _make_fix(pat=m.group(0), ind=indent, d=decl, vn=var_name, ie=init_expr):
+ def fixer(c: str) -> str:
+ return c.replace(pat, f"{d};\n{ind}{vn} = {ie};", 1)
+ return fixer
+
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Inline initialization not allowed: '{m.group(0).strip()}'",
+ line_number=_line_of(code, m.start()),
+ suggestion=f"Split into: {decl}; then {var_name} = {init_expr};",
+ auto_fixable=True,
+ fix_function=_make_fix(),
+ ))
+
+ return ValidationResult(
+ is_valid=not issues,
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 3. VarDeclarationOrderValidator — vars must precede statements
+# ---------------------------------------------------------------------------
+
+class VarDeclarationOrderValidator(Validator):
+ """
+ Detects variable declarations that appear after non-declaration
+ statements inside a function body and auto-fixes by hoisting them.
+ """
+
+ name = "VarDeclarationOrderValidator"
+ description = "Variable declarations must precede statements in functions"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ issues: List[ValidationIssue] = []
+ replacements = []
+
+ for block_name, header, body, start_pos, close_pos in iter_all_code_blocks(code):
+ lines = body.split("\n")
+ var_lines: List[str] = []
+ other_lines: List[str] = []
+
+ for line in lines:
+ stripped = line.strip()
+ if stripped.startswith("var ") and ";" in stripped:
+ var_lines.append(line)
+ else:
+ other_lines.append(line)
+
+ if not var_lines:
+ continue
+
+ first_non_var = -1
+ first_var = -1
+ last_var = -1
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ if not stripped or stripped.startswith("//"):
+ continue
+ if stripped.startswith("var ") and ";" in stripped:
+ if first_var == -1:
+ first_var = i
+ last_var = i
+ else:
+ if first_non_var == -1:
+ first_non_var = i
+
+ needs_reorder = (
+ first_var != -1
+ and first_non_var != -1
+ and (first_var > first_non_var or last_var > first_non_var)
+ )
+ if needs_reorder:
+ new_body = "\n".join(var_lines + other_lines)
+ replacement = header + "{" + new_body + "}"
+ replacements.append((start_pos, close_pos + 1, replacement))
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Variable declaration after statement in '{block_name}'",
+ suggestion="Move var declarations to the start of the block",
+ auto_fixable=True,
+ fix_function=self._make_bulk_fix(replacements[:]),
+ ))
+
+ # Also detect vars declared inside while/foreach loops
+ for m in re.finditer(r"\b(?:while|foreach)\s*\([^)]*\)\s*\{", code):
+ loop_body = _extract_body(code, m.end() - 1)
+ if not loop_body:
+ continue
+ for vm in re.finditer(r"^\s*var\s+(\w+)\s*:", loop_body, re.MULTILINE):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Variable '{vm.group(1)}' declared inside a loop body",
+ line_number=_line_of(code, m.start()),
+ suggestion="Move var declaration to the enclosing function/entry block",
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+ @staticmethod
+ def _make_bulk_fix(repls):
+ def fixer(code: str) -> str:
+ for start, end, repl in reversed(repls):
+ code = code[:start] + repl + code[end:]
+ return code
+ return fixer
+
+
+# ---------------------------------------------------------------------------
+# 4. CollectionOpsValidator — detect wrong seq/set/map operations
+# ---------------------------------------------------------------------------
+
+class CollectionOpsValidator(Validator):
+ """
+ Detects common collection operation mistakes:
+ - ``append(seq, x)`` (nonexistent function)
+ - ``seq += (value)`` without index (should be ``seq += (sizeof(seq), value)``)
+ """
+
+ name = "CollectionOpsValidator"
+ description = "Invalid collection operations (append, wrong seq +=)"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ issues: List[ValidationIssue] = []
+
+ for m in re.finditer(r"\bappend\s*\(", code):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message="P has no append() function",
+ line_number=_line_of(code, m.start()),
+ suggestion="Use seq += (sizeof(seq), element); to append",
+ ))
+
+ for m in re.finditer(r"\breceive\s*\(", code):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message="P has no receive() function — use event handler parameters",
+ line_number=_line_of(code, m.start()),
+ suggestion="Use 'on eEvent do (payload: Type) { ... }' instead",
+ ))
+
+ # seq = seq + (elem,) is wrong — should be seq += (sizeof(seq), elem)
+ for m in re.finditer(
+ r"\b(\w+)\s*=\s*\1\s*\+\s*\(", code
+ ):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=f"Possible wrong sequence concatenation: '{m.group(0).strip()}'",
+ line_number=_line_of(code, m.start()),
+ suggestion="Use 'seq += (sizeof(seq), element);' to append to a sequence",
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 5. TypeDeclarationValidator — types used but not declared
+# ---------------------------------------------------------------------------
+
+class TypeDeclarationValidator(Validator):
+ """Checks that types referenced in code are declared somewhere in the project."""
+
+ name = "TypeDeclarationValidator"
+ description = "Checks that all types are declared"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ context = context or {}
+ issues: List[ValidationIssue] = []
+
+ declared = set(_BUILTIN_TYPES)
+ all_code = [code] + list(context.values())
+ for src in all_code:
+ declared.update(re.findall(r"\btype\s+(\w+)\s*=", src))
+ declared.update(re.findall(r"\benum\s+(\w+)\s*\{", src))
+ declared.update(re.findall(r"\bmachine\s+(\w+)\s*\{", src))
+
+ used: Set[str] = set()
+ used.update(re.findall(r"\bvar\s+\w+\s*:\s*(\w+)", code))
+ for m in re.finditer(r"\bmap\[(\w+),\s*(\w+)\]", code):
+ used.add(m.group(1))
+ used.add(m.group(2))
+ used.update(re.findall(r"\bseq\[(\w+)\]", code))
+ used.update(re.findall(r"\bset\[(\w+)\]", code))
+ used -= _BUILTIN_TYPES
+
+ undefined = used - declared
+ for t in sorted(undefined):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=f"Type '{t}' may not be declared",
+ suggestion=f"Ensure 'type {t} = ...' exists in Enums_Types_Events.p",
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 6. EventDeclarationValidator — events used but not declared
+# ---------------------------------------------------------------------------
+
+class EventDeclarationValidator(Validator):
+ """Checks that events used in send/raise/on handlers are declared."""
+
+ name = "EventDeclarationValidator"
+ description = "Checks that all events are declared"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ context = context or {}
+ issues: List[ValidationIssue] = []
+
+ declared: Set[str] = set()
+ for src in [code] + list(context.values()):
+ declared.update(re.findall(r"\bevent\s+(\w+)", src))
+
+ used: Set[str] = set()
+ used.update(re.findall(r"\bsend\s+[^,]+,\s*(\w+)\s*(?:,|;)", code))
+ used.update(re.findall(r"\braise\s+(\w+)", code))
+ used.update(re.findall(r"\bon\s+(\w+)\s+(?:do|goto)\b", code))
+ used.update(re.findall(r"(?:ignore|defer)\s+(\w+)\s*[;,]", code))
+ used.update(re.findall(r"\bannounce\s+(\w+)\s*,", code))
+ used -= _P_KEYWORDS
+
+ undefined = used - declared
+ for ev in sorted(undefined):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=f"Event '{ev}' may not be declared",
+ suggestion=f"Ensure 'event {ev}' exists in Enums_Types_Events.p",
+ ))
+
+ return ValidationResult(
+ is_valid=True,
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 7. MachineStructureValidator — start state, non-empty states
+# ---------------------------------------------------------------------------
+
+class MachineStructureValidator(Validator):
+ """Checks that machines have a start state and states have handlers."""
+
+ name = "MachineStructureValidator"
+ description = "Checks machine structure validity"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ issues: List[ValidationIssue] = []
+
+ for m in re.finditer(r"\bmachine\s+(\w+)\s*\{", code):
+ machine_name = m.group(1)
+ body = _extract_body(code, m.end() - 1)
+ if not body:
+ continue
+
+ if "start state" not in body:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Machine '{machine_name}' has no start state",
+ suggestion="Add 'start state Init {{ ... }}'",
+ ))
+
+ defined_states: Set[str] = set()
+ for sm in re.finditer(r"\bstate\s+(\w+)\s*\{", body):
+ state_name = sm.group(1)
+ defined_states.add(state_name)
+ state_body = _extract_body(body, sm.end() - 1)
+ if state_body is not None and not state_body.strip():
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.INFO,
+ validator=self.name,
+ message=f"State '{state_name}' in machine '{machine_name}' has an empty body",
+ suggestion="Add entry/exit handlers or event handlers",
+ ))
+
+ goto_targets = set(re.findall(r"\bgoto\s+(\w+)\s*;", body))
+ undefined_targets = goto_targets - defined_states
+ for target in sorted(undefined_targets):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=f"Machine '{machine_name}' has goto to undefined state '{target}'",
+ suggestion=f"Define 'state {target} {{ ... }}' or fix the goto target name",
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 8. SpecObservesConsistencyValidator — spec observes ↔ handler sync
+# ---------------------------------------------------------------------------
+
+class SpecObservesConsistencyValidator(Validator):
+ """
+ For spec monitors, checks two things:
+ 1. Events in the ``observes`` clause are actually defined.
+ 2. Events handled inside the spec body (``on eX do/goto``) are listed
+ in the ``observes`` clause — otherwise the spec silently ignores them.
+ """
+
+ name = "SpecObservesConsistencyValidator"
+ description = "Spec monitor observes-clause / handler consistency"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ context = context or {}
+ issues: List[ValidationIssue] = []
+
+ all_defined_events: Set[str] = set()
+ for src in [code] + list(context.values()):
+ all_defined_events.update(re.findall(r"\bevent\s+(\w+)", src))
+
+ for m in re.finditer(r"\bspec\s+(\w+)\s+observes\s+([^{]+)\{", code):
+ spec_name = m.group(1)
+ observes_str = m.group(2).strip().rstrip(",")
+ observed = {e.strip() for e in observes_str.split(",") if e.strip()}
+
+ for ev in sorted(observed):
+ if ev not in all_defined_events:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=(
+ f"Spec '{spec_name}' observes undefined event '{ev}'"
+ ),
+ suggestion=f"Declare 'event {ev}' or remove from observes list",
+ ))
+
+ body = _extract_body(code, m.end() - 1)
+ if not body:
+ continue
+ handled = set(re.findall(r"\bon\s+(\w+)\s+(?:do|goto)\b", body))
+ handled -= _P_KEYWORDS
+
+ missing_from_observes = handled - observed
+ for ev in sorted(missing_from_observes):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=(
+ f"Spec '{spec_name}' handles event '{ev}' but does not "
+ f"list it in 'observes'. The spec will never receive it."
+ ),
+ suggestion=f"Add '{ev}' to the observes clause",
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 9. DuplicateDeclarationValidator — cross-file duplicate names
+# ---------------------------------------------------------------------------
+
+class DuplicateDeclarationValidator(Validator):
+ """
+ Detects duplicate type, event, machine, or spec declarations across
+ the file being validated and the rest of the project (context).
+ """
+
+ name = "DuplicateDeclarationValidator"
+ description = "Duplicate declarations across project files"
+
+ _DECL_PATTERNS = [
+ ("type", r"\btype\s+(\w+)\s*="),
+ ("enum", r"\benum\s+(\w+)\s*\{"),
+ ("event", r"\bevent\s+(\w+)"),
+ ("machine", r"\bmachine\s+(\w+)\s*\{"),
+ ("spec", r"\bspec\s+(\w+)\s+observes\b"),
+ ]
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ context = context or {}
+ issues: List[ValidationIssue] = []
+
+ context_names: Dict[str, List[str]] = {}
+ for filepath, src in context.items():
+ for kind, pattern in self._DECL_PATTERNS:
+ for name in re.findall(pattern, src):
+ context_names.setdefault(name, []).append(f"{kind} in {filepath}")
+
+ for kind, pattern in self._DECL_PATTERNS:
+ for name in re.findall(pattern, code):
+ if name in context_names:
+ existing = context_names[name]
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=(
+ f"Duplicate {kind} declaration '{name}' — "
+ f"already declared as {existing[0]}"
+ ),
+ suggestion=f"Remove the duplicate or rename '{name}'",
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 10. SpecForbiddenKeywordValidator — this/new/send/… in monitors
+# ---------------------------------------------------------------------------
+
+class SpecForbiddenKeywordValidator(Validator):
+ """
+ Detects forbidden keywords (this, new, send, announce, receive)
+ inside spec monitor bodies.
+ """
+
+ name = "SpecForbiddenKeywordValidator"
+ description = "Forbidden keywords inside spec monitors"
+
+ _FORBIDDEN = {
+ "this": r"\bthis\b",
+ "new": r"\bnew\s+\w+",
+ "send": r"\bsend\s+",
+ "announce": r"\bannounce\s+",
+ "receive": r"\breceive\s*\{",
+ }
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ issues: List[ValidationIssue] = []
+
+ for m in re.finditer(r"\bspec\s+(\w+)\s+observes\s+[^{]+\{", code):
+ spec_name = m.group(1)
+ body = _extract_body(code, m.end() - 1)
+ if not body:
+ continue
+
+ for kw, pat in self._FORBIDDEN.items():
+ if re.search(pat, body):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=(
+ f"Spec monitor '{spec_name}' uses forbidden keyword '{kw}'"
+ ),
+ suggestion=(
+ "Spec monitors cannot use this/new/send/announce/receive/$/$$/pop"
+ ),
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 11. TestFileValidator — test declarations, spec assertions, constructors
+# ---------------------------------------------------------------------------
+
+class TestFileValidator(Validator):
+ """
+ For test files (PTst), checks:
+ - Test declarations exist (PChecker needs them to discover tests).
+ - Test declarations assert all project specs.
+ - Machine constructors match expected config types.
+ """
+
+ name = "TestFileValidator"
+ description = "Test file completeness (declarations, spec assertions)"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ context = context or {}
+ issues: List[ValidationIssue] = []
+
+ has_test_decl = bool(re.search(r"^\s*test\s+\w+\s*\[", code, re.MULTILINE))
+ has_machines = bool(re.search(r"\bmachine\s+\w+", code))
+
+ if has_machines and not has_test_decl:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message="Test file has machines but no test declarations",
+ suggestion="Add 'test tcName [main=Machine]: assert Spec in { ... };'",
+ ))
+
+ if has_test_decl:
+ spec_names: List[str] = []
+ for src in context.values():
+ spec_names.extend(re.findall(r"\bspec\s+(\w+)\s+observes\b", src))
+
+ test_decls = re.findall(
+ r"test\s+(\w+)\s*\[main=\w+\]\s*:\s*(.*?);",
+ code,
+ re.DOTALL,
+ )
+ for test_name, body in test_decls:
+ for spec in spec_names:
+ if not re.search(rf"\bassert\s+{re.escape(spec)}\s+in\b", body):
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=(
+ f"Test '{test_name}' does not assert spec '{spec}'"
+ ),
+ suggestion=(
+ f"Add 'assert {spec} in' so PChecker verifies the property"
+ ),
+ ))
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+
+# ---------------------------------------------------------------------------
+# 12. PayloadFieldValidator — field name correctness
+# ---------------------------------------------------------------------------
+
+class PayloadFieldValidator(Validator):
+ """
+ Checks that field accesses on typed parameters (``param.field``) use
+ field names that actually exist in the corresponding type definition.
+ Requires context files containing the type definitions.
+ """
+
+ name = "PayloadFieldValidator"
+ description = "Payload field name correctness"
+
+ # Patterns that introduce a typed parameter: fun, entry, on...do
+ _PARAM_PATTERNS = [
+ re.compile(r"\bfun\s+\w+\s*\((\w+)\s*:\s*(\w+)"),
+ re.compile(r"\bentry\s*\((\w+)\s*:\s*(\w+)"),
+ re.compile(r"\bon\s+\w+\s+do\s*\((\w+)\s*:\s*(\w+)"),
+ ]
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ context = context or {}
+ issues: List[ValidationIssue] = []
+
+ type_fields = self._extract_type_fields(context)
+ if not type_fields:
+ return ValidationResult(is_valid=True, issues=[], original_code=code)
+
+ for pattern in self._PARAM_PATTERNS:
+ for func_match in pattern.finditer(code):
+ param_name = func_match.group(1)
+ param_type = func_match.group(2)
+ if param_type not in type_fields:
+ continue
+ valid = set(type_fields[param_type])
+ for access in re.finditer(
+ rf"\b{re.escape(param_name)}\.(\w+)\b", code
+ ):
+ fld = access.group(1)
+ if fld not in valid:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=(
+ f"'{param_name}.{fld}' — field '{fld}' not in type "
+ f"'{param_type}'. Valid: {sorted(valid)}"
+ ),
+ line_number=_line_of(code, access.start()),
+ ))
+
+ return ValidationResult(
+ is_valid=True,
+ issues=issues,
+ original_code=code,
+ )
+
+ @staticmethod
+ def _extract_type_fields(context: Dict[str, str]) -> Dict[str, List[str]]:
+ result: Dict[str, List[str]] = {}
+ for src in context.values():
+ for m in re.finditer(r"\btype\s+(\w+)\s*=\s*\(([^)]+)\)\s*;", src):
+ fields = [fm.group(1) for fm in re.finditer(r"(\w+)\s*:", m.group(2))]
+ if fields:
+ result[m.group(1)] = fields
+ return result
+
+
+# ---------------------------------------------------------------------------
+# Helpers shared by new validators
+# ---------------------------------------------------------------------------
+
+def _extract_type_defs(sources: List[str]) -> Dict[str, List[str]]:
+ """
+ Parse ``type tFoo = (field1: T1, field2: T2);`` from multiple source
+ strings and return ``{type_name: [field1, field2, ...]}``.
+ """
+ result: Dict[str, List[str]] = {}
+ for src in sources:
+ for m in re.finditer(r"\btype\s+(\w+)\s*=\s*\(([^)]+)\)\s*;", src):
+ fields = [fm.group(1) for fm in re.finditer(r"(\w+)\s*:", m.group(2))]
+ if fields:
+ result[m.group(1)] = fields
+ return result
+
+
+def _extract_event_payload_types(sources: List[str]) -> Dict[str, str]:
+ """
+ Parse ``event eFoo: tBarPayload;`` and return ``{eFoo: tBarPayload}``.
+ Events without a payload type are not included.
+ """
+ result: Dict[str, str] = {}
+ for src in sources:
+ for m in re.finditer(r"\bevent\s+(\w+)\s*:\s*(\w+)\s*;", src):
+ result[m.group(1)] = m.group(2)
+ return result
+
+
+# ---------------------------------------------------------------------------
+# 13. NamedTupleConstructionValidator — send/new must use named tuples
+# ---------------------------------------------------------------------------
+
+class NamedTupleConstructionValidator(Validator):
+ """
+ Cross-references ``type tFoo = (field: T, ...);`` declarations against
+ ``new Machine(...)`` and ``send target, eEvent, ...`` call sites to
+ detect cases where a bare value is passed instead of a named tuple.
+
+ For example, if ``type tConfig = (nodes: seq[machine]);`` is declared
+ and the code has ``new FailureDetector(nodeSeq)`` instead of
+ ``new FailureDetector((nodes = nodeSeq,))``, this validator flags it.
+ """
+
+ name = "NamedTupleConstructionValidator"
+ description = "Named tuple construction correctness at send/new call sites"
+
+ def validate(
+ self, code: str, context: Optional[Dict[str, str]] = None
+ ) -> ValidationResult:
+ context = context or {}
+ issues: List[ValidationIssue] = []
+ all_sources = [code] + list(context.values())
+
+ type_defs = _extract_type_defs(all_sources)
+ event_payloads = _extract_event_payload_types(all_sources)
+
+ # Map machine name -> its entry parameter type (if any).
+ machine_config_types: Dict[str, str] = {}
+ for src in all_sources:
+ self._extract_machine_config_types(src, type_defs, machine_config_types)
+
+ # Check `new Machine(args)` call sites
+ for m in re.finditer(r"\bnew\s+(\w+)\s*\(([^)]*)\)", code):
+ machine_name = m.group(1)
+ args = m.group(2).strip()
+ if not args or machine_name not in machine_config_types:
+ continue
+ config_type = machine_config_types[machine_name]
+ if config_type not in type_defs:
+ continue
+ fields = type_defs[config_type]
+ self._check_tuple_construction(
+ code, m, args, fields, config_type,
+ f"new {machine_name}(...)", issues,
+ )
+
+ # Check `send target, eEvent, args` call sites
+ for m in re.finditer(
+ r"\bsend\s+[^,]+,\s*(\w+)\s*,\s*(.+?)\s*;", code
+ ):
+ event_name = m.group(1)
+ args = m.group(2).strip()
+ if event_name not in event_payloads:
+ continue
+ payload_type = event_payloads[event_name]
+ if payload_type not in type_defs:
+ continue
+ fields = type_defs[payload_type]
+ self._check_tuple_construction(
+ code, m, args, fields, payload_type,
+ f"send ..., {event_name}, ...", issues,
+ )
+
+ return ValidationResult(
+ is_valid=not any(i.severity == IssueSeverity.ERROR for i in issues),
+ issues=issues,
+ original_code=code,
+ )
+
+ def _check_tuple_construction(
+ self,
+ code: str,
+ match: re.Match,
+ args: str,
+ fields: List[str],
+ type_name: str,
+ call_desc: str,
+ issues: List[ValidationIssue],
+ ) -> None:
+ """Check whether *args* looks like a proper named-tuple construction."""
+ # A proper named tuple has `(field = value, ...)` or `(field = value)`
+ # If args starts with '(' and contains '=' it's likely correct.
+ # If args starts with '(' but has NO '=' and the type has named fields,
+ # it's a bare/anonymous tuple — flag it.
+ inner = args
+ if inner.startswith("(") and inner.endswith(")"):
+ inner = inner[1:-1].strip()
+
+ if not inner:
+ return
+
+ has_named_fields = bool(re.search(r"\w+\s*=", inner))
+ if has_named_fields:
+ # Looks like a named tuple — check field names match
+ used_fields = set(re.findall(r"(\w+)\s*=", inner))
+ expected = set(fields)
+ missing = expected - used_fields
+ extra = used_fields - expected
+ if missing:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=(
+ f"In {call_desc}: missing field(s) {sorted(missing)} "
+ f"for type '{type_name}'"
+ ),
+ line_number=_line_of(code, match.start()),
+ suggestion=f"Expected fields: {sorted(fields)}",
+ ))
+ if extra:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.WARNING,
+ validator=self.name,
+ message=(
+ f"In {call_desc}: unexpected field(s) {sorted(extra)} "
+ f"for type '{type_name}'"
+ ),
+ line_number=_line_of(code, match.start()),
+ suggestion=f"Expected fields: {sorted(fields)}",
+ ))
+ else:
+ # No named fields — this is a bare/anonymous value
+ if len(fields) >= 1:
+ issues.append(ValidationIssue(
+ severity=IssueSeverity.ERROR,
+ validator=self.name,
+ message=(
+ f"In {call_desc}: passing bare value '{args.strip()[:50]}' "
+ f"but type '{type_name}' expects named tuple "
+ f"({', '.join(f + ' = ...' for f in fields)})"
+ ),
+ line_number=_line_of(code, match.start()),
+ suggestion=(
+ f"Use ({', '.join(f + ' = ' for f in fields)},) "
+ f"instead of a bare value"
+ ),
+ ))
+
+ @staticmethod
+ def _extract_machine_config_types(
+ src: str,
+ type_defs: Dict[str, List[str]],
+ result: Dict[str, str],
+ ) -> None:
+ """
+ Find machines whose start-state entry takes a typed parameter and
+ record the mapping machine_name -> config_type_name.
+
+ Handles both patterns:
+ - ``entry (param: tConfigType) { ... }``
+ - ``entry InitEntry;`` with ``fun InitEntry(param: tConfigType) { ... }``
+ """
+ for mm in re.finditer(r"\bmachine\s+(\w+)\s*\{", src):
+ machine_name = mm.group(1)
+ body = _extract_body(src, mm.end() - 1)
+ if not body:
+ continue
+
+ # Find start state
+ sm = re.search(r"\bstart\s+state\s+\w+\s*\{", body)
+ if not sm:
+ continue
+ state_body = _extract_body(body, sm.end() - 1)
+ if not state_body:
+ continue
+
+ # Pattern 1: inline entry with parameter
+ em = re.search(r"\bentry\s*\(\s*\w+\s*:\s*(\w+)\s*\)", state_body)
+ if em:
+ result[machine_name] = em.group(1)
+ continue
+
+ # Pattern 2: entry delegates to a named function
+ em = re.search(r"\bentry\s+(\w+)\s*;", state_body)
+ if em:
+ func_name = em.group(1)
+ fm = re.search(
+ rf"\bfun\s+{re.escape(func_name)}\s*\(\s*\w+\s*:\s*(\w+)\s*\)",
+ src,
+ )
+ if fm:
+ result[machine_name] = fm.group(1)
+ continue
diff --git a/Src/PeasyAI/src/core/workflow/__init__.py b/Src/PeasyAI/src/core/workflow/__init__.py
new file mode 100644
index 0000000000..a944b47642
--- /dev/null
+++ b/Src/PeasyAI/src/core/workflow/__init__.py
@@ -0,0 +1,83 @@
+"""
+Workflow Engine for PeasyAI.
+
+This module provides a flexible workflow execution system that supports:
+- Sequential and parallel step execution
+- Retry logic with configurable attempts
+- Human-in-the-loop escalation
+- Event-driven observability for multiple UIs
+
+Usage:
+ from src.core.workflow import WorkflowEngine, EventEmitter, WorkflowFactory
+ from src.core.workflow.p_steps import GenerateTypesEventsStep
+
+ # Create services
+ generation_service = GenerationService(...)
+ compilation_service = CompilationService(...)
+ fixer_service = FixerService(...)
+
+ # Create factory and engine
+ factory = WorkflowFactory(generation_service, compilation_service, fixer_service)
+ emitter = EventEmitter()
+ engine = WorkflowEngine(emitter)
+
+ # Register event listeners
+ emitter.on(WorkflowEvent.STEP_COMPLETED, lambda data: print(f"Completed: {data}"))
+
+ # Create and execute workflow
+ workflow = factory.create_full_generation_workflow(machine_names=["Client", "Server"])
+ engine.register_workflow(workflow)
+ result = engine.execute("full_generation", {"design_doc": doc, "project_path": path})
+"""
+
+from .steps import (
+ WorkflowStep,
+ StepStatus,
+ StepResult,
+ CompositeStep,
+)
+from .events import (
+ WorkflowEvent,
+ EventEmitter,
+ EventData,
+ LoggingEventListener,
+)
+from .engine import (
+ WorkflowEngine,
+ WorkflowDefinition,
+ WorkflowState,
+)
+try:
+ from .factory import (
+ WorkflowFactory,
+ extract_machine_names_from_design_doc,
+ create_workflow_engine_from_config,
+ )
+ HAS_FACTORY = True
+except ImportError:
+ # Optional dependency path (e.g., yaml) may be unavailable in lightweight environments.
+ HAS_FACTORY = False
+
+__all__ = [
+ # Steps
+ "WorkflowStep",
+ "StepStatus",
+ "StepResult",
+ "CompositeStep",
+ # Events
+ "WorkflowEvent",
+ "EventEmitter",
+ "EventData",
+ "LoggingEventListener",
+ # Engine
+ "WorkflowEngine",
+ "WorkflowDefinition",
+ "WorkflowState",
+]
+
+if HAS_FACTORY:
+ __all__.extend([
+ "WorkflowFactory",
+ "extract_machine_names_from_design_doc",
+ "create_workflow_engine_from_config",
+ ])
diff --git a/Src/PeasyAI/src/core/workflow/engine.py b/Src/PeasyAI/src/core/workflow/engine.py
new file mode 100644
index 0000000000..14812fc8d7
--- /dev/null
+++ b/Src/PeasyAI/src/core/workflow/engine.py
@@ -0,0 +1,508 @@
+"""
+Workflow Engine for PeasyAI.
+
+This module provides the core workflow execution engine that:
+- Executes workflows as sequences of steps
+- Handles retries with configurable limits
+- Supports human-in-the-loop escalation
+- Emits events for observability
+- Manages workflow state for pause/resume
+"""
+
+from dataclasses import dataclass, field
+from typing import Any, Callable, Dict, List, Optional
+import uuid
+from datetime import datetime
+import json
+from pathlib import Path
+
+from .steps import WorkflowStep, StepResult, StepStatus
+from .events import WorkflowEvent, EventEmitter
+
+
+@dataclass
+class WorkflowDefinition:
+ """
+ Definition of a workflow.
+
+ Attributes:
+ name: Unique identifier for the workflow
+ description: Human-readable description
+ steps: Ordered list of steps to execute
+ continue_on_failure: If True, continue executing after step failures
+ on_step_complete: Optional callback when a step completes
+ on_human_needed: Optional callback when human input is needed
+ """
+ name: str
+ description: str = ""
+ steps: List[WorkflowStep] = field(default_factory=list)
+ continue_on_failure: bool = False
+ on_step_complete: Optional[Callable[[str, StepResult], None]] = None
+ on_human_needed: Optional[Callable[[str, str, Dict], None]] = None
+
+
+@dataclass
+class WorkflowState:
+ """
+ State of a workflow execution.
+
+ Used for tracking progress, pause/resume, and debugging.
+ """
+ workflow_id: str
+ workflow_name: str
+ status: str # "running", "completed", "failed", "paused"
+ context: Dict[str, Any]
+ current_step_index: int = 0
+ started_at: Optional[datetime] = None
+ completed_at: Optional[datetime] = None
+ errors: List[str] = field(default_factory=list)
+ completed_steps: List[str] = field(default_factory=list)
+ skipped_steps: List[str] = field(default_factory=list)
+
+
+class WorkflowEngine:
+ """
+ Executes workflows with observability and human-in-the-loop support.
+
+ The engine manages workflow execution by:
+ 1. Iterating through steps in order
+ 2. Checking if steps can be skipped
+ 3. Executing steps with retry logic
+ 4. Emitting events for UI updates
+ 5. Handling human escalation when needed
+ 6. Managing state for pause/resume
+
+ Usage:
+ from src.core.workflow import WorkflowEngine, EventEmitter, WorkflowDefinition
+
+ emitter = EventEmitter()
+ engine = WorkflowEngine(emitter)
+
+ # Define workflow
+ workflow = WorkflowDefinition(
+ name="my_workflow",
+ steps=[step1, step2, step3]
+ )
+ engine.register_workflow(workflow)
+
+ # Execute
+ result = engine.execute("my_workflow", {"input": "value"})
+ """
+
+ def __init__(self, event_emitter: EventEmitter, state_store_path: Optional[str] = None):
+ self.emitter = event_emitter
+ self.workflows: Dict[str, WorkflowDefinition] = {}
+ self.active_states: Dict[str, WorkflowState] = {}
+ self.state_store_path = Path(state_store_path) if state_store_path else None
+ self._load_active_states()
+
+ def _serialize_context(self, value: Any) -> Any:
+ """Best-effort JSON serialization for workflow context values."""
+ if isinstance(value, (str, int, float, bool)) or value is None:
+ return value
+ if isinstance(value, datetime):
+ return value.isoformat()
+ if isinstance(value, dict):
+ return {str(k): self._serialize_context(v) for k, v in value.items()}
+ if isinstance(value, (list, tuple)):
+ return [self._serialize_context(v) for v in value]
+ return str(value)
+
+ def _save_active_states(self) -> None:
+ """Persist active/paused workflow states to disk."""
+ if not self.state_store_path:
+ return
+ try:
+ self.state_store_path.parent.mkdir(parents=True, exist_ok=True)
+ payload = {
+ "active_states": [
+ {
+ "workflow_id": s.workflow_id,
+ "workflow_name": s.workflow_name,
+ "status": s.status,
+ "context": self._serialize_context(s.context),
+ "current_step_index": s.current_step_index,
+ "started_at": s.started_at.isoformat() if s.started_at else None,
+ "completed_at": s.completed_at.isoformat() if s.completed_at else None,
+ "errors": s.errors,
+ "completed_steps": s.completed_steps,
+ "skipped_steps": s.skipped_steps,
+ }
+ for s in self.active_states.values()
+ if s.status in ["running", "paused"]
+ ]
+ }
+ with open(self.state_store_path, "w", encoding="utf-8") as f:
+ json.dump(payload, f, indent=2)
+ except Exception:
+ # Persistence should never break execution flow.
+ pass
+
+ def _load_active_states(self) -> None:
+ """Load previously persisted active/paused workflow states."""
+ if not self.state_store_path or not self.state_store_path.exists():
+ return
+ try:
+ with open(self.state_store_path, "r", encoding="utf-8") as f:
+ payload = json.load(f)
+ for raw in payload.get("active_states", []):
+ self.active_states[raw["workflow_id"]] = WorkflowState(
+ workflow_id=raw["workflow_id"],
+ workflow_name=raw["workflow_name"],
+ status=raw["status"],
+ context=raw.get("context", {}),
+ current_step_index=raw.get("current_step_index", 0),
+ started_at=datetime.fromisoformat(raw["started_at"]) if raw.get("started_at") else None,
+ completed_at=datetime.fromisoformat(raw["completed_at"]) if raw.get("completed_at") else None,
+ errors=raw.get("errors", []),
+ completed_steps=raw.get("completed_steps", []),
+ skipped_steps=raw.get("skipped_steps", []),
+ )
+ except Exception:
+ # Invalid state files should not block startup.
+ self.active_states = {}
+
+ def register_workflow(self, workflow: WorkflowDefinition) -> None:
+ """
+ Register a workflow definition.
+
+ Args:
+ workflow: The workflow definition to register
+ """
+ self.workflows[workflow.name] = workflow
+
+ def execute(
+ self,
+ workflow_name: str,
+ initial_context: Dict[str, Any],
+ resume_from: Optional[str] = None
+ ) -> Dict[str, Any]:
+ """
+ Execute a workflow.
+
+ Args:
+ workflow_name: Name of the registered workflow to execute
+ initial_context: Initial context/inputs for the workflow
+ resume_from: Optional step name to resume from (for paused workflows)
+
+ Returns:
+ Final context dictionary with results and any errors
+ """
+ if workflow_name not in self.workflows:
+ raise ValueError(f"Unknown workflow: {workflow_name}")
+
+ workflow = self.workflows[workflow_name]
+ workflow_id = str(uuid.uuid4())
+
+ # Initialize state
+ state = WorkflowState(
+ workflow_id=workflow_id,
+ workflow_name=workflow_name,
+ status="running",
+ context={**initial_context},
+ started_at=datetime.now()
+ )
+ self.active_states[workflow_id] = state
+ self._save_active_states()
+
+ # Set workflow ID for event tracking
+ self.emitter.set_workflow_id(workflow_id)
+
+ # Emit start event
+ self.emitter.emit(WorkflowEvent.STARTED, {
+ "workflow": workflow_name,
+ "workflow_id": workflow_id,
+ "context": {k: v for k, v in state.context.items() if not k.startswith("_")}
+ })
+
+ # Find starting step index
+ start_index = 0
+ if resume_from:
+ for i, step in enumerate(workflow.steps):
+ if step.name == resume_from:
+ start_index = i
+ break
+ state.current_step_index = start_index
+ return self._run_workflow(workflow, state, start_index)
+
+ def _run_workflow(
+ self,
+ workflow: WorkflowDefinition,
+ state: WorkflowState,
+ start_index: int,
+ ) -> Dict[str, Any]:
+ """Execute workflow steps from a specific index, mutating the same workflow state."""
+ workflow_id = state.workflow_id
+ for i in range(start_index, len(workflow.steps)):
+ step = workflow.steps[i]
+ state.current_step_index = i
+ self._save_active_states()
+
+ if step.can_skip(state.context):
+ self.emitter.emit(
+ WorkflowEvent.STEP_SKIPPED,
+ {"step": step.name},
+ step_name=step.name
+ )
+ state.skipped_steps.append(step.name)
+ continue
+
+ validation_error = step.validate_context(state.context)
+ if validation_error:
+ self.emitter.emit(
+ WorkflowEvent.STEP_FAILED,
+ {"step": step.name, "error": f"Validation failed: {validation_error}"},
+ step_name=step.name
+ )
+ if not workflow.continue_on_failure:
+ state.status = "failed"
+ state.errors.append(validation_error)
+ break
+ continue
+
+ result = self._execute_step_with_retry(step, state, workflow)
+
+ if result.status == StepStatus.COMPLETED:
+ state.context.update(result.output or {})
+ state.completed_steps.append(step.name)
+
+ self.emitter.emit(
+ WorkflowEvent.STEP_COMPLETED,
+ {"step": step.name, "output": result.output},
+ step_name=step.name
+ )
+
+ if workflow.on_step_complete:
+ workflow.on_step_complete(step.name, result)
+
+ elif result.status == StepStatus.WAITING_FOR_HUMAN:
+ state.status = "paused"
+ state.context["_paused_at"] = step.name
+ state.context["needs_guidance"] = True
+ state.context["guidance_context"] = result.human_prompt
+ state.context["_workflow_id"] = workflow_id
+ self._save_active_states()
+
+ self.emitter.emit(
+ WorkflowEvent.HUMAN_NEEDED,
+ {
+ "step": step.name,
+ "prompt": result.human_prompt,
+ "workflow_id": workflow_id
+ },
+ step_name=step.name
+ )
+
+ if workflow.on_human_needed:
+ workflow.on_human_needed(step.name, result.human_prompt, state.context)
+
+ return state.context
+
+ elif result.status == StepStatus.FAILED:
+ state.errors.append(result.error or "Unknown error")
+
+ self.emitter.emit(
+ WorkflowEvent.STEP_FAILED,
+ {"step": step.name, "error": result.error},
+ step_name=step.name
+ )
+
+ if not workflow.continue_on_failure:
+ state.status = "failed"
+ break
+
+ state.completed_at = datetime.now()
+ state.status = "completed" if not state.errors else "failed"
+ state.context["success"] = len(state.errors) == 0
+ state.context["errors"] = state.errors
+ state.context["completed_steps"] = state.completed_steps
+ state.context["skipped_steps"] = state.skipped_steps
+
+ self.emitter.emit(
+ WorkflowEvent.COMPLETED if state.status == "completed" else WorkflowEvent.FAILED,
+ {"context": state.context}
+ )
+
+ if workflow_id in self.active_states:
+ del self.active_states[workflow_id]
+ self._save_active_states()
+
+ return state.context
+
+ def resume(
+ self,
+ workflow_id: str,
+ user_guidance: str
+ ) -> Dict[str, Any]:
+ """
+ Resume a paused workflow with user guidance.
+
+ Args:
+ workflow_id: ID of the paused workflow
+ user_guidance: User's response/guidance
+
+ Returns:
+ Final context dictionary
+ """
+ if workflow_id not in self.active_states:
+ raise ValueError(f"No paused workflow with ID: {workflow_id}")
+
+ state = self.active_states[workflow_id]
+
+ if state.status != "paused":
+ raise ValueError(f"Workflow {workflow_id} is not paused (status: {state.status})")
+
+ # Add guidance to context
+ state.context["user_guidance"] = user_guidance
+ state.context.pop("needs_guidance", None)
+ state.context.pop("guidance_context", None)
+ state.status = "running"
+ self._save_active_states()
+
+ # Emit resume event
+ self.emitter.emit(
+ WorkflowEvent.RESUMED,
+ {"workflow_id": workflow_id, "guidance": user_guidance}
+ )
+
+ # Resume from paused step
+ paused_step = state.context.pop("_paused_at", None)
+ workflow = self.workflows.get(state.workflow_name)
+ if workflow is None:
+ raise ValueError(f"Unknown workflow: {state.workflow_name}")
+
+ start_index = state.current_step_index
+ if paused_step:
+ for i, step in enumerate(workflow.steps):
+ if step.name == paused_step:
+ start_index = i
+ break
+
+ return self._run_workflow(workflow, state, start_index)
+
+ def _execute_step_with_retry(
+ self,
+ step: WorkflowStep,
+ state: WorkflowState,
+ workflow: WorkflowDefinition
+ ) -> StepResult:
+ """
+ Execute a step with retry logic.
+
+ Retries up to step.max_retries times on failure.
+ After max retries, escalates to human-in-the-loop.
+
+ Args:
+ step: The step to execute
+ state: Current workflow state
+ workflow: The workflow definition
+
+ Returns:
+ StepResult from the step execution
+ """
+ last_result = None
+
+ for attempt in range(step.max_retries):
+ self.emitter.emit(
+ WorkflowEvent.STEP_STARTED,
+ {"step": step.name, "attempt": attempt + 1},
+ step_name=step.name
+ )
+
+ try:
+ result = step.execute(state.context)
+ except Exception as e:
+ result = StepResult.failure(str(e))
+
+ last_result = result
+
+ if result.status == StepStatus.COMPLETED:
+ return result
+
+ if result.status == StepStatus.WAITING_FOR_HUMAN:
+ return result
+
+ if result.status == StepStatus.SKIPPED:
+ return result
+
+ # Log retry
+ if attempt < step.max_retries - 1:
+ self.emitter.emit(
+ WorkflowEvent.STEP_RETRY,
+ {
+ "step": step.name,
+ "attempt": attempt + 1,
+ "error": result.error,
+ "remaining_attempts": step.max_retries - attempt - 1
+ },
+ step_name=step.name
+ )
+
+ # Max retries exceeded - escalate to human
+ return StepResult.needs_guidance(
+ f"Step '{step.name}' failed after {step.max_retries} attempts.\n"
+ f"Last error: {last_result.error if last_result else 'Unknown'}\n"
+ f"Please provide guidance on how to proceed.",
+ {"last_error": last_result.error if last_result else None}
+ )
+
+ def get_active_workflows(self) -> List[WorkflowState]:
+ """Get list of currently active/paused workflows."""
+ return list(self.active_states.values())
+
+ def get_persistence_status(self) -> Dict[str, Any]:
+ """
+ Get persistence diagnostics for workflow state storage.
+
+ Returns:
+ Dictionary with state store path and persisted workflow ids.
+ """
+ if not self.state_store_path:
+ return {
+ "enabled": False,
+ "state_store_path": None,
+ "persisted_workflow_ids": [],
+ }
+
+ persisted_workflow_ids: List[str] = []
+ if self.state_store_path.exists():
+ try:
+ with open(self.state_store_path, "r", encoding="utf-8") as f:
+ payload = json.load(f)
+ persisted_workflow_ids = [
+ item.get("workflow_id")
+ for item in payload.get("active_states", [])
+ if item.get("workflow_id")
+ ]
+ except Exception:
+ persisted_workflow_ids = []
+
+ return {
+ "enabled": True,
+ "state_store_path": str(self.state_store_path),
+ "persisted_workflow_ids": persisted_workflow_ids,
+ }
+
+ def cancel_workflow(self, workflow_id: str) -> bool:
+ """
+ Cancel an active workflow.
+
+ Args:
+ workflow_id: ID of the workflow to cancel
+
+ Returns:
+ True if cancelled, False if not found
+ """
+ if workflow_id in self.active_states:
+ state = self.active_states[workflow_id]
+ state.status = "cancelled"
+
+ self.emitter.emit(
+ WorkflowEvent.FAILED,
+ {"workflow_id": workflow_id, "reason": "cancelled"}
+ )
+
+ del self.active_states[workflow_id]
+ self._save_active_states()
+ return True
+ return False
diff --git a/Src/PeasyAI/src/core/workflow/events.py b/Src/PeasyAI/src/core/workflow/events.py
new file mode 100644
index 0000000000..48ac229207
--- /dev/null
+++ b/Src/PeasyAI/src/core/workflow/events.py
@@ -0,0 +1,306 @@
+"""
+Event system for workflow observability.
+
+This module provides an event-driven architecture for workflow monitoring:
+- WorkflowEvent: Enumeration of workflow events
+- EventEmitter: Pub/sub system for event listeners
+
+This allows multiple UIs (Streamlit, CLI, MCP) to observe workflow progress
+without tight coupling to the workflow engine.
+"""
+
+from enum import Enum
+from typing import Any, Callable, Dict, List, Optional
+from dataclasses import dataclass, field
+from datetime import datetime
+import threading
+
+
+class WorkflowEvent(Enum):
+ """Events emitted during workflow execution."""
+
+ # Workflow lifecycle
+ STARTED = "workflow.started"
+ COMPLETED = "workflow.completed"
+ FAILED = "workflow.failed"
+ PAUSED = "workflow.paused"
+ RESUMED = "workflow.resumed"
+
+ # Step lifecycle
+ STEP_STARTED = "step.started"
+ STEP_COMPLETED = "step.completed"
+ STEP_FAILED = "step.failed"
+ STEP_SKIPPED = "step.skipped"
+ STEP_RETRY = "step.retry"
+
+ # Human interaction
+ HUMAN_NEEDED = "human.needed"
+ HUMAN_RESPONSE = "human.response"
+
+ # Artifacts
+ FILE_GENERATED = "file.generated"
+ FILE_SAVED = "file.saved"
+
+ # Compilation
+ COMPILATION_STARTED = "compilation.started"
+ COMPILATION_COMPLETED = "compilation.completed"
+ COMPILATION_ERROR = "compilation.error"
+
+ # Checker
+ CHECKER_STARTED = "checker.started"
+ CHECKER_COMPLETED = "checker.completed"
+ CHECKER_ERROR = "checker.error"
+
+ # Progress
+ PROGRESS = "progress"
+ LOG = "log"
+
+
+@dataclass
+class EventData:
+ """Data structure for event payloads."""
+ event: WorkflowEvent
+ timestamp: datetime
+ data: Dict[str, Any]
+ workflow_id: Optional[str] = None
+ step_name: Optional[str] = None
+
+
+EventCallback = Callable[[EventData], None]
+
+
+class EventEmitter:
+ """
+ Thread-safe event emitter for workflow observability.
+
+ Supports:
+ - Multiple listeners per event
+ - Wildcard listeners (listen to all events)
+ - Event history for debugging
+ - Async emission (non-blocking)
+
+ Usage:
+ emitter = EventEmitter()
+
+ # Listen to specific event
+ emitter.on(WorkflowEvent.STEP_COMPLETED, lambda e: print(e.data))
+
+ # Listen to all events
+ emitter.on_all(lambda e: log(e))
+
+ # Emit event
+ emitter.emit(WorkflowEvent.STEP_COMPLETED, {"step": "generate_types"})
+ """
+
+ def __init__(self, keep_history: bool = True, max_history: int = 1000):
+ self._listeners: Dict[WorkflowEvent, List[EventCallback]] = {}
+ self._all_listeners: List[EventCallback] = []
+ self._history: List[EventData] = []
+ self._keep_history = keep_history
+ self._max_history = max_history
+ self._lock = threading.Lock()
+ self._workflow_id: Optional[str] = None
+
+ def set_workflow_id(self, workflow_id: str) -> None:
+ """Set the current workflow ID for event tracking."""
+ self._workflow_id = workflow_id
+
+ def on(self, event: WorkflowEvent, callback: EventCallback) -> None:
+ """
+ Register a listener for a specific event.
+
+ Args:
+ event: The event type to listen for
+ callback: Function to call when event is emitted
+ """
+ with self._lock:
+ if event not in self._listeners:
+ self._listeners[event] = []
+ self._listeners[event].append(callback)
+
+ def on_all(self, callback: EventCallback) -> None:
+ """
+ Register a listener for all events.
+
+ Args:
+ callback: Function to call for every event
+ """
+ with self._lock:
+ self._all_listeners.append(callback)
+
+ def off(self, event: WorkflowEvent, callback: EventCallback) -> None:
+ """
+ Remove a listener for a specific event.
+
+ Args:
+ event: The event type
+ callback: The callback to remove
+ """
+ with self._lock:
+ if event in self._listeners and callback in self._listeners[event]:
+ self._listeners[event].remove(callback)
+
+ def off_all(self, callback: EventCallback) -> None:
+ """
+ Remove a wildcard listener.
+
+ Args:
+ callback: The callback to remove
+ """
+ with self._lock:
+ if callback in self._all_listeners:
+ self._all_listeners.remove(callback)
+
+ def emit(
+ self,
+ event: WorkflowEvent,
+ data: Dict[str, Any],
+ step_name: Optional[str] = None
+ ) -> EventData:
+ """
+ Emit an event to all registered listeners.
+
+ Args:
+ event: The event type
+ data: Event payload data
+ step_name: Optional step name for context
+
+ Returns:
+ The EventData object that was emitted
+ """
+ event_data = EventData(
+ event=event,
+ timestamp=datetime.now(),
+ data=data,
+ workflow_id=self._workflow_id,
+ step_name=step_name
+ )
+
+ with self._lock:
+ # Store in history
+ if self._keep_history:
+ self._history.append(event_data)
+ if len(self._history) > self._max_history:
+ self._history = self._history[-self._max_history:]
+
+ # Get listeners (copy to avoid modification during iteration)
+ specific_listeners = list(self._listeners.get(event, []))
+ all_listeners = list(self._all_listeners)
+
+ # Call listeners outside lock to prevent deadlocks
+ for callback in specific_listeners:
+ try:
+ callback(event_data)
+ except Exception as e:
+ # Log but don't propagate listener errors
+ print(f"Error in event listener for {event}: {e}")
+
+ for callback in all_listeners:
+ try:
+ callback(event_data)
+ except Exception as e:
+ print(f"Error in wildcard listener for {event}: {e}")
+
+ return event_data
+
+ def get_history(
+ self,
+ event_filter: Optional[WorkflowEvent] = None,
+ limit: Optional[int] = None
+ ) -> List[EventData]:
+ """
+ Get event history, optionally filtered.
+
+ Args:
+ event_filter: Optional event type to filter by
+ limit: Optional maximum number of events to return
+
+ Returns:
+ List of EventData objects
+ """
+ with self._lock:
+ history = self._history.copy()
+
+ if event_filter:
+ history = [e for e in history if e.event == event_filter]
+
+ if limit:
+ history = history[-limit:]
+
+ return history
+
+ def clear_history(self) -> None:
+ """Clear the event history."""
+ with self._lock:
+ self._history.clear()
+
+ def clear_listeners(self) -> None:
+ """Remove all listeners."""
+ with self._lock:
+ self._listeners.clear()
+ self._all_listeners.clear()
+
+
+class LoggingEventListener:
+ """
+ A pre-built event listener that logs all events.
+
+ Useful for debugging and CLI output.
+ """
+
+ def __init__(self, verbose: bool = False):
+ self.verbose = verbose
+
+ def __call__(self, event_data: EventData) -> None:
+ """Handle an event by logging it."""
+ event = event_data.event
+ data = event_data.data
+
+ if event == WorkflowEvent.STARTED:
+ print(f"\n🚀 Workflow started: {data.get('workflow', 'unknown')}")
+
+ elif event == WorkflowEvent.COMPLETED:
+ success = data.get('context', {}).get('success', False)
+ icon = "✅" if success else "⚠️"
+ print(f"\n{icon} Workflow completed")
+
+ elif event == WorkflowEvent.FAILED:
+ print(f"\n❌ Workflow failed: {data.get('error', 'unknown error')}")
+
+ elif event == WorkflowEvent.STEP_STARTED:
+ step = data.get('step', 'unknown')
+ attempt = data.get('attempt', 1)
+ if attempt > 1:
+ print(f" 🔄 Retrying {step} (attempt {attempt})")
+ elif self.verbose:
+ print(f" ▶️ Starting: {step}")
+
+ elif event == WorkflowEvent.STEP_COMPLETED:
+ step = data.get('step', 'unknown')
+ print(f" ✓ Completed: {step}")
+
+ elif event == WorkflowEvent.STEP_FAILED:
+ step = data.get('step', 'unknown')
+ error = data.get('error', 'unknown error')
+ print(f" ✗ Failed: {step} - {error}")
+
+ elif event == WorkflowEvent.STEP_SKIPPED:
+ if self.verbose:
+ step = data.get('step', 'unknown')
+ print(f" ⏭️ Skipped: {step}")
+
+ elif event == WorkflowEvent.HUMAN_NEEDED:
+ step = data.get('step', 'unknown')
+ prompt = data.get('prompt', 'Guidance needed')
+ print(f"\n⚠️ Human input needed for '{step}':")
+ print(f" {prompt}")
+
+ elif event == WorkflowEvent.FILE_GENERATED:
+ if self.verbose:
+ path = data.get('path', 'unknown')
+ print(f" 📄 Generated: {path}")
+
+ elif event == WorkflowEvent.PROGRESS:
+ if self.verbose:
+ message = data.get('message', '')
+ print(f" ... {message}")
diff --git a/Src/PeasyAI/src/core/workflow/factory.py b/Src/PeasyAI/src/core/workflow/factory.py
new file mode 100644
index 0000000000..67f8a18b8b
--- /dev/null
+++ b/Src/PeasyAI/src/core/workflow/factory.py
@@ -0,0 +1,465 @@
+"""
+Workflow Factory for PeasyAI.
+
+This module provides functionality to create workflows from YAML configuration
+and from design documents. It handles:
+- Loading workflow definitions from YAML
+- Creating step instances with proper services
+- Extracting machine names from design documents
+"""
+
+import os
+import re
+from typing import Any, Dict, List, Optional
+import yaml
+
+from .steps import WorkflowStep, CompositeStep
+from .engine import WorkflowEngine, WorkflowDefinition
+from .events import EventEmitter
+from .p_steps import (
+ CreateProjectStructureStep,
+ GenerateTypesEventsStep,
+ GenerateMachineStep,
+ GenerateSpecStep,
+ GenerateTestStep,
+ SaveGeneratedFilesStep,
+ CompileProjectStep,
+ FixCompilationErrorsStep,
+ RunCheckerStep,
+ FixCheckerErrorsStep,
+)
+from ..services.generation import GenerationService
+from ..services.compilation import CompilationService
+from ..services.fixer import FixerService
+
+
+class WorkflowFactory:
+ """
+ Factory for creating workflows.
+
+ Creates workflow instances with proper service dependencies
+ and step configurations.
+
+ Usage:
+ factory = WorkflowFactory(generation_service, compilation_service, fixer_service)
+
+ # Create from design doc
+ workflow = factory.create_full_generation_workflow(
+ design_doc="...",
+ machine_names=["Client", "Server"]
+ )
+
+ # Or load from config
+ workflows = factory.load_from_yaml("configuration/workflows.yaml")
+ """
+
+ def __init__(
+ self,
+ generation_service: GenerationService,
+ compilation_service: CompilationService,
+ fixer_service: FixerService
+ ):
+ self.generation_service = generation_service
+ self.compilation_service = compilation_service
+ self.fixer_service = fixer_service
+
+ def create_full_generation_workflow(
+ self,
+ machine_names: List[str],
+ spec_name: str = "Safety",
+ test_name: str = "TestDriver",
+ ensemble_size: int = 3,
+ ) -> WorkflowDefinition:
+ """
+ Create a full project generation workflow.
+
+ Args:
+ machine_names: List of machine names to generate
+ spec_name: Name for the specification file
+ test_name: Name for the test file
+ ensemble_size: Number of candidates per file for ensemble selection
+
+ Returns:
+ WorkflowDefinition ready for execution
+ """
+ steps: List[WorkflowStep] = [
+ CreateProjectStructureStep(self.generation_service),
+ GenerateTypesEventsStep(self.generation_service),
+ ]
+
+ # Add machine generation steps
+ for machine_name in machine_names:
+ steps.append(
+ GenerateMachineStep(self.generation_service, machine_name, ensemble_size=ensemble_size)
+ )
+
+ # Add spec and test
+ steps.append(GenerateSpecStep(self.generation_service, spec_name, ensemble_size=ensemble_size))
+ steps.append(GenerateTestStep(self.generation_service, test_name, ensemble_size=ensemble_size))
+
+ # Save files
+ steps.append(SaveGeneratedFilesStep())
+
+ # Compile and fix
+ steps.append(CompileProjectStep(self.compilation_service))
+ steps.append(FixCompilationErrorsStep(self.fixer_service))
+
+ return WorkflowDefinition(
+ name="full_generation",
+ description="Generate complete P project from design document",
+ steps=steps,
+ continue_on_failure=False
+ )
+
+ def create_add_machine_workflow(self, machine_name: str, ensemble_size: int = 3) -> WorkflowDefinition:
+ """Create workflow to add a single machine to existing project."""
+ return WorkflowDefinition(
+ name=f"add_machine_{machine_name}",
+ description=f"Add {machine_name} machine to project",
+ steps=[
+ GenerateMachineStep(self.generation_service, machine_name, ensemble_size=ensemble_size),
+ SaveGeneratedFilesStep(),
+ CompileProjectStep(self.compilation_service),
+ FixCompilationErrorsStep(self.fixer_service),
+ ],
+ continue_on_failure=False
+ )
+
+ def create_add_spec_workflow(self, spec_name: str, ensemble_size: int = 3) -> WorkflowDefinition:
+ """Create workflow to add a specification to existing project."""
+ return WorkflowDefinition(
+ name=f"add_spec_{spec_name}",
+ description=f"Add {spec_name} specification to project",
+ steps=[
+ GenerateSpecStep(self.generation_service, spec_name, ensemble_size=ensemble_size),
+ SaveGeneratedFilesStep(),
+ CompileProjectStep(self.compilation_service),
+ FixCompilationErrorsStep(self.fixer_service),
+ ],
+ continue_on_failure=False
+ )
+
+ def create_compile_and_fix_workflow(self) -> WorkflowDefinition:
+ """Create workflow to compile and fix errors."""
+ return WorkflowDefinition(
+ name="compile_and_fix",
+ description="Compile project and fix errors",
+ steps=[
+ CompileProjectStep(self.compilation_service),
+ FixCompilationErrorsStep(self.fixer_service),
+ ],
+ continue_on_failure=False
+ )
+
+ def create_full_verification_workflow(
+ self,
+ schedules: int = 100,
+ timeout: int = 60
+ ) -> WorkflowDefinition:
+ """Create workflow for full compilation, verification, and automatic bug fixing.
+
+ Steps:
+ 1. Compile the project
+ 2. Fix any compilation errors (iteratively)
+ 3. Run PChecker to find bugs
+ 4. Automatically fix PChecker bugs using trace analysis
+
+ The RunCheckerStep always propagates its output (even on failure) so that
+ FixCheckerErrorsStep can read the trace files and apply AI-driven fixes.
+ """
+ return WorkflowDefinition(
+ name="full_verification",
+ description="Compile, fix errors, run PChecker, and automatically fix PChecker bugs",
+ steps=[
+ CompileProjectStep(self.compilation_service),
+ FixCompilationErrorsStep(self.fixer_service),
+ RunCheckerStep(self.compilation_service, schedules, timeout),
+ FixCheckerErrorsStep(self.fixer_service),
+ ],
+ continue_on_failure=False
+ )
+
+ def create_quick_check_workflow(
+ self,
+ schedules: int = 100,
+ timeout: int = 60
+ ) -> WorkflowDefinition:
+ """Create workflow for just running PChecker."""
+ return WorkflowDefinition(
+ name="quick_check",
+ description="Run PChecker on project",
+ steps=[
+ RunCheckerStep(self.compilation_service, schedules, timeout),
+ ],
+ continue_on_failure=False
+ )
+
+
+def _to_pascal_case(name: str) -> str:
+ """Convert a multi-word name to PascalCase for P machine naming.
+
+ "Front Desk" -> "FrontDesk", "Lock Server" -> "LockServer",
+ "CoffeeMaker" -> "CoffeeMaker" (already single word, preserved).
+ """
+ words = name.strip().split()
+ if len(words) == 1:
+ return words[0]
+ return "".join(w[0].upper() + w[1:] for w in words if w)
+
+
+# ── LLM-based extraction (primary) ──────────────────────────────────
+
+_EXTRACT_PROMPT = """\
+You are given a design document for a P language state machine system.
+
+Your task: identify every component that should become a P `machine`.
+For each component, return a PascalCase name suitable as a P machine
+identifier (no spaces, no special characters).
+
+Rules:
+- Multi-word names become PascalCase: "Front Desk" -> "FrontDesk", "Lock Server" -> "LockServer"
+- Only include components that are active participants (state machines). Do NOT include:
+ - Abstract concepts (e.g., "Safety", "Specification")
+ - Data types or events
+ - Roles or states within a machine (e.g., "Follower", "Candidate" are roles of a Server, not separate machines)
+- Return ONLY a JSON array of strings, nothing else.
+
+Example output: ["Coordinator", "Participant", "Client", "Timer"]
+"""
+
+
+def _extract_names_with_llm(
+ design_doc: str,
+ llm_provider=None,
+) -> Optional[List[str]]:
+ """Use the LLM to extract machine names. Returns None on failure."""
+ import json as _json
+ try:
+ from ..llm import LLMConfig, Message, MessageRole
+
+ if llm_provider is None:
+ from ..llm import get_default_provider
+ llm_provider = get_default_provider()
+
+ messages = [
+ Message(role=MessageRole.USER, content=_EXTRACT_PROMPT),
+ Message(role=MessageRole.USER, content=f"Design document:\n{design_doc}"),
+ ]
+ config = LLMConfig(max_tokens=256)
+ response = llm_provider.complete(messages, config)
+ text = response.content.strip()
+
+ # Extract JSON array from the response (may be wrapped in markdown)
+ arr_match = re.search(r'\[.*\]', text, re.DOTALL)
+ if not arr_match:
+ return None
+ names = _json.loads(arr_match.group(0))
+ if not isinstance(names, list):
+ return None
+
+ # Sanitise: PascalCase, no empty strings, no duplicates
+ clean = []
+ seen: set = set()
+ for n in names:
+ if not isinstance(n, str) or not n:
+ continue
+ pc = _to_pascal_case(n)
+ if pc and pc not in seen and pc[0].isupper():
+ clean.append(pc)
+ seen.add(pc)
+ return sorted(clean) if clean else None
+
+ except Exception:
+ return None
+
+
+# ── Regex fallback (for offline / no-LLM environments) ──────────────
+
+def _extract_section(text: str, section_name: str) -> Optional[str]:
+ """Extract a section from a design doc using markdown headings.
+
+ Matches ``# Section Name``, ``## Section Name``, etc. and returns
+ everything until the next heading of equal or higher level.
+ Underscores in *section_name* are treated as spaces so that
+ ``"test_scenarios"`` matches ``## Test Scenarios``.
+ """
+ heading_label = section_name.replace("_", " ")
+ md_match = re.search(
+ rf'^(#{1,3})\s+{re.escape(heading_label)}\s*$',
+ text, re.MULTILINE | re.IGNORECASE,
+ )
+ if md_match:
+ level = len(md_match.group(1))
+ rest = text[md_match.end():]
+ next_heading = re.search(rf'^#{{{1},{level}}}\s', rest, re.MULTILINE)
+ return rest[:next_heading.start()] if next_heading else rest
+
+ return None
+
+
+def _extract_title(text: str) -> Optional[str]:
+ """Extract the document title from a markdown ``# Title`` heading."""
+ m = re.search(r'^#\s+(.+?)\s*$', text, re.MULTILINE)
+ return m.group(1).strip() if m else None
+
+
+def _extract_names_regex(design_doc: str) -> List[str]:
+ """Regex extraction from Components section as fallback (XML or markdown)."""
+ names: set = set()
+
+ comp_text = _extract_section(design_doc, "components")
+ if comp_text:
+ for m in re.finditer(
+ r'^\s*(?:\d+\.|#{1,4})\s*(?:\d+\.\s*)?(.+?)\s*$',
+ comp_text, re.MULTILINE,
+ ):
+ raw = m.group(1).strip().strip('*').strip()
+ if raw and raw[0].isupper() and len(raw) < 40:
+ names.add(_to_pascal_case(raw))
+
+ if not names:
+ for m in re.finditer(r'\bmachine\s+(\w+)\s*[{:]', design_doc):
+ names.add(m.group(1))
+
+ stop = {
+ "machine", "state", "event", "type", "spec", "test", "main",
+ "source", "target", "payload", "description", "effects",
+ "source components", "test components", "role", "behavior",
+ }
+ return sorted(n for n in names if n.lower() not in stop and n[0].isupper())
+
+
+# ── Public API ───────────────────────────────────────────────────────
+
+def extract_machine_names_from_design_doc(
+ design_doc: str,
+ llm_provider=None,
+) -> List[str]:
+ """
+ Extract P machine names from a design document.
+
+ Uses the LLM for robust understanding of natural language component
+ descriptions, with a regex fallback for offline environments.
+
+ Multi-word names are automatically converted to PascalCase
+ (e.g., "Front Desk" -> "FrontDesk").
+
+ Args:
+ design_doc: The design document content.
+ llm_provider: Optional LLM provider instance. If not given,
+ the function tries ``get_default_provider()``.
+
+ Returns:
+ Sorted list of unique PascalCase machine names.
+ """
+ llm_result = _extract_names_with_llm(design_doc, llm_provider=llm_provider)
+ if llm_result:
+ return llm_result
+
+ return _extract_names_regex(design_doc)
+
+
+def validate_design_doc(design_doc: str) -> Dict[str, Any]:
+ """
+ Validate a design document for completeness and consistency.
+
+ Expects markdown-formatted design docs with headings:
+ ``# Title``, ``## Components``, ``## Interactions``,
+ ``## Specifications``, ``## Test Scenarios``.
+
+ Returns:
+ Dictionary with 'valid' (bool), 'warnings' (list), 'errors' (list),
+ 'components' (list), 'scenarios_count' (int)
+ """
+ result: Dict[str, Any] = {
+ "valid": True,
+ "warnings": [],
+ "errors": [],
+ "components": [],
+ "scenarios_count": 0,
+ }
+
+ # Check required sections
+ for section in ("title", "components", "interactions"):
+ if section == "title":
+ found = _extract_title(design_doc) is not None
+ else:
+ found = _extract_section(design_doc, section) is not None
+ if not found:
+ result["errors"].append(f"Missing required section: {section}")
+ result["valid"] = False
+
+ # Extract component names
+ components = extract_machine_names_from_design_doc(design_doc)
+ result["components"] = components
+ if not components:
+ result["errors"].append("No components/machines could be extracted from design doc")
+ result["valid"] = False
+
+ # Check for scenarios
+ scenarios_text = _extract_section(design_doc, "test scenarios")
+ if scenarios_text:
+ scenario_lines = [
+ l.strip() for l in scenarios_text.strip().split('\n')
+ if l.strip() and re.match(r'\d+\.', l.strip())
+ ]
+ result["scenarios_count"] = len(scenario_lines)
+ if not scenario_lines:
+ result["warnings"].append("No numbered scenarios found in Test Scenarios section")
+ else:
+ result["warnings"].append("Missing 'Test Scenarios' section — tests may not be generated")
+
+ # Check for specifications
+ if _extract_section(design_doc, "specifications") is None:
+ result["warnings"].append("Missing 'Specifications' — no safety/liveness specs will be generated")
+
+ return result
+
+
+def create_workflow_engine_from_config(
+ config_path: str,
+ generation_service: GenerationService,
+ compilation_service: CompilationService,
+ fixer_service: FixerService
+) -> WorkflowEngine:
+ """
+ Create a WorkflowEngine with workflows loaded from YAML config.
+
+ Args:
+ config_path: Path to workflows.yaml
+ generation_service: Service for code generation
+ compilation_service: Service for compilation
+ fixer_service: Service for error fixing
+
+ Returns:
+ Configured WorkflowEngine
+ """
+ emitter = EventEmitter()
+ engine = WorkflowEngine(emitter)
+ factory = WorkflowFactory(generation_service, compilation_service, fixer_service)
+
+ # Load config
+ with open(config_path, 'r') as f:
+ config = yaml.safe_load(f)
+
+ # Register standard workflows
+ engine.register_workflow(
+ factory.create_compile_and_fix_workflow()
+ )
+
+ checker_config = config.get('checker', {})
+ engine.register_workflow(
+ factory.create_full_verification_workflow(
+ schedules=checker_config.get('default_schedules', 100),
+ timeout=checker_config.get('default_timeout', 60)
+ )
+ )
+
+ engine.register_workflow(
+ factory.create_quick_check_workflow(
+ schedules=checker_config.get('default_schedules', 100),
+ timeout=checker_config.get('default_timeout', 60)
+ )
+ )
+
+ return engine
diff --git a/Src/PeasyAI/src/core/workflow/p_steps.py b/Src/PeasyAI/src/core/workflow/p_steps.py
new file mode 100644
index 0000000000..f419bcc797
--- /dev/null
+++ b/Src/PeasyAI/src/core/workflow/p_steps.py
@@ -0,0 +1,787 @@
+"""
+P Language Workflow Steps.
+
+This module provides concrete workflow step implementations for P code generation,
+compilation, and verification. These steps use the service layer (GenerationService,
+CompilationService, FixerService) to perform their work.
+"""
+
+import logging
+import os
+from typing import Any, Dict, List, Optional
+
+from .steps import WorkflowStep, StepResult, StepStatus
+from ..services.generation import GenerationService, GenerationResult
+from ..services.compilation import CompilationService, CompilationResult
+from ..services.fixer import FixerService
+
+logger = logging.getLogger(__name__)
+
+
+def _run_validation_pipeline(
+ code: str,
+ filename: str,
+ project_path: str,
+ is_test_file: bool = False,
+) -> str:
+ """Run the ValidationPipeline on generated code and return the fixed code.
+
+ This is the single place where post-processing + structured validation
+ happens for the workflow path. The MCP tool path has its own equivalent
+ call in ``_review_generated_code()`` (tools/generation.py).
+ """
+ try:
+ from ..validation.pipeline import ValidationPipeline
+
+ pipeline = ValidationPipeline(include_test_validators=is_test_file)
+ result = pipeline.validate(
+ code,
+ filename=filename,
+ project_path=project_path,
+ is_test_file=is_test_file,
+ )
+ if result.fixes_applied:
+ logger.info(
+ f"Validation pipeline applied {len(result.fixes_applied)} "
+ f"fix(es) to {filename}"
+ )
+ return result.fixed_code
+ except Exception as e:
+ logger.warning(f"Validation pipeline failed for {filename}: {e}")
+ return code
+
+
+def _run_documentation_review(
+ service: 'GenerationService',
+ code: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]] = None,
+) -> str:
+ """Run LLM-based documentation review and return the documented code.
+
+ Falls back to the original code if the LLM call fails.
+ """
+ try:
+ result = service.review_code_documentation(
+ code=code,
+ design_doc=design_doc,
+ context_files=context_files,
+ )
+ if result["status"] == "success":
+ logger.info("Documentation review added comments")
+ return result["code"]
+ else:
+ logger.warning(
+ f"Documentation review did not succeed: "
+ f"status={result['status']}, reason={result.get('reason')}"
+ )
+ except Exception as e:
+ logger.warning(f"Documentation review failed: {e}")
+ return code
+
+
+class CreateProjectStructureStep(WorkflowStep):
+ """Step to create P project directory structure."""
+
+ name = "create_project_structure"
+ description = "Create P project directories (PSrc, PSpec, PTst) and .pproj file"
+ max_retries = 1 # No point retrying filesystem operations
+
+ def __init__(self, generation_service: GenerationService):
+ self.service = generation_service
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ project_path = context.get("project_path")
+ project_name = context.get("project_name", "PProject")
+
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ try:
+ result = self.service.create_project_structure(
+ output_dir=project_path,
+ project_name=project_name
+ )
+
+ if result.success:
+ # The service creates a timestamped subdirectory
+ actual_project_path = result.file_path or project_path
+ return StepResult.success(
+ output={"project_path": actual_project_path},
+ artifacts={"pproj_file": os.path.join(actual_project_path, f"{project_name}.pproj")}
+ )
+ else:
+ return StepResult.failure(result.error or "Failed to create project structure")
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ project_path = context.get("project_path")
+ if not project_path:
+ return False
+ # Skip if PSrc directory already exists
+ return os.path.exists(os.path.join(project_path, "PSrc"))
+
+
+class GenerateTypesEventsStep(WorkflowStep):
+ """Step to generate Enums_Types_Events.p file."""
+
+ name = "generate_types_events"
+ description = "Generate shared types, enums, and events file"
+ max_retries = 3
+
+ def __init__(self, generation_service: GenerationService):
+ self.service = generation_service
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ design_doc = context.get("design_doc")
+ project_path = context.get("project_path")
+
+ if not design_doc:
+ return StepResult.failure("design_doc is required")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ try:
+ result = self.service.generate_types_events(
+ design_doc=design_doc,
+ project_path=project_path,
+ save_to_disk=False # Preview mode
+ )
+
+ if result.success:
+ filename = result.filename or "Enums_Types_Events.p"
+ code = _run_validation_pipeline(
+ result.code, filename, project_path,
+ )
+ code = _run_documentation_review(
+ self.service, code, design_doc,
+ context_files=context.get("context_files"),
+ )
+ return StepResult.success(
+ output={
+ "types_events_code": code,
+ "types_events_path": result.file_path,
+ "types_events_filename": filename,
+ },
+ artifacts={"types_events": code}
+ )
+ else:
+ return StepResult.failure(result.error or "Failed to generate types/events")
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ # Skip if we already have types code in context or any .p file in PSrc
+ if context.get("types_events_code"):
+ return True
+ project_path = context.get("project_path")
+ if not project_path:
+ return False
+ psrc = os.path.join(project_path, "PSrc")
+ if os.path.isdir(psrc):
+ return any(f.endswith(".p") for f in os.listdir(psrc))
+ return False
+
+
+class GenerateMachineStep(WorkflowStep):
+ """Step to generate a single P state machine."""
+
+ name = "generate_machine"
+ description = "Generate a P state machine implementation"
+ max_retries = 3
+
+ def __init__(self, generation_service: GenerationService, machine_name: str, ensemble_size: int = 3):
+ self.service = generation_service
+ self.machine_name = machine_name
+ self.ensemble_size = ensemble_size
+ self.name = f"generate_machine_{machine_name}"
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ design_doc = context.get("design_doc")
+ project_path = context.get("project_path")
+ ensemble_size = context.get("ensemble_size", self.ensemble_size)
+
+ if not design_doc:
+ return StepResult.failure("design_doc is required")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ # Collect context files from previous steps
+ context_files = {}
+ # Use actual types filename from context, fallback to convention
+ types_filename = context.get("types_events_filename", "Enums_Types_Events.p")
+ if "types_events_code" in context:
+ context_files[types_filename] = context["types_events_code"]
+
+ # Add previously generated machines
+ for key, value in context.items():
+ if key.startswith("machine_code_") and value:
+ machine_file = key.replace("machine_code_", "") + ".p"
+ context_files[machine_file] = value
+
+ try:
+ if ensemble_size > 1:
+ result = self.service.generate_machine_ensemble(
+ machine_name=self.machine_name,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ save_to_disk=False # Preview mode
+ )
+ else:
+ result = self.service.generate_machine(
+ machine_name=self.machine_name,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ save_to_disk=False # Preview mode
+ )
+
+ if result.success:
+ filename = result.filename or f"{self.machine_name}.p"
+ code = _run_validation_pipeline(
+ result.code, filename, project_path,
+ )
+ code = _run_documentation_review(
+ self.service, code, design_doc,
+ context_files=context_files,
+ )
+ return StepResult.success(
+ output={
+ f"machine_code_{self.machine_name}": code,
+ f"machine_path_{self.machine_name}": result.file_path
+ },
+ artifacts={f"machine_{self.machine_name}": code}
+ )
+ else:
+ return StepResult.failure(result.error or f"Failed to generate {self.machine_name}")
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ project_path = context.get("project_path")
+ if not project_path:
+ return False
+ path = os.path.join(project_path, "PSrc", f"{self.machine_name}.p")
+ return os.path.exists(path)
+
+
+class GenerateSpecStep(WorkflowStep):
+ """Step to generate a P specification/monitor."""
+
+ name = "generate_spec"
+ description = "Generate a P specification/monitor file"
+ max_retries = 3
+
+ def __init__(self, generation_service: GenerationService, spec_name: str = "Safety", ensemble_size: int = 3):
+ self.service = generation_service
+ self.spec_name = spec_name
+ self.ensemble_size = ensemble_size
+ self.name = f"generate_spec_{spec_name}"
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ design_doc = context.get("design_doc")
+ project_path = context.get("project_path")
+ ensemble_size = context.get("ensemble_size", self.ensemble_size)
+
+ if not design_doc:
+ return StepResult.failure("design_doc is required")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ # Collect context files
+ context_files = self._collect_context_files(context, project_path)
+
+ try:
+ if ensemble_size > 1:
+ result = self.service.generate_spec_ensemble(
+ spec_name=self.spec_name,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ save_to_disk=False
+ )
+ else:
+ result = self.service.generate_spec(
+ spec_name=self.spec_name,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ save_to_disk=False
+ )
+
+ if result.success:
+ filename = result.filename or f"{self.spec_name}.p"
+ code = _run_validation_pipeline(
+ result.code, filename, project_path,
+ )
+ code = _run_documentation_review(
+ self.service, code, design_doc,
+ context_files=context_files,
+ )
+ return StepResult.success(
+ output={
+ f"spec_code_{self.spec_name}": code,
+ f"spec_path_{self.spec_name}": result.file_path
+ },
+ artifacts={f"spec_{self.spec_name}": code}
+ )
+ else:
+ return StepResult.failure(result.error or f"Failed to generate {self.spec_name}")
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def _collect_context_files(self, context: Dict[str, Any], project_path: str) -> Dict[str, str]:
+ """Collect P source files for context."""
+ context_files = {}
+
+ # From context — use actual types filename
+ types_filename = context.get("types_events_filename", "Enums_Types_Events.p")
+ if "types_events_code" in context:
+ context_files[types_filename] = context["types_events_code"]
+
+ for key, value in context.items():
+ if key.startswith("machine_code_") and value:
+ machine_file = key.replace("machine_code_", "") + ".p"
+ context_files[machine_file] = value
+
+ # From disk (if not in context) — picks up whatever filenames exist
+ psrc_dir = os.path.join(project_path, "PSrc")
+ if os.path.exists(psrc_dir):
+ for filename in os.listdir(psrc_dir):
+ if filename.endswith(".p") and filename not in context_files:
+ filepath = os.path.join(psrc_dir, filename)
+ with open(filepath, "r") as f:
+ context_files[filename] = f.read()
+
+ return context_files
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ project_path = context.get("project_path")
+ if not project_path:
+ return False
+ path = os.path.join(project_path, "PSpec", f"{self.spec_name}.p")
+ return os.path.exists(path)
+
+
+class GenerateTestStep(WorkflowStep):
+ """Step to generate a P test file."""
+
+ name = "generate_test"
+ description = "Generate a P test driver file"
+ max_retries = 3
+
+ def __init__(self, generation_service: GenerationService, test_name: str = "TestDriver", ensemble_size: int = 3):
+ self.service = generation_service
+ self.test_name = test_name
+ self.ensemble_size = ensemble_size
+ self.name = f"generate_test_{test_name}"
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ design_doc = context.get("design_doc")
+ project_path = context.get("project_path")
+ ensemble_size = context.get("ensemble_size", self.ensemble_size)
+
+ if not design_doc:
+ return StepResult.failure("design_doc is required")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ # Collect all context files (source + spec)
+ context_files = self._collect_all_context(context, project_path)
+
+ try:
+ if ensemble_size > 1:
+ result = self.service.generate_test_ensemble(
+ test_name=self.test_name,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ ensemble_size=ensemble_size,
+ save_to_disk=False
+ )
+ else:
+ result = self.service.generate_test(
+ test_name=self.test_name,
+ design_doc=design_doc,
+ project_path=project_path,
+ context_files=context_files,
+ save_to_disk=False
+ )
+
+ if result.success:
+ filename = result.filename or f"{self.test_name}.p"
+ code = _run_validation_pipeline(
+ result.code, filename, project_path,
+ is_test_file=True,
+ )
+ code = _run_documentation_review(
+ self.service, code, design_doc,
+ context_files=context_files,
+ )
+ return StepResult.success(
+ output={
+ f"test_code_{self.test_name}": code,
+ f"test_path_{self.test_name}": result.file_path
+ },
+ artifacts={f"test_{self.test_name}": code}
+ )
+ else:
+ return StepResult.failure(result.error or f"Failed to generate {self.test_name}")
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def _collect_all_context(self, context: Dict[str, Any], project_path: str) -> Dict[str, str]:
+ """Collect all P files for context."""
+ context_files = {}
+
+ # From context (generated in this workflow run)
+ types_filename = context.get("types_events_filename", "Enums_Types_Events.p")
+ for key, value in context.items():
+ if value and ("_code_" in key or key == "types_events_code"):
+ if key == "types_events_code":
+ context_files[types_filename] = value
+ elif key.startswith("machine_code_"):
+ context_files[key.replace("machine_code_", "") + ".p"] = value
+ elif key.startswith("spec_code_"):
+ context_files[key.replace("spec_code_", "") + ".p"] = value
+
+ # From disk
+ for folder in ["PSrc", "PSpec"]:
+ folder_path = os.path.join(project_path, folder)
+ if os.path.exists(folder_path):
+ for filename in os.listdir(folder_path):
+ if filename.endswith(".p") and filename not in context_files:
+ filepath = os.path.join(folder_path, filename)
+ with open(filepath, "r") as f:
+ context_files[filename] = f.read()
+
+ return context_files
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ project_path = context.get("project_path")
+ if not project_path:
+ return False
+ path = os.path.join(project_path, "PTst", f"{self.test_name}.p")
+ return os.path.exists(path)
+
+
+class SaveGeneratedFilesStep(WorkflowStep):
+ """Step to save all generated files to disk."""
+
+ name = "save_generated_files"
+ description = "Save all generated P code files to disk"
+ max_retries = 1
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ project_path = context.get("project_path")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ saved_files = []
+ errors = []
+
+ # Save types/events
+ if "types_events_code" in context and "types_events_path" in context:
+ try:
+ self._save_file(context["types_events_path"], context["types_events_code"])
+ saved_files.append(context["types_events_path"])
+ except Exception as e:
+ errors.append(f"Failed to save types/events: {e}")
+
+ # Save machines
+ for key, value in context.items():
+ if key.startswith("machine_code_") and value:
+ path_key = key.replace("machine_code_", "machine_path_")
+ if path_key in context:
+ try:
+ self._save_file(context[path_key], value)
+ saved_files.append(context[path_key])
+ except Exception as e:
+ errors.append(f"Failed to save {key}: {e}")
+
+ # Save specs
+ for key, value in context.items():
+ if key.startswith("spec_code_") and value:
+ path_key = key.replace("spec_code_", "spec_path_")
+ if path_key in context:
+ try:
+ self._save_file(context[path_key], value)
+ saved_files.append(context[path_key])
+ except Exception as e:
+ errors.append(f"Failed to save {key}: {e}")
+
+ # Save tests
+ for key, value in context.items():
+ if key.startswith("test_code_") and value:
+ path_key = key.replace("test_code_", "test_path_")
+ if path_key in context:
+ try:
+ self._save_file(context[path_key], value)
+ saved_files.append(context[path_key])
+ except Exception as e:
+ errors.append(f"Failed to save {key}: {e}")
+
+ if errors:
+ return StepResult.failure("; ".join(errors))
+
+ return StepResult.success(
+ output={"saved_files": saved_files},
+ artifacts={"saved_files": saved_files}
+ )
+
+ def _save_file(self, path: str, content: str) -> None:
+ """Save content to file, creating directories if needed."""
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "w") as f:
+ f.write(content)
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ # Never skip saving
+ return False
+
+
+class CompileProjectStep(WorkflowStep):
+ """Step to compile the P project."""
+
+ name = "compile_project"
+ description = "Compile the P project and check for errors"
+ max_retries = 1 # Compilation itself doesn't benefit from retries
+
+ def __init__(self, compilation_service: CompilationService):
+ self.service = compilation_service
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ project_path = context.get("project_path")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ try:
+ result = self.service.compile(project_path)
+
+ if result.success:
+ return StepResult.success(
+ output={
+ "compilation_success": True,
+ "compilation_output": result.stdout
+ }
+ )
+ else:
+ error_output = result.stderr or result.stdout or "Unknown compilation error"
+ return StepResult.failure(
+ f"Compilation failed: {error_output[:500]}"
+ )
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ # Always compile to verify
+ return False
+
+
+class FixCompilationErrorsStep(WorkflowStep):
+ """Step to fix compilation errors iteratively."""
+
+ name = "fix_compilation_errors"
+ description = "Attempt to fix compilation errors using AI"
+ max_retries = 5 # More retries for fixing
+
+ def __init__(self, fixer_service: FixerService):
+ self.service = fixer_service
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ project_path = context.get("project_path")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ # Check if there are errors to fix
+ if context.get("compilation_success"):
+ return StepResult.skipped("No compilation errors to fix")
+
+ try:
+ result = self.service.fix_iteratively(
+ project_path=project_path,
+ max_iterations=10,
+ )
+
+ if result.get("success"):
+ return StepResult.success(
+ output={
+ "compilation_success": True,
+ "fixes_applied": result.get("iterations", [])
+ }
+ )
+ else:
+ iterations = result.get("iterations", [])
+ last_error = iterations[-1].get("error", "Unknown") if iterations else "Unknown"
+ return StepResult.failure(
+ f"Failed to fix compilation errors after {result.get('total_iterations', 0)} iterations: {last_error}"
+ )
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ # Skip if compilation succeeded
+ return context.get("compilation_success", False)
+
+
+class RunCheckerStep(WorkflowStep):
+ """Step to run PChecker on the project.
+
+ Always returns success so that downstream fix steps can run.
+ Sets checker_success=False in output when bugs are found, along with
+ the checker output for the fixer to analyze.
+ """
+
+ name = "run_checker"
+ description = "Run PChecker to verify correctness"
+ max_retries = 1
+
+ def __init__(self, compilation_service: CompilationService, schedules: int = 100, timeout: int = 60):
+ self.service = compilation_service
+ self.schedules = schedules
+ self.timeout = timeout
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ project_path = context.get("project_path")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ try:
+ result = self.service.run_checker(
+ project_path=project_path,
+ schedules=self.schedules,
+ timeout=self.timeout
+ )
+
+ summary = (
+ f"Passed: {result.passed_tests}, Failed: {result.failed_tests}"
+ if result.test_results else ""
+ )
+
+ if result.success:
+ return StepResult.success(
+ output={
+ "checker_success": True,
+ "checker_output": summary
+ }
+ )
+ else:
+ # Return success so the workflow continues to the fix step.
+ # checker_success=False signals that bugs were found.
+ return StepResult.success(
+ output={
+ "checker_success": False,
+ "checker_output": result.error or summary or ""
+ }
+ )
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ # Skip only if compilation explicitly failed (not if key is absent)
+ return context.get("compilation_success") is False
+
+
+class FixCheckerErrorsStep(WorkflowStep):
+ """Step to fix PChecker errors.
+
+ Reads the detailed trace file from PCheckerOutput/BugFinding/ for
+ accurate root-cause analysis, falling back to checker stdout if
+ no trace file is found.
+ """
+
+ name = "fix_checker_errors"
+ description = "Attempt to fix PChecker errors using AI"
+ max_retries = 3
+
+ def __init__(self, fixer_service: FixerService):
+ self.service = fixer_service
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ project_path = context.get("project_path")
+ if not project_path:
+ return StepResult.failure("project_path is required")
+
+ if context.get("checker_success"):
+ return StepResult.skipped("No checker errors to fix")
+
+ user_guidance = context.get("user_guidance")
+
+ # Read the detailed trace file from PCheckerOutput/BugFinding/
+ # This contains the full execution trace needed for analysis,
+ # rather than just the PChecker stdout summary.
+ trace_log = self._read_latest_trace(project_path)
+ if not trace_log:
+ # Fall back to checker stdout if no trace file found
+ trace_log = context.get("checker_output", "")
+
+ if not trace_log:
+ return StepResult.failure(
+ "No PChecker trace available. Cannot diagnose the bug."
+ )
+
+ try:
+ result = self.service.fix_checker_error(
+ project_path=project_path,
+ trace_log=trace_log,
+ user_guidance=user_guidance
+ )
+
+ if result.fixed:
+ return StepResult.success(
+ output={
+ "checker_fix_applied": True,
+ "checker_success": True,
+ "fix_file": result.file_path,
+ }
+ )
+ elif result.needs_guidance:
+ guidance_msg = "Unable to fix checker errors automatically."
+ if result.guidance_request:
+ guidance_msg = result.guidance_request.get("message", guidance_msg)
+ return StepResult.needs_guidance(
+ guidance_msg,
+ {"trace_log": trace_log}
+ )
+ else:
+ return StepResult.failure(result.error or "Failed to fix checker errors")
+
+ except Exception as e:
+ return StepResult.failure(str(e))
+
+ def _read_latest_trace(self, project_path: str) -> Optional[str]:
+ """Read the latest PChecker trace file from PCheckerOutput/BugFinding/."""
+ bug_dir = os.path.join(project_path, "PCheckerOutput", "BugFinding")
+ if not os.path.isdir(bug_dir):
+ return None
+
+ trace_files = sorted(
+ [f for f in os.listdir(bug_dir) if f.endswith(".txt")],
+ key=lambda f: os.path.getmtime(os.path.join(bug_dir, f)),
+ reverse=True
+ )
+
+ if not trace_files:
+ return None
+
+ trace_path = os.path.join(bug_dir, trace_files[0])
+ try:
+ with open(trace_path, "r") as f:
+ return f.read()
+ except Exception:
+ return None
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ return context.get("checker_success", False)
diff --git a/Src/PeasyAI/src/core/workflow/steps.py b/Src/PeasyAI/src/core/workflow/steps.py
new file mode 100644
index 0000000000..6bcaffc2d3
--- /dev/null
+++ b/Src/PeasyAI/src/core/workflow/steps.py
@@ -0,0 +1,196 @@
+"""
+Workflow Step definitions for PeasyAI.
+
+This module defines the base abstractions for workflow steps:
+- StepStatus: Enumeration of possible step states
+- StepResult: Data class for step execution results
+- WorkflowStep: Abstract base class for all workflow steps
+"""
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from typing import Any, Dict, Optional, List
+from enum import Enum
+
+
+class StepStatus(Enum):
+ """Status of a workflow step."""
+ PENDING = "pending"
+ RUNNING = "running"
+ COMPLETED = "completed"
+ FAILED = "failed"
+ WAITING_FOR_HUMAN = "waiting_for_human"
+ SKIPPED = "skipped"
+
+
+@dataclass
+class StepResult:
+ """Result of executing a workflow step."""
+ status: StepStatus
+ output: Optional[Dict[str, Any]] = None
+ error: Optional[str] = None
+ needs_human: bool = False
+ human_prompt: Optional[str] = None
+ artifacts: Dict[str, Any] = field(default_factory=dict)
+
+ @classmethod
+ def success(cls, output: Optional[Dict[str, Any]] = None, artifacts: Optional[Dict[str, Any]] = None) -> "StepResult":
+ """Create a successful result."""
+ return cls(
+ status=StepStatus.COMPLETED,
+ output=output or {},
+ artifacts=artifacts or {}
+ )
+
+ @classmethod
+ def failure(cls, error: str) -> "StepResult":
+ """Create a failed result."""
+ return cls(
+ status=StepStatus.FAILED,
+ error=error
+ )
+
+ @classmethod
+ def needs_guidance(cls, prompt: str, context: Optional[Dict[str, Any]] = None) -> "StepResult":
+ """Create a result that requires human guidance."""
+ return cls(
+ status=StepStatus.WAITING_FOR_HUMAN,
+ needs_human=True,
+ human_prompt=prompt,
+ output=context or {}
+ )
+
+ @classmethod
+ def skipped(cls, reason: Optional[str] = None) -> "StepResult":
+ """Create a skipped result."""
+ return cls(
+ status=StepStatus.SKIPPED,
+ output={"skip_reason": reason} if reason else {}
+ )
+
+
+class WorkflowStep(ABC):
+ """
+ Abstract base class for workflow steps.
+
+ Each step represents a discrete unit of work in a workflow.
+ Steps can be retried on failure and can request human intervention.
+
+ Attributes:
+ name: Unique identifier for the step
+ description: Human-readable description
+ max_retries: Maximum number of retry attempts (default: 3)
+ dependencies: List of step names that must complete first
+ """
+
+ name: str
+ description: str
+ max_retries: int = 3
+ dependencies: List[str] = []
+
+ @abstractmethod
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ """
+ Execute the step.
+
+ Args:
+ context: Dictionary containing workflow state and inputs.
+ Common keys include:
+ - design_doc: The design document content
+ - project_path: Path to the P project
+ - user_guidance: Optional guidance from user (for retries)
+
+ Returns:
+ StepResult indicating success, failure, or need for human input
+ """
+ pass
+
+ @abstractmethod
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ """
+ Check if this step can be skipped.
+
+ Useful for incremental workflows where some artifacts already exist.
+
+ Args:
+ context: Dictionary containing workflow state
+
+ Returns:
+ True if step can be skipped, False otherwise
+ """
+ pass
+
+ def validate_context(self, context: Dict[str, Any]) -> Optional[str]:
+ """
+ Validate that required context keys are present.
+
+ Override this to add custom validation.
+
+ Args:
+ context: Dictionary to validate
+
+ Returns:
+ Error message if validation fails, None if valid
+ """
+ return None
+
+ def rollback(self, context: Dict[str, Any]) -> None:
+ """
+ Rollback any changes made by this step.
+
+ Override this to implement cleanup on failure.
+
+ Args:
+ context: Dictionary containing workflow state
+ """
+ pass
+
+
+class CompositeStep(WorkflowStep):
+ """
+ A step that executes multiple sub-steps.
+
+ Useful for grouping related steps or implementing parallel execution.
+ """
+
+ def __init__(self, name: str, description: str, steps: List[WorkflowStep], parallel: bool = False):
+ self.name = name
+ self.description = description
+ self.steps = steps
+ self.parallel = parallel
+ self.max_retries = 1 # Don't retry composite steps, retry individual sub-steps
+
+ def execute(self, context: Dict[str, Any]) -> StepResult:
+ """Execute all sub-steps."""
+ results = []
+ combined_output = {}
+ combined_artifacts = {}
+
+ for step in self.steps:
+ if step.can_skip(context):
+ results.append(StepResult.skipped(f"Step {step.name} skipped"))
+ continue
+
+ result = step.execute(context)
+ results.append(result)
+
+ if result.status == StepStatus.FAILED:
+ return StepResult.failure(
+ f"Sub-step '{step.name}' failed: {result.error}"
+ )
+
+ if result.status == StepStatus.WAITING_FOR_HUMAN:
+ return result
+
+ if result.output:
+ combined_output.update(result.output)
+ context.update(result.output)
+
+ if result.artifacts:
+ combined_artifacts.update(result.artifacts)
+
+ return StepResult.success(combined_output, combined_artifacts)
+
+ def can_skip(self, context: Dict[str, Any]) -> bool:
+ """Skip if all sub-steps can be skipped."""
+ return all(step.can_skip(context) for step in self.steps)
diff --git a/Src/PeasyAI/src/evaluation/metrics/pass_at_k.py b/Src/PeasyAI/src/evaluation/metrics/pass_at_k.py
new file mode 100644
index 0000000000..637e56fb0a
--- /dev/null
+++ b/Src/PeasyAI/src/evaluation/metrics/pass_at_k.py
@@ -0,0 +1,37 @@
+def compute_total_tests(results):
+ total = 0
+ for test_name, subtest_dict in results.items():
+ total += len(subtest_dict.keys())
+ return total
+
+def compute_pass_at_k_value(results):
+ total = compute_total_tests(results)
+
+ passed = 0
+ for test_name, subtest_dict in results.items():
+ for subtest, has_passed in subtest_dict.items():
+ passed += 1 if has_passed else 0
+
+ return passed/total
+
+
+def compute(model_caller, oracle, k, n, t, tasks, **kwargs):
+ print("==== COMPUTING PASS@K ====")
+ print(f"k = {k}")
+ print(f"n = {n}")
+ print("==========================")
+
+ final_results = {} # "": [pass, fail, pass, fail....]
+ total_tasks = len(tasks)
+ for i, task in enumerate(tasks):
+ test_name, *_ = task
+ final_results[test_name] = []
+ llm_result = model_caller(task, temperature=t, k=k, n=n, task_number=i, total_tasks=total_tasks, **kwargs)
+
+ # The oracle may run several kinds of tests, e.g. multiple P test cases
+ # So the return value is a dictionary of :
+ test_result = oracle(task, llm_result)
+ final_results[test_name] = {**test_result}
+
+ p_at_k = compute_pass_at_k_value(final_results)
+ return final_results, p_at_k
diff --git a/Src/PeasyAI/src/evaluation/visualization/viz_pass_at_k.py b/Src/PeasyAI/src/evaluation/visualization/viz_pass_at_k.py
new file mode 100644
index 0000000000..e98a9ed7ed
--- /dev/null
+++ b/Src/PeasyAI/src/evaluation/visualization/viz_pass_at_k.py
@@ -0,0 +1,205 @@
+import json
+import matplotlib.pyplot as plt
+import os
+import numpy as np
+
+# Set style for better aesthetics
+plt.style.use('bmh') # Using a built-in style that provides good aesthetics
+
+def create_compile_vs_semantic_visualization(avg_pass_rates, args, parent_dir_name, avg_p_at_k, outname="p_at_k-compile-v-semantic.png"):
+ # Check if we're using the nested format
+ is_nested_format = any(isinstance(v, dict) for v in avg_pass_rates.values())
+ if not is_nested_format:
+ return # Skip if using legacy format
+
+ # Sort items by test name
+ sorted_items = sorted(avg_pass_rates.items())
+ test_names = [item[0] for item in sorted_items]
+
+ # Initialize arrays for compile and semantic values
+ compile_values = []
+ semantic_values = []
+
+ # Process each test
+ for test_name, test_data in sorted_items:
+ # Get compile value if it exists
+ compile_values.append(test_data.get('compile', 0))
+
+ # Get average of all tc* values
+ tc_values = [v for k, v in test_data.items() if k.startswith('tc')]
+ semantic_values.append(sum(tc_values) / len(tc_values) if tc_values else 0)
+
+ # Prepare data for grouped bars
+ x = np.arange(len(test_names))
+ width = 0.25 # Reduced width for more compact bars
+
+ # Create bar chart with improved aesthetics
+ plt.figure(figsize=(max(8, len(test_names) * 1.2), 6), facecolor='white')
+
+ # Create bars for compile and semantic values with gap between groups
+ gap = 0.05 # Small gap between bars in a group
+ colors = ['#3498db', '#2ecc71'] # Blue and green colors
+ bars1 = plt.bar(x - width/2 - gap/2, compile_values, width, label='Compile',
+ color=colors[0], edgecolor='white', linewidth=1.5, alpha=0.8)
+ bars2 = plt.bar(x + width/2 + gap/2, semantic_values, width, label='Correctness',
+ color=colors[1], edgecolor='white', linewidth=1.5, alpha=0.8)
+
+ # Add grid for better readability
+ plt.grid(True, axis='y', alpha=0.2, linestyle='--')
+
+ # Customize the plot
+ plt.ylim(0, 1.1)
+
+ # Add value labels on top of each bar
+ def add_value_labels(bars):
+ for bar in bars:
+ height = bar.get_height()
+ if height > 0:
+ plt.text(bar.get_x() + bar.get_width()/2., height,
+ f'{height:.2f}',
+ ha='center', va='bottom', fontsize=8)
+
+ add_value_labels(bars1)
+ add_value_labels(bars2)
+
+ # Set x-axis labels
+ plt.xticks(x, test_names, rotation=45, ha='right')
+
+ # Set title using the specified format
+ plt.title(f"ID: {parent_dir_name} pass@{args['k']}(n={args['n']},t={args['t']},trials={args['trials']}) = {avg_p_at_k:.2f} (avg)",
+ pad=20, fontsize=12, fontweight='bold')
+
+ plt.xlabel('Test Cases')
+ plt.ylabel('Pass Rate')
+ plt.legend()
+
+ # Adjust layout to prevent label cutoff
+ plt.tight_layout()
+
+ # Save the plot
+ output_path = os.path.join(os.path.dirname(outname), 'p_at_k-compile-v-semantic.png')
+ plt.savefig(output_path)
+ plt.close()
+
+def visualize_json_results(json_path, outname="p_at_k_.png"):
+ # Read and parse JSON file
+ with open(json_path, 'r') as f:
+ data = json.load(f)
+
+ # Extract data
+ args = data['args']
+ results = data['results']
+
+ # Get metrics
+ avg_pass_rates = results['avg_pass_rates']
+ avg_p_at_k = results['avg_p_at_k']
+
+ # Detect if we're using the new nested format or legacy format
+ is_nested_format = any(isinstance(v, dict) for v in avg_pass_rates.values())
+
+ # Sort items by test name
+ sorted_items = sorted(avg_pass_rates.items())
+ test_names = [item[0] for item in sorted_items]
+
+ if is_nested_format:
+ # Get all unique subtest types for nested format
+ subtest_types = set()
+ for test_data in avg_pass_rates.values():
+ subtest_types.update(test_data.keys())
+ subtest_types = sorted(list(subtest_types))
+ else:
+ # For legacy format, create a single "pass_rate" subtest type
+ subtest_types = ["pass_rate"]
+
+ # Prepare data for grouped bars
+ x = np.arange(len(test_names))
+ width = 0.8 / len(subtest_types) # Width of each bar, adjusted for number of subtests
+
+ # Create figure with more height to accommodate labels
+ plt.figure(figsize=(12, 8), dpi=100, facecolor='white')
+
+ # Define color palette
+ colors = ['#3498db', '#2ecc71', '#e74c3c', '#f1c40f', '#9b59b6']
+
+ # Create bars for each subtest type
+ bars = []
+ for i, subtest in enumerate(subtest_types):
+ subtest_values = []
+ for test in test_names:
+ if is_nested_format:
+ # For nested format, get the subtest value
+ subtest_values.append(avg_pass_rates[test].get(subtest, 0))
+ else:
+ # For legacy format, use the direct value
+ subtest_values.append(avg_pass_rates[test])
+
+ # Create offset bars for each subtest with gap
+ width_with_gap = width * 0.7 # Reduce width more to create larger gap
+ bar = plt.bar(x + i * width, subtest_values, width_with_gap,
+ label=subtest, color=colors[i % len(colors)],
+ edgecolor='white', linewidth=1, alpha=0.8) # Added slight transparency
+ bars.append(bar)
+
+ # Customize the plot
+ plt.xlabel('Test Cases')
+ plt.ylabel('Pass Rate')
+ plt.ylim(0, 1.1)
+
+ # Set x-axis labels at the center of grouped bars
+ plt.xticks(x + width * (len(subtest_types) - 1) / 2, test_names,
+ rotation=45, ha='right')
+
+ # Add legend for subtest types with better positioning
+ plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0)
+
+ # Add value labels on top of each bar
+ for bar_group in bars:
+ for bar in bar_group:
+ height = bar.get_height()
+ if height > 0: # Only show labels for non-zero values
+ plt.text(bar.get_x() + bar.get_width()/2., height,
+ f'{height:.2f}',
+ ha='center', va='bottom', fontsize=8)
+
+ # Get parent directory name from path
+ parent_dir_name = os.path.basename(os.path.dirname(json_path))
+
+ # Set title using the specified format with better font
+ plt.title(f"ID: {parent_dir_name}\npass@{args['k']}(n={args['n']},t={args['t']},trials={args['trials']}) = {avg_p_at_k:.2f} (avg)",
+ pad=20, fontsize=12, fontweight='bold')
+
+ # Add grid for better readability
+ plt.grid(True, axis='y', alpha=0.2, linestyle='--')
+
+ # Set background color
+ plt.gca().set_facecolor('white')
+
+ # Adjust layout to prevent label cutoff and accommodate legend
+ plt.tight_layout()
+
+ # Save the plot
+ output_path = os.path.join(os.path.dirname(json_path), outname)
+ plt.savefig(output_path)
+ plt.close()
+
+ # Create the compile vs semantic visualization if using nested format
+ if any(isinstance(v, dict) for v in avg_pass_rates.values()):
+ create_compile_vs_semantic_visualization(
+ avg_pass_rates,
+ args,
+ parent_dir_name,
+ avg_p_at_k,
+ output_path
+ )
+
+ return output_path
+
+if __name__ == "__main__":
+ import sys
+ if len(sys.argv) != 2:
+ print("Usage: python visualize_results.py ")
+ sys.exit(1)
+
+ json_path = sys.argv[1]
+ output_path = visualize_json_results(json_path)
+ print(f"Visualization saved to: {output_path}")
diff --git a/Src/PeasyAI/src/ui/assets/p_icon.ico b/Src/PeasyAI/src/ui/assets/p_icon.ico
new file mode 100644
index 0000000000..3031859059
Binary files /dev/null and b/Src/PeasyAI/src/ui/assets/p_icon.ico differ
diff --git a/Src/PeasyAI/src/ui/assets/pproj_template.txt b/Src/PeasyAI/src/ui/assets/pproj_template.txt
new file mode 100644
index 0000000000..8d0a3af4dc
--- /dev/null
+++ b/Src/PeasyAI/src/ui/assets/pproj_template.txt
@@ -0,0 +1,10 @@
+
+
+{project_name}
+
+ ./PSrc/
+ ./PSpec/
+ ./PTst/
+
+./PGenerated/
+
diff --git a/Src/PeasyAI/src/ui/cli/app.py b/Src/PeasyAI/src/ui/cli/app.py
new file mode 100644
index 0000000000..d96199cbd4
--- /dev/null
+++ b/Src/PeasyAI/src/ui/cli/app.py
@@ -0,0 +1,492 @@
+#!/usr/bin/env python3
+"""
+PeasyAI Command Line Interface.
+
+A CLI for generating, compiling, and verifying P code using the service layer.
+
+Usage:
+ python -m ui.cli.app generate --design-doc path/to/doc.txt --output path/to/project
+ python -m ui.cli.app compile path/to/project
+ python -m ui.cli.app check path/to/project --schedules 100 --timeout 60
+ python -m ui.cli.app fix path/to/project --error "error message"
+"""
+
+import argparse
+import sys
+import os
+from pathlib import Path
+from typing import Optional
+
+# Add project paths
+PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
+SRC_ROOT = Path(__file__).parent.parent.parent
+
+if str(SRC_ROOT) not in sys.path:
+ sys.path.insert(0, str(SRC_ROOT))
+if str(PROJECT_ROOT) not in sys.path:
+ sys.path.insert(0, str(PROJECT_ROOT))
+
+# Load environment
+from dotenv import load_dotenv
+load_dotenv(PROJECT_ROOT / ".env", override=True)
+
+from core.workflow import (
+ WorkflowEngine,
+ WorkflowFactory,
+ EventEmitter,
+ LoggingEventListener,
+ extract_machine_names_from_design_doc,
+)
+from core.services import (
+ GenerationService,
+ CompilationService,
+ FixerService,
+)
+from core.services.base import ResourceLoader
+from core.llm import get_default_provider
+
+
+class PeasyAICLI:
+ """Command-line interface for PeasyAI."""
+
+ def __init__(self, verbose: bool = False):
+ self.verbose = verbose
+ self._initialized = False
+ self._engine: Optional[WorkflowEngine] = None
+ self._factory: Optional[WorkflowFactory] = None
+ self._services = {}
+
+ def _ensure_initialized(self):
+ """Lazy initialization of services."""
+ if self._initialized:
+ return
+
+ print("🔧 Initializing PeasyAI...")
+
+ # Get LLM provider
+ provider = get_default_provider()
+ print(f" Using LLM provider: {provider.name}")
+
+ # Create resource loader
+ resource_loader = ResourceLoader(PROJECT_ROOT / "resources")
+
+ # Create services
+ self._services["generation"] = GenerationService(
+ llm_provider=provider,
+ resource_loader=resource_loader
+ )
+ self._services["compilation"] = CompilationService(
+ llm_provider=provider,
+ resource_loader=resource_loader
+ )
+ self._services["fixer"] = FixerService(
+ llm_provider=provider,
+ resource_loader=resource_loader,
+ compilation_service=self._services["compilation"]
+ )
+
+ # Create event emitter with logging
+ emitter = EventEmitter()
+ emitter.on_all(LoggingEventListener(verbose=self.verbose))
+
+ # Create engine and factory
+ self._engine = WorkflowEngine(emitter)
+ self._factory = WorkflowFactory(
+ generation_service=self._services["generation"],
+ compilation_service=self._services["compilation"],
+ fixer_service=self._services["fixer"]
+ )
+
+ # Register standard workflows
+ self._engine.register_workflow(
+ self._factory.create_compile_and_fix_workflow()
+ )
+ self._engine.register_workflow(
+ self._factory.create_full_verification_workflow()
+ )
+ self._engine.register_workflow(
+ self._factory.create_quick_check_workflow()
+ )
+
+ self._initialized = True
+ print("✅ Initialization complete\n")
+
+ def generate(
+ self,
+ design_doc_path: str,
+ output_path: str,
+ machine_names: Optional[list] = None,
+ save_files: bool = True
+ ) -> int:
+ """
+ Generate P code from a design document.
+
+ Args:
+ design_doc_path: Path to the design document
+ output_path: Where to save the generated project
+ machine_names: Optional list of machine names
+ save_files: Whether to save files to disk
+
+ Returns:
+ Exit code (0 for success, 1 for error)
+ """
+ self._ensure_initialized()
+
+ # Read design doc
+ print(f"📄 Reading design document: {design_doc_path}")
+ try:
+ with open(design_doc_path, 'r') as f:
+ design_doc = f.read()
+ except FileNotFoundError:
+ print(f"❌ File not found: {design_doc_path}")
+ return 1
+ except Exception as e:
+ print(f"❌ Error reading file: {e}")
+ return 1
+
+ # Extract machine names if not provided
+ if not machine_names:
+ machine_names = extract_machine_names_from_design_doc(design_doc)
+ if not machine_names:
+ print("❌ Could not extract machine names from design document.")
+ print(" Please provide them with --machines Machine1,Machine2")
+ return 1
+ print(f" Extracted machines: {', '.join(machine_names)}")
+
+ # Create output directory
+ os.makedirs(output_path, exist_ok=True)
+ print(f"📁 Output directory: {output_path}")
+
+ # Create and run workflow
+ workflow = self._factory.create_full_generation_workflow(machine_names)
+ self._engine.register_workflow(workflow)
+
+ result = self._engine.execute("full_generation", {
+ "design_doc": design_doc,
+ "project_path": output_path
+ })
+
+ # Handle results
+ if result.get("success"):
+ print(f"\n✅ Project generated successfully!")
+ print(f" Location: {output_path}")
+
+ # List generated files
+ if save_files:
+ completed = result.get("completed_steps", [])
+ print(f" Completed steps: {len(completed)}")
+
+ return 0
+ else:
+ print(f"\n❌ Generation failed!")
+ errors = result.get("errors", [])
+ for error in errors:
+ print(f" • {error}")
+
+ # Check if needs guidance
+ if result.get("needs_guidance"):
+ print(f"\n⚠️ Human guidance needed:")
+ print(f" {result.get('guidance_context', 'Unknown')}")
+
+ return 1
+
+ def compile(self, project_path: str, fix_errors: bool = True) -> int:
+ """
+ Compile a P project.
+
+ Args:
+ project_path: Path to the P project
+ fix_errors: Whether to attempt fixing errors
+
+ Returns:
+ Exit code
+ """
+ self._ensure_initialized()
+
+ print(f"🔨 Compiling project: {project_path}")
+
+ if not os.path.exists(project_path):
+ print(f"❌ Project path not found: {project_path}")
+ return 1
+
+ workflow_name = "compile_and_fix" if fix_errors else "compile_only"
+
+ # Use compile_and_fix workflow
+ result = self._engine.execute("compile_and_fix", {
+ "project_path": project_path
+ })
+
+ if result.get("success"):
+ print(f"\n✅ Compilation successful!")
+ return 0
+ else:
+ print(f"\n❌ Compilation failed!")
+ for error in result.get("errors", []):
+ print(f" • {error}")
+ return 1
+
+ def check(
+ self,
+ project_path: str,
+ schedules: int = 100,
+ timeout: int = 60
+ ) -> int:
+ """
+ Run PChecker on a P project.
+
+ Args:
+ project_path: Path to the P project
+ schedules: Number of schedules
+ timeout: Timeout in seconds
+
+ Returns:
+ Exit code
+ """
+ self._ensure_initialized()
+
+ print(f"🔍 Running PChecker: {project_path}")
+ print(f" Schedules: {schedules}, Timeout: {timeout}s")
+
+ if not os.path.exists(project_path):
+ print(f"❌ Project path not found: {project_path}")
+ return 1
+
+ result = self._services["compilation"].run_checker(
+ project_path=project_path,
+ schedules=schedules,
+ timeout=timeout
+ )
+
+ if result.success:
+ print(f"\n✅ PChecker passed!")
+ if self.verbose and result.output:
+ print(f"\nOutput:\n{result.output}")
+ return 0
+ else:
+ print(f"\n❌ PChecker found errors!")
+ if result.output:
+ print(f"\n{result.output}")
+ return 1
+
+ def fix(
+ self,
+ project_path: str,
+ error_message: Optional[str] = None,
+ file_path: Optional[str] = None,
+ max_iterations: int = 10
+ ) -> int:
+ """
+ Fix compilation errors in a P project.
+
+ Args:
+ project_path: Path to the P project
+ error_message: Optional specific error to fix
+ file_path: Optional file with the error
+ max_iterations: Maximum fix attempts
+
+ Returns:
+ Exit code
+ """
+ self._ensure_initialized()
+
+ print(f"🔧 Fixing errors in: {project_path}")
+
+ if not os.path.exists(project_path):
+ print(f"❌ Project path not found: {project_path}")
+ return 1
+
+ result = self._services["fixer"].fix_iteratively(
+ project_path=project_path,
+ max_iterations=max_iterations
+ )
+
+ if result.success:
+ print(f"\n✅ Errors fixed!")
+ if result.fixes_applied:
+ print(f" Fixes applied: {result.fixes_applied}")
+ return 0
+ elif result.needs_guidance:
+ print(f"\n⚠️ Could not fix automatically.")
+ print(f" Guidance needed: {result.guidance_questions}")
+ return 2
+ else:
+ print(f"\n❌ Failed to fix errors!")
+ if result.error:
+ print(f" {result.error}")
+ return 1
+
+ def list_workflows(self) -> int:
+ """List available workflows."""
+ self._ensure_initialized()
+
+ print("📋 Available Workflows:\n")
+
+ workflows = [
+ ("full_generation", "Generate complete P project from design document"),
+ ("compile_and_fix", "Compile project and fix errors"),
+ ("full_verification", "Compile, fix, and run PChecker"),
+ ("quick_check", "Run PChecker only"),
+ ]
+
+ for name, desc in workflows:
+ print(f" • {name}")
+ print(f" {desc}\n")
+
+ return 0
+
+
+def main():
+ """Main entry point for the CLI."""
+ parser = argparse.ArgumentParser(
+ description="PeasyAI - AI-powered P code generation and verification",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ Generate P code from design doc:
+ python -m ui.cli.app generate --design-doc design.txt --output ./my_project
+
+ Compile a P project:
+ python -m ui.cli.app compile ./my_project
+
+ Run PChecker:
+ python -m ui.cli.app check ./my_project --schedules 100 --timeout 60
+
+ Fix compilation errors:
+ python -m ui.cli.app fix ./my_project
+
+ List available workflows:
+ python -m ui.cli.app workflows
+ """
+ )
+
+ parser.add_argument(
+ "-v", "--verbose",
+ action="store_true",
+ help="Enable verbose output"
+ )
+
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
+
+ # Generate command
+ gen_parser = subparsers.add_parser("generate", help="Generate P code from design doc")
+ gen_parser.add_argument(
+ "-d", "--design-doc",
+ required=True,
+ help="Path to design document"
+ )
+ gen_parser.add_argument(
+ "-o", "--output",
+ required=True,
+ help="Output directory for generated project"
+ )
+ gen_parser.add_argument(
+ "-m", "--machines",
+ help="Comma-separated list of machine names (auto-extracted if not provided)"
+ )
+ gen_parser.add_argument(
+ "--no-save",
+ action="store_true",
+ help="Don't save files (preview only)"
+ )
+
+ # Compile command
+ compile_parser = subparsers.add_parser("compile", help="Compile a P project")
+ compile_parser.add_argument(
+ "project_path",
+ help="Path to the P project"
+ )
+ compile_parser.add_argument(
+ "--no-fix",
+ action="store_true",
+ help="Don't attempt to fix errors"
+ )
+
+ # Check command
+ check_parser = subparsers.add_parser("check", help="Run PChecker on a project")
+ check_parser.add_argument(
+ "project_path",
+ help="Path to the P project"
+ )
+ check_parser.add_argument(
+ "-s", "--schedules",
+ type=int,
+ default=100,
+ help="Number of schedules (default: 100)"
+ )
+ check_parser.add_argument(
+ "-t", "--timeout",
+ type=int,
+ default=60,
+ help="Timeout in seconds (default: 60)"
+ )
+
+ # Fix command
+ fix_parser = subparsers.add_parser("fix", help="Fix compilation errors")
+ fix_parser.add_argument(
+ "project_path",
+ help="Path to the P project"
+ )
+ fix_parser.add_argument(
+ "-e", "--error",
+ help="Specific error message to fix"
+ )
+ fix_parser.add_argument(
+ "-f", "--file",
+ help="File containing the error"
+ )
+ fix_parser.add_argument(
+ "-i", "--iterations",
+ type=int,
+ default=10,
+ help="Maximum fix iterations (default: 10)"
+ )
+
+ # Workflows command
+ subparsers.add_parser("workflows", help="List available workflows")
+
+ args = parser.parse_args()
+
+ if not args.command:
+ parser.print_help()
+ return 0
+
+ cli = PeasyAICLI(verbose=args.verbose)
+
+ if args.command == "generate":
+ machines = args.machines.split(",") if args.machines else None
+ return cli.generate(
+ design_doc_path=args.design_doc,
+ output_path=args.output,
+ machine_names=machines,
+ save_files=not args.no_save
+ )
+
+ elif args.command == "compile":
+ return cli.compile(
+ project_path=args.project_path,
+ fix_errors=not args.no_fix
+ )
+
+ elif args.command == "check":
+ return cli.check(
+ project_path=args.project_path,
+ schedules=args.schedules,
+ timeout=args.timeout
+ )
+
+ elif args.command == "fix":
+ return cli.fix(
+ project_path=args.project_path,
+ error_message=args.error,
+ file_path=args.file,
+ max_iterations=args.iterations
+ )
+
+ elif args.command == "workflows":
+ return cli.list_workflows()
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/Src/PeasyAI/src/ui/mcp/contracts.py b/Src/PeasyAI/src/ui/mcp/contracts.py
new file mode 100644
index 0000000000..5e34c2d27c
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/contracts.py
@@ -0,0 +1,54 @@
+"""Shared response contract helpers for MCP tools."""
+
+from typing import Dict, Any, Optional, Callable
+import uuid
+from datetime import datetime, timezone
+
+
+def tool_metadata(
+ tool_name: str,
+ token_usage: Optional[Dict[str, Any]] = None,
+ provider_name: Optional[str] = None,
+ model: Optional[str] = None,
+) -> Dict[str, Any]:
+ return {
+ "tool": tool_name,
+ "operation_id": str(uuid.uuid4()),
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
+ "provider": provider_name,
+ "model": model,
+ "token_usage": token_usage or {},
+ }
+
+
+def with_metadata(
+ tool_name: str,
+ payload: Dict[str, Any],
+ token_usage: Optional[Dict[str, Any]] = None,
+ provider_name: Optional[str] = None,
+ model: Optional[str] = None,
+ provider_resolver: Optional[Callable[[], Any]] = None,
+) -> Dict[str, Any]:
+ # Standard MCP envelope fields used by Cursor/agents.
+ payload.setdefault("api_version", "1.0")
+ success = bool(payload.get("success", True))
+ payload.setdefault("error_category", None if success else "internal")
+ payload.setdefault("retryable", not success)
+ payload.setdefault(
+ "next_actions",
+ [] if success else ["Retry the operation or inspect the error details."]
+ )
+
+ if provider_resolver:
+ provider_obj = provider_resolver()
+ if provider_obj:
+ provider_name = provider_name or getattr(provider_obj, "name", None)
+ model = model or getattr(provider_obj, "default_model", None)
+
+ payload["metadata"] = tool_metadata(
+ tool_name=tool_name,
+ token_usage=token_usage,
+ provider_name=provider_name,
+ model=model,
+ )
+ return payload
diff --git a/Src/PeasyAI/src/ui/mcp/entry.py b/Src/PeasyAI/src/ui/mcp/entry.py
new file mode 100644
index 0000000000..eb9e0c8b21
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/entry.py
@@ -0,0 +1,198 @@
+"""
+PeasyAI MCP Server – CLI entry point.
+
+This module is the console_scripts entry point installed by pip::
+
+ peasyai-mcp # start the MCP server (stdio transport)
+ peasyai-mcp init # create ~/.peasyai/settings.json
+
+Configuration is loaded from ``~/.peasyai/settings.json``
+(like ``~/.claude/settings.json``).
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import logging
+import os
+import sys
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+def _ensure_src_on_path() -> None:
+ """
+ When running from a development checkout (not a pip-installed wheel),
+ the ``src/`` directory might not be on ``sys.path``. Add it so that
+ bare imports like ``from core.llm import …`` keep working.
+ """
+ # In an installed wheel the hatch build already places core/ and ui/
+ # as top-level packages, so this is a no-op.
+ src_dir = Path(__file__).resolve().parent.parent.parent # …/src
+ project_root = src_dir.parent # …/PeasyAI
+
+ for p in (str(src_dir), str(project_root)):
+ if p not in sys.path:
+ sys.path.insert(0, p)
+
+
+def _resolve_resources_dir() -> Path:
+ """
+ Return the path to the ``resources/`` directory.
+
+ * **Development**: ``/resources/``
+ * **Installed wheel**: ``/peasyai_resources/``
+ (force-included by pyproject.toml)
+ """
+ # Dev checkout — resources/ next to src/
+ project_root = Path(__file__).resolve().parent.parent.parent.parent
+ dev_resources = project_root / "resources"
+ if dev_resources.is_dir():
+ return dev_resources
+
+ # Installed wheel — peasyai_resources/ lives next to core/ in site-packages.
+ # Walk up from this file (ui/mcp/entry.py → site-packages/) and look there.
+ import site
+ for sp in (site.getsitepackages() if hasattr(site, "getsitepackages") else []):
+ candidate = Path(sp) / "peasyai_resources"
+ if candidate.is_dir():
+ return candidate
+
+ # Last resort: relative to this file's site-packages root
+ site_resources = Path(__file__).resolve().parent.parent.parent.parent / "peasyai_resources"
+ if site_resources.is_dir():
+ return site_resources
+
+ return dev_resources # best effort
+
+
+# ---------------------------------------------------------------------------
+# Sub-commands
+# ---------------------------------------------------------------------------
+
+def _cmd_init(args: argparse.Namespace) -> int:
+ """Create ``~/.peasyai/settings.json`` with a starter template."""
+ from core.config import init_settings, SETTINGS_FILE
+
+ if SETTINGS_FILE.exists() and not args.force:
+ print(f"Settings file already exists: {SETTINGS_FILE}")
+ print("Use --force to overwrite.")
+ return 0
+
+ if args.force and SETTINGS_FILE.exists():
+ SETTINGS_FILE.unlink()
+
+ path = init_settings()
+ print(f"✅ Created {path}")
+ print()
+ print("Next steps:")
+ print(f" 1. Edit {path} with your LLM provider credentials")
+ print(" 2. Add the MCP server to Cursor or Claude Code (see below)")
+ print()
+ print("── Cursor (.cursor/mcp.json) ──────────────────────────────────")
+ print(json.dumps({
+ "mcpServers": {
+ "peasy-ai": {
+ "command": "peasyai-mcp",
+ "args": [],
+ }
+ }
+ }, indent=2))
+ print()
+ print("── Claude Code ────────────────────────────────────────────────")
+ print(" claude mcp add peasy-ai -- peasyai-mcp")
+ print()
+ return 0
+
+
+def _cmd_serve(args: argparse.Namespace) -> int:
+ """Start the PeasyAI MCP server (default action)."""
+ _ensure_src_on_path()
+
+ # Set PEASYAI_RESOURCES_DIR so ResourceLoader can find bundled resources
+ os.environ.setdefault("PEASYAI_RESOURCES_DIR", str(_resolve_resources_dir()))
+
+ # Load config from ~/.peasyai/settings.json → env vars
+ from core.config import apply_settings_to_env
+ apply_settings_to_env()
+
+ # Legacy .env fallback
+ try:
+ from dotenv import load_dotenv
+ project_root = Path(__file__).resolve().parent.parent.parent.parent
+ load_dotenv(project_root / ".env", override=False)
+ except ImportError:
+ pass
+
+ from ui.mcp.server import mcp
+ logger.info("Starting PeasyAI MCP Server …")
+ mcp.run()
+ return 0
+
+
+def _cmd_show_config(args: argparse.Namespace) -> int:
+ """Print the effective configuration (with secrets masked)."""
+ _ensure_src_on_path()
+ from core.config import load_settings, SETTINGS_FILE
+
+ settings = load_settings()
+ provider = settings.active_provider_name()
+ pc = settings.active_provider_config()
+
+ print(f"Config file : {SETTINGS_FILE}")
+ print(f"File exists : {SETTINGS_FILE.exists()}")
+ print(f"Provider : {provider}")
+ print(f"Model : {settings.llm.model or pc.model or '(default)'}")
+ print(f"Timeout : {settings.llm.timeout}s")
+
+ if pc.api_key:
+ masked = pc.api_key[:4] + "…" + pc.api_key[-4:] if len(pc.api_key) > 8 else "****"
+ print(f"API key : {masked}")
+ else:
+ print(f"API key : (not set)")
+
+ if pc.base_url:
+ print(f"Base URL : {pc.base_url}")
+ return 0
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ prog="peasyai-mcp",
+ description="PeasyAI – MCP server for P language code generation & verification",
+ )
+ sub = parser.add_subparsers(dest="command")
+
+ # peasyai-mcp (no sub-command → start server)
+ # peasyai-mcp init
+ init_p = sub.add_parser("init", help="Create ~/.peasyai/settings.json")
+ init_p.add_argument("--force", action="store_true", help="Overwrite existing file")
+
+ # peasyai-mcp config
+ sub.add_parser("config", help="Show effective configuration")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
+
+ if args.command == "init":
+ return _cmd_init(args)
+ elif args.command == "config":
+ return _cmd_show_config(args)
+ else:
+ # Default: start the MCP server
+ return _cmd_serve(args)
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Src/PeasyAI/src/ui/mcp/resources.py b/Src/PeasyAI/src/ui/mcp/resources.py
new file mode 100644
index 0000000000..f90b23da95
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/resources.py
@@ -0,0 +1,99 @@
+"""MCP resources for P language guides."""
+
+
+def register_resources(mcp, get_services):
+ """Register MCP resources."""
+
+ def _load_resource(path: str) -> str:
+ services = get_services()
+ try:
+ return services["resources"].load(f"context_files/{path}")
+ except Exception as e:
+ return f"Error loading resource: {e}"
+
+ @mcp.resource("p://guides/syntax")
+ def get_syntax_guide() -> str:
+ """Complete P language syntax reference"""
+ return _load_resource("P_syntax_guide.txt")
+
+ @mcp.resource("p://guides/basics")
+ def get_basics_guide() -> str:
+ """P language fundamentals and core concepts"""
+ return _load_resource("modular/p_basics.txt")
+
+ @mcp.resource("p://guides/machines")
+ def get_machines_guide() -> str:
+ """Guide to P state machines"""
+ return _load_resource("modular/p_machines_guide.txt")
+
+ @mcp.resource("p://guides/types")
+ def get_types_guide() -> str:
+ """P language type system guide"""
+ return _load_resource("modular/p_types_guide.txt")
+
+ @mcp.resource("p://guides/events")
+ def get_events_guide() -> str:
+ """P language events guide"""
+ return _load_resource("modular/p_events_guide.txt")
+
+ @mcp.resource("p://guides/enums")
+ def get_enums_guide() -> str:
+ """P language enums guide"""
+ return _load_resource("modular/p_enums_guide.txt")
+
+ @mcp.resource("p://guides/statements")
+ def get_statements_guide() -> str:
+ """P language statements guide"""
+ return _load_resource("modular/p_statements_guide.txt")
+
+ @mcp.resource("p://guides/specs")
+ def get_specs_guide() -> str:
+ """P specification and monitors guide"""
+ return _load_resource("modular/p_spec_monitors_guide.txt")
+
+ @mcp.resource("p://guides/tests")
+ def get_tests_guide() -> str:
+ """P test cases guide"""
+ return _load_resource("modular/p_test_cases_guide.txt")
+
+ @mcp.resource("p://guides/modules")
+ def get_modules_guide() -> str:
+ """P module system guide"""
+ return _load_resource("modular/p_module_system_guide.txt")
+
+ @mcp.resource("p://guides/compiler")
+ def get_compiler_guide() -> str:
+ """P compiler usage and error guide"""
+ return _load_resource("modular/p_compiler_guide.txt")
+
+ @mcp.resource("p://guides/common_errors")
+ def get_common_errors_guide() -> str:
+ """Common P compilation errors and fixes"""
+ return _load_resource("modular/p_common_compilation_errors.txt")
+
+ @mcp.resource("p://examples/program")
+ def get_program_example() -> str:
+ """Complete P program example"""
+ return _load_resource("modular/p_program_example.txt")
+
+ @mcp.resource("p://about")
+ def get_about_p() -> str:
+ """About the P language"""
+ return _load_resource("about_p.txt")
+
+ return {
+ "get_syntax_guide": get_syntax_guide,
+ "get_basics_guide": get_basics_guide,
+ "get_machines_guide": get_machines_guide,
+ "get_types_guide": get_types_guide,
+ "get_events_guide": get_events_guide,
+ "get_enums_guide": get_enums_guide,
+ "get_statements_guide": get_statements_guide,
+ "get_specs_guide": get_specs_guide,
+ "get_tests_guide": get_tests_guide,
+ "get_modules_guide": get_modules_guide,
+ "get_compiler_guide": get_compiler_guide,
+ "get_common_errors_guide": get_common_errors_guide,
+ "get_program_example": get_program_example,
+ "get_about_p": get_about_p,
+ }
diff --git a/Src/PeasyAI/src/ui/mcp/server.py b/Src/PeasyAI/src/ui/mcp/server.py
new file mode 100644
index 0000000000..c76b582262
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/server.py
@@ -0,0 +1,168 @@
+"""
+PeasyAI MCP Server for Cursor IDE Integration.
+
+This MCP server exposes tools for P code generation, compilation, and error fixing.
+It uses the Phase 1 service layer for all operations, ensuring consistency with
+the Streamlit and CLI interfaces.
+"""
+
+from fastmcp import FastMCP
+import logging
+from typing import Dict, Any, Optional
+from pathlib import Path
+import os
+import sys
+
+# ============================================================================
+# PATH SETUP
+# ============================================================================
+
+PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
+SRC_ROOT = Path(__file__).parent.parent.parent
+
+if str(SRC_ROOT) not in sys.path:
+ sys.path.insert(0, str(SRC_ROOT))
+if str(PROJECT_ROOT) not in sys.path:
+ sys.path.insert(0, str(PROJECT_ROOT))
+
+# ============================================================================
+# CONFIGURATION: load from ~/.peasyai/settings.json (like ~/.claude/settings.json)
+# Env vars still win if already set; the settings file fills in the rest.
+# ============================================================================
+
+from core.config import apply_settings_to_env
+
+_peasyai_settings = apply_settings_to_env()
+
+# Legacy fallback: also read .env if it exists (lowest priority —
+# values are only set when not already provided by settings.json or env).
+try:
+ from dotenv import load_dotenv
+ load_dotenv(PROJECT_ROOT / ".env", override=False)
+except ImportError:
+ pass # python-dotenv is optional when using ~/.peasyai/settings.json
+
+# ============================================================================
+# IMPORTS FROM PHASE 1 SERVICE LAYER
+# ============================================================================
+
+from core.llm import get_default_provider
+from core.services import GenerationService, CompilationService, FixerService
+from core.services.base import ResourceLoader
+from ui.mcp.contracts import with_metadata as contract_with_metadata
+
+try:
+ from core.compilation import ensure_environment
+ HAS_NEW_COMPILATION = True
+except ImportError:
+ HAS_NEW_COMPILATION = False
+
+# ============================================================================
+# MCP SERVER SETUP
+# ============================================================================
+
+mcp = FastMCP("peasy-ai")
+
+logging.basicConfig(
+ level=logging.INFO,
+ format='[%(levelname)s] %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+# ============================================================================
+# SERVICE INITIALIZATION
+# ============================================================================
+
+_services: Dict[str, Any] = {}
+
+
+def get_services() -> Dict[str, Any]:
+ """Get or create service instances."""
+ if not _services:
+ logger.info("Initializing services...")
+
+ if HAS_NEW_COMPILATION:
+ env_info = ensure_environment()
+ if env_info.is_valid:
+ logger.info(f"P environment: P={env_info.p_compiler_path}, dotnet={env_info.dotnet_path}")
+ else:
+ logger.warning(f"P environment issues: {env_info.issues}")
+
+ provider = get_default_provider()
+ logger.info(f"Using LLM provider: {provider.name}")
+
+ resource_loader = ResourceLoader()
+
+ _services["llm_provider"] = provider
+ _services["generation"] = GenerationService(
+ llm_provider=provider,
+ resource_loader=resource_loader
+ )
+ _services["compilation"] = CompilationService(
+ llm_provider=provider,
+ resource_loader=resource_loader
+ )
+ _services["fixer"] = FixerService(
+ llm_provider=provider,
+ resource_loader=resource_loader,
+ compilation_service=_services["compilation"]
+ )
+ _services["resources"] = resource_loader
+
+ logger.info("Services initialized")
+
+ return _services
+
+
+# ============================================================================
+# METADATA HELPERS
+# ============================================================================
+
+def _with_metadata(
+ tool_name: str,
+ payload: Dict[str, Any],
+ token_usage: Optional[Dict[str, Any]] = None,
+ provider_name: Optional[str] = None,
+ model: Optional[str] = None,
+) -> Dict[str, Any]:
+ return contract_with_metadata(
+ tool_name=tool_name,
+ token_usage=token_usage,
+ payload=payload,
+ provider_name=provider_name,
+ model=model,
+ provider_resolver=lambda: _services.get("llm_provider"),
+ )
+
+
+# ============================================================================
+# TOOL REGISTRATION
+# ============================================================================
+
+from ui.mcp.tools.env import register_env_tools
+from ui.mcp.tools.generation import register_generation_tools
+from ui.mcp.tools.compilation import register_compilation_tools
+from ui.mcp.tools.fixing import register_fixing_tools
+from ui.mcp.tools.query import register_query_tools
+from ui.mcp.tools.rag_tools import register_rag_tools
+from ui.mcp.tools.workflows import register_workflow_tools
+from ui.mcp.resources import register_resources
+
+register_env_tools(mcp, _with_metadata)
+register_generation_tools(mcp, get_services, _with_metadata)
+register_compilation_tools(mcp, get_services, _with_metadata)
+register_fixing_tools(mcp, get_services, _with_metadata)
+register_query_tools(mcp, get_services, _with_metadata)
+register_rag_tools(mcp, _with_metadata)
+register_workflow_tools(mcp, get_services, _with_metadata)
+register_resources(mcp, get_services)
+
+
+# ============================================================================
+# MAIN
+# ============================================================================
+
+if __name__ == "__main__":
+ logger.info("Starting PeasyAI MCP Server...")
+ logger.info(f"Project root: {PROJECT_ROOT}")
+ mcp.run()
diff --git a/Src/PeasyAI/src/ui/mcp/tools/__init__.py b/Src/PeasyAI/src/ui/mcp/tools/__init__.py
new file mode 100644
index 0000000000..0709b359c4
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/__init__.py
@@ -0,0 +1 @@
+"""MCP tool registration modules."""
diff --git a/Src/PeasyAI/src/ui/mcp/tools/compilation.py b/Src/PeasyAI/src/ui/mcp/tools/compilation.py
new file mode 100644
index 0000000000..82e3a47143
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/compilation.py
@@ -0,0 +1,122 @@
+"""Compilation-related MCP tools."""
+
+from typing import Dict, Any
+from pydantic import BaseModel, Field
+import logging
+
+from core.security import validate_project_path, PathSecurityError, sanitize_error
+
+logger = logging.getLogger(__name__)
+
+try:
+ from core.compilation import parse_compilation_output
+ HAS_NEW_COMPILATION = True
+except ImportError:
+ HAS_NEW_COMPILATION = False
+
+
+class PCompileParams(BaseModel):
+ """Parameters for compilation"""
+ path: str = Field(
+ ...,
+ description="Absolute path to the P project directory (must contain .pproj file)"
+ )
+
+
+class PCheckParams(BaseModel):
+ """Parameters for PChecker"""
+ path: str = Field(..., description="Absolute path to the P project directory")
+ schedules: int = Field(default=100, description="Number of schedules to explore")
+ timeout: int = Field(default=60, description="Timeout in seconds")
+ max_steps: int = Field(default=10000, description="Maximum steps per schedule before moving on")
+
+
+def register_compilation_tools(mcp, get_services, with_metadata):
+ """Register compilation tools."""
+
+ @mcp.tool(
+ name="peasy-ai-compile",
+ description="Compile a P project and return compilation results. The project directory must contain a .pproj file. On failure, the response includes parsed errors with file, line, and message details. Use peasy-ai-fix-compile-error or peasy-ai-fix-all to resolve compilation errors."
+ )
+ def p_compile(params: PCompileParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-compile: {params.path}")
+
+ try:
+ validated = validate_project_path(params.path)
+ except PathSecurityError as e:
+ return with_metadata("peasy-ai-compile", {
+ "success": False, "error": str(e), "return_code": -1,
+ })
+
+ services = get_services()
+ result = services["compilation"].compile(str(validated))
+
+ response = {
+ "success": result.success,
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ "return_code": result.return_code,
+ "error": result.error
+ }
+
+ if not result.success and HAS_NEW_COMPILATION:
+ try:
+ parsed = parse_compilation_output(result.stdout or result.stderr or "")
+ if parsed.errors:
+ response["parsed_errors"] = [
+ {
+ "file": e.file,
+ "line": e.line,
+ "column": e.column,
+ "category": e.category.value,
+ "message": e.message,
+ "suggestion": e.suggestion,
+ }
+ for e in parsed.errors
+ ]
+ response["error_summary"] = f"Found {len(parsed.errors)} error(s)"
+ if parsed.errors:
+ first_error = parsed.errors[0]
+ response["first_error"] = {
+ "file": first_error.file,
+ "line": first_error.line,
+ "message": first_error.message,
+ "suggestion": first_error.suggestion,
+ }
+ except Exception as e:
+ logger.debug(f"Error parsing compilation output: {e}")
+
+ return with_metadata("peasy-ai-compile", response)
+
+ @mcp.tool(
+ name="peasy-ai-check",
+ description="Run PChecker on a compiled P project to verify correctness via model checking. The project must compile successfully first (use peasy-ai-compile). Explores random schedules to find concurrency bugs like deadlocks, assertion failures, and unhandled events. On failure, use peasy-ai-fix-checker-error or peasy-ai-fix-bug to diagnose and fix the bug."
+ )
+ def p_check(params: PCheckParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-check: {params.path}")
+
+ try:
+ validated = validate_project_path(params.path)
+ except PathSecurityError as e:
+ return with_metadata("peasy-ai-check", {
+ "success": False, "error": str(e),
+ })
+
+ services = get_services()
+ result = services["compilation"].run_checker(
+ project_path=str(validated),
+ schedules=params.schedules,
+ timeout=params.timeout,
+ max_steps=params.max_steps,
+ )
+
+ payload = {
+ "success": result.success,
+ "test_results": result.test_results,
+ "passed_tests": result.passed_tests,
+ "failed_tests": result.failed_tests,
+ "error": result.error
+ }
+ return with_metadata("peasy-ai-check", payload)
+
+ return {"p_compile": p_compile, "p_check": p_check}
diff --git a/Src/PeasyAI/src/ui/mcp/tools/env.py b/Src/PeasyAI/src/ui/mcp/tools/env.py
new file mode 100644
index 0000000000..fc818d1060
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/env.py
@@ -0,0 +1,89 @@
+"""Environment validation tools for MCP."""
+
+from typing import Dict, Any, List
+from pydantic import BaseModel
+import os
+import shutil
+
+try:
+ from core.compilation import ensure_environment
+ HAS_NEW_COMPILATION = True
+except ImportError:
+ HAS_NEW_COMPILATION = False
+
+from core.config import SETTINGS_FILE, load_settings
+
+
+class ValidateEnvironmentParams(BaseModel):
+ """Parameters for environment validation"""
+ pass
+
+
+def register_env_tools(mcp, with_metadata):
+ """Register environment validation tools."""
+
+ @mcp.tool(
+ name="peasy-ai-validate-env",
+ description="Validate local P toolchain and LLM provider environment."
+ )
+ def validate_environment(params: ValidateEnvironmentParams) -> Dict[str, Any]:
+ issues: List[str] = []
+ details: Dict[str, Any] = {}
+
+ # ── Check ~/.peasyai/settings.json ─────────────────────────────
+ details["config_file"] = str(SETTINGS_FILE)
+ details["config_file_exists"] = SETTINGS_FILE.exists()
+ if not SETTINGS_FILE.exists():
+ issues.append(
+ f"Config file not found at {SETTINGS_FILE}. "
+ "Create it with: peasyai-mcp init"
+ )
+ else:
+ settings = load_settings()
+ details["configured_provider"] = settings.active_provider_name()
+
+ # ── Check P toolchain ──────────────────────────────────────────
+ if HAS_NEW_COMPILATION:
+ env_info = ensure_environment()
+ details["p_compiler_path"] = env_info.p_compiler_path
+ details["dotnet_path"] = env_info.dotnet_path
+ details["toolchain_issues"] = env_info.issues
+ if not env_info.is_valid:
+ issues.extend(env_info.issues or ["P environment is not valid"])
+ else:
+ p_path = shutil.which("p")
+ dotnet_path = shutil.which("dotnet")
+ details["p_compiler_path"] = p_path
+ details["dotnet_path"] = dotnet_path
+ if not p_path:
+ issues.append("P compiler not found in PATH")
+ if not dotnet_path:
+ issues.append("dotnet not found in PATH")
+
+ # ── Check LLM provider ─────────────────────────────────────────
+ provider = None
+ if os.environ.get("OPENAI_API_KEY") and os.environ.get("OPENAI_BASE_URL"):
+ provider = "snowflake_cortex"
+ elif os.environ.get("ANTHROPIC_API_KEY"):
+ provider = "anthropic_direct"
+ elif os.environ.get("OPENAI_API_KEY"):
+ provider = "openai"
+ elif os.environ.get("AWS_ACCESS_KEY_ID") and os.environ.get("AWS_SECRET_ACCESS_KEY"):
+ provider = "bedrock"
+
+ details["llm_provider_detected"] = provider
+ if not provider:
+ issues.append(
+ "No LLM provider credentials detected. "
+ "Configure them in ~/.peasyai/settings.json"
+ )
+
+ payload = {
+ "success": len(issues) == 0,
+ "issues": issues,
+ "details": details,
+ "message": "Environment valid" if len(issues) == 0 else "Environment issues detected",
+ }
+ return with_metadata("peasy-ai-validate-env", payload)
+
+ return validate_environment
diff --git a/Src/PeasyAI/src/ui/mcp/tools/fixing.py b/Src/PeasyAI/src/ui/mcp/tools/fixing.py
new file mode 100644
index 0000000000..d5b754098a
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/fixing.py
@@ -0,0 +1,665 @@
+"""Fixing-related MCP tools."""
+
+from typing import Dict, Any, List, Optional, Tuple
+from pydantic import BaseModel, Field
+from pathlib import Path
+import logging
+
+from core.services.compilation import ParsedError
+from core.security import (
+ validate_project_path,
+ validate_file_read_path,
+ PathSecurityError,
+ sanitize_error,
+ check_input_size,
+ MAX_ERROR_MESSAGE_BYTES,
+ MAX_TRACE_LOG_BYTES,
+ MAX_USER_GUIDANCE_BYTES,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class FixCompilerErrorParams(BaseModel):
+ """Parameters for fixing compilation errors"""
+ project_path: str = Field(..., description="Absolute path to the P project")
+ error_message: str = Field(..., description="The compiler error message", max_length=10_000)
+ file_path: str = Field(..., description="Path to the file with the error")
+ line_number: int = Field(default=0, description="Line number of the error")
+ column_number: int = Field(default=0, description="Column number of the error")
+ user_guidance: Optional[str] = Field(
+ default=None,
+ description="User guidance after failed attempts (provide when needs_guidance was returned)",
+ max_length=50_000,
+ )
+
+
+class FixCheckerErrorParams(BaseModel):
+ """Parameters for fixing PChecker errors"""
+ project_path: str = Field(..., description="Absolute path to the P project")
+ trace_log: str = Field(..., description="The PChecker trace log showing the error", max_length=1_000_000)
+ error_category: Optional[str] = Field(
+ default=None,
+ description="Category of the error (e.g., 'assertion_failure', 'deadlock')"
+ )
+ user_guidance: Optional[str] = Field(
+ default=None,
+ description="User guidance after failed attempts",
+ max_length=50_000,
+ )
+
+
+class FixIterativelyParams(BaseModel):
+ """Parameters for iterative compilation fixing"""
+ project_path: str = Field(..., description="Absolute path to the P project")
+ max_iterations: int = Field(default=10, description="Maximum fix iterations")
+
+
+class FixBuggyProgramParams(BaseModel):
+ """Parameters for fixing a buggy P program after PChecker failure"""
+ project_path: str = Field(..., description="Absolute path to the P project")
+ test_name: Optional[str] = Field(
+ default=None,
+ description="Name of the failed test (optional, auto-detected from latest run)"
+ )
+
+
+def _get_manual_fix_guidance(analysis) -> Dict[str, Any]:
+ """Generate detailed manual fix guidance based on error category."""
+ from core.compilation import CheckerErrorCategory
+
+ error = analysis.error
+ guidance = {
+ "category": error.category.value,
+ "steps": [],
+ "recommended_changes": [],
+ "example_fix": None
+ }
+
+ if error.category == CheckerErrorCategory.UNHANDLED_EVENT:
+ if error.is_test_driver_bug:
+ guidance["steps"] = [
+ "1. Identify why the test driver is sending this protocol event",
+ "2. Create a dedicated setup event in Enums_Types_Events.p",
+ "3. Add a handler for the setup event in the receiving machine's Init state",
+ "4. Update the test driver to use the new setup event instead",
+ "5. Add 'ignore' for the protocol event in all machines that may receive it",
+ "6. Ensure safety specs can still observe the events they monitor",
+ ]
+ guidance["example_fix"] = f"""
+// In Enums_Types_Events.p — add a dedicated setup event:
+event eSetup{error.machine_type}Components: seq[machine];
+
+// In {error.machine_type}.p — handle the setup event:
+start state Init {{
+ entry InitEntry;
+ on eSetup{error.machine_type}Components do (payload: seq[machine]) {{
+ components = payload;
+ }}
+ ignore {error.event_name};
+}}
+
+// In TestDriver.p — use the new setup event instead:
+send {error.machine_type.lower()}, eSetup{error.machine_type}Components, allComponents;
+"""
+ else:
+ guidance["steps"] = [
+ "1. Trace back to the sender — find where the event is sent",
+ "2. Determine if this is a test-driver bug or protocol-logic bug",
+ "3. Add a handler, ignore, or defer in the receiving state",
+ "4. Check if other machines also lack handlers for this event",
+ "5. Update specs/tests if expected behavior changes",
+ ]
+ guidance["example_fix"] = f"""
+state {error.machine_state} {{
+ // Option A: Ignore the event
+ ignore {error.event_name};
+
+ // Option B: Defer until another state
+ defer {error.event_name};
+
+ // Option C: Handle explicitly
+ on {error.event_name} do Handle{error.event_name.replace('e', '', 1)};
+}}
+"""
+ elif error.category == CheckerErrorCategory.ASSERTION_FAILURE:
+ guidance["steps"] = [
+ "1. Examine the assertion that failed in the trace",
+ "2. Identify what condition was expected vs actual",
+ "3. Trace back through the execution to find where the invariant was violated",
+ "4. Fix the logic that leads to the invalid state",
+ ]
+ elif error.category == CheckerErrorCategory.DEADLOCK:
+ guidance["steps"] = [
+ "1. Check which machines are waiting and in which states",
+ "2. Look for circular dependencies in event handling",
+ "3. Ensure all expected events are being sent",
+ "4. Add timeout mechanisms if appropriate",
+ ]
+
+ return guidance
+
+
+def _try_llm_checker_fix(
+ services: Dict[str, Any],
+ project_path: str,
+ project_files: Dict[str, str],
+ trace_content: str,
+ analysis,
+ response: Dict[str, Any],
+) -> bool:
+ """
+ LLM-based fallback for checker errors the specialised fixer cannot handle.
+
+ Sends the trace excerpt + all project files to the LLM and asks it to
+ produce a fixed version of the most-likely-broken file.
+
+ Returns True if the fix compiled and passed PChecker, False otherwise.
+ """
+ try:
+ from core.llm.base import Message, MessageRole, LLMConfig
+
+ error = analysis.error
+ # Determine which file to ask the LLM to fix.
+ # Heuristic: assertion failures → spec file, others → the machine file.
+ target_file = None
+ target_content = None
+ from core.compilation import CheckerErrorCategory
+
+ if error.category == CheckerErrorCategory.ASSERTION_FAILURE:
+ for fname, content in project_files.items():
+ if "PSpec" in fname or "spec " in content:
+ target_file = fname
+ target_content = content
+ break
+ if not target_file:
+ for fname, content in project_files.items():
+ if error.machine_type and f"machine {error.machine_type}" in content:
+ target_file = fname
+ target_content = content
+ break
+ if not target_file:
+ return False
+
+ # Build context from other files
+ context_parts = []
+ for fname, content in project_files.items():
+ if fname != target_file:
+ short = content[:3000] if len(content) > 3000 else content
+ context_parts.append(f"<{Path(fname).name}>\n{short}\n{Path(fname).name}>")
+
+ # Truncate trace to last 150 lines
+ trace_lines = trace_content.splitlines()
+ trace_excerpt = "\n".join(trace_lines[-150:]) if len(trace_lines) > 150 else trace_content
+
+ target_basename = Path(target_file).name
+ messages = [
+ Message(role=MessageRole.USER, content="\n".join(context_parts)),
+ Message(role=MessageRole.USER, content=(
+ f"The P project compiles but PChecker found a bug.\n\n"
+ f"Error category: {error.category.value}\n"
+ f"Error message: {error.message}\n"
+ f"Machine: {error.machine_type}, State: {error.machine_state}\n\n"
+ f"Trace excerpt (last 150 lines):\n```\n{trace_excerpt}\n```\n\n"
+ f"The file that most likely needs fixing is {target_basename}:\n"
+ f"```\n{target_content}\n```\n\n"
+ f"Please rewrite {target_basename} to fix this bug. "
+ f"Return the complete file in <{target_basename}>...{target_basename}> tags."
+ )),
+ ]
+
+ import re as _re
+ provider = services["llm_provider"]
+ system_prompt = services["resources"].load_context("about_p.txt")
+ config = LLMConfig(max_tokens=4096)
+ llm_resp = provider.complete(messages, config, system_prompt)
+
+ from core.compilation.p_code_utils import extract_p_code_from_response
+ _, new_code = extract_p_code_from_response(
+ llm_resp.content, expected_filename=target_basename
+ )
+ if not new_code:
+ return False
+ backup = target_content
+
+ # Resolve the full path for writing
+ full_path = target_file
+ if not Path(full_path).is_absolute():
+ full_path = str(Path(project_path) / target_file)
+
+ services["compilation"].write_file(full_path, new_code)
+
+ compile_result = services["compilation"].compile(project_path)
+ if not compile_result.success:
+ services["compilation"].write_file(full_path, backup)
+ return False
+
+ check_result = services["compilation"].run_checker(
+ project_path, schedules=50, timeout=60
+ )
+ if check_result.success:
+ response["fixed"] = True
+ response["verification"] = "LLM-based fix verified - PChecker passed 50 schedules"
+ response["fix_applied"] = {
+ "description": f"LLM rewrote {target_basename} based on trace analysis",
+ "file": full_path,
+ "strategy": "llm_fallback",
+ }
+ logger.info(f"LLM checker fix succeeded for {target_basename}")
+ return True
+ else:
+ services["compilation"].write_file(full_path, backup)
+ return False
+
+ except Exception as e:
+ logger.warning(f"LLM checker fix fallback failed: {e}")
+ return False
+
+
+def _basic_trace_analysis(trace_content: str, project_path: str, services) -> Dict[str, Any]:
+ """Basic trace analysis without specialized modules."""
+ import re
+
+ error_match = re.search(r'\s*(.+)', trace_content)
+ error_msg = error_match.group(1) if error_match else "Unknown error"
+
+ if "null" in error_msg.lower():
+ category = "null_target"
+ elif "cannot be handled" in error_msg.lower():
+ category = "unhandled_event"
+ elif "assert" in error_msg.lower():
+ category = "assertion_failure"
+ elif "deadlock" in error_msg.lower():
+ category = "deadlock"
+ else:
+ category = "unknown"
+
+ return {
+ "success": True,
+ "analysis": {
+ "error_category": category,
+ "error_message": error_msg,
+ },
+ "root_cause": f"PChecker found a {category.replace('_', ' ')} error",
+ "suggested_fixes": [
+ "Review the trace file for detailed execution path",
+ "Check the error message for specific machine and state information",
+ ],
+ "fixed": False,
+ "requires_manual_fix": True,
+ }
+
+
+def register_fixing_tools(mcp, get_services, with_metadata):
+ """Register fixing tools."""
+
+ @mcp.tool(
+ name="peasy-ai-fix-compile-error",
+ description="""Fix a single P compiler error using AI. Provide the error message, file path, and optionally line/column numbers from the peasy-ai-compile output.
+
+To fix all compilation errors at once, use peasy-ai-fix-all instead.
+
+After 3 failed attempts, returns needs_guidance=true with questions for the user. If you receive needs_guidance, ask the user the questions and call again with user_guidance."""
+ )
+ def fix_compiler_error(params: FixCompilerErrorParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-fix-compile-error: {params.file_path}")
+
+ try:
+ validate_project_path(params.project_path)
+ validate_file_read_path(params.file_path, params.project_path)
+ check_input_size(params.error_message, "error_message", MAX_ERROR_MESSAGE_BYTES)
+ if params.user_guidance:
+ check_input_size(params.user_guidance, "user_guidance", MAX_USER_GUIDANCE_BYTES)
+ except (PathSecurityError, ValueError) as e:
+ return with_metadata("peasy-ai-fix-compile-error", {
+ "success": False, "error": str(e),
+ })
+
+ services = get_services()
+
+ error = ParsedError(
+ file_path=params.file_path,
+ line_number=params.line_number,
+ column_number=params.column_number,
+ message=params.error_message
+ )
+
+ result = services["fixer"].fix_compilation_error(
+ project_path=params.project_path,
+ error=error,
+ user_guidance=params.user_guidance
+ )
+
+ response = {
+ "success": result.success,
+ "fixed": result.fixed,
+ "filename": result.filename,
+ "file_path": result.file_path,
+ "error": result.error,
+ "token_usage": result.token_usage
+ }
+
+ if result.needs_guidance:
+ response["needs_guidance"] = True
+ response["guidance_request"] = result.guidance_request
+
+ return with_metadata("peasy-ai-fix-compile-error", response, token_usage=result.token_usage)
+
+ @mcp.tool(
+ name="peasy-ai-fix-checker-error",
+ description="""Fix a PChecker error using AI.
+
+Analyzes the execution trace and fixes state machine logic issues.
+After 3 failed attempts, returns needs_guidance=true with questions for the user.
+
+If you receive needs_guidance, ask the user the questions and call again with user_guidance."""
+ )
+ def fix_checker_error(params: FixCheckerErrorParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-fix-checker-error: {params.project_path}")
+
+ try:
+ validate_project_path(params.project_path)
+ check_input_size(params.trace_log, "trace_log", MAX_TRACE_LOG_BYTES)
+ if params.user_guidance:
+ check_input_size(params.user_guidance, "user_guidance", MAX_USER_GUIDANCE_BYTES)
+ except (PathSecurityError, ValueError) as e:
+ return with_metadata("peasy-ai-fix-checker-error", {
+ "success": False, "error": str(e),
+ })
+
+ services = get_services()
+ result = services["fixer"].fix_checker_error(
+ project_path=params.project_path,
+ trace_log=params.trace_log,
+ error_category=params.error_category,
+ user_guidance=params.user_guidance
+ )
+
+ response = {
+ "success": result.success,
+ "fixed": result.fixed,
+ "error": result.error,
+ "token_usage": result.token_usage
+ }
+
+ if result.analysis:
+ response["analysis"] = result.analysis
+
+ if result.root_cause:
+ response["root_cause"] = result.root_cause
+
+ if result.suggested_fixes:
+ response["suggested_fixes"] = result.suggested_fixes
+
+ if result.confidence > 0:
+ response["confidence"] = result.confidence
+
+ if result.needs_guidance:
+ response["needs_guidance"] = True
+ response["guidance_request"] = result.guidance_request
+
+ if result.fixed:
+ response["filename"] = result.filename
+ response["file_path"] = result.file_path
+
+ # Surface vacuous pass warnings if present
+ if result.analysis and "vacuous_pass_warning" in result.analysis:
+ response["vacuous_pass_warning"] = result.analysis["vacuous_pass_warning"]
+
+ return with_metadata("peasy-ai-fix-checker-error", response, token_usage=result.token_usage)
+
+ @mcp.tool(
+ name="peasy-ai-fix-all",
+ description="Iteratively compile, detect errors, and fix them in a loop until the project compiles successfully or max_iterations is reached. This is the recommended way to fix multiple compilation errors at once — it automatically re-compiles after each fix to catch cascading issues. Use this instead of calling peasy-ai-fix-compile-error repeatedly."
+ )
+ def fix_iteratively(params: FixIterativelyParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-fix-all: {params.project_path}")
+
+ try:
+ validate_project_path(params.project_path)
+ except PathSecurityError as e:
+ return with_metadata("peasy-ai-fix-all", {
+ "success": False, "error": str(e),
+ })
+
+ services = get_services()
+ result = services["fixer"].fix_iteratively(
+ project_path=params.project_path,
+ max_iterations=params.max_iterations
+ )
+
+ return with_metadata("peasy-ai-fix-all", result)
+
+ @mcp.tool(
+ name="peasy-ai-fix-bug",
+ description="""Automatically diagnose and fix a buggy P program after a PChecker failure.
+
+Use this after peasy-ai-check returns a failure. It reads the latest PChecker trace from PCheckerOutput/BugFinding/, identifies the bug type (null_target, unhandled_event, assertion_failure, deadlock), provides root cause analysis, attempts an automatic fix, and verifies by recompiling and re-running PChecker.
+
+If the auto-fix fails, returns requires_manual_fix=true with step-by-step guidance and example code for manual intervention."""
+ )
+ def fix_buggy_program(params: FixBuggyProgramParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-fix-bug: {params.project_path}")
+
+ try:
+ validate_project_path(params.project_path)
+ except PathSecurityError as e:
+ return with_metadata("peasy-ai-fix-bug", {
+ "success": False, "error": str(e),
+ })
+
+ project_path = Path(params.project_path)
+
+ # Find the most recent BugFinding*/ directory — that contains
+ # the trace for the latest failing test.
+ checker_output = project_path / "PCheckerOutput"
+ if not checker_output.exists():
+ payload = {
+ "success": False,
+ "error": f"No PChecker output found at {checker_output}. Run p_check first.",
+ }
+ return with_metadata("peasy-ai-fix-bug", payload)
+
+ bug_dirs = sorted(
+ [d for d in checker_output.glob("BugFinding*") if d.is_dir()],
+ key=lambda d: d.stat().st_mtime,
+ reverse=True,
+ )
+ if not bug_dirs:
+ payload = {
+ "success": False,
+ "error": "No BugFinding directories found. Run p_check first.",
+ }
+ return with_metadata("peasy-ai-fix-bug", payload)
+
+ # Collect traces from ALL BugFinding*/ directories (one per failing test).
+ # We pick the best trace to analyze — preferring the one whose error
+ # category is most actionable.
+ all_traces: List[Tuple[Path, str]] = []
+ for bug_dir in bug_dirs:
+ for tf in bug_dir.glob("*_0_0.txt"):
+ try:
+ all_traces.append((tf, tf.read_text()))
+ except Exception:
+ pass
+
+ if not all_traces:
+ payload = {
+ "success": False,
+ "error": f"No trace files found in BugFinding directories. "
+ "The program may have passed all tests.",
+ }
+ return with_metadata("peasy-ai-fix-bug", payload)
+
+ # Rank traces by error category actionability so we fix the most
+ # impactful bug first. Order: unhandled_event > null_target >
+ # assertion_failure > deadlock > unknown.
+ import re as _trace_re
+ _PRIORITY = {"unhandled_event": 0, "null_target": 1, "assertion_failure": 2, "deadlock": 3}
+
+ def _trace_priority(content: str) -> int:
+ error_match = _trace_re.search(r'\s*(.+)', content)
+ if not error_match:
+ return 99
+ msg = error_match.group(1).lower()
+ if "cannot be handled" in msg or "unhandled" in msg:
+ return _PRIORITY["unhandled_event"]
+ if "null" in msg:
+ return _PRIORITY["null_target"]
+ if "assert" in msg:
+ return _PRIORITY["assertion_failure"]
+ if "deadlock" in msg:
+ return _PRIORITY["deadlock"]
+ return 99
+
+ all_traces.sort(key=lambda t: _trace_priority(t[1]))
+ latest_trace, trace_content = all_traces[0]
+
+ services = get_services()
+
+ try:
+ from core.compilation import (
+ PCheckerErrorParser,
+ PCheckerErrorFixer,
+ CheckerErrorCategory,
+ analyze_and_suggest_fix,
+ )
+
+ project_files = services["compilation"].get_project_files(str(project_path))
+
+ analysis, specialized_fix = analyze_and_suggest_fix(
+ trace_content, str(project_path), project_files
+ )
+
+ response = {
+ "success": True,
+ "trace_file": str(latest_trace),
+ "analysis": {
+ "error_category": analysis.error.category.value,
+ "error_message": analysis.error.message,
+ "machine": analysis.error.machine,
+ "machine_type": analysis.error.machine_type,
+ "state": analysis.error.machine_state,
+ "event": analysis.error.event_name,
+ "target_field": analysis.error.target_field,
+ "execution_steps": analysis.execution_steps,
+ "machines_involved": analysis.machines_involved,
+ "last_actions": analysis.last_actions,
+ },
+ "root_cause": analysis.error.root_cause,
+ "suggested_fixes": analysis.error.suggested_fixes,
+ "fixed": False,
+ "requires_manual_fix": False,
+ }
+
+ # Include enhanced analysis fields in response
+ if analysis.error.sender_info:
+ sender = analysis.error.sender_info
+ response["analysis"]["sender"] = {
+ "machine": sender.machine,
+ "state": sender.state,
+ "is_test_driver": sender.is_test_driver,
+ "is_initialization_pattern": sender.is_initialization_pattern,
+ "semantic_mismatch": sender.semantic_mismatch,
+ }
+ if analysis.error.cascading_impact:
+ cascade = analysis.error.cascading_impact
+ response["analysis"]["cascading_impact"] = {
+ "unhandled_in": cascade.unhandled_in,
+ "broadcasters": cascade.broadcasters,
+ "all_receivers": cascade.all_receivers,
+ }
+ response["analysis"]["is_test_driver_bug"] = analysis.error.is_test_driver_bug
+ response["analysis"]["requires_new_event"] = analysis.error.requires_new_event
+ response["analysis"]["requires_multi_file_fix"] = analysis.error.requires_multi_file_fix
+
+ if specialized_fix:
+ logger.info(f"Attempting specialized fix ({specialized_fix.fix_strategy or 'auto'}): {specialized_fix.description}")
+
+ try:
+ backups = {}
+
+ backups[specialized_fix.file_path] = specialized_fix.original_code
+ services["compilation"].write_file(
+ specialized_fix.file_path,
+ specialized_fix.fixed_code
+ )
+
+ if specialized_fix.is_multi_file and specialized_fix.additional_patches:
+ for patch in specialized_fix.additional_patches:
+ backups[patch.file_path] = patch.original_code
+ services["compilation"].write_file(
+ patch.file_path,
+ patch.fixed_code
+ )
+
+ response["fix_applied"] = {
+ "description": specialized_fix.description,
+ "file": specialized_fix.file_path,
+ "confidence": specialized_fix.confidence,
+ "requires_review": specialized_fix.requires_review,
+ "review_notes": specialized_fix.review_notes,
+ "strategy": specialized_fix.fix_strategy,
+ "is_multi_file": specialized_fix.is_multi_file,
+ "files_modified": list(backups.keys()),
+ }
+
+ compile_result = services["compilation"].compile(str(project_path))
+
+ if compile_result.success:
+ check_result = services["compilation"].run_checker(
+ str(project_path), schedules=50, timeout=60
+ )
+
+ if check_result.success:
+ response["fixed"] = True
+ response["verification"] = "Fix verified - PChecker passed 50 schedules"
+ else:
+ response["fixed"] = False
+ response["verification"] = "Fix applied but bug persists"
+ # Revert specialized fix, then try LLM fallback
+ for file_path, original_code in backups.items():
+ services["compilation"].write_file(file_path, original_code)
+ response["fix_applied"]["reverted"] = True
+ else:
+ for file_path, original_code in backups.items():
+ services["compilation"].write_file(file_path, original_code)
+ response["fix_applied"]["reverted"] = True
+ response["verification"] = f"Fix caused compilation error: {compile_result.stdout[:200]}"
+
+ except Exception as e:
+ import traceback
+ logger.error(f"Error applying fix: {type(e).__name__}: {e}\n{traceback.format_exc()}")
+ response["fix_error"] = sanitize_error(e, "fix_buggy_program")
+
+ # --- LLM fallback: if specialized fix didn't work, ask the LLM ---
+ if not response.get("fixed"):
+ llm_fix_ok = _try_llm_checker_fix(
+ services, str(project_path), project_files,
+ trace_content, analysis, response,
+ )
+ if not llm_fix_ok:
+ response["requires_manual_fix"] = True
+ response["manual_fix_guidance"] = _get_manual_fix_guidance(analysis)
+
+ return with_metadata("peasy-ai-fix-bug", response)
+
+ except ImportError as e:
+ logger.warning(f"Checker analysis modules not available: {e}")
+ payload = _basic_trace_analysis(trace_content, str(project_path), services)
+ return with_metadata("peasy-ai-fix-bug", payload)
+ except Exception as e:
+ import traceback
+ logger.error(f"Error in fix_buggy_program: {e}\n{traceback.format_exc()}")
+ payload = {
+ "success": False,
+ "error": sanitize_error(e, "fix_buggy_program"),
+ }
+ return with_metadata("peasy-ai-fix-bug", payload)
+
+ return {
+ "fix_compiler_error": fix_compiler_error,
+ "fix_checker_error": fix_checker_error,
+ "fix_iteratively": fix_iteratively,
+ "fix_buggy_program": fix_buggy_program,
+ }
diff --git a/Src/PeasyAI/src/ui/mcp/tools/generation.py b/Src/PeasyAI/src/ui/mcp/tools/generation.py
new file mode 100644
index 0000000000..83c5a1619e
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/generation.py
@@ -0,0 +1,658 @@
+"""Generation-related MCP tools."""
+
+from typing import Dict, Any, List, Optional
+from pathlib import Path
+from pydantic import BaseModel, Field
+import logging
+
+from core.security import (
+ validate_project_path,
+ validate_file_write_path,
+ PathSecurityError,
+ sanitize_error,
+ check_input_size,
+ MAX_DESIGN_DOC_BYTES,
+ MAX_CODE_BYTES,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def _build_generation_payload(
+ tool_name: str,
+ result,
+ review: Dict[str, Any],
+ code: Optional[str],
+ with_metadata,
+ *,
+ extra: Optional[Dict[str, Any]] = None,
+) -> Dict[str, Any]:
+ """Build a standardized MCP response payload for generation tools.
+
+ Surfaces validation severity (is_valid, error_count, warning_count) at
+ the top level so the calling agent can decide whether to save the file
+ or request regeneration.
+ """
+ errors: List[str] = review.get("errors", [])
+ warnings: List[str] = review.get("warnings", [])
+ is_valid: bool = review.get("is_valid", True)
+
+ if not result.success:
+ message = result.error or f"{tool_name} failed"
+ elif errors:
+ message = (
+ f"Code generated with {len(errors)} validation error(s) "
+ f"that will likely cause compilation failure. "
+ f"Review the 'review.errors' field and consider regenerating."
+ )
+ elif warnings:
+ message = (
+ "Code generated for preview with warnings. "
+ "Use peasy-ai-save-file to save to disk."
+ )
+ else:
+ message = (
+ "Code generated for preview. "
+ "Use peasy-ai-save-file to save to disk."
+ )
+
+ payload: Dict[str, Any] = {
+ "success": result.success,
+ "filename": result.filename,
+ "file_path": result.file_path,
+ "code": code,
+ "error": result.error,
+ "token_usage": result.token_usage,
+ "preview_only": True,
+ "is_valid": is_valid,
+ "error_count": len(errors),
+ "warning_count": len(warnings),
+ "review": review,
+ "message": message,
+ }
+ if extra:
+ payload.update(extra)
+ return with_metadata(tool_name, payload, token_usage=result.token_usage)
+
+
+def _find_project_root(file_path: str) -> Optional[str]:
+ """
+ Walk up from file_path to find the P project root (directory containing .pproj).
+ Returns the project root path or None.
+ """
+ current = Path(file_path).parent
+ for _ in range(10):
+ if any(current.glob("*.pproj")):
+ return str(current)
+ parent = current.parent
+ if parent == current:
+ break
+ current = parent
+ return None
+
+
+def _review_generated_code(
+ code: str,
+ filename: str,
+ project_path: str,
+ is_test_file: bool = False,
+ context_files: Optional[Dict[str, str]] = None,
+) -> Dict[str, Any]:
+ """
+ Run the unified validation pipeline on generated code.
+
+ Stage 1 — deterministic auto-fixes (PCodePostProcessor).
+ Stage 2 — structured validators (syntax, types, events, specs, duplicates, …).
+
+ Args:
+ context_files: Previously generated files (filename -> code) that
+ haven't been saved to disk yet. Merged with any on-disk project
+ files so cross-file validators (e.g. NamedTupleConstructionValidator)
+ can resolve type definitions from earlier generation steps.
+
+ Returns a dict with:
+ - code: the (possibly fixed) code
+ - fixes_applied: list of auto-fixes that were applied
+ - warnings: list of issues that need manual attention
+ - errors: list of issues that will likely cause compilation failure
+ - is_valid: whether the code passed all error-level checks
+ - validators_run: names of validators that ran
+ """
+ from core.validation.pipeline import ValidationPipeline
+
+ pipeline = ValidationPipeline(include_test_validators=is_test_file)
+
+ merged_context: Optional[Dict[str, str]] = None
+ if context_files:
+ merged_context = dict(context_files)
+
+ result = pipeline.validate(
+ code,
+ context=merged_context,
+ filename=filename,
+ project_path=project_path,
+ is_test_file=is_test_file,
+ )
+ return result.to_review_dict()
+
+
+def _run_doc_review(
+ services,
+ code: str,
+ design_doc: str,
+ context_files: Optional[Dict[str, str]],
+ review: Dict[str, Any],
+) -> tuple:
+ """Run the LLM documentation review and update the review dict.
+
+ Returns (code, doc_review_status) where doc_review_status is a dict
+ with 'status' and 'reason' fields surfaced in the MCP response.
+ """
+ try:
+ result = services["generation"].review_code_documentation(
+ code=code,
+ design_doc=design_doc,
+ context_files=context_files,
+ )
+ if result["status"] == "success":
+ code = result["code"]
+ review["fixes_applied"].append(
+ "[DocReview] LLM added documentation comments"
+ )
+ else:
+ logger.warning(
+ f"Documentation review did not succeed: "
+ f"status={result['status']}, reason={result.get('reason')}"
+ )
+ doc_status = {"status": result["status"], "reason": result.get("reason")}
+ except Exception as e:
+ reason = f"Unexpected error: {e}"
+ logger.warning(f"Documentation review skipped: {reason}")
+ doc_status = {"status": "error", "reason": reason}
+ return code, doc_status
+
+
+def _validate_generation_inputs(
+ tool_name: str,
+ with_metadata,
+ project_path: Optional[str] = None,
+ design_doc: Optional[str] = None,
+) -> Optional[Dict[str, Any]]:
+ """Validate common generation tool inputs. Returns an error payload or None."""
+ try:
+ if project_path:
+ validate_project_path(project_path)
+ if design_doc:
+ check_input_size(design_doc, "design_doc", MAX_DESIGN_DOC_BYTES)
+ except (PathSecurityError, ValueError) as e:
+ return with_metadata(tool_name, {"success": False, "error": str(e)})
+ return None
+
+
+class GenerateProjectParams(BaseModel):
+ """Parameters for project structure creation (STEP 1)"""
+ design_doc: str = Field(
+ ...,
+ description="The design document content describing the P program in markdown format. "
+ "Should include headings: # Title, ## Introduction, ## Components, ## Interactions."
+ )
+ output_dir: str = Field(
+ ...,
+ description="Absolute path to the directory where the project should be created"
+ )
+ project_name: str = Field(
+ default="PProject",
+ description="Name for the P project"
+ )
+
+
+class GenerateTypesEventsParams(BaseModel):
+ """Parameters for types/events generation"""
+ design_doc: str = Field(..., description="The design document content", max_length=500_000)
+ project_path: str = Field(..., description="Absolute path to the P project root")
+ context_files: Optional[Dict[str, str]] = Field(
+ default=None,
+ description="Additional context files (filename -> content)"
+ )
+
+
+class GenerateMachineParams(BaseModel):
+ """Parameters for machine generation"""
+ machine_name: str = Field(..., description="Name of the machine to generate")
+ design_doc: str = Field(..., description="The design document content", max_length=500_000)
+ project_path: str = Field(..., description="Absolute path to the P project root")
+ context_files: Optional[Dict[str, str]] = Field(
+ default=None,
+ description="Additional context files (filename -> content)"
+ )
+ ensemble_size: int = Field(
+ default=3,
+ description="Number of candidate generations for ensemble selection (best-of-N). "
+ "Set to 1 to disable ensemble."
+ )
+
+
+class GenerateSpecParams(BaseModel):
+ """Parameters for specification generation"""
+ spec_name: str = Field(..., description="Name of the specification file to generate")
+ design_doc: str = Field(..., description="The design document content", max_length=500_000)
+ project_path: str = Field(..., description="Absolute path to the P project root")
+ context_files: Optional[Dict[str, str]] = Field(
+ default=None,
+ description="Additional context files"
+ )
+ ensemble_size: int = Field(
+ default=3,
+ description="Number of candidate generations for ensemble selection (best-of-N). "
+ "Set to 1 to disable ensemble."
+ )
+ checker_feedback: Optional[str] = Field(
+ default=None,
+ description="PChecker bug report from a previous failing run. "
+ "If provided, injected as context so the LLM avoids the same bug."
+ )
+
+
+class GenerateTestParams(BaseModel):
+ """Parameters for test generation"""
+ test_name: str = Field(..., description="Name of the test file to generate")
+ design_doc: str = Field(..., description="The design document content", max_length=500_000)
+ project_path: str = Field(..., description="Absolute path to the P project root")
+ context_files: Optional[Dict[str, str]] = Field(
+ default=None,
+ description="Additional context files"
+ )
+ ensemble_size: int = Field(
+ default=3,
+ description="Number of candidate generations for ensemble selection (best-of-N). "
+ "Set to 1 to disable ensemble."
+ )
+ checker_feedback: Optional[str] = Field(
+ default=None,
+ description="PChecker bug report from a previous failing run. "
+ "If provided, injected as context so the LLM avoids the same bug."
+ )
+
+
+class SavePFileParams(BaseModel):
+ """Parameters for saving a P file"""
+ file_path: str = Field(..., description="Absolute path where to save the file")
+ code: str = Field(..., description="The P code content to save", max_length=200_000)
+
+
+def register_generation_tools(mcp, get_services, with_metadata):
+ """Register generation tools."""
+
+ @mcp.tool(
+ name="peasy-ai-create-project",
+ description="STEP 1 of the recommended step-by-step workflow. Creates a P project skeleton with PSrc, PSpec, PTst folders and .pproj file. After this, use peasy-ai-gen-types-events to define types and events."
+ )
+ def generate_project_structure(params: GenerateProjectParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-create-project: {params.project_name}")
+
+ services = get_services()
+ result = services["generation"].create_project_structure(
+ output_dir=params.output_dir,
+ project_name=params.project_name
+ )
+
+ payload = {
+ "success": result.success,
+ "project_path": result.file_path,
+ "project_name": result.filename,
+ "error": result.error,
+ "message": f"Created P project at {result.file_path}" if result.success else result.error
+ }
+ return with_metadata("peasy-ai-create-project", payload, token_usage=result.token_usage)
+
+ @mcp.tool(
+ name="peasy-ai-gen-types-events",
+ description="STEP 2 of the recommended step-by-step workflow. Generates the types, enums, and events file (Enums_Types_Events.p) from the design document. Returns code for preview so the user can review it before saving with peasy-ai-save-file. Run this after peasy-ai-create-project."
+ )
+ def generate_types_events(params: GenerateTypesEventsParams) -> Dict[str, Any]:
+ logger.info("[TOOL] peasy-ai-gen-types-events (preview)")
+
+ err = _validate_generation_inputs(
+ "peasy-ai-gen-types-events", with_metadata,
+ project_path=params.project_path, design_doc=params.design_doc,
+ )
+ if err:
+ return err
+
+ services = get_services()
+ result = services["generation"].generate_types_events(
+ design_doc=params.design_doc,
+ project_path=params.project_path,
+ save_to_disk=False # Preview only
+ )
+
+ review: Dict[str, Any] = {"fixes_applied": [], "warnings": [], "errors": [], "is_valid": True}
+ code = result.code
+ doc_review_status: Optional[Dict[str, Any]] = None
+ if result.success and code:
+ review = _review_generated_code(
+ code, result.filename or "Enums_Types_Events.p", params.project_path,
+ )
+ code = review["code"]
+
+ code, doc_review_status = _run_doc_review(
+ services, code, params.design_doc, params.context_files, review,
+ )
+
+ return _build_generation_payload(
+ "peasy-ai-gen-types-events", result, review, code, with_metadata,
+ extra={"doc_review_status": doc_review_status} if doc_review_status else None,
+ )
+
+ @mcp.tool(
+ name="peasy-ai-gen-machine",
+ description="STEP 3 of the recommended step-by-step workflow. Generates a single P state machine implementation using two-stage generation (structure first, then implementation). Call once per machine in the design. Returns code for preview so the user can review it before saving with peasy-ai-save-file. Pass previously generated files as context_files for cross-file consistency."
+ )
+ def generate_machine(params: GenerateMachineParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-gen-machine: {params.machine_name} (preview, ensemble={params.ensemble_size})")
+
+ err = _validate_generation_inputs(
+ "peasy-ai-gen-machine", with_metadata,
+ project_path=params.project_path, design_doc=params.design_doc,
+ )
+ if err:
+ return err
+
+ services = get_services()
+ if params.ensemble_size > 1:
+ result = services["generation"].generate_machine_ensemble(
+ machine_name=params.machine_name,
+ design_doc=params.design_doc,
+ project_path=params.project_path,
+ context_files=params.context_files,
+ ensemble_size=params.ensemble_size,
+ save_to_disk=False # Preview only
+ )
+ else:
+ result = services["generation"].generate_machine(
+ machine_name=params.machine_name,
+ design_doc=params.design_doc,
+ project_path=params.project_path,
+ context_files=params.context_files,
+ two_stage=True,
+ save_to_disk=False # Preview only
+ )
+
+ review: Dict[str, Any] = {"fixes_applied": [], "warnings": [], "errors": [], "is_valid": True}
+ code = result.code
+ doc_review_status: Optional[Dict[str, Any]] = None
+ if result.success and code:
+ review = _review_generated_code(
+ code, result.filename or f"{params.machine_name}.p", params.project_path,
+ context_files=params.context_files,
+ )
+ code = review["code"]
+
+ code, doc_review_status = _run_doc_review(
+ services, code, params.design_doc, params.context_files, review,
+ )
+
+ return _build_generation_payload(
+ "peasy-ai-gen-machine", result, review, code, with_metadata,
+ extra={"doc_review_status": doc_review_status} if doc_review_status else None,
+ )
+
+ @mcp.tool(
+ name="peasy-ai-gen-spec",
+ description="STEP 4 of the recommended step-by-step workflow. Generates a P safety specification/monitor file. Returns code for preview so the user can review it before saving with peasy-ai-save-file. Run this after all machines have been generated, passing them as context_files."
+ )
+ def generate_spec(params: GenerateSpecParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-gen-spec: {params.spec_name} (preview, ensemble={params.ensemble_size})")
+
+ err = _validate_generation_inputs(
+ "peasy-ai-gen-spec", with_metadata,
+ project_path=params.project_path, design_doc=params.design_doc,
+ )
+ if err:
+ return err
+
+ ctx = dict(params.context_files) if params.context_files else {}
+ if params.checker_feedback:
+ ctx["__checker_bug_report__"] = params.checker_feedback
+
+ services = get_services()
+ if params.ensemble_size > 1:
+ result = services["generation"].generate_spec_ensemble(
+ spec_name=params.spec_name,
+ design_doc=params.design_doc,
+ project_path=params.project_path,
+ context_files=ctx or None,
+ ensemble_size=params.ensemble_size,
+ save_to_disk=False
+ )
+ else:
+ result = services["generation"].generate_spec(
+ spec_name=params.spec_name,
+ design_doc=params.design_doc,
+ project_path=params.project_path,
+ context_files=ctx or None,
+ save_to_disk=False
+ )
+
+ review: Dict[str, Any] = {"fixes_applied": [], "warnings": [], "errors": [], "is_valid": True}
+ code = result.code
+ spec_fixes: Dict[str, str] = {}
+ doc_review_status: Optional[Dict[str, Any]] = None
+ if result.success and code:
+ # Stage A: regex/structural validation
+ review = _review_generated_code(
+ code, result.filename or f"{params.spec_name}.p", params.project_path,
+ context_files=params.context_files,
+ )
+ code = review["code"]
+
+ # Stage B: LLM-based spec correctness review (observes
+ # completeness, assertion logic, payload usage).
+ try:
+ spec_fixes = services["generation"].review_spec_correctness(
+ spec_code=code,
+ design_doc=params.design_doc,
+ context_files=params.context_files,
+ )
+ spec_filename = result.filename or f"{params.spec_name}.p"
+ for candidate in [spec_filename, "Specification.p", "Spec.p", "Safety.p"]:
+ if candidate in spec_fixes:
+ code = spec_fixes.pop(candidate)
+ review["fixes_applied"].append(
+ "[SpecReview] LLM corrected spec monitor logic"
+ )
+ break
+ except Exception as e:
+ logger.warning(f"Spec review skipped: {e}")
+
+ # Stage C: LLM-based documentation comments
+ code, doc_review_status = _run_doc_review(
+ services, code, params.design_doc, params.context_files, review,
+ )
+
+ extra: Dict[str, Any] = {}
+ if spec_fixes:
+ extra["spec_fixes"] = spec_fixes
+ if doc_review_status:
+ extra["doc_review_status"] = doc_review_status
+
+ return _build_generation_payload(
+ "peasy-ai-gen-spec", result, review, code, with_metadata,
+ extra=extra,
+ )
+
+ @mcp.tool(
+ name="peasy-ai-gen-test",
+ description="STEP 5 of the recommended step-by-step workflow. Generates a P test driver file. Returns code for preview so the user can review it before saving with peasy-ai-save-file. Run this after all machines and specs have been generated, passing them as context_files. After saving, use peasy-ai-compile to compile and peasy-ai-check to verify."
+ )
+ def generate_test(params: GenerateTestParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-gen-test: {params.test_name} (preview, ensemble={params.ensemble_size})")
+
+ err = _validate_generation_inputs(
+ "peasy-ai-gen-test", with_metadata,
+ project_path=params.project_path, design_doc=params.design_doc,
+ )
+ if err:
+ return err
+
+ ctx = dict(params.context_files) if params.context_files else {}
+ if params.checker_feedback:
+ ctx["__checker_bug_report__"] = params.checker_feedback
+
+ services = get_services()
+ if params.ensemble_size > 1:
+ result = services["generation"].generate_test_ensemble(
+ test_name=params.test_name,
+ design_doc=params.design_doc,
+ project_path=params.project_path,
+ context_files=ctx or None,
+ ensemble_size=params.ensemble_size,
+ save_to_disk=False
+ )
+ else:
+ result = services["generation"].generate_test(
+ test_name=params.test_name,
+ design_doc=params.design_doc,
+ project_path=params.project_path,
+ context_files=ctx or None,
+ save_to_disk=False
+ )
+
+ review: Dict[str, Any] = {"fixes_applied": [], "warnings": [], "errors": [], "is_valid": True}
+ code = result.code
+ wiring_fixes: Dict[str, str] = {}
+ doc_review_status: Optional[Dict[str, Any]] = None
+ if result.success and code:
+ # Stage A: regex/structural validation
+ review = _review_generated_code(
+ code,
+ result.filename or f"{params.test_name}.p",
+ params.project_path,
+ is_test_file=True,
+ context_files=params.context_files,
+ )
+ code = review["code"]
+
+ # Stage B: LLM-based wiring review (initialization order,
+ # circular dependencies, empty collections).
+ try:
+ wiring_fixes = services["generation"].review_test_wiring(
+ test_code=code,
+ design_doc=params.design_doc,
+ context_files=params.context_files,
+ )
+ test_filename = result.filename or f"{params.test_name}.p"
+ if test_filename in wiring_fixes:
+ code = wiring_fixes.pop(test_filename)
+ review["fixes_applied"].append(
+ "[WiringReview] LLM rewired machine initialization"
+ )
+ elif "TestDriver.p" in wiring_fixes:
+ code = wiring_fixes.pop("TestDriver.p")
+ review["fixes_applied"].append(
+ "[WiringReview] LLM rewired machine initialization"
+ )
+ except Exception as e:
+ logger.warning(f"Wiring review skipped: {e}")
+
+ # Stage C: LLM-based documentation comments
+ code, doc_review_status = _run_doc_review(
+ services, code, params.design_doc, params.context_files, review,
+ )
+
+ extra: Dict[str, Any] = {}
+ if wiring_fixes:
+ extra["wiring_fixes"] = wiring_fixes
+ if doc_review_status:
+ extra["doc_review_status"] = doc_review_status
+
+ return _build_generation_payload(
+ "peasy-ai-gen-test", result, review, code, with_metadata,
+ extra=extra,
+ )
+
+ @mcp.tool(
+ name="peasy-ai-save-file",
+ description="Save generated P code to a file on disk and run a proactive compilation check. "
+ "In the step-by-step workflow, call this after the user reviews and approves the code "
+ "returned by peasy-ai-gen-types-events, peasy-ai-gen-machine, peasy-ai-gen-spec, or peasy-ai-gen-test. "
+ "Provide the absolute file_path (from the generate tool's response) and the code content. "
+ "The response includes a 'compilation_check' field with any syntax errors found in this file."
+ )
+ def save_p_file(params: SavePFileParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-save-file: {params.file_path}")
+
+ project_root = _find_project_root(params.file_path)
+ if project_root:
+ try:
+ validate_file_write_path(params.file_path, project_root)
+ except PathSecurityError as e:
+ return with_metadata("peasy-ai-save-file", {
+ "success": False, "error": str(e),
+ })
+ else:
+ resolved = Path(params.file_path).resolve()
+ if resolved.suffix not in {".p", ".pproj"}:
+ return with_metadata("peasy-ai-save-file", {
+ "success": False,
+ "error": f"Only .p files can be saved; got '{resolved.suffix}'",
+ })
+
+ try:
+ check_input_size(params.code, "code", MAX_CODE_BYTES)
+ except ValueError as e:
+ return with_metadata("peasy-ai-save-file", {
+ "success": False, "error": str(e),
+ })
+
+ services = get_services()
+ result = services["generation"].save_p_file(
+ file_path=params.file_path,
+ code=params.code
+ )
+
+ compilation_check = None
+ if result.success:
+ try:
+ project_path = _find_project_root(params.file_path)
+ if project_path:
+ compile_result = services["compilation"].compile(project_path)
+ if compile_result.success:
+ compilation_check = {"success": True, "errors": []}
+ else:
+ raw_output = compile_result.stdout or compile_result.stderr or ""
+ # Extract errors relevant to this file
+ file_basename = Path(params.file_path).name
+ file_errors = []
+ all_errors = []
+ for line in raw_output.splitlines():
+ if "error" in line.lower() or "parse error" in line.lower():
+ all_errors.append(line.strip())
+ if file_basename in line:
+ file_errors.append(line.strip())
+ compilation_check = {
+ "success": False,
+ "file_errors": file_errors,
+ "all_errors": all_errors[:10],
+ }
+ except Exception as e:
+ logger.debug(f"Proactive compilation check skipped: {e}")
+
+ payload = {
+ "success": result.success,
+ "filename": result.filename,
+ "file_path": result.file_path,
+ "error": result.error,
+ "compilation_check": compilation_check,
+ "message": f"Saved {result.filename} to disk" if result.success else result.error
+ }
+ return with_metadata("peasy-ai-save-file", payload, token_usage=result.token_usage)
+
+ return {
+ "generate_project_structure": generate_project_structure,
+ "generate_types_events": generate_types_events,
+ "generate_machine": generate_machine,
+ "generate_spec": generate_spec,
+ "generate_test": generate_test,
+ "save_p_file": save_p_file,
+ }
diff --git a/Src/PeasyAI/src/ui/mcp/tools/query.py b/Src/PeasyAI/src/ui/mcp/tools/query.py
new file mode 100644
index 0000000000..1a75f2e92c
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/query.py
@@ -0,0 +1,79 @@
+"""Query tools for MCP."""
+
+from typing import Dict, Any
+from pydantic import BaseModel, Field
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class SyntaxHelperParams(BaseModel):
+ """Parameters for syntax help"""
+ topic: str = Field(
+ ...,
+ description="The P language topic to get help on (e.g., 'state machines', 'events', 'types', 'send', 'goto')"
+ )
+
+
+def register_query_tools(mcp, get_services, with_metadata):
+ """Register query tools."""
+
+ @mcp.tool(
+ name="peasy-ai-syntax-help",
+ description="Get syntax help and examples for P language constructs. Provide a topic like 'state machines', 'events', 'types', 'enums', 'statements', 'specs', 'monitors', 'tests', 'modules', 'send', 'goto', 'raise', 'compiler', or 'errors'. Returns relevant documentation from the P language guides."
+ )
+ def syntax_help(params: SyntaxHelperParams) -> Dict[str, Any]:
+ logger.info(f"[TOOL] peasy-ai-syntax-help: {params.topic}")
+
+ services = get_services()
+ resources = services["resources"]
+
+ topic_lower = params.topic.lower()
+
+ topic_files = {
+ "machine": "modular/p_machines_guide.txt",
+ "state": "modular/p_machines_guide.txt",
+ "event": "modular/p_events_guide.txt",
+ "type": "modular/p_types_guide.txt",
+ "enum": "modular/p_enums_guide.txt",
+ "statement": "modular/p_statements_guide.txt",
+ "spec": "modular/p_spec_monitors_guide.txt",
+ "monitor": "modular/p_spec_monitors_guide.txt",
+ "test": "modular/p_test_cases_guide.txt",
+ "module": "modular/p_module_system_guide.txt",
+ "syntax": "P_syntax_guide.txt",
+ "basic": "modular/p_basics.txt",
+ "example": "modular/p_program_example.txt",
+ "compiler": "modular/p_compiler_guide.txt",
+ "error": "modular/p_common_compilation_errors.txt",
+ "send": "modular/p_statements_guide.txt",
+ "goto": "modular/p_machines_guide.txt",
+ "raise": "modular/p_statements_guide.txt",
+ }
+
+ matching_files = []
+ for keyword, filepath in topic_files.items():
+ if keyword in topic_lower and filepath not in matching_files:
+ matching_files.append(filepath)
+
+ if not matching_files:
+ matching_files = ["P_syntax_guide.txt", "modular/p_basics.txt"]
+
+ content_parts = []
+ for filepath in matching_files[:3]:
+ try:
+ content = resources.load(f"context_files/{filepath}")
+ content_parts.append(f"=== {filepath} ===\n{content}")
+ except Exception as e:
+ logger.warning(f"Could not load {filepath}: {e}")
+
+ payload = {
+ "topic": params.topic,
+ "content": "\n\n".join(content_parts),
+ "files_referenced": matching_files[:3]
+ }
+ return with_metadata("peasy-ai-syntax-help", payload)
+
+ return {
+ "syntax_help": syntax_help,
+ }
diff --git a/Src/PeasyAI/src/ui/mcp/tools/rag_tools.py b/Src/PeasyAI/src/ui/mcp/tools/rag_tools.py
new file mode 100644
index 0000000000..5a9255cafb
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/rag_tools.py
@@ -0,0 +1,240 @@
+"""RAG tools for MCP."""
+
+from typing import Dict, Any, Optional
+from pydantic import BaseModel, Field
+from pathlib import Path
+import logging
+
+from core.security import PathSecurityError, validate_project_path
+
+logger = logging.getLogger(__name__)
+
+try:
+ from core.rag import RAGService, get_rag_service, PExample
+ HAS_RAG = True
+except ImportError:
+ HAS_RAG = False
+ logger.warning("RAG module not available")
+
+
+class SearchExamplesParams(BaseModel):
+ """Parameters for searching P examples"""
+ query: str = Field(..., description="Natural language query or code snippet to search for")
+ category: Optional[str] = Field(
+ default=None,
+ description="Filter by category: 'machine', 'spec', 'test', 'types', 'documentation', 'full_project'"
+ )
+ top_k: int = Field(default=5, description="Number of results to return")
+
+
+class GetContextParams(BaseModel):
+ """Parameters for getting generation context"""
+ context_type: str = Field(
+ ...,
+ description="Type of context: 'machine', 'spec', 'test', 'types'"
+ )
+ description: str = Field(..., description="Description of what you're generating")
+ design_doc: Optional[str] = Field(
+ default=None,
+ description="Optional design document for additional context"
+ )
+ num_examples: int = Field(default=3, description="Number of examples to include")
+
+
+class IndexPFilesParams(BaseModel):
+ """Parameters for indexing P files"""
+ path: str = Field(..., description="Path to a P file or directory to index")
+ project_name: Optional[str] = Field(
+ default=None,
+ description="Optional project name for the indexed files"
+ )
+
+
+
+
+def register_rag_tools(mcp, with_metadata):
+ """Register RAG tools."""
+
+ @mcp.tool(
+ name="peasy-ai-search-examples",
+ description="""Search the P program database for similar examples.
+
+Use this to find real P code examples that match your needs:
+- Search by concept: "distributed lock protocol"
+- Search by pattern: "state machine with deferred events"
+- Search by protocol: "paxos", "two-phase commit", "raft", "failure detector"
+- Search by code: paste a code snippet to find similar code
+
+Filter results by category: 'machine', 'spec', 'test', 'types', 'documentation', 'full_project'.
+Returns relevant P code examples with descriptions, similarity scores, and corpus size."""
+ )
+ def search_p_examples(params: SearchExamplesParams) -> Dict[str, Any]:
+ if not HAS_RAG:
+ payload = {
+ "success": False,
+ "error": "RAG module not available. Install sentence-transformers for full functionality."
+ }
+ return with_metadata("peasy-ai-search-examples", payload)
+
+ logger.info(f"[TOOL] peasy-ai-search-examples: {params.query[:50]}...")
+
+ try:
+ rag = get_rag_service()
+ results = rag.search(
+ query=params.query,
+ top_k=params.top_k
+ )
+
+ if params.category:
+ results = [r for r in results if r.document.metadata.get("category") == params.category]
+
+ examples = []
+ for result in results:
+ examples.append({
+ "name": result.document.metadata.get("name"),
+ "description": result.document.metadata.get("description"),
+ "category": result.document.metadata.get("category"),
+ "code": result.document.metadata.get("code"),
+ "project": result.document.metadata.get("project_name"),
+ "source_file": result.document.metadata.get("source_file"),
+ "relevance_score": round(result.score, 3)
+ })
+
+ payload = {
+ "success": True,
+ "query": params.query,
+ "results": examples,
+ "total_found": len(examples),
+ "corpus_size": rag.get_stats()["total_examples"]
+ }
+ return with_metadata("peasy-ai-search-examples", payload)
+ except Exception as e:
+ logger.error(f"Search error: {e}")
+ payload = {"success": False, "error": f"Search failed: {type(e).__name__}"}
+ return with_metadata("peasy-ai-search-examples", payload)
+
+ @mcp.tool(
+ name="peasy-ai-get-context",
+ description="""Get contextual examples and hints for P code generation.
+
+Before generating P code, call this to get relevant examples that will improve generation quality:
+- For machines: get similar state machine implementations
+- For specs: get similar safety/liveness specifications
+- For tests: get similar test drivers
+- For types: get similar type/event definitions
+
+Returns examples with code and syntax hints to use in your prompt."""
+ )
+ def get_generation_context(params: GetContextParams) -> Dict[str, Any]:
+ if not HAS_RAG:
+ payload = {
+ "success": False,
+ "error": "RAG module not available. Install sentence-transformers for full functionality."
+ }
+ return with_metadata("peasy-ai-get-context", payload)
+
+ logger.info(f"[TOOL] peasy-ai-get-context: {params.context_type}")
+
+ try:
+ rag = get_rag_service()
+
+ if params.context_type == "machine":
+ context = rag.get_machine_context(
+ params.description,
+ design_doc=params.design_doc,
+ num_examples=params.num_examples
+ )
+ elif params.context_type == "spec":
+ context = rag.get_spec_context(
+ params.description,
+ num_examples=params.num_examples
+ )
+ elif params.context_type == "test":
+ context = rag.get_test_context(
+ params.description,
+ num_examples=params.num_examples
+ )
+ elif params.context_type == "types":
+ context = rag.get_types_context(
+ params.description,
+ num_examples=params.num_examples
+ )
+ else:
+ payload = {
+ "success": False,
+ "error": f"Unknown context type: {params.context_type}. Use: machine, spec, test, types"
+ }
+ return with_metadata("peasy-ai-get-context", payload)
+
+ payload = {
+ "success": True,
+ "context_type": params.context_type,
+ "examples": context.examples,
+ "syntax_hints": context.syntax_hints,
+ "documentation": context.documentation,
+ "prompt_section": context.to_prompt_section()
+ }
+ return with_metadata("peasy-ai-get-context", payload)
+ except Exception as e:
+ logger.error(f"Context error: {e}")
+ payload = {"success": False, "error": f"Context lookup failed: {type(e).__name__}"}
+ return with_metadata("peasy-ai-get-context", payload)
+
+ @mcp.tool(
+ name="peasy-ai-index-examples",
+ description="""Index P files into the examples database.
+
+Use this to add your own P programs to the searchable corpus:
+- Index a single P file
+- Index an entire directory of P files
+- Index the official P tutorial examples
+
+Indexed examples can then be found via peasy-ai-search-examples."""
+ )
+ def index_p_examples(params: IndexPFilesParams) -> Dict[str, Any]:
+ if not HAS_RAG:
+ payload = {
+ "success": False,
+ "error": "RAG module not available."
+ }
+ return with_metadata("peasy-ai-index-examples", payload)
+
+ logger.info(f"[TOOL] peasy-ai-index-examples: {params.path}")
+
+ try:
+ rag = get_rag_service()
+ path = Path(params.path).resolve()
+
+ # Validate path to prevent path traversal attacks
+ try:
+ validate_project_path(str(path))
+ except PathSecurityError as e:
+ payload = {"success": False, "error": str(e)}
+ return with_metadata("peasy-ai-index-examples", payload)
+
+ if not path.exists():
+ payload = {"success": False, "error": f"Path not found: {path.name}"}
+ return with_metadata("peasy-ai-index-examples", payload)
+
+ if path.is_file():
+ count = rag.index_file(str(path))
+ else:
+ count = rag.index_directory(str(path))
+
+ payload = {
+ "success": True,
+ "indexed_count": count,
+ "total_in_corpus": rag.get_stats()["total_examples"],
+ "message": f"Indexed {count} examples from {params.path}"
+ }
+ return with_metadata("peasy-ai-index-examples", payload)
+ except Exception as e:
+ logger.error(f"Index error: {e}")
+ payload = {"success": False, "error": f"Indexing failed: {type(e).__name__}"}
+ return with_metadata("peasy-ai-index-examples", payload)
+
+ return {
+ "search_p_examples": search_p_examples,
+ "get_generation_context": get_generation_context,
+ "index_p_examples": index_p_examples,
+ }
diff --git a/Src/PeasyAI/src/ui/mcp/tools/workflows.py b/Src/PeasyAI/src/ui/mcp/tools/workflows.py
new file mode 100644
index 0000000000..50c826ec73
--- /dev/null
+++ b/Src/PeasyAI/src/ui/mcp/tools/workflows.py
@@ -0,0 +1,262 @@
+"""Workflow tools for MCP."""
+
+from typing import Dict, Any, Optional, List
+from pydantic import BaseModel, Field
+import logging
+import os
+
+from core.security import (
+ validate_project_path,
+ PathSecurityError,
+ check_input_size,
+ MAX_DESIGN_DOC_BYTES,
+)
+
+logger = logging.getLogger(__name__)
+
+from core.workflow import (
+ WorkflowEngine,
+ WorkflowFactory,
+ EventEmitter,
+ WorkflowEvent,
+ LoggingEventListener,
+ extract_machine_names_from_design_doc,
+)
+
+_workflow_engine: Optional[WorkflowEngine] = None
+_workflow_factory: Optional[WorkflowFactory] = None
+
+
+class RunWorkflowParams(BaseModel):
+ """Parameters for running a workflow"""
+ workflow_name: str = Field(description="Name of the workflow to run: 'full_generation', 'compile_and_fix', 'full_verification', 'quick_check'")
+ project_path: str = Field(description="Absolute path to the P project directory")
+ design_doc: Optional[str] = Field(default=None, description="Design document (required for generation workflows)")
+ machine_names: Optional[List[str]] = Field(default=None, description="List of machine names (auto-extracted from design_doc if not provided)")
+ schedules: int = Field(default=100, description="Number of schedules for PChecker")
+ timeout: int = Field(default=60, description="Timeout in seconds for PChecker")
+ ensemble_size: int = Field(default=3, description="Number of candidates per file for ensemble selection")
+
+
+class ResumeWorkflowParams(BaseModel):
+ """Parameters for resuming a paused workflow"""
+ workflow_id: str = Field(description="ID of the paused workflow")
+ user_guidance: str = Field(description="User guidance to continue the workflow")
+
+
+class ListWorkflowsParams(BaseModel):
+ """Parameters for listing workflows"""
+ pass
+
+
+def _get_workflow_engine(get_services) -> tuple[WorkflowEngine, WorkflowFactory]:
+ global _workflow_engine, _workflow_factory
+
+ if _workflow_engine is None:
+ services = get_services()
+
+ emitter = EventEmitter()
+ emitter.on_all(LoggingEventListener(verbose=True))
+
+ state_store_path = os.environ.get("PCHATBOT_WORKFLOW_STATE_FILE", ".peasyai_workflows.json")
+ _workflow_engine = WorkflowEngine(emitter, state_store_path=state_store_path)
+ _workflow_factory = WorkflowFactory(
+ generation_service=services["generation"],
+ compilation_service=services["compilation"],
+ fixer_service=services["fixer"]
+ )
+
+ _workflow_engine.register_workflow(
+ _workflow_factory.create_compile_and_fix_workflow()
+ )
+ _workflow_engine.register_workflow(
+ _workflow_factory.create_full_verification_workflow()
+ )
+ _workflow_engine.register_workflow(
+ _workflow_factory.create_quick_check_workflow()
+ )
+
+ logger.info("Workflow engine initialized")
+
+ return _workflow_engine, _workflow_factory
+
+
+def register_workflow_tools(mcp, get_services, with_metadata):
+ """Register workflow tools."""
+
+ @mcp.tool(
+ name="peasy-ai-run-workflow",
+ description="""Execute a predefined multi-step workflow. Available workflows:
+- compile_and_fix: Compile the project and automatically fix errors (requires project_path).
+- full_verification: Compile, fix compilation errors, run PChecker, and automatically fix any PChecker bugs found (requires project_path). IMPORTANT: Always use this workflow after generating P code to verify and fix correctness end-to-end. Do NOT manually edit files to fix PChecker errors — this workflow handles it automatically by reading trace files and applying AI-driven fixes.
+- quick_check: Run PChecker only on an already-compiled project (requires project_path).
+- full_generation: Generate a complete P project from a design doc (requires design_doc and project_path). NOTE: Prefer the step-by-step generation tools instead for better quality and user control. Only use full_generation when the user explicitly requests hands-off automated generation."""
+ )
+ def run_workflow(params: RunWorkflowParams) -> Dict[str, Any]:
+ try:
+ validate_project_path(params.project_path)
+ if params.design_doc:
+ check_input_size(params.design_doc, "design_doc", MAX_DESIGN_DOC_BYTES)
+ except (PathSecurityError, ValueError) as e:
+ return with_metadata("peasy-ai-run-workflow", {
+ "success": False, "error": str(e),
+ })
+
+ engine, factory = _get_workflow_engine(get_services)
+
+ context = {
+ "project_path": params.project_path,
+ }
+
+ if params.design_doc:
+ context["design_doc"] = params.design_doc
+
+ if params.workflow_name == "full_generation":
+ if not params.design_doc:
+ payload = {
+ "success": False,
+ "error": "design_doc is required for full_generation workflow"
+ }
+ return with_metadata("peasy-ai-run-workflow", payload)
+
+ machine_names = params.machine_names
+ if not machine_names:
+ machine_names = extract_machine_names_from_design_doc(params.design_doc)
+ if not machine_names:
+ payload = {
+ "success": False,
+ "error": "Could not extract machine names from design_doc. Please provide machine_names explicitly."
+ }
+ return with_metadata("peasy-ai-run-workflow", payload)
+
+ workflow = factory.create_full_generation_workflow(
+ machine_names=machine_names,
+ ensemble_size=params.ensemble_size,
+ )
+ engine.register_workflow(workflow)
+
+ try:
+ result = engine.execute(params.workflow_name, context)
+
+ if result.get("needs_guidance"):
+ payload = {
+ "success": False,
+ "paused": True,
+ "workflow_id": result.get("_workflow_id"),
+ "guidance_needed": result.get("guidance_context"),
+ "message": "Workflow paused - human guidance needed"
+ }
+ return with_metadata("peasy-ai-run-workflow", payload)
+
+ payload = {
+ "success": result.get("success", False),
+ "completed_steps": result.get("completed_steps", []),
+ "skipped_steps": result.get("skipped_steps", []),
+ "errors": result.get("errors", [])
+ }
+ return with_metadata("peasy-ai-run-workflow", payload)
+
+ except Exception as e:
+ logger.error(f"Workflow execution failed: {e}")
+ payload = {
+ "success": False,
+ "error": f"Workflow failed: {type(e).__name__}",
+ }
+ return with_metadata("peasy-ai-run-workflow", payload)
+
+ @mcp.tool(
+ name="peasy-ai-resume-workflow",
+ description="Resume a paused workflow with user guidance. Call this after peasy-ai-run-workflow returns paused=true and needs_guidance. Provide the workflow_id from the paused response and the user's guidance text."
+ )
+ def resume_workflow(params: ResumeWorkflowParams) -> Dict[str, Any]:
+ engine, _ = _get_workflow_engine(get_services)
+
+ try:
+ result = engine.resume(params.workflow_id, params.user_guidance)
+
+ if result.get("needs_guidance"):
+ payload = {
+ "success": False,
+ "paused": True,
+ "workflow_id": result.get("_workflow_id"),
+ "guidance_needed": result.get("guidance_context"),
+ "message": "Workflow still needs guidance"
+ }
+ return with_metadata("peasy-ai-resume-workflow", payload)
+
+ payload = {
+ "success": result.get("success", False),
+ "completed_steps": result.get("completed_steps", []),
+ "errors": result.get("errors", [])
+ }
+ return with_metadata("peasy-ai-resume-workflow", payload)
+
+ except ValueError as e:
+ payload = {
+ "success": False,
+ "error": str(e)
+ }
+ return with_metadata("peasy-ai-resume-workflow", payload)
+ except Exception as e:
+ logger.error(f"Workflow resume failed: {e}")
+ payload = {
+ "success": False,
+ "error": f"Workflow resume failed: {type(e).__name__}",
+ }
+ return with_metadata("peasy-ai-resume-workflow", payload)
+
+ @mcp.tool(
+ name="peasy-ai-list-workflows",
+ description="List available workflows and any active or paused workflows. Returns the set of workflow names you can pass to peasy-ai-run-workflow, plus status of any in-flight workflows."
+ )
+ def list_workflows(params: ListWorkflowsParams) -> Dict[str, Any]:
+ engine, _ = _get_workflow_engine(get_services)
+
+ available = [
+ {
+ "name": "full_generation",
+ "description": "Generate complete P project from design document",
+ "requires": ["design_doc", "project_path"]
+ },
+ {
+ "name": "compile_and_fix",
+ "description": "Compile project and automatically fix errors",
+ "requires": ["project_path"]
+ },
+ {
+ "name": "full_verification",
+ "description": "Compile, fix compilation errors, run PChecker, and automatically fix any PChecker bugs (full end-to-end verification)",
+ "requires": ["project_path"]
+ },
+ {
+ "name": "quick_check",
+ "description": "Run PChecker on compiled project",
+ "requires": ["project_path"]
+ }
+ ]
+
+ active = [
+ {
+ "workflow_id": state.workflow_id,
+ "name": state.workflow_name,
+ "status": state.status,
+ "current_step": state.current_step_index,
+ "completed_steps": state.completed_steps
+ }
+ for state in engine.get_active_workflows()
+ ]
+
+ persistence = engine.get_persistence_status()
+
+ payload = {
+ "available_workflows": available,
+ "active_workflows": active,
+ "persistence": persistence,
+ }
+ return with_metadata("peasy-ai-list-workflows", payload)
+
+ return {
+ "run_workflow": run_workflow,
+ "resume_workflow": resume_workflow,
+ "list_workflows": list_workflows,
+ }
diff --git a/Src/PeasyAI/src/ui/stlit/SideNav.py b/Src/PeasyAI/src/ui/stlit/SideNav.py
new file mode 100644
index 0000000000..0b6d8b8bba
--- /dev/null
+++ b/Src/PeasyAI/src/ui/stlit/SideNav.py
@@ -0,0 +1,122 @@
+import streamlit as st
+from utils import file_utils
+from utils import global_state
+import os
+import json
+from utils.ConversationHistory import ConversationHistory
+
+class SideNav:
+ def __init__(self):
+ self.status_container = st.container()
+ self.messages = st.container()
+ self.bottom_path = st.container()
+
+ self.displaySideNav()
+
+ def change_mode(self):
+ """
+ This function corresponds to the admin/user toggle.
+ It changes the tabs available to the user based on the admin/user toggle.
+ """
+ if st.session_state.get("toggle_mode", False):
+ global_state.current_mode = "admin"
+ else:
+ global_state.current_mode = "user"
+
+ def change_llm_model(self):
+ """
+ This function corresponds to the LLM dropdown menu.
+ It changes the LLM model being run based on the value selected by the user.
+ """
+ try:
+ if st.session_state["llm_model"] == "Claude 3.7 Sonnet":
+ global_state.model_id = global_state.model_id_sonnet3_7
+ elif st.session_state["llm_model"] == "Claude 4 Opus":
+ global_state.model_id = global_state.model_id_opus_4
+ elif st.session_state["llm_model"] == "Claude 4 Sonnet":
+ global_state.model_id = global_state.model_id_sonnet4
+ elif st.session_state["llm_model"] == "Claude 3.5 Sonnet v2":
+ global_state.model_id = global_state.model_id_sonnet3_5_v2
+ elif st.session_state["llm_model"] == "Claude 3.5 Sonnet":
+ global_state.model_id = global_state.model_id_sonnet3_5
+ elif st.session_state["llm_model"] == "Claude 3 Sonnet":
+ global_state.model_id = global_state.model_id_sonnet3
+ elif st.session_state["llm_model"] == "Mistral Large":
+ global_state.model_id = global_state.model_id_mistral
+
+ # Update maxTokens based on model's limit
+ global_state.maxTokens = global_state.model_token_limits[global_state.model_id]
+ print(f"Model {st.session_state['llm_model']} - Token limit: {global_state.maxTokens}")
+ st.toast(f"Model changed to: {st.session_state['llm_model']}")
+ except Exception as e:
+ st.warning(f"An error occurred when switching the LLM through the LLM dropdown menu: {str(e)}", icon="⚠️")
+
+
+ def switch_main_menu(self):
+ """
+ This function corresponds to the home button in the UI.
+ Navigates back to home page
+ """
+ st.session_state["display_history"] = False
+ st.session_state['p_easyai_state'] = None
+ if "mode_state" in st.session_state:
+ del st.session_state["mode_state"]
+ global_state.chat_history.save_conversation()
+ global_state.chat_history.clear_conversation()
+
+
+
+ def change_temp(self):
+ """
+ This function corresponds to the temperature slider in the UI.
+ It changes the temperature value used by the app.
+ """
+ global_state.temperature = st.session_state['temp']
+
+ def change_top_p(self):
+ """
+ This function corresponds to the top P slider in the UI.
+ It changes the temperature value used by the app.
+ """
+ global_state.topP = st.session_state['top_p']
+
+ def displaySideNav(self):
+ """
+ Creates and displays components of side navigarion bar
+ """
+ with st.sidebar:
+ # Home button to navigate back to home on click
+ st.button("Home", on_click=self.switch_main_menu)
+
+ # Creating and setting the user/admin toggle
+ _ = """
+ Creates two separate tabs if the user is in admin mode: History Tab and Configurations Tab
+ Creates a single tab if the user is in user mode: History Tab
+ """
+ self.change_mode()
+ what_mode = (
+ "Admin Mode"
+ if st.session_state.get("toggle_mode", False)
+ else "User Mode"
+ )
+ mode = st.session_state.get("toggle_mode", False)
+ st.toggle(what_mode, value=mode, key="toggle_mode", on_change = self.change_mode)
+
+ if global_state.current_mode == "admin":
+ history, configurations = st.tabs(["PeasyAI History", "Configurations"])
+ configurations.title("Configurations")
+ configurations.selectbox("Which Large Language model would you like to use today?",
+ ("Claude 4 Opus", "Claude 4 Sonnet", "Claude 3.7 Sonnet", "Claude 3.5 Sonnet v2", "Claude 3.5 Sonnet", "Claude 3 Sonnet", "Mistral Large"),
+ index=2, # Set default to Claude 3.7 Sonnet to match global_state
+ on_change=self.change_llm_model,
+ key="llm_model")
+ configurations.slider("Temperature", 0.0, 1.0, global_state.temperature, key = "temp", on_change=self.change_temp)
+ configurations.write("Change the temperature of PeasyAI to increase or decrease the creativity of the AI's responses!")
+ configurations.slider("Top P", 0.0, 1.0, global_state.topP, key = "top_p", on_change=self.change_top_p)
+ configurations.write("Sample from the smallest possible set of tokens whose cumulative probability exceeds the threshold p!")
+ else:
+ history = st.tabs(["PeasyAI History"])[0]
+
+ # Display the History Tab's Contents
+ history.title("PeasyAI History")
+ ConversationHistory(self.messages, history)
diff --git a/Src/PeasyAI/src/ui/stlit/adapters.py b/Src/PeasyAI/src/ui/stlit/adapters.py
new file mode 100644
index 0000000000..b2a63f22f6
--- /dev/null
+++ b/Src/PeasyAI/src/ui/stlit/adapters.py
@@ -0,0 +1,403 @@
+"""
+Streamlit Adapters for PeasyAI.
+
+This module provides adapters that connect the workflow engine and services
+to Streamlit's UI components. It handles:
+- Converting workflow events to Streamlit status updates
+- Managing Streamlit session state
+- Providing callbacks for workflow progress
+"""
+
+import streamlit as st
+from typing import Any, Callable, Dict, List, Optional
+from pathlib import Path
+import os
+import sys
+
+# Add project paths
+PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
+SRC_ROOT = Path(__file__).parent.parent.parent
+
+if str(SRC_ROOT) not in sys.path:
+ sys.path.insert(0, str(SRC_ROOT))
+
+from core.workflow import (
+ WorkflowEngine,
+ WorkflowFactory,
+ EventEmitter,
+ WorkflowEvent,
+ EventData,
+ extract_machine_names_from_design_doc,
+)
+from core.services import (
+ GenerationService,
+ CompilationService,
+ FixerService,
+)
+from core.services.base import ResourceLoader
+from core.llm import get_default_provider
+
+# Load environment
+from dotenv import load_dotenv
+load_dotenv(PROJECT_ROOT / ".env", override=True)
+
+
+class StreamlitEventListener:
+ """
+ Event listener that updates Streamlit UI elements based on workflow events.
+
+ Converts workflow events into Streamlit status updates, progress bars,
+ and informational messages.
+ """
+
+ def __init__(self, status_container=None):
+ """
+ Initialize the listener.
+
+ Args:
+ status_container: Optional Streamlit container for status updates
+ """
+ self.status_container = status_container
+ self.current_status = None
+ self.metrics = {
+ "steps_completed": 0,
+ "steps_total": 0,
+ "errors": [],
+ }
+
+ def __call__(self, event_data: EventData) -> None:
+ """Handle a workflow event."""
+ event = event_data.event
+ data = event_data.data
+
+ if self.status_container is None:
+ return
+
+ if event == WorkflowEvent.STARTED:
+ workflow = data.get("workflow", "Workflow")
+ self.current_status = self.status_container.status(
+ f"Running {workflow}...",
+ expanded=True
+ )
+ self.current_status.write(f"🚀 Started: {workflow}")
+
+ elif event == WorkflowEvent.STEP_STARTED:
+ step = data.get("step", "step")
+ attempt = data.get("attempt", 1)
+ if self.current_status:
+ if attempt > 1:
+ self.current_status.write(f"🔄 Retrying: {step} (attempt {attempt})")
+ else:
+ self.current_status.write(f"▶️ Running: {step}")
+
+ elif event == WorkflowEvent.STEP_COMPLETED:
+ step = data.get("step", "step")
+ self.metrics["steps_completed"] += 1
+ if self.current_status:
+ self.current_status.write(f"✅ Completed: {step}")
+
+ elif event == WorkflowEvent.STEP_FAILED:
+ step = data.get("step", "step")
+ error = data.get("error", "Unknown error")
+ self.metrics["errors"].append(error)
+ if self.current_status:
+ self.current_status.write(f"❌ Failed: {step} - {error}")
+
+ elif event == WorkflowEvent.STEP_SKIPPED:
+ step = data.get("step", "step")
+ if self.current_status:
+ self.current_status.write(f"⏭️ Skipped: {step}")
+
+ elif event == WorkflowEvent.HUMAN_NEEDED:
+ step = data.get("step", "step")
+ prompt = data.get("prompt", "Guidance needed")
+ if self.current_status:
+ self.current_status.write(f"⚠️ Human input needed for: {step}")
+ self.current_status.write(f" {prompt}")
+
+ elif event == WorkflowEvent.COMPLETED:
+ success = data.get("context", {}).get("success", False)
+ if self.current_status:
+ if success:
+ self.current_status.update(
+ label="✅ Workflow completed successfully!",
+ state="complete",
+ expanded=False
+ )
+ else:
+ errors = data.get("context", {}).get("errors", [])
+ self.current_status.update(
+ label=f"⚠️ Workflow completed with {len(errors)} error(s)",
+ state="complete",
+ expanded=True
+ )
+
+ elif event == WorkflowEvent.FAILED:
+ error = data.get("error", "Unknown error")
+ if self.current_status:
+ self.current_status.update(
+ label=f"❌ Workflow failed: {error}",
+ state="error",
+ expanded=True
+ )
+
+ elif event == WorkflowEvent.FILE_GENERATED:
+ path = data.get("path", "file")
+ if self.current_status:
+ self.current_status.write(f"📄 Generated: {os.path.basename(path)}")
+
+
+class StreamlitWorkflowAdapter:
+ """
+ Adapter that connects the workflow engine to Streamlit.
+
+ Provides a clean interface for running workflows from Streamlit
+ components with automatic UI updates.
+ """
+
+ _instance = None
+
+ @classmethod
+ def get_instance(cls) -> "StreamlitWorkflowAdapter":
+ """Get or create the singleton adapter instance."""
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ def __init__(self):
+ """Initialize the adapter with services and engine."""
+ self._initialized = False
+ self._engine: Optional[WorkflowEngine] = None
+ self._factory: Optional[WorkflowFactory] = None
+ self._emitter: Optional[EventEmitter] = None
+ self._services: Dict[str, Any] = {}
+
+ def _ensure_initialized(self) -> None:
+ """Lazy initialization of services."""
+ if self._initialized:
+ return
+
+ # Get LLM provider
+ provider = get_default_provider()
+
+ # Create resource loader
+ resource_loader = ResourceLoader(PROJECT_ROOT / "resources")
+
+ # Create services
+ self._services["generation"] = GenerationService(
+ llm_provider=provider,
+ resource_loader=resource_loader
+ )
+ self._services["compilation"] = CompilationService(
+ llm_provider=provider,
+ resource_loader=resource_loader
+ )
+ self._services["fixer"] = FixerService(
+ llm_provider=provider,
+ resource_loader=resource_loader,
+ compilation_service=self._services["compilation"]
+ )
+
+ # Create event emitter
+ self._emitter = EventEmitter()
+
+ # Create engine and factory
+ self._engine = WorkflowEngine(self._emitter)
+ self._factory = WorkflowFactory(
+ generation_service=self._services["generation"],
+ compilation_service=self._services["compilation"],
+ fixer_service=self._services["fixer"]
+ )
+
+ # Register standard workflows
+ self._engine.register_workflow(
+ self._factory.create_compile_and_fix_workflow()
+ )
+ self._engine.register_workflow(
+ self._factory.create_full_verification_workflow()
+ )
+ self._engine.register_workflow(
+ self._factory.create_quick_check_workflow()
+ )
+
+ self._initialized = True
+
+ def generate_project(
+ self,
+ design_doc: str,
+ project_path: str,
+ status_container=None,
+ machine_names: Optional[List[str]] = None
+ ) -> Dict[str, Any]:
+ """
+ Generate a P project from a design document.
+
+ Args:
+ design_doc: The design document content
+ project_path: Where to create the project
+ status_container: Optional Streamlit status container
+ machine_names: Optional list of machine names (auto-extracted if not provided)
+
+ Returns:
+ Dictionary with results including generated files and any errors
+ """
+ self._ensure_initialized()
+
+ # Extract machine names if not provided
+ if not machine_names:
+ machine_names = extract_machine_names_from_design_doc(design_doc)
+ if not machine_names:
+ return {
+ "success": False,
+ "error": "Could not extract machine names from design document"
+ }
+
+ # Create workflow
+ workflow = self._factory.create_full_generation_workflow(machine_names)
+ self._engine.register_workflow(workflow)
+
+ # Add Streamlit listener
+ listener = StreamlitEventListener(status_container)
+ self._emitter.on_all(listener)
+
+ try:
+ # Execute workflow
+ result = self._engine.execute("full_generation", {
+ "design_doc": design_doc,
+ "project_path": project_path
+ })
+
+ # Collect generated files
+ generated_files = {}
+ for key, value in result.items():
+ if key.endswith("_code") and value:
+ # Extract filename from key
+ if key == "types_events_code":
+ filename = "Enums_Types_Events.p"
+ elif key.startswith("machine_code_"):
+ filename = key.replace("machine_code_", "") + ".p"
+ elif key.startswith("spec_code_"):
+ filename = key.replace("spec_code_", "") + ".p"
+ elif key.startswith("test_code_"):
+ filename = key.replace("test_code_", "") + ".p"
+ else:
+ continue
+ generated_files[filename] = value
+
+ return {
+ "success": result.get("success", False),
+ "files": generated_files,
+ "project_path": result.get("project_path", project_path), # Use updated path from workflow
+ "completed_steps": result.get("completed_steps", []),
+ "errors": result.get("errors", []),
+ "metrics": listener.metrics
+ }
+
+ finally:
+ # Remove listener
+ self._emitter.off_all(listener)
+
+ def compile_project(
+ self,
+ project_path: str,
+ status_container=None
+ ) -> Dict[str, Any]:
+ """
+ Compile a P project and fix errors.
+
+ Args:
+ project_path: Path to the P project
+ status_container: Optional Streamlit status container
+
+ Returns:
+ Compilation results
+ """
+ self._ensure_initialized()
+
+ listener = StreamlitEventListener(status_container)
+ self._emitter.on_all(listener)
+
+ try:
+ result = self._engine.execute("compile_and_fix", {
+ "project_path": project_path
+ })
+
+ return {
+ "success": result.get("success", False),
+ "errors": result.get("errors", []),
+ "completed_steps": result.get("completed_steps", [])
+ }
+ finally:
+ self._emitter.off_all(listener)
+
+ def run_checker(
+ self,
+ project_path: str,
+ schedules: int = 100,
+ timeout: int = 60,
+ status_container=None
+ ) -> Dict[str, Any]:
+ """
+ Run PChecker on a project.
+
+ Args:
+ project_path: Path to the P project
+ schedules: Number of schedules
+ timeout: Timeout in seconds
+ status_container: Optional Streamlit status container
+
+ Returns:
+ Checker results
+ """
+ self._ensure_initialized()
+
+ # Use compilation service directly for checker
+ result = self._services["compilation"].run_checker(
+ project_path=project_path,
+ schedules=schedules,
+ timeout=timeout
+ )
+
+ return {
+ "success": result.success,
+ "output": result.output,
+ "errors": result.errors if hasattr(result, "errors") else []
+ }
+
+ def fix_checker_error(
+ self,
+ project_path: str,
+ trace_log: str,
+ user_guidance: Optional[str] = None
+ ) -> Dict[str, Any]:
+ """
+ Fix a PChecker error.
+
+ Args:
+ project_path: Path to the P project
+ trace_log: The error trace log
+ user_guidance: Optional user guidance
+
+ Returns:
+ Fix results
+ """
+ self._ensure_initialized()
+
+ result = self._services["fixer"].fix_checker_error(
+ project_path=project_path,
+ trace_log=trace_log,
+ user_guidance=user_guidance
+ )
+
+ return {
+ "success": result.success,
+ "needs_guidance": result.needs_guidance,
+ "guidance_questions": result.guidance_questions,
+ "fix_description": result.fix_description,
+ "error": result.error
+ }
+
+
+def get_adapter() -> StreamlitWorkflowAdapter:
+ """Get the Streamlit workflow adapter singleton."""
+ return StreamlitWorkflowAdapter.get_instance()
diff --git a/Src/PeasyAI/src/utils/ConversationHistory.py b/Src/PeasyAI/src/utils/ConversationHistory.py
new file mode 100644
index 0000000000..45b2059ca7
--- /dev/null
+++ b/Src/PeasyAI/src/utils/ConversationHistory.py
@@ -0,0 +1,88 @@
+import streamlit as st
+from utils import file_utils
+from utils import global_state
+from pathlib import Path
+from datetime import datetime
+import os
+import json
+
+class ConversationHistory:
+ def __init__(self, messages, history):
+ self.history = history
+ self.messages = messages
+ self.sorted_history_files = self.get_sorted_history_files()
+ self.display_history()
+
+ def display_history(self):
+ options = self.get_pagination_options()
+ if len(options) > 0:
+ page_range = self.history.selectbox('Page:', options)
+ self.display_history_for_range(page_range)
+ else:
+ self.display_history_for_range(None)
+
+
+ def get_pagination_options(self):
+ # Generate page ranges for dropdown
+ max_per_page = 5
+ options = []
+ start_index = 0
+ end_index = 0
+ no_of_history_files = len(self.sorted_history_files)
+ while start_index < no_of_history_files:
+ end_index = min(start_index + max_per_page, no_of_history_files)
+ options.append(str(start_index) + "-" + str(end_index - 1))
+ start_index += max_per_page
+ return options
+
+ def display_history_for_range(self, range):
+ if range:
+ start_index, end_index = list(map(int, range.split("-")))
+ sorted_info = self.sorted_history_files[start_index : end_index + 1]
+ for info in sorted_info:
+ filepath = os.path.join(global_state.chat_history_path, info[0])
+
+ self.history.button(info[1][:100] + "...", on_click=self.display_interaction, args = [filepath], key=info[2])
+ else:
+ self.history.write("No history to show")
+
+ def display_interaction(self, filepath):
+ try:
+ st.session_state["display_history"] = True
+ messages = json.loads(file_utils.read_file(filepath))["messages"]
+ for message in messages:
+ self.messages.chat_message(message["role"]).write(message["chat_msg"])
+
+ except Exception as e:
+ st.warning("An error occured when displaying chat history ", icon="⚠️")
+ print(e)
+
+ def get_sorted_history_files(self):
+ if file_utils.check_directory(global_state.chat_history_path):
+ all_history_files = file_utils.list_top_level_contents(global_state.chat_history_path)
+
+ # Extract end time from filename
+ end_timestamps = []
+ titles = []
+ for history_file in all_history_files:
+ title, _, end_timestamp = self.get_history_info(history_file)
+ titles.append(title)
+ end_timestamps.append(end_timestamp)
+
+ combined = list(zip(all_history_files, titles, end_timestamps))
+
+ # Sort the combined list based on timestamps (list2)
+ sorted_file_info = sorted(combined, key=lambda x: datetime.strptime(x[2], "%Y-%m-%dT%H:%M:%SZ"), reverse=True)
+
+
+ # Print sorted filenames
+ return sorted_file_info
+ else:
+ file_utils.create_directory(global_state.chat_history_path)
+ return []
+
+ def get_history_info(self, history_file):
+ file_path = os.path.join(global_state.chat_history_path, history_file)
+ content = json.loads(file_utils.read_file(file_path))
+ return content["title"], content["start_timestamp"], content["end_timestamp"]
+
\ No newline at end of file
diff --git a/Src/PeasyAI/src/utils/__init__.py b/Src/PeasyAI/src/utils/__init__.py
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/Src/PeasyAI/src/utils/__init__.py
@@ -0,0 +1 @@
+
diff --git a/Src/PeasyAI/src/utils/chat_history.py b/Src/PeasyAI/src/utils/chat_history.py
new file mode 100644
index 0000000000..5f6143d966
--- /dev/null
+++ b/Src/PeasyAI/src/utils/chat_history.py
@@ -0,0 +1,57 @@
+import streamlit as st
+from datetime import datetime, timezone
+from utils import file_utils
+import os
+import json
+
+class ChatHistory:
+ def __init__(self):
+ if 'conversation' not in st.session_state:
+ st.session_state.conversation = []
+ self.start_timestamp = self.get_current_timestamp()
+ self.end_timestamp = None
+ self.chat_history_dir = "chat_history"
+ self.title = None
+
+ def get_current_timestamp(self):
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ def add_exchange(self, role, query, response,download_path):
+ # add to the conversation
+ exchange = {'role': role, 'query_sent_to_llm': query, 'chat_msg': response,'download_path':download_path}
+ if "conversation" not in st.session_state:
+ st.session_state["conversation"] = []
+ self.title = None
+ self.start_timestamp = None
+ if self.title == None and role == "user":
+ self.title = response
+ self.start_timestamp = self.get_current_timestamp()
+ st.session_state.conversation.append(exchange)
+
+
+ def get_conversation(self):
+ # Retrieve the entire conversation
+ if 'conversation' not in st.session_state:
+ st.session_state.conversation = []
+ self.title = None
+ self.start_timestamp = None
+ return st.session_state.conversation
+
+ def clear_conversation(self):
+ # Clear the conversation for a fresh start
+ st.session_state.conversation = []
+ self.title = None
+ self.start_timestamp = None
+
+ def save_conversation(self):
+ if 'conversation' not in st.session_state:
+ return
+ # Saves the conversation to a file
+ if (len(st.session_state.conversation) > 1):
+ self.end_timestamp = self.get_current_timestamp()
+ filename = "chat_history_" + self.start_timestamp + "_" + self.end_timestamp + ".json"
+ file_path = os.path.join(self.chat_history_dir, filename)
+ content = {"title": self.title, "start_timestamp": self.start_timestamp, "end_timestamp": self.end_timestamp, "messages": st.session_state.conversation}
+ file_utils.write_file(file_path, json.dumps(content))
+
+
diff --git a/Src/PeasyAI/src/utils/chat_utils.py b/Src/PeasyAI/src/utils/chat_utils.py
new file mode 100644
index 0000000000..90530ed8fb
--- /dev/null
+++ b/Src/PeasyAI/src/utils/chat_utils.py
@@ -0,0 +1,39 @@
+import streamlit as st
+from utils import generate_p_code
+from utils import global_state
+import os
+
+def generate_response(design_doc_content, status_container):
+ """
+ Call the LLM and obtain the LLM's response and metrics to display for the UI.
+
+ Parameters:
+ input_text (str): The input text to generate a response for.
+ """
+ try:
+ with status_container.status("Generating P code... ", expanded = True) as status:
+ final_response = generate_p_code.entry_point(design_doc_content, status)
+ if not final_response:
+ raise ValueError('Please try running the app again.')
+ st.session_state["response"] = final_response
+ st.session_state["input_tokens"] = global_state.model_metrics["inputTokens"]
+ st.session_state["output_tokens"] = global_state.model_metrics["outputTokens"]
+ st.session_state["latency"] = global_state.model_metrics["latencyMs"]/1000
+ st.session_state["total_runtime"] = global_state.total_runtime
+ st.session_state["compile_iterations"] = global_state.compile_iterations
+ if global_state.compile_success:
+ status.update(label="P project generation complete! Compilation succeeded.", state="complete", expanded=False)
+ else:
+ status.update(label="P project generation complete! Compilation failed.", state="complete", expanded=False)
+
+ except Exception as e:
+ st.error(" There was an error calling the LLM: " + str(e), icon="⚠️")
+
+def render_chat_messages(messages):
+ # Display chat messages from history on app rerun
+ for message in global_state.chat_history.get_conversation():
+ if message["download_path"] is not None and os.path.exists(message["download_path"]):
+ with open(message["download_path"]) as f:
+ messages.download_button('Download Design Doc', f, file_name=os.path.basename(message["download_path"]), key=message["download_path"])
+ if message["chat_msg"]:
+ messages.chat_message(message["role"]).write(message["chat_msg"])
diff --git a/Src/PeasyAI/src/utils/checker_utils.py b/Src/PeasyAI/src/utils/checker_utils.py
new file mode 100644
index 0000000000..d64d489fde
--- /dev/null
+++ b/Src/PeasyAI/src/utils/checker_utils.py
@@ -0,0 +1,80 @@
+import subprocess
+from utils import file_utils
+import os
+from datetime import datetime
+from glob import glob
+
+def run_pchecker_test_case(test_name, project_path, schedules=100, timeout_seconds=300, seed="default-seed", max_steps=10000):
+ cmd = ['p', 'check', '-tc', test_name, '-s', str(schedules), '--max-steps', str(max_steps), "--seed", str(hash(seed) & 0xffffffff)]
+ print(" ".join(cmd))
+ try:
+ result = subprocess.run(cmd, capture_output=True, cwd=project_path, timeout=timeout_seconds)
+ return result.returncode == 0, result
+ except subprocess.TimeoutExpired as e:
+ return False, None
+ except Exception as e:
+ return False, None
+
+def starts_with_letter(s):
+ stripped = s.strip()
+ return stripped and stripped[0].isalpha()
+
+def discover_tests(project_path):
+ cmd = ['p', 'check', '--list-tests']
+ result = subprocess.run(cmd, capture_output=True, cwd=project_path)
+ lines = result.stdout.decode('utf-8').split("\n")
+ return list(filter(lambda l: starts_with_letter(l), lines))
+
+def try_pchecker(project_path, captured_streams_output_dir=None, schedules=100, timeout=300, seed="default-seed", max_steps=10000):
+
+ if not captured_streams_output_dir:
+ timestamp = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
+ captured_streams_output_dir = f"/tmp/checker-utils/{timestamp}"
+
+ tests = discover_tests(project_path)
+ results = {}
+ trace_dicts = {}
+ trace_logs = {}
+
+ for test_name in tests:
+ is_pass, result_obj = run_pchecker_test_case(test_name, project_path, schedules=schedules, timeout_seconds=timeout, seed=seed, max_steps=max_steps)
+ out_dir = f"{captured_streams_output_dir}/{test_name}" if captured_streams_output_dir else None
+
+ if out_dir:
+ os.makedirs(out_dir, exist_ok=True)
+
+ file_utils.write_output_streams(result_obj, out_dir)
+ results[test_name] = is_pass
+ if not is_pass:
+ if result_obj:
+ bug_finding_dir = f"{project_path}/PCheckerOutput/BugFinding"
+ trace_dict_file = glob(f"{bug_finding_dir}/*.trace.json")[0]
+ trace_log_file = glob(f"{bug_finding_dir}/*_0_0.txt")[0]
+ trace_dicts[test_name] = file_utils.read_json_file(trace_dict_file)
+ trace_logs[test_name] = file_utils.read_file(trace_log_file)
+ if out_dir:
+ file_utils.copy_file(trace_dict_file, f"{out_dir}/trace.json")
+ file_utils.copy_file(trace_log_file, f"{out_dir}/trace.txt")
+ else: # This means that the checker timed out
+ trace_dicts[test_name] = [{
+ "type": "PChecker Timed Out",
+ "details": {
+ "log": "",
+ "error": "",
+ "payload": ""
+ }
+ }]
+ trace_logs[test_name] = " Checker Timed Out\n"
+ if out_dir:
+ file_utils.write_file(f"{out_dir}/trace.txt", trace_logs[test_name])
+ file_utils.write_file(f"{out_dir}/trace.json", f"{trace_dicts[test_name]}")
+
+
+ return results, trace_dicts, trace_logs
+
+
+def try_pchecker_on_dict(project_state):
+ timestamp = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
+ out_dir = f"/tmp/checker-utils/{timestamp}"
+ file_utils.write_project_state(project_state, out_dir)
+ return try_pchecker(out_dir)
\ No newline at end of file
diff --git a/Src/PeasyAI/src/utils/compile_utils.py b/Src/PeasyAI/src/utils/compile_utils.py
new file mode 100644
index 0000000000..0bf566316c
--- /dev/null
+++ b/Src/PeasyAI/src/utils/compile_utils.py
@@ -0,0 +1,62 @@
+"""Compilation utilities for P projects.
+
+This module provides functions for compiling P projects and managing
+compilation artifacts. For full compilation services including error
+parsing and fixing, use :class:`core.services.compilation.CompilationService`.
+"""
+
+import logging
+import os
+import subprocess
+from datetime import datetime
+from pathlib import Path
+
+from utils import file_utils
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
+
+
+def try_compile(ppath, captured_streams_output_dir):
+ """Attempt to compile a P project.
+
+ Args:
+ ppath: Path to the P project directory or .pproj file.
+ captured_streams_output_dir: Directory to save stdout/stderr output.
+
+ Returns:
+ True if compilation succeeded, False otherwise.
+ """
+ pgen_dir = Path(ppath).resolve() / "PGenerated"
+ if pgen_dir.is_dir():
+ import shutil
+ shutil.rmtree(str(pgen_dir))
+
+ p = Path(ppath)
+ flags = ['-pf', ppath, "-o", str(p.parent)] if p.is_file() else []
+
+ final_cmd_arr = ['p', 'compile', *flags]
+ result = subprocess.run(final_cmd_arr, capture_output=True, cwd=ppath if not p.is_file() else None)
+
+ out_dir = f"{captured_streams_output_dir}/compile"
+ os.makedirs(out_dir, exist_ok=True)
+ file_utils.write_output_streams(result, out_dir)
+ return result.returncode == 0
+
+
+def try_compile_project_state(project_state, captured_streams_output_dir=None):
+ """Write project state to a temp directory and attempt compilation.
+
+ Args:
+ project_state: Dict mapping relative file paths to file contents.
+ captured_streams_output_dir: Optional directory to save stdout/stderr.
+
+ Returns:
+ Tuple of (success: bool, project_dir: str, stdout: str).
+ """
+ timestamp = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
+ tmp_project_dir = f"/tmp/compile-utils/{timestamp}"
+ file_utils.write_project_state(project_state, tmp_project_dir)
+ passed = try_compile(tmp_project_dir, f"{tmp_project_dir}/std_streams")
+ stdout = file_utils.read_file(f"{tmp_project_dir}/std_streams/compile/stdout.txt")
+ return passed, tmp_project_dir, stdout
diff --git a/Src/PeasyAI/src/utils/constants.py b/Src/PeasyAI/src/utils/constants.py
new file mode 100644
index 0000000000..71852e5ff8
--- /dev/null
+++ b/Src/PeasyAI/src/utils/constants.py
@@ -0,0 +1,51 @@
+from utils import file_utils, global_state
+
+CLAUDE_3_7 = "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
+# CLAUDE_3_7 = "us.anthropic.claude-sonnet-4-20250514-v1:0"
+
+LIST_OF_MACHINE_NAMES = 'list_of_machine_names'
+LIST_OF_FILE_NAMES = 'list_of_file_names'
+ENUMS_TYPES_EVENTS = 'enums_types_events'
+MACHINE = 'machine'
+MACHINE_STRUCTURE = "MACHINE_STRUCTURE"
+PROJECT_STRUCTURE="PROJECT_STRUCTURE"
+
+PSRC = 'PSrc'
+PSPEC = 'PSpec'
+PTST = 'PTst'
+
+instructions = {
+ LIST_OF_MACHINE_NAMES: file_utils.read_file(global_state.initial_instructions_path),
+ LIST_OF_FILE_NAMES: file_utils.read_file(global_state.generate_filenames_instruction_path),
+ ENUMS_TYPES_EVENTS: file_utils.read_file(global_state.generate_enums_types_events_instruction_path),
+ MACHINE_STRUCTURE: file_utils.read_file(global_state.generate_machine_structure_path),
+ PROJECT_STRUCTURE: file_utils.read_file(global_state.generate_project_structure_path),
+ MACHINE: file_utils.read_file(global_state.generate_machine_instruction_path),
+ PSRC: file_utils.read_file(global_state.generate_modules_file_instruction_path),
+ PSPEC: file_utils.read_file(global_state.generate_spec_files_instruction_path),
+ PTST: file_utils.read_file(global_state.generate_test_files_instruction_path)
+}
+
+def get_context_files():
+ return {
+ "ENUMS_TYPES_EVENTS": [
+ global_state.P_ENUMS_GUIDE,
+ global_state.P_TYPES_GUIDE,
+ global_state.P_EVENTS_GUIDE
+ ],
+ "MACHINE_STRUCTURE": [
+ global_state.P_MACHINES_GUIDE
+ ],
+ "MACHINE": [
+ global_state.P_STATEMENTS_GUIDE
+ ],
+ PSRC: [
+ global_state.P_MODULE_SYSTEM_GUIDE
+ ],
+ PSPEC: [
+ global_state.P_SPEC_MONITORS_GUIDE
+ ],
+ PTST: [
+ global_state.P_TEST_CASES_GUIDE
+ ]
+ }
\ No newline at end of file
diff --git a/Src/PeasyAI/src/utils/file_utils.py b/Src/PeasyAI/src/utils/file_utils.py
new file mode 100644
index 0000000000..0ee06d2006
--- /dev/null
+++ b/Src/PeasyAI/src/utils/file_utils.py
@@ -0,0 +1,367 @@
+import os, logging, json, shutil, re
+from utils import global_state as globals
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+def read_file(file_path):
+ """
+ Reads the content of a file in the local directory.
+
+ Parameters:
+ file_path (str): The path of the file to be read.
+
+ Returns:
+ str: The content of the file.
+ """
+ try:
+ if not os.path.isabs(file_path):
+ file_path = os.path.join(os.getcwd(), file_path)
+ with open(file_path, 'r') as file:
+ content = file.read()
+ return content
+ except Exception as e:
+ raise e
+
+
+def write_file(file_path, content):
+ """
+ Writes content to a file, creating the directory and file if they do not exist.
+
+ Parameters:
+ file_path (str): The path to the file to be written to.
+ content (str): The content to be written to the file.
+
+ Returns:
+ bool: true if the write operation was successful, false otherwise
+ """
+ try:
+ file_path = os.path.join(os.getcwd(), file_path)
+ dir_name = os.path.dirname(file_path)
+ ensure_dir_exists(dir_name)
+
+ with open(file_path, 'w') as file:
+ file.write(content)
+ return True
+ except IOError as e:
+ logger.info(f"Error: An IOError occurred while writing to the file '{file_path}'. Details: {e}")
+ return False
+
+
+def empty_file(file_path):
+ """
+ Empties the content of a file.
+
+ Parameters:
+ file_path (str): The path to the file to be emptied.
+
+ Returns:
+ bool: true if the empty operation was successful, false otherwise
+ """
+ try:
+ file_path = os.path.join(os.getcwd(), file_path)
+ with open(file_path, 'w') as file:
+ file.truncate(0)
+ return True
+ except IOError as e:
+ logger.info(f"Error: An IOError occurred while emptying the file '{file_path}'. Details: {e}")
+ return False
+
+
+def append_file(file_path, content):
+ """
+ Appends content to a file.
+
+ Parameters:
+ file_path (str): The path to the file to be written to.
+ content (str): The content to be appended to the file.
+
+ Returns:
+ bool: true if the append operation was successful, false otherwise.
+ """
+ try:
+ file_path = os.path.join(os.getcwd(), file_path)
+ with open(file_path, 'a') as file:
+ file.write(content)
+ return True
+ except IOError as e:
+ logger.info(f"Error: An IOError occurred while appends to the file '{file_path}'. Details: {e}")
+ return False
+
+
+def read_json_file(file_path):
+ """
+ Reads the content of a JSON file.
+
+ Parameters:
+ file_path (str): The path of the JSON file to be read.
+
+ Returns:
+ data (dict): The content of the JSON file as a dictionary.
+ """
+ try:
+ file_path = os.path.join(os.getcwd(), file_path)
+ with open(file_path, 'r') as file:
+ data = json.load(file)
+ return data
+ except FileNotFoundError:
+ return f"Error: The file '{file_path}' was not found."
+ except json.JSONDecodeError:
+ return f"Error: The file '{file_path}' is not a valid JSON file."
+ except IOError as e:
+ return f"Error: An IOError occurred while reading the file '{file_path}'. Details: {e}"
+
+
+def ensure_dir_exists(dir_path):
+ """
+ Ensure that a directory exists; create it if it doesn't.
+
+ Parameters:
+ dir_path (str): The relative or absolute path of the directory to check or create.
+
+ Creates:
+ If the directory does not exist, it will be created along with any necessary parent directories.
+ """
+ dir_path = os.path.join(os.getcwd(), dir_path)
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+
+
+def is_dir_empty(dir_path):
+ """
+ Check if a directory is empty.
+
+ Parameters:
+ dir_path (str): The relative or absolute path of the directory to check.
+
+ Returns:
+ bool: True if the directory is empty, False otherwise.
+ """
+ dir_path = os.path.join(os.getcwd(), dir_path)
+ return not os.listdir(dir_path)
+
+
+def move_dir_contents(source_dir, destination_dir):
+ """
+ Move all contents from the source directory to the destination directory.
+
+ Parameters:
+ source_dir (str): The relative or absolute path of the source directory.
+ destination_dir (str): The relative or absolute path of the destination directory.
+
+ Moves:
+ All files and subdirectories from the source directory to the destination directory.
+ """
+ source_dir = os.path.join(os.getcwd(), source_dir)
+ destination_dir = os.path.join(os.getcwd(), destination_dir)
+
+ for item in os.listdir(source_dir):
+ source_path = os.path.join(source_dir, item)
+ destination_path = os.path.join(destination_dir, item)
+ logger.info("Moving %s to %s", source_path, destination_path)
+ shutil.move(source_path, destination_path)
+
+
+def check_directory(file_path):
+ """
+ Returns whether the file path is to a directory or not.
+ Parameters:
+ file_path (str): The path to the file to check
+
+ Returns: bool: Returns True if the file path is a directory and False if the file path is not a directory
+ """
+ return os.path.isdir(os.path.join(os.getcwd(), file_path))
+
+def create_directory(path):
+ os.mkdir(os.path.join(os.getcwd(), path))
+
+
+def list_top_level_contents(directory):
+ """
+ Lists the top-level contents of the specified directory.
+ Parameters:
+ directory (str): The path to the directory to list contents of.
+
+ Returns: A list of top-level contents in the directory.
+ """
+ try:
+ directory = os.path.join(os.getcwd(), directory)
+ contents = os.listdir(directory)
+ return contents
+ except Exception as e:
+ return str(e)
+
+
+def get_recent_project_path():
+ """
+ Returns the path to the most recent P project.
+
+ Returns:
+ file_path (str): The path to the most recent project, or None
+ """
+ if globals.custom_dir_path != None:
+ file_path = os.path.join(globals.custom_dir_path, globals.project_name_with_timestamp)
+ return file_path
+ if check_directory(globals.recent_dir_path):
+ for proj in list_top_level_contents(globals.recent_dir_path):
+ file_path = os.path.join(os.getcwd(), globals.recent_dir_path + "/" + proj)
+ return file_path
+ return None
+
+def map_p_file_to_path(p_files):
+ """
+ Maps P file names to their full paths.
+
+ Parameters:
+ p_files (list): List of P file paths.
+
+ Returns: mapped_files (dict): A dictionary where the keys are P file names and the values are their full paths.
+ """
+ mapped_files = {}
+ for file in p_files:
+ mapped_files[os.path.basename(file)] = file
+ return mapped_files
+
+
+def get_all_files(file_path, filter_ext=None):
+ """
+ Retrieves all files in the given directory or returns the file if a single file is specified.
+ Can optionally filter files by extension.
+
+ Parameters:
+ file_path (str): The path to the file or directory to be scanned.
+ filter_ext (list, optional): List of file extensions to include (without the dot).
+ For example: ["p", "pproj"]
+
+ Returns:
+ file_path (list): A list of file paths found in the directory or a single file path if a file is provided.
+
+ Raises:
+ ValueError: If the file path does not exist.
+ """
+ all_files = []
+ if not os.path.exists(file_path):
+ raise ValueError("The file path provided does not exist. ")
+
+ if check_directory(file_path):
+ for root, dirs, files in os.walk(file_path):
+ for file in files:
+ # If filter_ext is provided, only include files with matching extensions
+ if filter_ext is not None:
+ ext = os.path.splitext(file)[1].lstrip('.')
+ if ext not in filter_ext:
+ continue
+ all_files.append(os.path.join(root, file))
+ elif os.path.isfile(file_path):
+ # For single file, check extension if filter is provided
+ if filter_ext is not None:
+ ext = os.path.splitext(file_path)[1].lstrip('.')
+ if ext in filter_ext:
+ all_files.append(file_path)
+ else:
+ all_files.append(file_path)
+
+ return all_files
+
+def combine_files(doc_list, pre="", post="", preprocessing_function=lambda _, c:c):
+ combined_text = ""
+ for doc_path in doc_list:
+ # Read the actual document content
+ content = read_file(doc_path)
+
+ # Apply preprocessing to each document and append to combined text
+ processed_content = preprocessing_function(doc_path, content)
+ combined_text += processed_content
+
+ combined_text = f"{pre}{combined_text}{post}"
+ return combined_text
+
+def capture_project_state(project_dir):
+ """
+ Captures the state of all files in a project directory.
+
+ Parameters:
+ project_dir (str): Path to the project directory
+
+ Returns:
+ dict: Dictionary mapping relative file paths to file contents
+ """
+ state = {}
+ project_dir = os.path.abspath(project_dir)
+
+ # Get all files in the project directory
+ files = get_all_files(project_dir, filter_ext=["p", "pproj", "ddoc"])
+
+ # Read each file and store with relative path
+ for file_path in files:
+ rel_path = os.path.relpath(file_path, project_dir)
+ state[rel_path] = read_file(file_path)
+
+ return state
+
+def copy_file(src, dest):
+ """
+ Copies a single file from source to destination.
+
+ Parameters:
+ src (str): Source file path
+ dest (str): Destination file path
+
+ Raises:
+ ValueError: If source is a directory
+ FileNotFoundError: If source doesn't exist
+ """
+ src = os.path.join(os.getcwd(), src)
+ dest = os.path.join(os.getcwd(), dest)
+
+ if os.path.isdir(src):
+ raise ValueError("Source is a directory. Use copytree for directories.")
+
+ # Create parent directories if needed
+ dest_dir = os.path.dirname(dest)
+ ensure_dir_exists(dest_dir)
+
+ shutil.copy2(src, dest)
+
+def write_project_state(state, write_dir):
+ """
+ Writes files from a project state dictionary to a directory.
+
+ Parameters:
+ state (dict): Dictionary mapping relative file paths to file contents
+ write_dir (str): Directory to write the files to
+ """
+ # Create the write directory if it doesn't exist
+ ensure_dir_exists(write_dir)
+
+ # Write each file
+ for rel_path, content in state.items():
+ full_path = os.path.join(write_dir, rel_path)
+ write_file(full_path, content)
+
+def make_copy(src_dir, dest_dir, new_name=None):
+ os.makedirs(dest_dir, exist_ok=True)
+ dir_name = new_name if new_name else os.path.basename(src_dir)
+ new_dir = os.path.join(dest_dir, dir_name)
+ shutil.copytree(src_dir, new_dir, dirs_exist_ok=True)
+ return new_dir
+
+def write_output_streams(result, captured_streams_output_dir=None):
+ if not captured_streams_output_dir:
+ return
+
+ stdout_file = f"{captured_streams_output_dir}/stdout.txt"
+ stderr_file = f"{captured_streams_output_dir}/stderr.txt"
+
+ if not result:
+ with open(stdout_file, 'w') as f:
+ f.write("..... Found 1 bug.\nTest timed out.\nThis message was written manually")
+ return
+
+ if stdout_file:
+ with open(stdout_file, 'wb') as f:
+ f.write(result.stdout)
+
+ if stderr_file:
+ with open(stderr_file, 'wb') as f:
+ f.write(result.stderr)
diff --git a/Src/PeasyAI/src/utils/generate_p_code.py b/Src/PeasyAI/src/utils/generate_p_code.py
new file mode 100644
index 0000000000..b4eace724d
--- /dev/null
+++ b/Src/PeasyAI/src/utils/generate_p_code.py
@@ -0,0 +1,630 @@
+import logging
+import time
+import traceback
+import re
+from botocore.exceptions import ClientError
+import os
+from utils import file_utils, regex_utils, log_utils, global_state
+from core.pipelining import prompting_pipeline
+from core.compilation.p_post_processor import PCodePostProcessor
+from core.services.compilation import CompilationService
+import json
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
+
+
+def _safe_format(template: str, **kwargs) -> str:
+ """Format a template, falling back to manual replacement on unescaped braces."""
+ try:
+ return template.format(**kwargs)
+ except (KeyError, ValueError, IndexError):
+ result = template
+ for key, value in kwargs.items():
+ result = result.replace("{" + key + "}", str(value))
+ return result
+
+
+# File handler
+file_handler = logging.FileHandler(os.path.join(global_state.PROJECT_ROOT, "peasyai_debug.log"), mode='w') # 'w' to overwrite
+file_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
+
+# Console handler
+console_handler = logging.StreamHandler()
+console_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
+
+# Add handlers to logger
+logger.addHandler(file_handler)
+logger.addHandler(console_handler)
+
+LIST_OF_MACHINE_NAMES = 'list_of_machine_names'
+LIST_OF_FILE_NAMES = 'list_of_file_names'
+ENUMS_TYPES_EVENTS = 'enums_types_events'
+MACHINE = 'machine'
+MACHINE_STRUCTURE = "MACHINE_STRUCTURE"
+PROJECT_STRUCTURE="PROJECT_STRUCTURE"
+SANITY_CHECK = 'sanity_check'
+
+PSRC = 'PSrc'
+PSPEC = 'PSpec'
+PTST = 'PTst'
+
+def read_instructions():
+ return {
+ LIST_OF_MACHINE_NAMES: file_utils.read_file(global_state.initial_instructions_path),
+ LIST_OF_FILE_NAMES: file_utils.read_file(global_state.generate_filenames_instruction_path),
+ ENUMS_TYPES_EVENTS: file_utils.read_file(global_state.generate_enums_types_events_instruction_path),
+ MACHINE_STRUCTURE: file_utils.read_file(global_state.generate_machine_structure_path),
+ PROJECT_STRUCTURE: file_utils.read_file(global_state.generate_project_structure_path),
+ SANITY_CHECK: file_utils.read_file(global_state.sanity_check_instructions_path),
+ MACHINE: file_utils.read_file(global_state.generate_machine_instruction_path),
+ PSRC: file_utils.read_file(global_state.generate_modules_file_instruction_path),
+ PSPEC: file_utils.read_file(global_state.generate_spec_files_instruction_path),
+ PTST: file_utils.read_file(global_state.generate_test_files_instruction_path)
+ }
+
+def get_context_files():
+ return {
+ "ENUMS_TYPES_EVENTS": [
+ global_state.P_ENUMS_GUIDE,
+ global_state.P_TYPES_GUIDE,
+ global_state.P_EVENTS_GUIDE
+ ],
+ "P_PROGRAM_STRUCTURE": [
+ global_state.P_PROGRAM_STRUCTURE_GUIDE
+ ],
+ "MACHINE_STRUCTURE": [
+ global_state.P_MACHINES_GUIDE
+ ],
+ PSRC: [
+ global_state.P_TYPES_GUIDE,
+ global_state.P_STATEMENTS_GUIDE,
+ global_state.P_MODULE_SYSTEM_GUIDE
+ ],
+ PSPEC: [
+ global_state.P_TYPES_GUIDE,
+ global_state.P_SPEC_MONITORS_GUIDE
+ ],
+ PTST: [
+ global_state.P_TYPES_GUIDE,
+ global_state.P_TEST_CASES_GUIDE
+ ],
+ "COMPILE" :[
+ global_state.P_SYNTAX_SUMMARY,
+ global_state.P_COMPILER_GUIDE
+ ]
+ }
+
+def entry_point(design_doc_content, backend_status):
+ """
+ Entry point function that processes user input and generates response.
+ """
+ start_time = time.time()
+ set_project_name_from_design_doc(design_doc_content)
+ log_utils.move_recent_to_archive()
+ file_utils.empty_file(global_state.full_log_path)
+ file_utils.empty_file(global_state.code_diff_log_path)
+ create_proj_files(backend_status)
+ all_responses = generate_p_code(design_doc_content, backend_status)
+ global_state.total_runtime = round(time.time() - start_time, 3)
+ return all_responses
+
+
+def create_proj_files(backend_status):
+
+ # Create project structure with folders and pproj file
+ from utils.project_structure_utils import setup_project_structure
+ parent_abs_path = global_state.custom_dir_path or os.path.join(os.getcwd(), global_state.recent_dir_path)
+ project_root = os.path.join(parent_abs_path, global_state.project_name_with_timestamp)
+ backend_status.write("Step 0: Creating project structure...")
+ setup_project_structure(project_root, global_state.project_name)
+ backend_status.write(f":white_check_mark: Project structure created at: {project_root}")
+
+
+def generate_p_code(design_doc_content, backend_status):
+ try:
+ """
+ Invokes the prompting_pipeline that calls llm to generate conversation responses and related code files.
+
+ Returns:
+ all_responses (dict): A dictionary where keys are log filenames and values are generated P code.
+ """
+ backend_status.write("Here in Generating P Code ")
+ system_prompt = file_utils.read_file(global_state.system_prompt_path)
+ all_responses = {}
+ parent_abs_path = global_state.custom_dir_path or os.path.join(os.getcwd(), global_state.recent_dir_path)
+ backend_status.write(f"Parent Abs path : {parent_abs_path} ")
+
+ try:
+ machines_list = generate_machine_names(system_prompt,design_doc_content, backend_status)
+ # Generate filenames
+ generate_filenames(system_prompt,design_doc_content,machines_list, backend_status, True)
+ # Generate enums, types, and events
+ response = generate_enum_types_events(system_prompt,design_doc_content,machines_list, backend_status)
+ backend_status.write("Running sanity check on response...")
+ # Run sanity check on the generated code
+ fixed_response = sanity_check(system_prompt, response, all_responses, machines_list, backend_status)
+
+ log_filename, Pcode = extract_validate_and_log_Pcode(fixed_response,
+ global_state.project_name_with_timestamp, PSRC)
+ if log_filename is not None and Pcode is not None:
+ file_abs_path = os.path.join(parent_abs_path, global_state.project_name_with_timestamp, PSRC, log_filename)
+ backend_status.write(f":blue[. . . filepath: {file_abs_path}]")
+ all_responses[log_filename] = Pcode
+
+ # Generate P models, specs, and tests
+ step_no = 2
+
+ for dirname, filenames in global_state.filenames_map.items():
+ if dirname != PSRC:
+ backend_status.write(f"Step {step_no}: Generating {dirname}")
+ step_no += 1
+
+ for filename in filenames:
+ backend_status.write(f"Generating file {filename}.p")
+ if dirname == PSRC and filename in machines_list:
+ log_filename, Pcode = generate_machine_code(system_prompt, design_doc_content, machines_list, filename, backend_status,
+ dirname, all_responses)
+ else:
+ log_filename, Pcode = generate_generic_file(system_prompt, design_doc_content, machines_list, filename, backend_status,
+ dirname, all_responses)
+
+ log_file_full_path = os.path.join(parent_abs_path, global_state.project_name_with_timestamp, dirname, log_filename)
+ backend_status.write(f":blue[. . . filepath: {log_file_full_path}]")
+ if log_filename is not None and Pcode is not None:
+ all_responses[log_filename] = Pcode
+
+ backend_status.write(f"Running the P compiler and analyzer on {dirname}...")
+ num_iterations = 15
+ compiler_analysis(system_prompt,all_responses, machines_list,num_iterations, backend_status)
+ return all_responses
+
+ except ClientError as err:
+ user_message = err.response['Error']['Message']
+ logger.error("A client error occurred: %s", user_message)
+ except Exception as e:
+ logger.error(e)
+ traceback.print_exc()
+ except FileNotFoundError as fe :
+ logger.error(fe)
+ traceback.print_exc()
+
+def generate_machine_names(system_prompt,design_doc_content,backend_status):
+ # Initialize the Pipeline
+ pipeline = prompting_pipeline.PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ # Get initial machine list
+ instructions = read_instructions()[LIST_OF_MACHINE_NAMES].format(userText=design_doc_content)
+ pipeline.add_user_msg(instructions, [global_state.P_basics_path])
+ pipeline.add_documents_inline(get_context_files()["P_PROGRAM_STRUCTURE"], tag_surround)
+ response = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+ log_token_count(pipeline, backend_status, "Generate Machine names")
+ return response
+
+def generate_filenames(system_prompt,design_doc_content,machines_list, backend_status, design_to_code_mode = False):
+ pipeline = prompting_pipeline.PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(design_doc_content)
+ pipeline.add_user_msg(machines_list)
+ pipeline.add_documents_inline(get_context_files()["P_PROGRAM_STRUCTURE"], tag_surround)
+ pipeline.add_user_msg(read_instructions()[LIST_OF_FILE_NAMES], [global_state.P_basics_path])
+ file_names = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+ log_token_count(pipeline, backend_status, "Generate Filenames")
+ global_state.filenames_map = extract_filenames(file_names)
+ return file_names
+
+def generate_enum_types_events(system_prompt,design_doc_content,machines_list,backend_status):
+ pipeline = prompting_pipeline.PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+
+ pipeline.add_user_msg(machines_list)
+ pipeline.add_documents_inline(get_context_files()["ENUMS_TYPES_EVENTS"], tag_surround)
+ pipeline.add_user_msg(f"All of the above are for your reference and context")
+
+ backend_status.write("Step 1: Generating PSrc")
+ backend_status.write("Generating a P file for P enums, types, and events...")
+
+ pipeline.add_user_msg(read_instructions()[ENUMS_TYPES_EVENTS], [global_state.P_basics_path])
+ pipeline.add_user_msg(f"This is the Design Document for which I want you to generate the code for : /n {design_doc_content}")
+
+ response = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+ log_token_count(pipeline, backend_status, "Generating enum_type_events.p")
+ return response
+
+def tag_surround(tagname, contents):
+ return f"<{os.path.basename(tagname)}>\n{contents}\n{os.path.basename(tagname)}>"
+
+
+def generate_machine_code(system_prompt, design_doc_content,machines_list, filename, backend_status, dirname, all_responses):
+ """Generate machine code using either two-stage or single-stage process."""
+ # Stage 1: Generate structure
+ backend_status.write(f" . . . Stage 1: Generating structure for {filename}.p")
+ pipeline = prompting_pipeline.PromptingPipeline()
+ pipeline.add_system_prompt(_safe_format(read_instructions()['MACHINE_STRUCTURE'], machineName=filename))
+ pipeline.add_documents_inline(get_context_files()["MACHINE_STRUCTURE"], tag_surround)
+ pipeline.add_user_msg("P_basics_file",[global_state.P_basics_path])
+ pipeline.add_user_msg(f"This is the Design Document for which I want you to generate the code for : /n {design_doc_content}")
+ pipeline.add_user_msg(f"Other relevant files of this P Program that may contain declarations: {json.dumps(all_responses)}")
+
+ stage1_response = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+
+ structure_pattern = r'(.*?)'
+ match = re.search(structure_pattern, stage1_response, re.DOTALL)
+ if match:
+ # Two-stage generation
+ machine_structure = match.group(1).strip()
+ backend_status.write(f" . . . Stage 2: Implementing function bodies for {filename}.p")
+ # Format instruction text first, then combine with machine structure
+ instruction_text = _safe_format(read_instructions()[MACHINE], machineName=filename)
+ # Replace any curly braces in machine structure with escaped versions
+ pipeline.add_user_msg(f"{instruction_text}\n\nHere is the starting structure:\n\n"+ machine_structure)
+ pipeline.add_documents_inline(get_context_files()[dirname], tag_surround)
+ response = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+ else:
+ # Fallback to single-stage
+ backend_status.write(f" . . . :red[Failed to extract structure for {filename}.p. Falling back to single-stage generation.]")
+ pipeline.add_user_msg(_safe_format(read_instructions()[MACHINE], machineName=filename))
+ pipeline.add_documents_inline(get_context_files()[dirname], tag_surround)
+ response = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+
+ log_token_count(pipeline, backend_status, f"Generate {filename}.p")
+ backend_status.write(f"Running sanity check on response...")
+ fixed_response = sanity_check(system_prompt, response, all_responses, machines_list, backend_status)
+ log_filename, Pcode = extract_validate_and_log_Pcode(fixed_response, global_state.project_name_with_timestamp, dirname)
+ return log_filename, Pcode
+
+
+def generate_generic_file(system_prompt, design_doc_content,machines_list, filename, backend_status,
+ dirname, all_responses):
+ """Generate a generic P file."""
+ pipeline = prompting_pipeline.PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_documents_inline(get_context_files()[dirname])
+ pipeline.add_user_msg(read_instructions()[dirname].format(filename=filename),[global_state.P_basics_path])
+ pipeline.add_user_msg(f"This is the Design Document for which I want you to generate the code for : /n {design_doc_content}")
+ pipeline.add_user_msg(f"Other relevant files of this P Program that may contain declarations: {json.dumps(all_responses)}")
+ response = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+ log_token_count(pipeline, backend_status, f"Generate {filename}.p")
+ backend_status.write(f"Running sanity check on response...")
+ fixed_response = sanity_check(system_prompt, response, all_responses, machines_list, backend_status)
+ log_filename, Pcode = extract_validate_and_log_Pcode(fixed_response, global_state.project_name_with_timestamp, dirname)
+ return log_filename, Pcode
+
+
+def extract_filenames(llm_response):
+ """
+ Extracts filenames from the LLM response and maps them to their respective folders.
+
+ Parameters:
+ llm_response (str): The response from the LLM containing filenames categorized by folders.
+
+ Returns:
+ filenames_map (dict): A dictionary where the keys are folder names and the values are lists of filenames without the '.p' extension.
+ """
+ folders = [PSRC, PSPEC, PTST]
+ filenames_map = {folder: [] for folder in folders}
+
+ lines = llm_response.split('\n')
+ for line in lines:
+ line = line.strip()
+ if not line or ': ' not in line:
+ continue
+ parts = line.split(': ', 1) # Split only on first ': '
+ if len(parts) != 2:
+ continue
+ folder, files = parts
+ folder = folder.strip()
+ if folder in folders:
+ filenames_map[folder] = [file.strip().replace('.p', '') for file in files.split(',') if file.strip()]
+
+ return filenames_map
+
+
+def extract_validate_and_log_Pcode(llm_response, parent_dirname, dirname, logging_enabled=True):
+ """
+ Extracts the P code from the LLM response, validates it, and logs it.
+
+ Parameters:
+ llm_response (str): The response from the LLM containing P code enclosed within <*.p> tags.
+ parent_dirname (str): The name of the parent directory where the log will be stored.
+ dirname (str): The name of the subdirectory where the log will be stored.
+
+ Returns:
+ tuple: A tuple containing the filename (str) and the validated P code (str).
+ If no matching content is found, returns None.
+ """
+ # for debugging original LLM response
+ log_utils.log_llmresponse(llm_response + "\n==================================================================\n")
+
+ filename, Pcode = parse_llm_response_with_code(llm_response)
+ if filename and Pcode:
+ # Run post-processing to fix common LLM mistakes
+ post_processor = PCodePostProcessor()
+ result = post_processor.process(Pcode)
+ Pcode = result.code
+
+ # Log validated P code
+ if logging_enabled:
+ log_utils.log_Pcode(Pcode, parent_dirname, dirname, filename)
+ return filename, Pcode
+ else:
+ logger.error("No matching .p file content found in the input string.")
+ return None, None
+
+def parse_llm_response_with_code(llm_response):
+ # Regular expression pattern to match content between <*.p> and *.p> tags
+ p_file_pattern = r'<(\w+\.p)>(.*?)\1>'
+
+ # Extract tag name and content
+ match = re.search(p_file_pattern, llm_response, re.DOTALL)
+ if match:
+ filename = match.group(1)
+ Pcode = match.group(2).strip()
+ return filename, Pcode
+ return None, None
+
+
+def set_project_name_from_design_doc(userTextInput):
+ """
+ Extracts and sets the project name from the design document.
+
+ Parameters:
+ userTextInput (str): The content of the design document as a string.
+
+ Sets:
+ global_state.project_name (str): The project name extracted from the top-level markdown heading, with spaces replaced by underscores.
+ global_state.project_name_with_timestamp (str): The project name appended with a timestamp in the format 'YYYY_MM_DD_HH_MM_SS'.
+ """
+ # Extract title from top-level markdown heading (# Title)
+ project_name_pattern = r'^#\s+(.+?)\s*$'
+
+ match = re.search(project_name_pattern, userTextInput, re.MULTILINE | re.IGNORECASE)
+ if match:
+ global_state.project_name = match.group(1).strip().replace(" ", "_")
+
+ timestamp = global_state.current_time.strftime('%Y_%m_%d_%H_%M_%S')
+ global_state.project_name_with_timestamp = f"{global_state.project_name}_{timestamp}"
+
+
+def sanity_check(system_prompt, response, all_responses, machines_list, backend_status):
+ """
+ Performs comprehensive sanity checks on a P code response using multiple focused tasks.
+
+ Args:
+ system_prompt: The system prompt for the LLM pipeline
+ response: The LLM response containing P code to check
+ all_responses: Dictionary containing all generated P files
+ machines_list: List of machine names in the project
+ backend_status: writing to streamlit interface
+
+ Returns:
+ Response string in the same format with fixed P code if changes were needed
+ """
+
+ # Get all .txt files from sanity_check directory
+ sanity_check_dir = global_state.sanity_check_folder
+ if not os.path.exists(sanity_check_dir):
+ backend_status.write(f"Warning: {sanity_check_dir} directory not found")
+ return response
+
+ # Get all .txt files and sort them to ensure consistent order for all the files
+ task_files = [f for f in os.listdir(sanity_check_dir) if f.endswith('.txt')]
+ task_files.sort()
+ if not task_files:
+ backend_status.write(f"No .txt files found in {sanity_check_dir} directory")
+ return response
+
+ enums_types_events = all_responses.get("Enums_Types_Events.p", "")
+ current_response = response
+
+ # Process each task sequentially
+ for i, task_file in enumerate(task_files, 1):
+ backend_status.write(f"Running sanity check task {i}/{len(task_files)}: {task_file}")
+
+ # Create a fresh pipeline for each task
+ pipeline = prompting_pipeline.PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+
+ # Add the specific task instructions
+ pipeline.add_user_msg(
+ f"Please follow the instructions in the attached task file to check and fix P language compliance issues.",
+ [global_state.P_basics_path, f"{sanity_check_dir}/{task_file}"]
+ )
+
+ # Add context and current response
+ pipeline.add_user_msg(f"""
+ CONTEXT:
+ 1. Core declarations (Enums_Types_Events.p):
+ {enums_types_events}
+
+ 2. Machine names in project:
+ {machines_list}
+
+ 3. Response to check and fix:
+ {current_response}
+
+ 4. Other relevant files that may contain declarations:
+ {json.dumps(all_responses, indent=2)}
+
+ IMPORTANT: Do NOT change any logic or functionality. Only fix syntax and compliance issues
+ as specified in the task file. Preserve all existing functionality while making syntax corrections.
+
+ Please analyze this response for the specific task requirements and apply ONLY the fixes
+ relevant to this task. Return the response in the same format with tags.
+ """)
+
+ # Get the fixed response for this task
+ fixed_response = pipeline.invoke_llm( global_state.model_id,candidates=1, heuristic='random')
+
+ # Update current_response for the next task
+ current_response = fixed_response
+
+ # Optional: Add a small delay between tasks to avoid rate limiting
+ time.sleep(0.5)
+
+ return current_response
+
+
+def get_latest_context(all_responses, machines_list):
+ enums_types_events = {"Enums_Types_Events.p": all_responses["Enums_Types_Events.p"]}
+ return {
+ "declarations": enums_types_events,
+ "all_files": list(all_responses.keys()),
+ "machine_names": machines_list
+ }
+
+
+def compiler_analysis(system_prompt,all_responses,machines_list, num_of_iterations, backend_status, ctx_pruning=None):
+ max_iterations = num_of_iterations
+ recent_project_path = file_utils.get_recent_project_path()
+
+ compilation_service = CompilationService()
+ compile_result = compilation_service.compile(recent_project_path)
+ compilation_success = compile_result.success
+ compilation_result = compile_result.stdout or compile_result.stderr or ""
+
+ if compilation_success:
+ backend_status.write(f":white_check_mark: :green[Compilation succeeded in {max_iterations - num_of_iterations} iterations.]")
+ logger.info(f"Compilation succeeded in {max_iterations - num_of_iterations} iterations.")
+ return
+
+ P_filenames_dict = regex_utils.get_all_P_files(compilation_result)
+ while (not compilation_success and num_of_iterations > 0):
+ # Parse error from compilation output
+ errors = compilation_service.get_all_errors(compilation_result)
+ if not errors:
+ break
+ error = errors[0]
+ file_name = os.path.basename(error.file_path)
+ line_number = error.line_number
+ column_number = error.column_number
+ error_message = error.message
+
+ current_iteration = max_iterations - num_of_iterations
+ backend_status.write(f". . . :red[[Iteration #{current_iteration}] Compilation failed in {file_name} at line {line_number}:{column_number}. Fixing the error...]")
+
+ # Get file path and contents
+ file_path = P_filenames_dict.get(file_name, error.file_path)
+ file_contents = file_utils.read_file(file_path) if os.path.exists(file_path) else ""
+
+ # Build correction instruction
+ custom_msg = f"Fix the following compilation error in {file_name} at line {line_number}, column {column_number}:\n{error_message}"
+
+ # Get latest context with most up-to-date file versions
+ latest_context = get_latest_context(all_responses, machines_list)
+ # Get the required specific filename from llm to fix the issue
+ required_machines = get_required_filename_from_llm(system_prompt, latest_context, file_name, line_number,
+ column_number, custom_msg, file_contents, backend_status)
+ additional_context = {}
+ if required_machines:
+ for machine_name in required_machines:
+ additional_context[machine_name] = all_responses[machine_name] # Access dictionary directly
+ custom_msg += "\nReturn only the generated P code without any explanation attached. Return the P code enclosed in XML tags where the tag name is the filename."
+
+ # Actual Pipeline call
+ pipeline = prompting_pipeline.PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_documents_inline(get_context_files()["COMPILE"], tag_surround)
+
+ # Send context and error information
+ pipeline.add_user_msg(f"""
+ Here is the current context for fixing the compilation error:
+
+ 1. Core declarations (types, events, enums):
+ {json.dumps(latest_context["declarations"], indent=2)}
+
+ 2. Available machines in the program:
+ {json.dumps(latest_context["machine_names"], indent=2)}
+
+ 3. Error details:
+ - File to fix: {file_name}
+ - Error location: Line {line_number}, Column {column_number}
+ - Error message: {custom_msg}
+
+ 4. Current file contents:
+ {file_contents}
+
+ 5. Additional Context : {additional_context}
+
+ """)
+
+ response = pipeline.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+ log_token_count(pipeline, backend_status, "Compiler Analysis")
+ backend_status.write(f". . . . . . Compiling the fixed code...")
+ log_filename, Pcode = extract_validate_and_log_Pcode(response, "", "", logging_enabled=False)
+ if log_filename is not None and Pcode is not None:
+ log_utils.log_Pcode_to_file(Pcode, file_path)
+ all_responses[log_filename] = Pcode
+ num_of_iterations -= 1
+
+ # Log the diff
+ log_utils.log_code_diff(file_contents, response, "After fixing the P code")
+ compile_result = compilation_service.compile(recent_project_path)
+ compilation_success = compile_result.success
+ compilation_result = compile_result.stdout or compile_result.stderr or ""
+
+ if compilation_success:
+ backend_status.write(f":green[Compilation succeeded in {max_iterations - num_of_iterations} iterations.]")
+ logger.info(f"Compilation succeeded in {max_iterations - num_of_iterations} iterations.")
+ global_state.compile_iterations += (max_iterations - num_of_iterations)
+ global_state.compile_success = compilation_success
+
+
+def get_required_filename_from_llm(system_prompt, latest_context, file_name, line_number, column_number, custom_msg, file_contents, backend_status):
+ # Ask llm for the required specific file names - instead of sending all the files
+ pipeline_for_req_files = prompting_pipeline.PromptingPipeline()
+ pipeline_for_req_files.add_system_prompt(system_prompt)
+ pipeline_for_req_files.add_user_msg(f"""
+ Here is the current context for fixing the compilation error:
+
+ 1. Core declarations (types, events, enums):
+ {json.dumps(latest_context["declarations"], indent=2)}
+
+
+ 2. Available machines in the program:
+ {json.dumps(latest_context["machine_names"], indent=2)}
+ 3. Error details:
+ - File to fix: {file_name}
+ - Error location: Line {line_number}, Column {column_number}
+ - Error message: {custom_msg}
+
+ 4. Current file contents:
+ {file_contents}
+
+ 5. All Available Files : {json.dumps(list(latest_context["all_files"]))}
+ Before you proceed with fixing this compilation error, do you need the implementation details of any specific file from the All Available Files to understand the context better and to help fix the issue?. Note that the file must be existing in teh provided context
+
+ Only respond with a JSON list of files that you need to see the implementation for without any additional explanation. If you don't need any additional machine context, return an empty list [].
+
+ Example response format:
+ ["MachineName1.p", "MachineName2.p", "Spec1.p", "TestDriver".p] or []
+ """)
+
+ required_machines_response = pipeline_for_req_files.invoke_llm(global_state.model_id, candidates=1, heuristic='random')
+ logger.info("Required machines response: %s", repr(required_machines_response))
+
+ if isinstance(required_machines_response, str):
+ try:
+ required_machines = json.loads(required_machines_response.strip())
+ except json.JSONDecodeError as e:
+ logger.error(f"Failed to parse JSON from required_machines_response: {repr(required_machines_response)}")
+ backend_status.write(f"Failed to parse JSON from required_machines_response: {repr(required_machines_response)}")
+ raise e
+ except Exception as ex:
+ logger.error("Exception occurred in compiler_analysis ")
+ backend_status.write("Exception occurred in compiler_analysis ")
+ raise ex
+ else:
+ logger.error(f"Expected string response, got {type(required_machines_response)} instead.")
+ raise TypeError("Expected string response from LLM")
+
+ return required_machines
+
+
+def log_token_count(pipeline, backend_status, task = ""):
+ input_tokens = pipeline.get_total_input_tokens()
+ output_tokens = pipeline.get_total_output_tokens()
+ logger.info(f"{task} :::: Input tokens: {input_tokens}, Output tokens: {output_tokens}")
+ backend_status.write(f"{task} - Total input tokens : {input_tokens}")
+ backend_status.write(f"{task} - Total output tokens : {output_tokens}")
+
diff --git a/Src/PeasyAI/src/utils/global_state.py b/Src/PeasyAI/src/utils/global_state.py
new file mode 100644
index 0000000000..eed589360b
--- /dev/null
+++ b/Src/PeasyAI/src/utils/global_state.py
@@ -0,0 +1,160 @@
+import os
+from datetime import datetime, timezone
+
+# Base paths
+PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+SRC_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+def _resolve_resources_dir():
+ """Resolve the resources directory, handling both dev and installed layouts."""
+ # 1. Env var set by the pip-installed entry point
+ env_dir = os.environ.get("PEASYAI_RESOURCES_DIR")
+ if env_dir and os.path.isdir(env_dir):
+ return env_dir
+ # 2. Dev checkout: resources/ next to src/
+ dev_dir = os.path.join(PROJECT_ROOT, "resources")
+ if os.path.isdir(dev_dir):
+ return dev_dir
+ # 3. Installed wheel: peasyai_resources/ in site-packages
+ site_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "peasyai_resources")
+ site_dir = os.path.normpath(site_dir)
+ if os.path.isdir(site_dir):
+ return site_dir
+ return dev_dir
+
+RESOURCES_DIR = _resolve_resources_dir()
+
+# P Language Constants
+REGION = 'us-west-2'
+
+
+# model ids
+# Mistral Large 2 (24.07)
+model_id_mistral = "mistral.mistral-large-2407-v1:0"
+
+# Claude 3 Sonnet
+model_id_sonnet3 = "anthropic.claude-3-sonnet-20240229-v1:0"
+
+# Claude 3.5 Sonnet
+model_id_sonnet3_5 = "anthropic.claude-3-5-sonnet-20240620-v1:0"
+model_id_sonnet3_5_v2 = "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
+
+# Claude 3.7 Sonnet
+model_id_sonnet3_7 = "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
+model_id_sonnet4 = "us.anthropic.claude-sonnet-4-20250514-v1:0"
+model_id_opus_4 = "us.anthropic.claude-opus-4-20250514-v1:0"
+
+# Model-specific token limits
+model_token_limits = {
+ "us.anthropic.claude-opus-4-20250514-v1:0": 65536,
+ "us.anthropic.claude-sonnet-4-20250514-v1:0": 65536,
+ "us.anthropic.claude-3-7-sonnet-20250219-v1:0": 100000,
+ "us.anthropic.claude-3-5-sonnet-20241022-v2:0": 100000,
+ "us.anthropic.claude-3-5-sonnet-20240620-v1:0": 100000,
+ "anthropic.claude-3-sonnet-20240229-v1:0": 100000,
+ "mistral.mistral-large-2407-v1:0": 100000
+}
+
+# Default model and its token limit
+model_id = model_id_sonnet3_7
+maxTokens = model_token_limits[model_id] # Initialize with default model's limit
+temperature = 1.0
+topP = 0.999
+
+model_metrics = {
+ "inputTokens": 0,
+ "outputTokens": 0,
+ "latencyMs": 0
+}
+compile_iterations = 0
+compile_success = False
+total_runtime = 0
+
+project_name = "PeasyAI"
+project_name_with_timestamp = "PeasyAI"
+filenames_map = {}
+
+current_time = datetime.now(timezone.utc)
+
+# P Instruction Files
+system_prompt_path = os.path.join(RESOURCES_DIR, "context_files", "about_p.txt")
+P_syntax_guide_path = os.path.join(RESOURCES_DIR, "context_files", "P_syntax_guide.txt")
+P_basics_path = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_basics.txt")
+P_program_example_path = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_program_example.txt")
+initial_instructions_path = os.path.join(RESOURCES_DIR, "instructions", "initial_instructions.txt")
+generate_filenames_instruction_path = os.path.join(RESOURCES_DIR, "instructions", "generate_filenames.txt")
+generate_enums_types_events_instruction_path = os.path.join(RESOURCES_DIR, "instructions", "generate_enums_types_events.txt")
+generate_machine_instruction_path = os.path.join(RESOURCES_DIR, "instructions", "generate_machine.txt")
+generate_modules_file_instruction_path = os.path.join(RESOURCES_DIR, "instructions", "generate_modules_file.txt")
+generate_spec_files_instruction_path = os.path.join(RESOURCES_DIR, "instructions", "generate_spec_files.txt")
+generate_test_files_instruction_path = os.path.join(RESOURCES_DIR, "instructions", "generate_test_files.txt")
+generate_design_doc = os.path.join(RESOURCES_DIR, "instructions", "generate_design_doc.txt")
+generate_code_description = os.path.join(RESOURCES_DIR, "instructions", "generate_code_description.txt")
+generate_sop_spec = os.path.join(RESOURCES_DIR, "instructions", "generate_sop_spec.txt")
+generate_files_to_fix_for_pchecker = os.path.join(RESOURCES_DIR, "instructions", "generate_files_to_fix_for_pchecker.txt")
+generate_fixed_file_for_pchecker = os.path.join(RESOURCES_DIR, "instructions", "generate_fixed_file_for_pchecker.txt")
+generate_machine_structure_path = os.path.join(RESOURCES_DIR, "instructions", "generate_machine_structure.txt")
+generate_project_structure_path = os.path.join(RESOURCES_DIR, "instructions", "generate_project_structure.txt")
+sanity_check_instructions_path = os.path.join(RESOURCES_DIR, "instructions", "p_code_sanity_check.txt")
+sanity_check_folder = os.path.join(RESOURCES_DIR, "instructions", "sanity_checks")
+
+template_path_hot_state_bug_analysis = os.path.join(RESOURCES_DIR, "instructions", "semantic-fix-sets", "hot-state", "analysis_prompt.txt")
+template_ask_llm_which_files_it_needs = os.path.join(RESOURCES_DIR, "instructions", "streamlit-snappy", "1_ask_llm_which_files_it_needs.txt")
+generate_fix_patches_for_file = os.path.join(RESOURCES_DIR, "instructions", "streamlit-snappy", "2_generate_fix_patches_for_file.txt")
+
+# P Language Context Files
+P_ENUMS_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_enums_guide.txt")
+P_TYPES_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_types_guide.txt")
+P_EVENTS_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_events_guide.txt")
+P_MACHINES_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_machines_guide.txt")
+P_STATEMENTS_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_statements_guide.txt")
+P_MODULE_SYSTEM_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_module_system_guide.txt")
+P_SPEC_MONITORS_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_spec_monitors_guide.txt")
+P_TEST_CASES_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_test_cases_guide.txt")
+P_PROGRAM_STRUCTURE_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_program_structure_guide.txt")
+P_SYNTAX_SUMMARY = os.path.join(RESOURCES_DIR, "context_files", "modular-fewshot", "p-fewshot-formatted.txt")
+P_COMPILER_GUIDE = os.path.join(RESOURCES_DIR, "context_files", "modular", "p_compiler_guide.txt")
+
+# generated code
+logs_dir_path = os.path.join(PROJECT_ROOT, "generated_code")
+recent_dir_path = os.path.join(PROJECT_ROOT, "generated_code", "recent")
+archive_base_dir_path = os.path.join(PROJECT_ROOT, "generated_code", "archive")
+full_log_path = os.path.join(PROJECT_ROOT, "generated_code", "full_log.txt")
+code_diff_log_path = os.path.join(PROJECT_ROOT, "generated_code", "code_diff_log.txt")
+custom_dir_path = None
+
+# generated docs
+docs_recent_dir_path = os.path.join(SRC_DIR, "generated_docs")
+
+# chat history files
+chat_history_path = os.path.join(SRC_DIR, "chat_history")
+
+# checker output folder name
+pchecker_output_folder = "PCheckerOutput"
+bugfinding_folder = "BugFinding"
+
+pproj_template_path = os.path.join(RESOURCES_DIR, "assets", "pproj_template.txt")
+
+# compiler error files
+general_errors_list_path = os.path.join(RESOURCES_DIR, "compile_analysis", "generic_errors.json")
+specific_errors_list_path = os.path.join(RESOURCES_DIR, "compile_analysis", "errors.json")
+
+# user mode vs admin mode
+current_mode = "admin"
+
+class _LazyChatHistory:
+ """Defers ChatHistory import so non-Streamlit callers never touch streamlit."""
+
+ _instance = None
+
+ def __getattr__(self, name):
+ if _LazyChatHistory._instance is None:
+ from utils.chat_history import ChatHistory
+ _LazyChatHistory._instance = ChatHistory()
+ return getattr(_LazyChatHistory._instance, name)
+
+
+chat_history = _LazyChatHistory()
+
+# Flag to track if we've already attempted a restart
+has_restarted = False
diff --git a/Src/PeasyAI/src/utils/log_utils.py b/Src/PeasyAI/src/utils/log_utils.py
new file mode 100644
index 0000000000..d57527a61e
--- /dev/null
+++ b/Src/PeasyAI/src/utils/log_utils.py
@@ -0,0 +1,73 @@
+from utils import file_utils
+from utils import global_state as globals
+import os, logging, difflib
+from io import StringIO
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
+
+def log_Pcode(Pcode, parent_dirname, dirname, filename):
+ """
+ Logs the provided P code to a specified file within a directory structure.
+ """
+ folder_path = globals.custom_dir_path or globals.recent_dir_path
+ # If filename already contains dirname (e.g. "PSrc/Client.p"), don't add dirname again
+ if dirname in filename:
+ file_path = os.path.join(folder_path, parent_dirname, filename)
+ else:
+ file_path = os.path.join(folder_path, parent_dirname, dirname, filename)
+ logger.info(f"Writing P code to: {file_path}")
+ file_utils.write_file(file_path, Pcode)
+
+
+def log_Pcode_to_file(Pcode, file_path):
+ """
+ Logs the provided P code to the specified file path.
+ """
+ logger.info(f"Writing P code to: {file_path}")
+ file_utils.write_file(file_path, Pcode)
+
+
+def move_recent_to_archive():
+ """
+ Moves the recent directory content to the archive base directory.
+ """
+ file_utils.ensure_dir_exists(globals.recent_dir_path)
+ if not file_utils.is_dir_empty(globals.recent_dir_path):
+ file_utils.ensure_dir_exists(globals.archive_base_dir_path)
+ file_utils.move_dir_contents(globals.recent_dir_path, globals.archive_base_dir_path)
+
+
+def log_llmresponse(llmresponse):
+ """
+ Logs the LLM response to a file within the logs directory.
+ """
+ file_utils.append_file(globals.full_log_path, llmresponse)
+
+
+def log_code_diff(original_code, modified_code, header_message):
+ """
+ Generate and log the diff between original and modified code.
+
+ Args:
+ original_code (str): Original P code content
+ modified_code (str): Modified P code content after fixes
+ """
+ original_lines = original_code.splitlines()
+ modified_lines = modified_code.splitlines()
+
+ # Generate diff
+ diff = difflib.unified_diff(original_lines, modified_lines, lineterm='')
+
+ # Use StringIO for efficient string concatenation
+ diff_output = StringIO()
+ diff_output.write(f"{header_message}:\n")
+ diff_output.write('\n'.join(diff))
+
+ # Log the entire diff at once
+ file_utils.append_file(globals.code_diff_log_path, diff_output.getvalue())
+
+
+
+
+
diff --git a/Src/PeasyAI/src/utils/module_utils.py b/Src/PeasyAI/src/utils/module_utils.py
new file mode 100644
index 0000000000..30c269ce37
--- /dev/null
+++ b/Src/PeasyAI/src/utils/module_utils.py
@@ -0,0 +1,10 @@
+def save_module_state(module):
+ state = {}
+ for attr in dir(module):
+ if not attr.startswith("__"): # Skip built-in attributes
+ state[attr] = getattr(module, attr)
+ return state
+
+def restore_module_state(module, state):
+ for var_name, value in state.items():
+ setattr(module, var_name, value)
\ No newline at end of file
diff --git a/Src/PeasyAI/src/utils/project_structure_utils.py b/Src/PeasyAI/src/utils/project_structure_utils.py
new file mode 100644
index 0000000000..1d34c7d5fa
--- /dev/null
+++ b/Src/PeasyAI/src/utils/project_structure_utils.py
@@ -0,0 +1,65 @@
+"""
+Utility functions for creating P project structure.
+"""
+
+import os
+import shutil
+from utils import file_utils, global_state
+
+def create_project_directories(project_root):
+ """
+ Creates the standard P project directories.
+
+ Args:
+ project_root: Root directory for the P project
+ """
+ directories = [
+ os.path.join(project_root, "PSrc"),
+ os.path.join(project_root, "PSpec"),
+ os.path.join(project_root, "PTst"),
+ os.path.join(project_root, "PGenerated"),
+ ]
+
+ for directory in directories:
+ os.makedirs(directory, exist_ok=True)
+
+ return directories
+
+def generate_pproj_file(project_root, project_name):
+ """
+ Creates a .pproj file in the project root directory.
+
+ Args:
+ project_root: Root directory for the P project
+ project_name: Name of the P project
+ """
+ pproj_template = file_utils.read_file(global_state.pproj_template_path)
+ pproj_content = pproj_template.replace("{project_name}", project_name)
+
+ pproj_path = os.path.join(project_root, f"{project_name}.pproj")
+ file_utils.write_file(pproj_path, pproj_content)
+ print("Crerated Project Structure :white_check_mark:")
+
+ return pproj_path
+
+def setup_project_structure(project_root, project_name):
+ """
+ Sets up the complete P project structure including directories and .pproj file.
+
+ Args:
+ project_root: Root directory for the P project
+ project_name: Name of the P project
+
+ Returns:
+ Dictionary with created directories and files
+ """
+ # Create project directories
+ directories = create_project_directories(project_root)
+
+ # Create .pproj file
+ pproj_path = generate_pproj_file(project_root, project_name)
+
+ return {
+ "directories": directories,
+ "pproj_file": pproj_path
+ }
diff --git a/Src/PeasyAI/src/utils/regex_utils.py b/Src/PeasyAI/src/utils/regex_utils.py
new file mode 100644
index 0000000000..1df92fbd22
--- /dev/null
+++ b/Src/PeasyAI/src/utils/regex_utils.py
@@ -0,0 +1,8 @@
+import re
+from utils import file_utils
+
+
+def get_all_P_files(text):
+ """Extract P file paths from compiler output and map filenames to full paths."""
+ p_file_pattern = r'file: ([\S\s]+?\.p)'
+ return file_utils.map_p_file_to_path(re.findall(p_file_pattern, text))
diff --git a/Src/PeasyAI/src/utils/string_utils.py b/Src/PeasyAI/src/utils/string_utils.py
new file mode 100644
index 0000000000..89505db412
--- /dev/null
+++ b/Src/PeasyAI/src/utils/string_utils.py
@@ -0,0 +1,225 @@
+import whatthepatch
+import re
+
+def tag_surround(tagname, contents):
+ return f"<{tagname}>\n{contents}\n{tagname}>"
+
+def file_dict_to_prompt(file_dict, pre="", post=""):
+ """
+ Converts a dictionary of file paths and contents into a single string with XML-style tags.
+
+ Parameters:
+ file_dict (dict): Dictionary mapping file paths to file contents
+ pre (str): String to prepend to the result
+ post (str): String to append to the result
+
+ Returns:
+ str: Combined string with each file's content wrapped in XML tags
+ """
+ result = pre
+
+ for filepath, contents in file_dict.items():
+ result += f"<{filepath}>\n{contents}\n{filepath}>\n"
+
+ result += post
+ return result
+
+def snake_to_title(s):
+ """
+ Converts a snake_case string to Title Case.
+
+ Parameters:
+ s (str): Snake case string (e.g., "section_name_here")
+
+ Returns:
+ str: Title case string (e.g., "Section Name Here")
+ """
+ return " ".join(word.capitalize() for word in s.split("_"))
+
+def tags_to_md(s, tag_level=4):
+ """
+ Converts a string with XML-style tagged sections into markdown format.
+
+ Parameters:
+ s (str): Input string with sections wrapped in tags like content
+ tag_level (int): The heading level to use for section names in markdown (default: 4)
+
+ Returns:
+ str: Markdown formatted string with section tags converted to headings
+ """
+
+ # Initialize result string
+ result = ""
+
+ # Pattern to match tagged sections: content
+ pattern = r"<([^>]+)>(.*?)\1>"
+
+ # Find all matches in the input string
+ matches = re.finditer(pattern, s, re.DOTALL)
+
+ # Process each match
+ last_end = 0
+ for match in matches:
+ # Add any text between matches
+ result += s[last_end:match.start()]
+
+ # Extract tag name and content
+ tag_name = match.group(1)
+ content = match.group(2).strip()
+
+ # Convert tag name from snake_case to Title Case
+ title = snake_to_title(tag_name)
+
+ # Create markdown heading with appropriate level
+ heading = "#" * tag_level
+
+ # Add heading and content to result
+ result += f"{heading} {title}\n{content}\n\n"
+
+ last_end = match.end()
+
+ # Add any remaining text after last match
+ result += s[last_end:]
+
+ return result.strip()
+
+def add_line_numbers(s):
+ """
+ Prepends line numbers to each line in the input string.
+
+ Parameters:
+ s (str): Input string with multiple lines
+
+ Returns:
+ str: String with line numbers added at the start of each line
+ """
+ # Split the string into lines while preserving empty lines
+ lines = s.splitlines()
+
+ # Process each line
+ numbered_lines = []
+ for i, line in enumerate(lines, 1):
+ lstripped = line.lstrip()
+ leading_space = len(line) - len(lstripped)
+
+ if not line and not lstripped:
+ lstripped = "[empty line]"
+ if not lstripped and line:
+ lstripped = f"[{leading_space} spaces]"
+
+ # Preserve any leading whitespace
+ # Add line number to all lines, including empty ones
+ numbered_line = f"{' ' * leading_space}{i}. {lstripped}"
+ numbered_lines.append(numbered_line)
+
+ # Join the lines back together
+ return "\n".join(numbered_lines)
+
+def apply_patch_whatthepatch_per_file(patch_content_dict, file_contents):
+ """
+ Apply a unified diff patch using the whatthepatch library.
+
+ Requirements:
+ pip install whatthepatch
+ """
+ result = {k:(c, "") for k,c in file_contents.copy().items()}
+
+ for fname, patch_content in patch_content_dict.items():
+ print(f"Applying patch for {fname}")
+ diffs = list(whatthepatch.parse_patch(patch_content))
+ print(f"DIFFS LENGTH: {len(diffs)}")
+ if len(diffs) > 1:
+ raise Exception(f"More than expected diffs per file {len(diffs)}")
+
+ diff = diffs[0]
+ file_path = diff.header.new_path
+
+ if file_path not in file_contents:
+ print(f"{file_path} not in file_contents dictionary")
+ continue
+
+ original_lines = file_contents[file_path].splitlines()
+
+ try:
+ # whatthepatch can apply patches directly
+ new_content = whatthepatch.apply_diff(diff, original_lines)
+ result[file_path] = ('\n'.join(new_content), "")
+
+ except Exception as e:
+ err_msg = f"Could not apply patch to {file_path}: {e}"
+ result[file_path] = (file_contents[file_path], err_msg)
+ print(err_msg)
+ continue
+
+ return result
+
+
+def extract_tag_contents(full_string, tag_name):
+ """
+ Extracts the contents between XML-style tags in a multiline string.
+
+ Parameters:
+ full_string (str): The input string containing tagged content
+ tag_name (str): The name of the tag to extract content from
+
+ Returns:
+ str: The content between the opening and closing tags, or None if not found
+ """
+ pattern = f"<{tag_name}>(.*?){tag_name}>"
+ match = re.search(pattern, full_string, re.DOTALL)
+ if match:
+ return match.group(1).strip()
+ return None
+
+
+def parse_patches_by_file(patch_content):
+ """
+ Parse a string containing multiple patches and return a dictionary
+ mapping file paths to their individual patch content.
+
+ Args:
+ patch_content (str): String containing multiple patches
+
+ Returns:
+ dict: Dictionary mapping file paths to patch strings
+ """
+ patches_by_file = {}
+
+ lines = patch_content.strip().split('\n')
+ current_patch_lines = []
+ current_file = None
+
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+
+ # Check if this is the start of a new patch
+ if line.startswith('--- '):
+ # Save previous patch if it exists
+ if current_file and current_patch_lines:
+ patches_by_file[current_file] = '\n'.join(current_patch_lines)
+
+ # Start new patch
+ current_patch_lines = [line]
+
+ # Get the next line which should be the +++ line
+ if i + 1 < len(lines) and lines[i + 1].startswith('+++ '):
+ i += 1
+ plus_line = lines[i]
+ current_patch_lines.append(plus_line)
+
+ # Extract file path from +++ line
+ current_file = plus_line[4:].split('\t')[0].strip()
+
+ else:
+ # Add line to current patch
+ if current_patch_lines:
+ current_patch_lines.append(line)
+
+ i += 1
+
+ # Don't forget the last patch
+ if current_file and current_patch_lines:
+ patches_by_file[current_file] = '\n'.join(current_patch_lines)
+
+ return patches_by_file
\ No newline at end of file
diff --git a/Src/PeasyAI/tests/pipeline/pipeline_tests.py b/Src/PeasyAI/tests/pipeline/pipeline_tests.py
new file mode 100644
index 0000000000..1aeea5627e
--- /dev/null
+++ b/Src/PeasyAI/tests/pipeline/pipeline_tests.py
@@ -0,0 +1,1786 @@
+import os, logging, shutil
+from pathlib import Path
+from core.pipelining.prompting_pipeline import PromptingPipeline
+from utils import file_utils
+import pytest
+import subprocess
+import json
+# from legacy.app_modes import DesignDocInputMode
+from glob import glob
+from sentence_transformers import SentenceTransformer
+from utils.constants import CLAUDE_3_7
+
+logger = logging.getLogger(__name__)
+
+# CLAUDE_3_7 = "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
+SAVED_GLOBAL_STATE = None
+
+# RAG_MODEL_aMLML6v2 = SentenceTransformer('all-MiniLM-L6-v2')
+# RAG_MODEL_aMLML12v2 = SentenceTransformer('all-MiniLM-L12-v2')
+
+
+
+def test1(ddoc: str | None = None, out_dir: str | None = None):
+ from core.modes.pipelines import old_pipeline_replicated
+ project_root = Path(__file__).parent.parent.parent
+ ddoc = ddoc or str(project_root / "resources" / "p-model-benchmark" / "3_designdoc2pprojA" / "1_lightswitch.txt")
+ out_dir = out_dir or str(project_root / ".tmp" / "pipeline-tests")
+ old_pipeline_replicated(ddoc, out_dir=out_dir)
+
+
+class MockStatus:
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ def write(self, msg):
+ pass
+
+ def update(self, **kwargs):
+ pass
+
+class MockContainer:
+ def status(self, msg, expanded=True):
+ return MockStatus()
+
+def tag_surround(tagname, contents):
+ return f"<{tagname}>\n{contents}\n{tagname}>"
+
+@pytest.fixture
+def doc_list():
+ modular_dir = Path("resources/context_files/modular")
+ return [str(path) for path in modular_dir.glob("*.txt")]
+
+def tag_surround_relative(path, contents):
+ tag_name = str(Path(*Path(path).parts[-2:]))
+ return f"<{tag_name}>\n{contents}\n{tag_name}>"
+
+# @pytest.mark.parametrize("benchmark_dir", ["resources/p-model-benchmark/8_dd2psrcA/"])
+# def test_taskgen_dd2psrc(benchmark_dir):
+# task_dirs = glob(f"{benchmark_dir}/*")
+# tasks = []
+# for task_dir_path in task_dirs:
+# task_name = Path(task_dir_path).stem
+# design_doc = glob(f"{task_dir_path}/*.txt")[0]
+# prompt_pre = "Here are the PSpec and PTst files:\n"
+# prompt_post = "\n\nGenerate the code for file(s) that should be in PSrc, that satisfy this test configuration. All the types, enums, and events you need are already declared in PSrc/Enums_Types_Events.p.\n"
+# p_files = glob(f"{task_dir_path}/PSpec/*.p") + glob(f"{task_dir_path}/PTst/*.p")
+# print(f"p_files = {p_files}")
+# full_prompt = file_utils.combine_files(p_files, pre=prompt_pre, post=prompt_post, preprocessing_function=tag_surround_relative)
+# task = (task_name, design_doc, full_prompt)
+# tasks.append(task)
+# print(f"tasks = {tasks}")
+# return tasks
+
+@pytest.mark.parametrize("benchmark_dir", ["resources/p-model-benchmark/8_dd2psrcA/"])
+def test_taskgen_dd2psrc(benchmark_dir):
+ task_dirs = glob(f"{benchmark_dir}/*")
+ tasks = []
+ for task_dir_path in task_dirs:
+ task_name = Path(task_dir_path).stem
+ globout = glob(f"{task_dir_path}/*.txt")
+ print(f"globout = {globout}")
+ design_doc = globout[0]
+ # p_files = glob(f"{task_dir_path}/PSpec/*.p") + glob(f"{task_dir_path}/PTst/*.p")
+ task = (task_name, design_doc, task_dir_path)
+ tasks.append(task)
+ print(f"tasks = {tasks}")
+ return tasks
+
+def taskgen_dd2proj(design_docs_dir):
+ design_docs = glob(f"{design_docs_dir}/*.txt") + glob(f"{design_docs_dir}/*.md")
+ tests = list(map(lambda dd: (Path(dd).stem, dd), design_docs))
+ print(f"DETECTED TESTS: {tests}")
+ return tests
+
+
+# =======================================================================================
+from utils import global_state, log_utils, compile_utils, regex_utils
+from core.services.compilation import CompilationService
+import re
+import os
+from utils.generate_p_code import extract_filenames, extract_validate_and_log_Pcode, get_context_files
+
+LIST_OF_MACHINE_NAMES = 'list_of_machine_names'
+LIST_OF_FILE_NAMES = 'list_of_file_names'
+ENUMS_TYPES_EVENTS = 'enums_types_events'
+MACHINE = 'machine'
+MACHINE_STRUCTURE = "MACHINE_STRUCTURE"
+PROJECT_STRUCTURE="PROJECT_STRUCTURE"
+
+PSRC = 'PSrc'
+PSPEC = 'PSpec'
+PTST = 'PTst'
+
+
+instructions = {
+ LIST_OF_MACHINE_NAMES: file_utils.read_file(global_state.initial_instructions_path),
+ LIST_OF_FILE_NAMES: file_utils.read_file(global_state.generate_filenames_instruction_path),
+ ENUMS_TYPES_EVENTS: file_utils.read_file(global_state.generate_enums_types_events_instruction_path),
+ MACHINE_STRUCTURE: file_utils.read_file(global_state.generate_machine_structure_path),
+ PROJECT_STRUCTURE: file_utils.read_file(global_state.generate_project_structure_path),
+ MACHINE: file_utils.read_file(global_state.generate_machine_instruction_path),
+ PSRC: file_utils.read_file(global_state.generate_modules_file_instruction_path),
+ PSPEC: file_utils.read_file(global_state.generate_spec_files_instruction_path),
+ PTST: file_utils.read_file(global_state.generate_test_files_instruction_path)
+}
+
+def get_context_files():
+ return {
+ "ENUMS_TYPES_EVENTS": [
+ global_state.P_ENUMS_GUIDE,
+ global_state.P_TYPES_GUIDE,
+ global_state.P_EVENTS_GUIDE
+ ],
+ "MACHINE_STRUCTURE": [
+ global_state.P_MACHINES_GUIDE
+ ],
+ "MACHINE": [
+ global_state.P_STATEMENTS_GUIDE
+ ],
+ PSRC: [
+ global_state.P_MODULE_SYSTEM_GUIDE
+ ],
+ PSPEC: [
+ global_state.P_SPEC_MONITORS_GUIDE
+ ],
+ PTST: [
+ global_state.P_TEST_CASES_GUIDE
+ ]
+ }
+
+def get_recent_project_path():
+
+ if global_state.custom_dir_path != None:
+ file_path = os.path.join(global_state.custom_dir_path, global_state.project_name_with_timestamp)
+ return file_path
+ if file_utils.check_directory(global_state.recent_dir_path):
+ for proj in file_utils.list_top_level_contents(global_state.recent_dir_path):
+ file_path = os.path.join(os.getcwd(), global_state.recent_dir_path + "/" + proj)
+ return file_path
+ return None
+
+def compiler_analysis(model_id, pipeline, all_responses, num_of_iterations, ctx_pruning=None):
+ max_iterations = num_of_iterations
+ recent_project_path = file_utils.get_recent_project_path()
+
+ compilation_service = CompilationService()
+ compile_result = compilation_service.compile(recent_project_path)
+ compilation_success = compile_result.success
+ compilation_output = compile_result.stdout or compile_result.stderr or ""
+
+ if compilation_success:
+ logger.info(f":white_check_mark: :green[Compilation succeeded in {max_iterations - num_of_iterations} iterations.]")
+ logger.info(f"Compilation succeeded in {max_iterations - num_of_iterations} iterations.")
+ return
+
+ P_filenames_dict = regex_utils.get_all_P_files(compilation_output)
+ while (not compilation_success and num_of_iterations > 0):
+ # Parse errors using CompilationService
+ errors = compilation_service.get_all_errors(compilation_output)
+ if not errors:
+ break
+ error = errors[0]
+ file_name = os.path.basename(error.file_path)
+ line_number = error.line_number
+ column_number = error.column_number
+
+ logger.info(f". . . :red[[Iteration #{(max_iterations - num_of_iterations)}] Compilation failed in {file_name} at line {line_number}:{column_number}. Fixing the error...]")
+
+ # Get file path and contents
+ file_path = P_filenames_dict.get(file_name, error.file_path)
+ file_contents = file_utils.read_file(file_path) if os.path.exists(file_path) else ""
+
+ # Build correction instruction
+ custom_msg = f"Fix the following compilation error in {file_name} at line {line_number}, column {column_number}:\n{error.message}"
+
+ # Continue the conversation to fix compiler errors
+ # Apply context pruning before fixing errors
+ if ctx_pruning:
+ original_messages = messages.copy()
+ messages = ctx_pruning.prune_context(messages, file_name)
+ ctx_pruning.log_context_metrics(original_messages, messages, MockStatus())
+
+ pipeline.add_user_msg(custom_msg)
+ response = pipeline.invoke_llm(model_id, candidates=1, heuristic='random')
+ logger.info(f". . . . . . Compiling the fixed code...")
+ log_filename, Pcode = extract_validate_and_log_Pcode(response, "", "", logging_enabled=False)
+ if log_filename is not None and Pcode is not None:
+ log_utils.log_Pcode_to_file(Pcode, file_path)
+ all_responses[log_filename] = Pcode
+ num_of_iterations -= 1
+
+ # Log the diff
+ log_utils.log_code_diff(file_contents, response, "After fixing the P code")
+ compile_result = compilation_service.compile(recent_project_path)
+ compilation_success = compile_result.success
+ compilation_output = compile_result.stdout or compile_result.stderr or ""
+ logger.info("============================DEBUG NOW ===========")
+ logger.info(f"COMPILATION RESULT : {compilation_output}")
+ logger.info(f"COMPILATION STATUS : {compilation_success}")
+ if compilation_success:
+ logger.info(f":green[Compilation succeeded in {max_iterations - num_of_iterations} iterations.]")
+ # backend_status.write(f":green[Total compilation token usage - Input: {globals.model_metrics['inputTokens']} tokens, Output: {globals.model_metrics['outputTokens']} tokens]")
+ logger.info(f"Compilation succeeded in {max_iterations - num_of_iterations} iterations.")
+
+ global_state.compile_iterations += (max_iterations - num_of_iterations)
+
+ global_state.compile_success = compilation_success
+
+def generate_machine_code(model, pipeline, instructions, filename, dirname):
+ """Generate machine code using either two-stage or single-stage process."""
+ # Stage 1: Generate structure
+ pipeline.add_user_msg(instructions['MACHINE_STRUCTURE'].format(machineName=filename))
+ pipeline.add_documents_inline(get_context_files()["MACHINE_STRUCTURE"], tag_surround)
+
+ stage1_response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ structure_pattern = r'(.*?)'
+ match = re.search(structure_pattern, stage1_response, re.DOTALL)
+
+ if match:
+ # Two-stage generation
+ machine_structure = match.group(1).strip()
+ logger.info(f" . . . Stage 2: Implementing function bodies for {filename}.p")
+
+ pipeline.add_user_msg(instructions[MACHINE].format(machineName=filename)+ "\n\nHere is the starting structure:\n\n" + machine_structure)
+ pipeline.add_documents_inline(get_context_files()["MACHINE"], tag_surround)
+
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ logger.info(response)
+ else:
+ # Fallback to single-stage
+ logger.info(f" . . . :red[Failed to extract structure for {filename}.p. Falling back to single-stage generation.]")
+ pipeline.add_user_msg(instructions[MACHINE].format(machineName=filename))
+ pipeline.add_documents_inline(get_context_files()["MACHINE"], tag_surround)
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ logger.info(response)
+
+ # token_usage = response["current_tokens"]
+ # log_token_usage(token_usage, backend_status)
+
+ return extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, dirname)
+
+
+def generate_generic_file(model_id, pipeline, instructions, filename, dirname):
+
+ pipeline.add_user_msg(instructions[dirname].format(filename=filename))
+ pipeline.add_documents_inline(get_context_files()[dirname], tag_surround)
+ response = pipeline.invoke_llm(model_id, candidates=1, heuristic='random')
+
+ return extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, dirname)
+
+def generate_generic_file_mock(model_id, pipeline, instructions, filename, dirname, benchmark_dir):
+
+ pipeline.add_user_msg(instructions[dirname].format(filename=filename))
+ pipeline.add_documents_inline(get_context_files()[dirname], tag_surround)
+
+ mock_response = tag_surround(f"{filename}.p", file_utils.read_file(f"{benchmark_dir}/{dirname}/{filename}.p"))
+ pipeline.add_assistant_msg(mock_response)
+ response = pipeline.get_last_response()
+
+ # response = pipeline.invoke_llm(model_id, candidates=1, heuristic='random')
+
+ return extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, dirname)
+
+
+def create_proj_files(project_root):
+
+ # Create project structure with folders and pproj file
+ from utils.project_structure_utils import setup_project_structure
+ parent_abs_path = global_state.custom_dir_path or os.path.join(os.getcwd(), global_state.recent_dir_path)
+ logger.info("Step 0: Creating project structure...")
+ setup_project_structure(project_root, global_state.project_name)
+ logger.info(f":white_check_mark: Project structure created at: {project_root}")
+
+def mock_generate_pproj_file(project_root, project_name, benchmark_dir):
+ pproj_file = glob(f"{benchmark_dir}/*.pproj")[0]
+ pproj_content = file_utils.read_file(pproj_file)
+
+ pproj_path = os.path.join(project_root, f"{project_name}.pproj")
+ file_utils.write_file(pproj_path, pproj_content)
+ print("Created Project Structure :white_check_mark:")
+
+ return pproj_path
+
+def mock_setup_project_structure(project_root, project_name, benchmark_dir):
+ from utils.project_structure_utils import create_project_directories
+ directories = create_project_directories(project_root)
+
+ # Create .pproj file
+ pproj_path = mock_generate_pproj_file(project_root, project_name, benchmark_dir)
+
+ return {
+ "directories": directories,
+ "pproj_file": pproj_path
+ }
+
+def mock_create_proj_files(project_root, benchmark_dir):
+
+ # Create project structure with folders and pproj file
+ logger.info("Step 0: Creating project structure...")
+ mock_setup_project_structure(project_root, global_state.project_name, benchmark_dir)
+ logger.info(f":white_check_mark: Project structure created at: {project_root}")
+
+
+def test_dd2proj_current(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ from utils.chat_utils import generate_response
+ global SAVED_GLOBAL_STATE
+ SAVED_GLOBAL_STATE = save_module_state(global_state)
+
+
+ global_state.temperature = temperature
+ global_state.model_id = model
+
+ dd_name, dd_path = task
+ destination_path = ".tmp"
+ if destination_path and destination_path.strip() != "" and file_utils.check_directory(destination_path.strip()):
+ global_state.custom_dir_path = destination_path.strip()
+
+ design_doc_content = file_utils.read_file(dd_path)
+ user_inp = "User uploaded Design Document: " + dd_name
+ global_state.chat_history.add_exchange("user", None, user_inp, None)
+
+ generate_response(design_doc_content, MockContainer())
+ project_root = f"{destination_path}/{global_state.project_name_with_timestamp}"
+
+ return project_root
+
+
+from utils.module_utils import save_module_state, restore_module_state
+@pytest.mark.parametrize("task", [("1_lightswitch", "resources/p-model-benchmark/3_designdoc2pprojA/1_lightswitch.txt")])
+def test_dd2proj_replicated(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, out_dir=".tmp", **kwargs):
+
+ global SAVED_GLOBAL_STATE
+ SAVED_GLOBAL_STATE = save_module_state(global_state)
+
+ all_responses = {}
+
+ _, dd_path = task
+ dd_content = file_utils.read_file(dd_path)
+
+ project_name_pattern = r'^#\s+(.+?)\s*$'
+
+ match = re.search(project_name_pattern, dd_content, re.MULTILINE | re.IGNORECASE)
+ if match:
+ global_state.project_name = match.group(1).strip().replace(" ", "_")
+
+ timestamp = global_state.current_time.strftime('%Y_%m_%d_%H_%M_%S')
+ global_state.project_name_with_timestamp = f"{global_state.project_name}_{timestamp}"
+ log_utils.move_recent_to_archive()
+ file_utils.empty_file(global_state.full_log_path)
+ file_utils.empty_file(global_state.code_diff_log_path)
+
+ destination_path = ".tmp"
+ if destination_path and destination_path.strip() != "" and file_utils.check_directory(destination_path.strip()):
+ global_state.custom_dir_path = destination_path.strip()
+
+ parent_abs_path = global_state.custom_dir_path or os.path.join(os.getcwd(), global_state.recent_dir_path)
+
+ project_root = os.path.join(parent_abs_path, global_state.project_name_with_timestamp)
+
+ create_proj_files(project_root)
+
+ pipeline = PromptingPipeline()
+ system_prompt = file_utils.read_file(global_state.system_prompt_path)
+ pipeline.add_system_prompt(system_prompt)
+
+ # Get initial machine list
+ text = instructions[LIST_OF_MACHINE_NAMES].format(userText=dd_content)
+ pipeline.add_user_msg(text, [global_state.P_basics_path])
+ pipeline.add_user_msg("These are the example P Programs ",[global_state.P_program_example_path])
+ machines_list = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ # Generate filenames
+ pipeline.add_user_msg(instructions[LIST_OF_FILE_NAMES])
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ global_state.filenames_map = extract_filenames(response)
+ # Generate enums, types, and events
+ pipeline.add_user_msg(instructions[ENUMS_TYPES_EVENTS])
+ pipeline.add_documents_inline(get_context_files()["ENUMS_TYPES_EVENTS"], tag_surround)
+ response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ log_filename, Pcode = extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, PSRC)
+
+ file_abs_path = os.path.join(project_root, PSRC, log_filename)
+ logger.info(f":blue[. . . filepath: {file_abs_path}]")
+
+ if log_filename is not None and Pcode is not None:
+ all_responses[log_filename] = Pcode
+
+ step_no = 2
+
+ for dirname, filenames in global_state.filenames_map.items():
+ if dirname != PSRC:
+ logger.info(f"Step {step_no}: Generating {dirname}")
+ step_no += 1
+
+ for filename in filenames:
+ logger.info(f"Generating file {filename}.p")
+ if dirname == PSRC and filename in machines_list:
+ log_filename, Pcode = generate_machine_code(model, pipeline, instructions, filename, dirname)
+ else:
+ log_filename, Pcode = generate_generic_file(model, pipeline, instructions, filename, dirname)
+
+ log_file_full_path = os.path.join(project_root, dirname, log_filename)
+ logger.info(f":blue[. . . filepath: {log_file_full_path}]")
+ if log_filename is not None and Pcode is not None:
+ all_responses[log_filename] = Pcode
+
+ logger.info(f"Running the P compiler and analyzer on {dirname}...")
+ num_iterations = 20 if dirname == PTST else 15
+ compiler_analysis(model, pipeline, all_responses, num_iterations)
+
+ # return all_responses
+ return (pipeline, project_root)
+
+def create_base_pipeline_fewshot():
+ p_few_shot = 'resources/context_files/modular-fewshot/p-fewshot-formatted.txt'
+ p_nuances = 'resources/context_files/p_nuances.txt'
+
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary."
+
+ initial_msg = 'Here are some information relevant to P.'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg, documents = [p_few_shot,p_nuances])
+ return pipeline
+
+def taskgen_pchecker_fix_single(generated_dir):
+ p = Path(generated_dir)
+ test_name = p.parent.stem
+
+ return [(f"{test_name}", generated_dir)]
+
+# results_dir should be the top level dir like key-results/semantic/2025-07-27-16-26-24
+def taskgen_pchecker_fix(results_dir):
+ tasks = []
+ # Find all trial directories
+ trial_dirs = glob(f"{results_dir}/trial_*")
+
+ # For each trial directory
+ for trial_dir in trial_dirs:
+ # Find all test folders (non-empty directories) in the trial directory
+ test_dirs = [d for d in glob(f"{trial_dir}/*") if os.path.isdir(d)]
+
+ # For each test directory
+ for test_dir in test_dirs:
+ # Get test name from directory name
+ test_name = f"{Path(test_dir).parent.name}/{Path(test_dir).name}"
+ # Create path to generated code
+ generated_dir = os.path.join(test_dir, "generated")
+
+ # Only add if generated directory exists
+ if os.path.exists(generated_dir):
+ tasks.append((test_name, generated_dir))
+
+ print(f"TASKS: {tasks}")
+ return tasks
+
+
+def attempt_fix_pchecker_error(pipeline, project_state, file_dict):
+ return project_state
+
+def file_dict_to_prompt(file_dict, pre="", post=""):
+ result = pre
+
+ for filepath, contents in file_dict.items():
+ result += f"<{filepath}>\n{contents}\n{filepath}>\n"
+
+ result += post
+ return result
+
+# def attempt_fix_pchecker_errors(pipeline: PromptingPipeline, current_project_state, test_case, trace_dict, trace_log, out_dir):
+
+# prompt = file_dict_to_prompt(current_project_state, pre="Here are the project files\n", post=f"\Attached is the error trace when the test case {test_case} is run:\n")
+# new_project_state = {}
+# pipeline.add_user_msg(f"""{prompt}
+# Trace:\n{trace_log}
+
+# What do you think the issue is? Format your response as follows:
+#
+# filename: description of fix
+# ...
+# """)
+# pipeline.invoke_llm()
+# with open(f"{out_dir}/llm_analysis.txt", "w") as f:
+# analysis = pipeline.get_last_response()
+# f.write(analysis)
+# print("===== ANALYSIS ======")
+# print(analysis)
+# print("="*20)
+# input("press ENTER to continue...")
+
+
+# # Ask LLM to provide fixed code
+# pipeline.add_user_msg("""Based on your analysis above, provide the complete fixed code for each file that needs to be modified.
+# Format your response with the complete file contents wrapped in XML tags using the filename, like:
+#
+# [complete fixed file content]
+#
+# """)
+# pipeline.invoke_llm()
+# response = pipeline.get_last_response()
+
+# # Save the fix attempt
+# with open(f"{out_dir}/llm_fix.txt", "w") as f:
+# f.write(response)
+
+# print(f"Wrote LLM fix to {out_dir}/llm_fix.txt. Go take a look")
+# input("press ENTER to continue...")
+
+# # Extract updated files from response
+# for line in response.split('\n'):
+# if line.startswith('<') and not line.startswith('') and line.endswith('>'):
+# # Found start tag
+# filename = line[1:-1] # Remove < and >
+# content = []
+# elif line.startswith(''):
+# # Found end tag, save the content
+# if content:
+# new_project_state[filename] = '\n'.join(content)
+# else:
+# # Collecting content lines
+# content.append(line)
+
+# return new_project_state
+
+# def test_fix_pchecker_errors(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+# user_quit = False
+# max_attempts = 10
+# test_name, project_root = task
+# out_dir = f"{kwargs['out_dir']}/{test_name}"
+
+# attempt = 0
+# pipeline = create_base_pipeline_fewshot()
+
+
+# prev_project_root = project_root
+# new_project_root = file_utils.make_copy(project_root, out_dir)
+# current_project_state = file_utils.capture_project_state(new_project_root)
+# checker_out, trace_dicts, trace_logs = {}, {}, {}
+# while True:
+
+# attempt_dir = f"{out_dir}/attempt_{attempt}"
+# print(f"COMPILING: {new_project_root}")
+# compilable = try_compile(new_project_root, f"{attempt_dir}/std_streams")
+# if not compilable:
+# new_project_root = prev_project_root
+# print("NOT COMPILABLE")
+# continue
+# else:
+# print(f"CHECKING: {new_project_root}")
+# checker_out, trace_dicts, trace_logs = try_pchecker(new_project_root, f"{attempt_dir}/std_streams")
+# prev_project_root = new_project_root
+# print(f"CHECKER RESULT: {checker_out}")
+
+# if not trace_dicts.keys():
+# # There are no pchecker bugs to fix
+# break
+
+# if user_quit or attempt > max_attempts:
+# break
+
+# new_project_root = f"{attempt_dir}/modified"
+# test_case = list(trace_dicts.keys())[0]
+# print(f"FIXING TEST: {test_case}")
+# trace_dict = trace_dicts[test_case]
+# trace_log = trace_logs[test_case]
+
+# new_state = attempt_fix_pchecker_errors(pipeline, current_project_state, test_case, trace_dict, trace_log, out_dir=attempt_dir)
+
+# current_project_state = {**current_project_state, **new_state}
+
+# file_utils.write_project_state(current_project_state, new_project_root)
+
+# attempt += 1
+
+# return (pipeline, new_project_root)
+
+def reduce_trace_size(trace_log):
+ lines = trace_log.splitlines()
+ reduced = "\n".join(lines[-50:])
+ return reduced
+
+def generate_generic_analysis_prompt(p_code, trace_log):
+ with open(global_state.generate_files_to_fix_for_pchecker, "r") as file:
+ template = file.read()
+ llm_query = template.format(
+ error_trace=reduce_trace_size(trace_log),
+ p_code=p_code,
+ tool="PChecker",
+ additional_error_info="The line that starts with \"\" has the error message"
+ )
+ return llm_query
+
+def generate_hot_state_analysis_prompt(test_case, p_code, trace_log):
+ with open(global_state.template_path_hot_state_bug_analysis, "r") as file:
+ template = file.read()
+ llm_query = template.format(
+ error_trace=reduce_trace_size(trace_log),
+ p_code=p_code,
+ tool="PChecker",
+ additional_error_info="The line that starts with \"\" has the error message. You are only given the last 50 lines."
+ )
+ return llm_query
+
+
+# def generate_analysis_prompt(p_code, test_case, trace_log, error_category):
+# """Generate the prompt for analyzing P checker errors."""
+# llm_query = ""
+# if error_category == ErrorCategories.ENDED_IN_HOT_STATE:
+# llm_query = generate_hot_state_analysis_prompt(test_case, p_code, trace_log)
+# else:
+# llm_query = generate_generic_analysis_prompt(p_code, trace_log)
+# return llm_query
+
+def generate_analysis_prompt(p_code, test_case, trace_log, error_category):
+ """Generate the prompt for analyzing P checker errors."""
+ llm_query = generate_generic_analysis_prompt(p_code, trace_log)
+ return llm_query
+
+def request_and_save_analysis(pipeline, prompt, out_dir, error_category):
+ """Request error analysis from LLM and save the response."""
+ pipeline.add_user_msg(prompt)
+ pipeline.invoke_llm()
+ analysis = pipeline.get_last_response()
+
+ with open(f"{out_dir}/llm_analysis.txt", "w") as f:
+ f.write(analysis)
+
+ return analysis
+
+def process_llm_response_with_tags(llm_analysis):
+ pattern = r'<([^>]+)>(.*?)\1>'
+ matches = re.findall(pattern, llm_analysis, re.DOTALL)
+ return {tag: content.strip() for tag, content in matches}
+
+def generate_fix_prompt(p_code, analysis, error_category):
+ with open(global_state.generate_fixed_file_for_pchecker, "r") as f:
+ template = f.read()
+ params = {"p_code": p_code, "fix_description": analysis}
+ llm_query = template.format(**params)
+ return llm_query
+
+def request_and_save_fix(pipeline, prompt, out_dir, error_category):
+ pipeline.add_user_msg(f"{prompt}\n\nApply the recommended fixes")
+ pipeline.invoke_llm()
+ response = pipeline.get_last_response()
+ with open(f"{out_dir}/llm_fix.txt", "w") as f:
+ f.write(response)
+
+ return process_llm_response_with_tags(response)
+
+def parse_fix_response(response):
+ """Parse the fix response and extract updated files."""
+ new_project_state = {}
+ content = []
+ current_filename = None
+ inside_tags = False
+
+ for line in response.split('\n'):
+ if line.startswith('<') and not line.startswith('') and line.endswith('>'):
+ # Found start tag
+ current_filename = line[1:-1] # Remove < and >
+ inside_tags = True
+ content = []
+ elif line.startswith(''):
+ # Found end tag, save the content
+ if content and current_filename:
+ new_project_state[current_filename] = '\n'.join(content)
+ else:
+ # Collecting content lines
+ content.append(line)
+
+ return new_project_state
+
+def attempt_fix_pchecker_errors(pipeline: PromptingPipeline, current_project_state, test_case, trace_dict, trace_log, error_category, out_dir):
+ """Attempt to fix P checker errors by getting analysis and fixes from LLM."""
+ # Generate prompt and get analysis
+ p_code = file_dict_to_prompt(current_project_state)
+ prompt = generate_analysis_prompt(p_code, test_case, trace_log, error_category)
+ analysis = request_and_save_analysis(pipeline, prompt, out_dir, error_category)
+
+ # Get and save fix
+
+ fix_prompt = generate_fix_prompt(p_code, analysis, error_category)
+ fix_response = request_and_save_fix(pipeline, fix_prompt, out_dir, error_category)
+
+ return fix_response
+
+def setup_fix_pipeline():
+ """Create and setup the pipeline for fixing P checker errors."""
+ pipeline = create_base_pipeline_fewshot()
+ # pipeline = create_base_old_pipeline()
+ return pipeline
+
+def setup_project_state(project_root, out_dir):
+ """Setup initial project state."""
+ new_project_root = file_utils.make_copy(project_root, out_dir, new_name="original_project")
+ current_project_state = file_utils.capture_project_state(new_project_root)
+ return new_project_root, current_project_state
+
+def attempt_fix_iteration(pipeline, current_project_state, test_case, trace_dict, trace_log, error_category, attempt_dir):
+ """Run one iteration of the fix attempt."""
+ new_state = attempt_fix_pchecker_errors(
+ pipeline, current_project_state, test_case, trace_dict, trace_log, error_category=error_category, out_dir=attempt_dir
+ )
+
+ return {**current_project_state, **new_state}
+
+
+def get_failing_test_names(result_dict):
+ failing_tests = []
+ for test_name in result_dict:
+ if not result_dict[test_name]:
+ failing_tests.append(test_name)
+
+ return failing_tests
+
+from enum import Enum
+
+class ErrorCategories(Enum):
+ DEADLOCK = 1
+ UNHANDLED_EVENT = 2
+ ENDED_IN_HOT_STATE = 3
+ FAILED_ASSERTION = 4
+ EXCEPTION = 5
+ UNKNOWN = 6
+
+def categorize_error(log):
+ """Categorize an error log based on its content."""
+ log_lower = log.lower()
+ if "deadlock detected" in log_lower:
+ return ErrorCategories.DEADLOCK
+ elif "received event" in log_lower and "cannot be handled" in log_lower:
+ return ErrorCategories.UNHANDLED_EVENT
+ elif "in hot state" in log_lower and "at the end of program" in log_lower:
+ return ErrorCategories.ENDED_IN_HOT_STATE
+ elif "assertion failed" in log_lower:
+ return ErrorCategories.FAILED_ASSERTION
+ elif "exception" in log_lower:
+ return ErrorCategories.EXCEPTION
+ else:
+ return ErrorCategories.UNKNOWN
+
+def identify_error_category(trace_str):
+ error_lines = re.findall(r' (.*?)$', trace_str, re.MULTILINE)
+ return categorize_error(error_lines[0])
+
+
+def compute_progress_percentage(i, total):
+ return f"{i/total*100:.2f}"
+
+from utils import checker_utils
+def test_fix_pchecker_errors(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ total_progress = compute_progress_percentage(kwargs["task_number"], kwargs["total_tasks"])
+ print(f"==== TASK {total_progress}% =====")
+ """Test fixing P checker errors with multiple attempts."""
+ user_quit = False
+ max_attempts = 3
+ test_name, project_root = task
+ test_name_dir = f"{kwargs['out_dir']}/{test_name}"
+ out_dir = f"{test_name_dir}/fix_attempts"
+
+ # Setup
+ pipeline = setup_fix_pipeline()
+ prev_project_root = project_root
+ new_project_root, current_project_state = setup_project_state(project_root, test_name_dir)
+ prev_project_state = current_project_state
+
+
+ orig_results, _, orig_trace_logs = checker_utils.try_pchecker(project_root, "/tmp")
+ test_cases_to_fix = get_failing_test_names(orig_results)
+ subprocess.run(['cp', '-r', f'{project_root}/../std_streams', f"{test_name_dir}/original_std_streams"])
+ all_tests_fixed = False
+ latest_results = {}
+ latest_trace_logs = {}
+ total_tokens_input = 0
+ total_tokens_output = 0
+ grace_points = 1.0
+
+ for i, test_case in enumerate(test_cases_to_fix):
+ attempt = 0
+ prev_error_category = identify_error_category(orig_trace_logs[test_case])
+ error_category = prev_error_category
+
+
+ if all_tests_fixed:
+ break
+
+ while attempt < max_attempts and not user_quit:
+ print(f"==== ({total_progress}%) TASK SUB-PROGRESS {compute_progress_percentage(i+(attempt/(max_attempts+0.1)), len(test_cases_to_fix))}% ====")
+ print(f"FIXING TEST: {test_case}\nATTEMPT {attempt}")
+ attempt_dir = f"{out_dir}/{test_case}/attempt_{attempt}"
+
+ current_results, trace_dicts, trace_logs = try_pchecker(new_project_root, f"{attempt_dir}/std_streams")
+ latest_results = current_results
+ latest_trace_logs = trace_logs
+ prev_project_root = new_project_root
+
+ if not trace_dicts.keys():
+ all_tests_fixed = True
+ print("ALL TESTS FIXED!")
+ break # No more errors to fix
+
+ if test_case not in trace_dicts:
+ print(f"TEST CASE FIXED: {test_case}")
+ break # Current error has been fixed
+
+
+ # Setup for next fix attempt
+ new_project_root = f"{attempt_dir}/modified"
+ # test_case = list(trace_dicts.keys())[0]
+
+ prev_error_category = error_category
+ error_category = identify_error_category(trace_logs[test_case])
+
+ if error_category != prev_error_category:
+ # Give the model a few more chances if it made some progress
+ awarded = round(grace_points)
+ max_attempts += awarded
+ grace_points -= 0.1
+ print(f"Awarded {awarded} grace point for changed error.")
+ print(f"Remaning grace points {(grace_points-0.5)/0.1}")
+ print(f"New max_attempts = {max_attempts}, attempt = {attempt}")
+ else:
+ # Reset the context
+ pipeline = setup_fix_pipeline()
+
+
+ try:
+ # Attempt fix
+ prev_project_state = current_project_state
+ current_project_state = attempt_fix_iteration(
+ pipeline, current_project_state, test_case,
+ trace_dicts[test_case], trace_logs[test_case],
+ error_category, attempt_dir
+ )
+ total_tokens_input += pipeline.get_total_input_tokens()
+ total_tokens_output += pipeline.get_total_output_tokens()
+ except Exception as e:
+ print(f"EXCEPTION WHILE FIXING:\n\t{e}")
+ file_utils.write_file(f"{new_project_root}/exception.txt", f"{e}")
+ new_project_root = prev_project_root
+ current_project_state = prev_project_state
+ attempt += 1
+ continue
+
+ # Write updated state
+ file_utils.write_project_state(current_project_state, new_project_root)
+
+ # Try compilation
+ print(f"COMPILING: {new_project_root}")
+ compilable = compile_utils.try_compile(new_project_root, f"{attempt_dir}/std_streams")
+
+
+ if not compilable:
+ # Ideally we should add sanity check here to see if that fixes the issue.
+ print("COMPILATION FAILED...")
+ error_msg = file_utils.read_file(f"{attempt_dir}/std_streams/compile/stdout.txt").splitlines()
+ truncated_msg = "\n".join(error_msg[-10:])
+ print("----- COMPILE ERROR --------")
+ print(truncated_msg)
+ print("----------------------------")
+ print(f"Reverting {new_project_root} -> {prev_project_root}")
+ print(f"Next attempt: {attempt+1}")
+ print("----------------------------")
+ new_project_root = prev_project_root # handle_compilation_failure(new_project_root, prev_project_root)
+ current_project_state = prev_project_state
+ attempt += 1
+ continue
+
+ attempt += 1
+
+ write_fix_diff_log(orig_results, orig_trace_logs, latest_results, latest_trace_logs, f"{test_name_dir}/fix_diff.json")
+
+ with open(f"{test_name_dir}/token_usage_for_fixer.json", "w") as f:
+ json.dump({"input":total_tokens_input, "output":total_tokens_output}, f, indent=4)
+
+ return (pipeline, prev_project_root)
+
+def write_fix_diff_log(orig_results, orig_trace_logs, new_results, new_trace_logs, out_file):
+
+ diff_dict = {}
+ for test_name in orig_results:
+
+
+ if test_name not in new_results:
+ diff_dict[test_name] = {"changed":True, "new":"DOES NOT EXIST!", "original":orig_results[test_name]}
+ continue
+
+ orig_error_category = f"{identify_error_category(orig_trace_logs[test_name]) if test_name in orig_trace_logs else None}"
+ new_error_category = f"{identify_error_category(new_trace_logs[test_name]) if test_name in new_trace_logs else None}"
+
+ old_result = orig_results[test_name]
+ new_result = new_results[test_name]
+
+ changed = old_result != new_result or (old_result == new_result and orig_error_category != new_error_category)
+ diff_dict[test_name] = {
+ "changed": changed,
+ "new": {
+ "result":new_result,
+ "category":new_error_category
+ },
+ "original": {
+ "result": old_result,
+ "category": orig_error_category
+ }
+ }
+
+ with open(out_file, "w") as f:
+ json.dump(diff_dict, f, indent=4)
+
+def oracle_fix_pchecker_errors(task, test_func_out, out_dir=None):
+ test_name, _ = task
+ pipeline, generated_dir = test_func_out
+ # print(f"task = {task}")
+ # print(f"test_func_out = {test_func_out}")
+ # print(f"out_dir = {out_dir}")
+
+ out_dir_test = f"{out_dir}/{test_name}"
+ os.makedirs(out_dir_test, exist_ok=True)
+
+ token_usage = pipeline.get_token_usage()
+ with open(f'{out_dir_test}/conversation.json', 'w') as f:
+ json.dump(pipeline.get_conversation(), f, cls=BytesEncoder, indent=4)
+
+ with open(f'{out_dir_test}/token_usage.json', 'w') as f:
+ json.dump(token_usage, f, cls=BytesEncoder, indent=4)
+
+ results = {}
+
+ compilable = compile_utils.try_compile(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ results['compile'] = compilable
+ if compilable:
+ checker_out, *_ = try_pchecker(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ results = {**results, **checker_out}
+
+ return results
+
+def test_dd2proj_replicated_with_pchecker(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ (pipeline, project_root) = test_dd2proj_replicated(task, model, temperature, n, heuristic, max_tokens, top_p, **kwargs)
+ (pipeline2, project_root2) = test_fix_pchecker_errors(project_root)
+
+
+def test_dd2proj_psrc(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+
+ global SAVED_GLOBAL_STATE
+ SAVED_GLOBAL_STATE = save_module_state(global_state)
+
+ all_responses = {}
+
+ _, dd_path, benchmark_dir = task
+ dd_content = file_utils.read_file(dd_path)
+
+ project_name_pattern = r'^#\s+(.+?)\s*$'
+
+ match = re.search(project_name_pattern, dd_content, re.MULTILINE | re.IGNORECASE)
+ if match:
+ global_state.project_name = match.group(1).strip().replace(" ", "_")
+
+ timestamp = global_state.current_time.strftime('%Y_%m_%d_%H_%M_%S')
+ global_state.project_name_with_timestamp = f"{global_state.project_name}_{timestamp}"
+ log_utils.move_recent_to_archive()
+ file_utils.empty_file(global_state.full_log_path)
+ file_utils.empty_file(global_state.code_diff_log_path)
+
+ destination_path = ".tmp"
+ if destination_path and destination_path.strip() != "" and file_utils.check_directory(destination_path.strip()):
+ global_state.custom_dir_path = destination_path.strip()
+
+ parent_abs_path = global_state.custom_dir_path or os.path.join(os.getcwd(), global_state.recent_dir_path)
+
+ project_root = os.path.join(parent_abs_path, global_state.project_name_with_timestamp)
+
+ mock_create_proj_files(project_root, benchmark_dir)
+
+ pipeline = PromptingPipeline()
+ system_prompt = file_utils.read_file(global_state.system_prompt_path)
+ pipeline.add_system_prompt(system_prompt)
+
+ # Get initial machine list
+ text = instructions[LIST_OF_MACHINE_NAMES].format(userText=dd_content)
+ pipeline.add_user_msg(text, [global_state.P_basics_path])
+ pipeline.add_user_msg("These are the example P Programs ",[global_state.P_program_example_path])
+
+ fixed_responses_dir = f"{benchmark_dir}/fixed_responses"
+ psrc_dir_path = f"{benchmark_dir}/PSrc"
+ # TODO: Simulate this call with hard coded answer for the benchmark using add_assistant_msg()
+ machines_list = file_utils.read_file(f"{fixed_responses_dir}/machine_list.txt") # pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ pipeline.add_assistant_msg(machines_list)
+
+ # Generate filenames
+ pipeline.add_user_msg(instructions[LIST_OF_FILE_NAMES])
+ response = file_utils.read_file(f"{fixed_responses_dir}/filenames_list.txt") # pipeline.invoke_llm(model, candidates=1, heuristic='random')
+
+
+ global_state.filenames_map = extract_filenames(response)
+ # Generate enums, types, and events
+ pipeline.add_user_msg(instructions[ENUMS_TYPES_EVENTS])
+ pipeline.add_documents_inline(get_context_files()["ENUMS_TYPES_EVENTS"], tag_surround)
+
+
+ # response = pipeline.invoke_llm(model, candidates=1, heuristic='random')
+ mock_response = tag_surround("Enums_Types_Events.p", file_utils.read_file(f"{psrc_dir_path}/Enums_Types_Events.p"))
+ pipeline.add_assistant_msg(mock_response)
+ response = pipeline.get_last_response()
+ log_filename, Pcode = extract_validate_and_log_Pcode(response, global_state.project_name_with_timestamp, PSRC)
+
+ file_abs_path = os.path.join(project_root, PSRC, log_filename)
+ logger.info(f":blue[. . . filepath: {file_abs_path}]")
+
+ if log_filename is not None and Pcode is not None:
+ all_responses[log_filename] = Pcode
+
+ step_no = 2
+
+ def p_dirs_sort_func(item):
+ dirname, _ = item
+ dirname_priority_map = {PSRC: 3, PSPEC: 2, PTST: 1}
+ return dirname_priority_map[dirname]
+
+ for dirname, filenames in sorted(global_state.filenames_map.items(), key=p_dirs_sort_func):
+ if dirname != PSRC:
+ logger.info(f"Step {step_no}: Generating {dirname}")
+ step_no += 1
+
+ for filename in filenames:
+ logger.info(f"Generating file {filename}.p")
+ if dirname == PSRC and filename in machines_list:
+ log_filename, Pcode = generate_machine_code(model, pipeline, instructions, filename, dirname)
+ else:
+ if dirname != PSRC:
+ log_filename, Pcode = generate_generic_file_mock(model, pipeline, instructions, filename, dirname, benchmark_dir)
+ else:
+ log_filename, Pcode = generate_generic_file(model, pipeline, instructions, filename, dirname)
+
+
+ log_file_full_path = os.path.join(project_root, dirname, log_filename)
+ logger.info(f":blue[. . . filepath: {log_file_full_path}]")
+ if log_filename is not None and Pcode is not None:
+ all_responses[log_filename] = Pcode
+
+ logger.info(f"Running the P compiler and analyzer on {dirname}...")
+ num_iterations = 20 if dirname == PTST else 15
+
+ if dirname == PSRC:
+ compiler_analysis(model, pipeline, all_responses, num_iterations)
+
+ # return all_responses
+ return (pipeline, project_root)
+
+def oracle_dd2proj_replicated(task, test_func_out, out_dir=None):
+ global SAVED_GLOBAL_STATE
+ dd_name, *_ = task
+ pipeline, project_root = test_func_out
+ out_dir_test = f"{out_dir}/{dd_name}"
+ os.makedirs(out_dir_test, exist_ok=True)
+
+ token_usage = pipeline.get_token_usage()
+
+ generated_dir = f"{out_dir_test}/generated"
+ subprocess.run(['cp', '-r', project_root, generated_dir])
+ subprocess.run(['rm', '-rf', f"{generated_dir}/PGenerated"])
+ subprocess.run(['cp', '-r', global_state.full_log_path, f"{out_dir_test}/full_log.txt"])
+ subprocess.run(['cp', '-r', global_state.code_diff_log_path, f"{out_dir_test}/code_diff_log.txt"])
+ # subprocess.run(['cp', '-r', global_state.communication_log_file, f"{out_dir_test}/comm_log.txt"])
+
+ with open(f'{out_dir_test}/token_usage.json', 'w') as f:
+ json.dump(token_usage, f, cls=BytesEncoder, indent=4)
+
+ restore_module_state(global_state, SAVED_GLOBAL_STATE)
+
+ results = {}
+ compilable = compile_utils.try_compile(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ results['compile'] = compilable
+ if compilable:
+ checker_out, *_ = try_pchecker(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ results = {**results, **checker_out}
+
+ return results
+
+from utils.checker_utils import try_pchecker
+
+def oracle_dd2psrc_correctness(task, test_func_out, out_dir=None):
+ global SAVED_GLOBAL_STATE
+ dd_name, *_ = task
+ pipeline, project_root = test_func_out
+ out_dir_test = f"{out_dir}/{dd_name}"
+ os.makedirs(out_dir_test, exist_ok=True)
+
+ token_usage = pipeline.get_token_usage()
+
+ generated_dir = f"{out_dir_test}/generated"
+ subprocess.run(['cp', '-r', project_root, generated_dir])
+ subprocess.run(['rm', '-rf', f"{generated_dir}/PGenerated"])
+ subprocess.run(['cp', '-r', global_state.full_log_path, f"{out_dir_test}/full_log.txt"])
+ subprocess.run(['cp', '-r', global_state.code_diff_log_path, f"{out_dir_test}/code_diff_log.txt"])
+ # subprocess.run(['cp', '-r', global_state.communication_log_file, f"{out_dir_test}/comm_log.txt"])
+
+ with open(f'{out_dir_test}/token_usage.json', 'w') as f:
+ json.dump(token_usage, f, cls=BytesEncoder, indent=4)
+
+ restore_module_state(global_state, SAVED_GLOBAL_STATE)
+ results = {}
+
+ compilable = compile_utils.try_compile(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ results['compile'] = compilable
+ if compilable:
+ checker_out, *_ = try_pchecker(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ results = {**results, **checker_out}
+
+ return results
+
+# =======================================================================================
+
+
+# EXAMPLE TEST COMMAND:
+# python evaluate_peasyai.py --metric pass_at_k -k 1 -n 1 -t 1.0 --trials 2 --benchmark-dir resources/evaluation/p-model-benchmark/3_designdoc2pproj
+@pytest.mark.parametrize("task", [("1_lightswitch", "resources/p-model-benchmark/3_designdoc2pprojA/1_lightswitch.txt")])
+@pytest.mark.skip(reason="Legacy module no longer exists")
+def test_dd2proj_legacy(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ import legacy.utils.chat_utils as legacy_chat_utils
+ from legacy.utils import global_variables
+
+ global SAVED_GLOBAL_STATE
+ SAVED_GLOBAL_STATE = save_module_state(global_variables)
+
+ global_variables.temperature = temperature
+ global_variables.model_id = model
+
+ dd_name, dd_path = task
+ destination_path = ".tmp"
+ if destination_path and destination_path.strip() != "" and file_utils.check_directory(destination_path.strip()):
+ global_variables.custom_dir_path = destination_path.strip()
+
+ design_doc_content = file_utils.read_file(dd_path)
+ user_inp = "User uploaded Design Document: " + dd_name
+ global_variables.chat_history.add_exchange("user", None, user_inp, None)
+
+ legacy_chat_utils.generate_response(design_doc_content, MockContainer())
+ # global_variables.custom_dir_path = None
+ # global_variables.chat_history.clear_conversation()
+ # return ".tmp/Light_Control_System_2025_06_28_00_41_29"
+ return f"{destination_path}/{global_variables.project_name_with_timestamp}"
+
+
+def oracle_dd2proj(task, pproj_path, out_dir=None):
+ global SAVED_GLOBAL_STATE
+ from legacy.utils import global_variables
+ dd_name, _ = task
+ out_dir_test = f"{out_dir}/{dd_name}"
+ os.makedirs(out_dir_test, exist_ok=True)
+
+ token_usage = {
+ "cumulative": {
+ **global_variables.model_metrics
+ }
+ }
+
+ generated_dir = f"{out_dir_test}/generated"
+ subprocess.run(['cp', '-r', pproj_path, generated_dir])
+ subprocess.run(['rm', '-rf', f"{generated_dir}/PGenerated"])
+ subprocess.run(['cp', '-r', global_variables.full_log_path, f"{out_dir_test}/full_log.txt"])
+ subprocess.run(['cp', '-r', global_variables.code_diff_log_path, f"{out_dir_test}/code_diff_log.txt"])
+ try:
+ subprocess.run(['cp', '-r', global_variables.communication_log_file, f"{out_dir_test}/comm_log.txt"])
+ except:
+ pass
+
+ with open(f'{out_dir_test}/token_usage.json', 'w') as f:
+ json.dump(token_usage, f, cls=BytesEncoder, indent=4)
+
+ restore_module_state(global_variables, SAVED_GLOBAL_STATE)
+
+ compilable = compile_utils.try_compile(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ return {'compile': compilable}
+
+def oracle_dd2proj_current(task, pproj_path, out_dir=None):
+ global SAVED_GLOBAL_STATE
+ from utils import global_state
+ dd_name, _ = task
+ out_dir_test = f"{out_dir}/{dd_name}"
+ os.makedirs(out_dir_test, exist_ok=True)
+
+ token_usage = {
+ "cumulative": {
+ **global_state.model_metrics
+ }
+ }
+
+ generated_dir = f"{out_dir_test}/generated"
+
+ print(f"pproj_path = {pproj_path}")
+ print(f"generated_dir = {generated_dir}")
+
+ subprocess.run(['cp', '-r', pproj_path, generated_dir])
+ subprocess.run(['rm', '-rf', f"{generated_dir}/PGenerated"])
+ subprocess.run(['cp', '-r', global_state.full_log_path, f"{out_dir_test}/full_log.txt"])
+ subprocess.run(['cp', '-r', global_state.code_diff_log_path, f"{out_dir_test}/code_diff_log.txt"])
+
+
+ with open(f'{out_dir_test}/token_usage.json', 'w') as f:
+ json.dump(token_usage, f, cls=BytesEncoder, indent=4)
+
+ restore_module_state(global_state, SAVED_GLOBAL_STATE)
+
+ compilable = compile_utils.try_compile(generated_dir, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ return {'compile': compilable}
+
+IGNORE_TESTS = [
+ "1_lightSwitch",
+ "2_other",
+ # "1_basicMachineStructure",
+ # "2_basicTypeDecl",
+ # "3_basicEventDecl",
+ # "4_basicParameterizedStateEntry",
+ # "5_basicCompleteSystem"
+ ]
+
+def taskgen_base(pdir):
+ prompts = {} # "": ""
+
+ # subdirs = [f.path for f in os.scandir(pdir) if f.is_dir()]
+ # for subdir in subdirs:
+ # prompts = {**prompts, **construct_prompts_from_dir(subdir)}
+
+ prompts = construct_prompts_from_dir(pdir)
+ for t in IGNORE_TESTS:
+ if t in prompts:
+ del prompts[t]
+
+ return prompts.items()
+
+
+def create_base_old_pipeline():
+ p_basics_file = 'resources/context_files/P_syntax_guide.txt'
+ about_p_file = "resources/context_files/about_p.txt"
+ system_prompt = file_utils.read_file(about_p_file)
+ initial_msg = 'Read the attached P language basics guide for reference. You can refer to this document to understand P syntax and answer accordingly. Additional specific syntax guides will be provided as needed for each task.'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg, documents = [p_basics_file])
+ pipeline.add_assistant_msg('I understand. I will refer to the P language guides to provide accurate information about P syntax when answering questions.')
+ return pipeline
+
+def test_base_old(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ _, user_prompt = task
+
+ p_basics_file = 'resources/context_files/modular/p_basics.txt'
+ about_p_file = "resources/context_files/about_p.txt"
+ p_basics_file_contents = file_utils.read_file(p_basics_file)
+ system_prompt = file_utils.read_file(about_p_file)
+ initial_msg = 'Read the attached P language basics guide for reference. You can refer to this document to understand P syntax and answer accordingly. Additional specific syntax guides will be provided as needed for each task.'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg, documents = [p_basics_file])
+ pipeline.add_assistant_msg('I understand. I will refer to the P language guides to provide accurate information about P syntax when answering questions.')
+ pipeline.add_user_msg(f"{user_prompt}\n\n{p_basics_file_contents}")
+ pipeline.invoke_llm(model=model, candidates=n, heuristic=heuristic, inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p})
+ return pipeline
+
+def test_base_all_docs(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ _, user_prompt = task
+
+ p_basics_files = glob("resources/context_files/modular/*.txt")
+ about_p_file = "resources/context_files/about_p.txt"
+ system_prompt = file_utils.read_file(about_p_file)
+ initial_msg = 'Read the attached P language basics guide for reference. You can refer to this document to understand P syntax and answer accordingly. Additional specific syntax guides will be provided as needed for each task.'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+
+ pipeline.add_documents_inline(
+ p_basics_files,
+ pre=f"{initial_msg}\n",
+ post=f"\n\n{user_prompt}\n",
+ preprocessing_function=lambda _,c: f"{c}\n")
+
+ pipeline.invoke_llm(model=model, candidates=n, heuristic=heuristic, inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p})
+ return pipeline
+
+def test_base_few_shot(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ _, user_prompt = task
+
+ p_few_shot = 'resources/context_files/modular-fewshot/p-fewshot-formatted.txt'
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary."
+
+ initial_msg = 'Here are some information relevant to P.'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg, documents = [p_few_shot])
+ pipeline.add_user_msg(f"{user_prompt}")
+ pipeline.invoke_llm(model=model, candidates=n, heuristic=heuristic, inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p})
+ return pipeline
+
+def test_base_RAG2000_inline(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ from rag.load_rag_index import load_index, search_index
+
+ _, user_prompt = task
+
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary. Declare anything you wish to use."
+
+ indices_root = "resources/rag/indices/2025-07-02-12-18-53/faiss_index_2000"
+ index, chunks = load_index(RAG_MODEL_aMLML6v2, f"{indices_root}.faiss",f"{indices_root}.pkl")
+ result = search_index(RAG_MODEL_aMLML6v2, index, user_prompt, chunks)
+ retrieved_chunks = list(zip(*result))[0]
+ chunks_str = "\n".join(retrieved_chunks)
+ initial_msg = f'Here is some information relevant to the query.\n{chunks_str}'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg)
+ pipeline.add_user_msg(f"{user_prompt}")
+ pipeline.invoke_llm(
+ model=model,
+ candidates=n,
+ heuristic=heuristic,
+ inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p}
+ )
+ return pipeline
+
+
+def test_base_RAG1000_inline(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ from rag.load_rag_index import load_index, search_index
+
+ _, user_prompt = task
+
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary. Declare anything you wish to use."
+
+ indices_root = "resources/rag/indices/2025-07-02-11-15-36/faiss_index_1000"
+ index, chunks = load_index(RAG_MODEL_aMLML6v2, f"{indices_root}.faiss",f"{indices_root}.pkl")
+ result = search_index(RAG_MODEL_aMLML6v2, index, user_prompt, chunks)
+ retrieved_chunks = list(zip(*result))[0]
+ chunks_str = "\n".join(retrieved_chunks)
+ initial_msg = f'Here is some information relevant to the query.\n{chunks_str}'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg)
+ pipeline.add_user_msg(f"{user_prompt}")
+ pipeline.invoke_llm(
+ model=model,
+ candidates=n,
+ heuristic=heuristic,
+ inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p}
+ )
+ return pipeline
+
+def test_base_RAG2000_inline_fewshot(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ from rag.load_rag_index import load_index, search_index
+
+ _, user_prompt = task
+
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary. Declare anything you wish to use."
+
+ indices_root = "resources/rag/aMLML6v2/fewshot-only/2025-07-03-14-29-32/faiss_index_2000"
+ index, chunks = load_index(RAG_MODEL_aMLML6v2, f"{indices_root}.faiss",f"{indices_root}.pkl")
+ result = search_index(RAG_MODEL_aMLML6v2, index, user_prompt, chunks)
+ retrieved_chunks = list(zip(*result))[0]
+ chunks_str = "\n".join(retrieved_chunks)
+ initial_msg = f'Here is some information relevant to the query.\n{chunks_str}'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg)
+ pipeline.add_user_msg(f"{user_prompt}")
+ pipeline.invoke_llm(
+ model=model,
+ candidates=n,
+ heuristic=heuristic,
+ inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p}
+ )
+ return pipeline
+
+def test_base_RAG2000_inline_aMLML12v2(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ from rag.load_rag_index import load_index, search_index
+
+ _, user_prompt = task
+
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary. Declare anything you wish to use."
+
+ indices_root = "resources/rag/aMLML12v2/2025-07-03-13-15-34/faiss_index_2000"
+ index, chunks = load_index(RAG_MODEL_aMLML12v2, f"{indices_root}.faiss",f"{indices_root}.pkl")
+ result = search_index(RAG_MODEL_aMLML12v2, index, user_prompt, chunks)
+ retrieved_chunks = list(zip(*result))[0]
+ chunks_str = "\n".join(retrieved_chunks)
+ initial_msg = f'Here is some information relevant to the query.\n{chunks_str}'
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(initial_msg)
+ pipeline.add_user_msg(f"{user_prompt}")
+ pipeline.invoke_llm(
+ model=model,
+ candidates=n,
+ heuristic=heuristic,
+ inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p}
+ )
+ return pipeline
+
+
+def test_base_RAG2000_asdoc(task, model=CLAUDE_3_7, temperature=1.0, n=1, heuristic='random', max_tokens=100000, top_p=0.999, **kwargs):
+ from rag.load_rag_index import load_index, search_index
+
+ _, user_prompt = task
+
+ chunks_file = "/tmp/rag_chunks.txt"
+ system_prompt = \
+ "You are an AI assistant dedicated to help with the P language. " + \
+ "P provides a high-level state machine based programming language to formally model and specify distributed systems. " + \
+ "P supports specifying and checking both safety as well as liveness specifications (global invariants)." + \
+ "Avoid discussing and writing anything other than P." + \
+ "Only write code, no commentary. Declare anything you wish to use."
+
+ indices_root = "resources/rag/indices/2025-07-02-12-18-53/faiss_index_2000"
+ index, chunks = load_index(RAG_MODEL_aMLML6v2, f"{indices_root}.faiss",f"{indices_root}.pkl")
+ result = search_index(RAG_MODEL_aMLML6v2, index, user_prompt, chunks)
+ retrieved_chunks = list(zip(*result))[0]
+ chunks_str = "\n".join(retrieved_chunks)
+ initial_msg = f'Attached is some information about p.'
+
+ with open(chunks_file, 'w') as f:
+ f.write(chunks_str)
+
+ pipeline = PromptingPipeline()
+ pipeline.add_system_prompt(system_prompt)
+ pipeline.add_user_msg(f"{initial_msg}\n{user_prompt}", documents=[chunks_file])
+ pipeline.invoke_llm(
+ model=model,
+ candidates=n,
+ heuristic=heuristic,
+ inference_config={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p}
+ )
+ return pipeline
+
+
+def extract_code(llm_output):
+ try:
+ s = llm_output.split("```")[1]
+ if s.startswith("p"):
+ s = s[1:]
+ except:
+ s = llm_output.strip('`')
+ return s
+
+def logger_generic(task, pipeline, out_dir):
+ test_name, _ = task
+ print(f"[PASS@K] RUNNING ORACLE FOR TEST {test_name}")
+
+ out_dir_test = f"{out_dir}/{test_name}"
+ os.makedirs(out_dir_test, exist_ok=True)
+
+ with open(f'{out_dir_test}/conversation.json', 'w') as f:
+ json.dump(pipeline.get_conversation(), f, cls=BytesEncoder, indent=4)
+
+ with open(f'{out_dir_test}/token_usage.json', 'w') as f:
+ json.dump(pipeline.get_token_usage(), f, cls=BytesEncoder, indent=4)
+
+ with open(f'{out_dir_test}/system_prompt.txt', 'w') as f:
+ f.write(f"{pipeline.get_system_prompt()}")
+
+ return out_dir_test
+
+
+def oracle_base(task, pipeline, out_dir=None):
+ llm_output = pipeline.get_last_response()
+
+ out_dir_test = logger_generic(task, pipeline, out_dir)
+ p_file = f'{out_dir_test}/generated.p'
+
+ final_code = extract_code(llm_output)
+ if not final_code:
+ with open(f'{out_dir_test}/failed_model_call.txt', 'w') as f:
+ f.write('model_call_failed')
+ return False
+
+ with open(p_file, 'w') as pfile:
+ pfile.write(final_code)
+
+ is_compilable = compile_utils.try_compile(p_file, captured_streams_output_dir=f"{out_dir_test}/std_streams")
+ return {'compile': is_compilable}
+
+class BytesEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, bytes):
+ return f"{obj}"
+
+ return super().default(obj)
+
+def construct_prompt_from_pproj(pdir):
+ return "TODO: Process project folder to create prompt."
+
+def construct_prompts_from_pprojs(pdir):
+ return {d.name:construct_prompt_from_pproj(d) for d in os.scandir(pdir) if d.is_dir()}
+
+def construct_prompt_from_pfile(f):
+ with open(f.path, 'r') as prompt_file:
+ prompt = prompt_file.read()
+
+ return prompt
+
+def strip_ext(f):
+ return f.rsplit('.', 1)[0]
+
+def construct_prompts_from_pfiles(pdir):
+ def make_test_name(f):
+ return strip_ext(f.name)
+
+ return {make_test_name(f):construct_prompt_from_pfile(f) for f in os.scandir(pdir) if is_prompt_file(f)}
+
+def is_prompt_file(f):
+ ret = f.name.endswith(".prompt")
+ return ret
+
+def has_prompt_files(pdir):
+ return any([is_prompt_file(f) for f in os.scandir(pdir)])
+
+def construct_prompts_from_dir(pdir):
+ if has_prompt_files(pdir):
+ return construct_prompts_from_pfiles(pdir)
+ else:
+ return construct_prompts_from_pprojs(pdir)
+
+
+def test_singleshot_designdoc_to_pcode(doc_list):
+ """
+ Test converting a design document to P code using the single-shot prompt.
+ Uses singleshot_prompt.txt to generate complete P code in one LLM call.
+
+ Args:
+ doc_list: Fixture providing list of P language documentation files
+ """
+ # Get project root path
+ logger.info(f"args: {doc_list}")
+
+ project_root = Path(__file__).parent.parent.parent
+
+ pipeline = PromptingPipeline()
+
+ # Add system prompt
+ system_prompt = file_utils.read_file(project_root / "resources/context_files/singleshot_prompt.txt")
+ pipeline.add_system_prompt(system_prompt)
+
+ # Add P language docs
+ pipeline.add_text("Here are the P language documentation files for reference:")
+ pipeline.add_documents_inline(doc_list, lambda fname, contents: tag_surround(fname, contents))
+
+ # Add design doc
+ pipeline.add_text("Here is the design document to implement:")
+ designdoc_content = file_utils.read_file(project_root / "resources/p-model-benchmark/3_designdoc2pprojA/1_lightswitch.txt")
+ pipeline.add_text(designdoc_content)
+
+ # Add generation instruction
+ pipeline.add_text("Generate the complete P code implementation following the structure specified in the system prompt.")
+ logger.info("Going to call it")
+ # Generate P code
+ logger.info("Invoking LLM...")
+ response = pipeline.invoke_llm(model=CLAUDE_3_7, candidates=1, heuristic='random')
+
+ # Log the response
+ logger.info("LLM Response received:")
+ logger.info("-" * 80)
+ logger.info(response)
+ logger.info("-" * 80)
+
+ # Basic validation that response contains key P elements
+ assert "machine" in response, "Generated code should contain state machines"
+ assert "event" in response, "Generated code should contain events"
+ assert "spec" in response, "Generated code should contain specifications"
+
+ # Extract components using tags
+ components = {}
+ for tag in ["project_structure", "enums_and_types", "events", "machines",
+ "monitors", "module_structure", "spec_files", "test_files",
+ "file_organization"]:
+ start_tag = f"<{tag}>"
+ end_tag = f"{tag}>"
+ if start_tag in response and end_tag in response:
+ start = response.find(start_tag) + len(start_tag)
+ end = response.find(end_tag)
+ components[tag] = response[start:end].strip()
+ logger.info(f"\n{tag}:\n{components[tag]}")
+
+ # Write generated code to files
+ written_files = write_generated_p_code(components)
+ logger.info(f"Written files: {written_files}")
+
+ # Verify files were created
+ assert len(written_files) > 0, "No files were generated"
+ for filepath in written_files:
+ assert os.path.exists(filepath), f"File not created: {filepath}"
+
+ # Verify response contains required elements
+ assert "machine" in response, "Generated code should contain state machines"
+ assert "event" in response, "Generated code should contain events"
+ assert "spec" in response, "Generated code should contain specifications"
+
+def write_generated_p_code(components):
+ """
+ Write the generated P code components to files in a generated_code directory.
+
+ Args:
+ components: Dictionary containing the tagged components from LLM response
+ Returns:
+ list: Paths of written files
+ """
+ written_files = []
+
+ # Extract project name from pproj_config
+ project_name = None
+ if 'pproj_config' in components:
+ config = components['pproj_config']
+ if 'ProjectName:' in config:
+ project_name = config.split('ProjectName:', 1)[1].split('\n')[0].strip().strip('{}')
+
+ # Create base directory structure in project root
+ project_root = Path(__file__).parent.parent.parent
+ base_dir = os.path.join(str(project_root), "generated_code")
+ if project_name:
+ base_dir = os.path.join(base_dir, project_name)
+
+ # Create directories based on project_structure from LLM response
+ if 'project_structure' in components:
+ for line in components['project_structure'].split('\n'):
+ if line.strip():
+ dir_path = os.path.join(base_dir, line.strip().rstrip('/'))
+ os.makedirs(dir_path, exist_ok=True)
+ logger.info(f"Created directory: {dir_path}")
+
+ # Write .pproj file
+ if 'pproj_config' in components:
+ pproj_path = os.path.join(base_dir, f"{project_name}.pproj" if project_name else "Project.pproj")
+ pproj_content = f"""
+
+ {project_name if project_name else "Project"}
+
+ ./PSrc
+ ./PSpec
+ ./PTst
+
+ ./PGenerated
+"""
+ with open(pproj_path, 'w') as f:
+ f.write(pproj_content)
+ written_files.append(pproj_path)
+ logger.info(f"Wrote .pproj file to: {pproj_path}")
+
+ # Parse file organization from LLM response
+ file_mapping = {}
+ if 'file_organization' in components:
+ for line in components['file_organization'].split('\n'):
+ if ':' in line:
+ filename, content_type = line.split(':', 1)
+ file_mapping[content_type.strip()] = filename.strip()
+
+ # Write enums and types
+ if 'enums_and_types' in components:
+ # Get filename from file_mapping or use default, ensuring no prefix dashes
+ filename = next((f.strip('-').strip() for f, t in file_mapping.items() if 'type' in t.lower()), 'Types.p')
+ filepath = os.path.join(base_dir, "PSrc", filename)
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ with open(filepath, 'w') as f:
+ f.write(components['enums_and_types'])
+ written_files.append(filepath)
+ logger.info(f"Wrote enums and types to: {filepath}")
+
+ # Write events
+ if 'events' in components:
+ # Get filename from file_mapping or use default, ensuring no prefix dashes
+ filename = next((f.strip('-').strip() for f, t in file_mapping.items() if 'event' in t.lower()), 'Events.p')
+ filepath = os.path.join(base_dir, "PSrc", filename)
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ with open(filepath, 'w') as f:
+ f.write(components['events'])
+ written_files.append(filepath)
+ logger.info(f"Wrote events to: {filepath}")
+
+ # Write machine files
+ if 'machines' in components:
+ for machine in components['machines'].split(''):
+ if '' in machine:
+ machine_content = machine.split('')[0].strip()
+ if machine_content:
+ # Try to extract machine name from content
+ machine_name = None
+ if 'machine' in machine_content.lower():
+ try:
+ machine_name = machine_content.split('machine', 1)[1].split()[0].strip()
+ except:
+ pass
+
+ # If no name found in content, look in file_mapping
+ if not machine_name:
+ machine_name = next((f.replace('.p', '').strip('-').strip() for f, t in file_mapping.items()
+ if 'machine' in t.lower()), 'Machine')
+
+ filepath = os.path.join(base_dir, "PSrc", f"{machine_name}.p")
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ with open(filepath, 'w') as f:
+ f.write(machine_content)
+ written_files.append(filepath)
+ logger.info(f"Wrote machine to: {filepath}")
+
+ # Write spec files
+ if 'spec_files' in components:
+ for spec in components['spec_files'].split('' in spec:
+ name = spec.split('name="')[1].split('"')[0].strip('-').strip()
+ content = spec.split('>')[1].split('')[0].strip()
+ # Use name directly from LLM response, ensuring no prefix dashes
+ filepath = os.path.join(base_dir, "PSpec", name)
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ with open(filepath, 'w') as f:
+ f.write(content)
+ written_files.append(filepath)
+ logger.info(f"Wrote spec file to: {filepath}")
+
+ # Write test files
+ if 'test_files' in components:
+ for test in components['test_files'].split('' in test:
+ name = test.split('name="')[1].split('"')[0].strip('-').strip()
+ content = test.split('>')[1].split('')[0].strip()
+ # Use name directly from LLM response, ensuring no prefix dashes
+ filepath = os.path.join(base_dir, "PTst", name)
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ with open(filepath, 'w') as f:
+ f.write(content)
+ written_files.append(filepath)
+ logger.info(f"Wrote test file to: {filepath}")
+
+ return written_files
diff --git a/Src/PeasyAI/tests/rag/test_rag_index.py b/Src/PeasyAI/tests/rag/test_rag_index.py
new file mode 100644
index 0000000000..2a87e4617f
--- /dev/null
+++ b/Src/PeasyAI/tests/rag/test_rag_index.py
@@ -0,0 +1,93 @@
+import pytest
+import os
+from pathlib import Path
+import sys
+
+PROJECT_ROOT = Path(__file__).parent.parent.parent
+SRC_ROOT = PROJECT_ROOT / "src"
+sys.path.insert(0, str(SRC_ROOT))
+
+from core.rag.vector_store import VectorStore, Document, SearchResult
+from core.rag.embeddings import get_embedding_provider
+from core.rag.p_corpus import PCorpus, PExample
+
+
+def test_vector_store_add_and_search(tmp_path):
+ store = VectorStore(persist_path=str(tmp_path / "test_vectors.json"))
+ provider = get_embedding_provider()
+
+ doc1 = Document(
+ id="doc1",
+ content="The P programming language is designed for distributed systems.",
+ embedding=provider.embed("The P programming language is designed for distributed systems."),
+ metadata={"name": "doc1", "category": "documentation"},
+ )
+ doc2 = Document(
+ id="doc2",
+ content="Python is a popular programming language.",
+ embedding=provider.embed("Python is a popular programming language."),
+ metadata={"name": "doc2", "category": "documentation"},
+ )
+ store.add(doc1)
+ store.add(doc2)
+
+ assert store.count() == 2
+
+ query_embedding = provider.embed("What is P programming language?")
+ results = store.search(query_embedding, top_k=2)
+ assert len(results) == 2
+ assert isinstance(results[0], SearchResult)
+
+
+def test_vector_store_persistence(tmp_path):
+ persist_path = str(tmp_path / "persist_test.json")
+ provider = get_embedding_provider()
+
+ store1 = VectorStore(persist_path=persist_path)
+ store1.add(Document(
+ id="persist1",
+ content="Test document one.",
+ embedding=provider.embed("Test document one."),
+ metadata={"name": "persist1"},
+ ))
+ store1.add(Document(
+ id="persist2",
+ content="Test document two.",
+ embedding=provider.embed("Test document two."),
+ metadata={"name": "persist2"},
+ ))
+ assert store1.count() == 2
+
+ store2 = VectorStore(persist_path=persist_path)
+ assert store2.count() == 2
+
+ query_embedding = provider.embed("test document")
+ results = store2.search(query_embedding, top_k=1)
+ assert len(results) == 1
+
+
+def test_corpus_index_and_search(tmp_path):
+ corpus = PCorpus(store_path=str(tmp_path / "test_corpus"))
+
+ example = PExample(
+ id="test_machine_1",
+ name="TestMachine",
+ description="A simple test machine for distributed locking",
+ code='machine TestMachine {\n start state Init {\n entry { }\n }\n}',
+ category="machine",
+ tags=["test", "distributed-lock"],
+ project_name="TestProject",
+ )
+ corpus.add_example(example)
+ assert corpus.count() == 1
+
+ results = corpus.search("distributed lock machine", top_k=1)
+ assert len(results) >= 1
+ assert results[0].document.metadata["name"] == "TestMachine"
+
+
+def test_corpus_index_bundled_resources(tmp_path):
+ corpus = PCorpus(store_path=str(tmp_path / "bundled_corpus"))
+ count = corpus.index_bundled_resources()
+ assert count > 0, "Should index at least some bundled resources"
+ assert corpus.count() > 0
diff --git a/Src/PeasyAI/tests/regression_baseline.json b/Src/PeasyAI/tests/regression_baseline.json
new file mode 100644
index 0000000000..a5f311827d
--- /dev/null
+++ b/Src/PeasyAI/tests/regression_baseline.json
@@ -0,0 +1,1541 @@
+{
+ "timestamp": "2026-02-23T16:01:03.978589",
+ "output_root": "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103",
+ "environment": {
+ "success": true,
+ "issues": [],
+ "details": {
+ "config_file": "/Users/adesai/.peasyai/settings.json",
+ "config_file_exists": true,
+ "configured_provider": "snowflake",
+ "p_compiler_path": "/Users/adesai/.dotnet/tools/p",
+ "dotnet_path": "/usr/local/share/dotnet/dotnet",
+ "toolchain_issues": [],
+ "llm_provider_detected": "snowflake_cortex"
+ },
+ "message": "Environment valid",
+ "api_version": "1.0",
+ "error_category": null,
+ "retryable": false,
+ "next_actions": [],
+ "metadata": {
+ "tool": "validate_environment",
+ "operation_id": "ca718425-ff39-4b40-91ee-a58284b6437d",
+ "timestamp": "2026-02-24T00:01:03.978111Z",
+ "provider": null,
+ "model": null,
+ "token_usage": {}
+ }
+ },
+ "protocols": [
+ {
+ "project_name": "Paxos",
+ "success": false,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Acceptor",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Client",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Learner",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Proposer",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/Paxos_full_retry/Paxos_2026_02_23_16_03_37/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/Paxos_full_retry/Paxos_2026_02_23_16_03_37/PSrc/Acceptor.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/Paxos_full_retry/Paxos_2026_02_23_16_03_37/PSrc/Client.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/Paxos_full_retry/Paxos_2026_02_23_16_03_37/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/Paxos_full_retry/Paxos_2026_02_23_16_03_37/PSrc/Learner.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/Paxos_full_retry/Paxos_2026_02_23_16_03_37/PSrc/Proposer.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/Paxos_full_retry/Paxos_2026_02_23_16_03_37/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "no viable alternative at input 'funInitEntry(config:tProposerInit,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "no viable alternative at input 'funInitEntry(config:tAcceptorInit,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 4,
+ "error": "no viable alternative at input 'funInitEntry(config:tClientInit,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 5,
+ "error": "no viable alternative at input 'value=valueToPropose)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 6,
+ "error": "no viable alternative at input 'funInitEntry(config:tClientInit,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 7,
+ "error": "no viable alternative at input 'funInitEntry(config:tLearnerInit,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 8,
+ "error": "no viable alternative at input 'funCheckSingleValueChosen(payload:tLearnPayload,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ }
+ ],
+ "success": false,
+ "total_iterations": 8,
+ "api_version": "1.0",
+ "error_category": "internal",
+ "retryable": true,
+ "next_actions": [
+ "Retry the operation or inspect the error details."
+ ],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "fe3a9f94-6dd5-48f6-a108-8d70a4dc6089",
+ "timestamp": "2026-02-24T00:06:07.066484Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": false,
+ "failed_tests": [
+ "tcBasicPaxos",
+ "tcTwoProposers",
+ "tcNetworkPartition"
+ ],
+ "passed_tests": [],
+ "error": null
+ },
+ "checker_fixes": [
+ {
+ "round": 1,
+ "fixed": false
+ }
+ ],
+ "errors": [
+ "PChecker reported failing tests"
+ ],
+ "timing": {
+ "total_s": 151.5
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "_checker_analysis": {
+ "error_category": "unknown",
+ "error_message": " Exception 'System.ArgumentException' was thrown in Learner(2) (state 'Learning', action '_HandleAccepted'): An item with the same key has already been added. Key: 2001",
+ "machine": null,
+ "machine_type": null,
+ "state": null,
+ "event": null,
+ "target_field": null,
+ "execution_steps": 120,
+ "machines_involved": {
+ "Scenario3_NetworkPartition": [
+ "Init"
+ ],
+ "Learner": [
+ "Init",
+ "Learning"
+ ],
+ "Acceptor": [
+ "Init",
+ "Ready"
+ ],
+ "Proposer": [
+ "Init",
+ "ProposalPhase"
+ ],
+ "Client": [
+ "Init",
+ "WaitingForConsensus"
+ ]
+ },
+ "last_actions": [
+ "'Proposer(9)' in state 'ProposalPhase' sent event 'eAcceptRequest with payload ()' to 'Acceptor(4)'.",
+ "'Acceptor(3)' in state 'Ready' sent event 'eAccepted with payload ()' to 'Learner(2)'.",
+ "'Learner(2)' dequeued event 'eAccepted with payload ()' in state 'Learning'.",
+ "'Acceptor(4)' dequeued event 'eAcceptRequest with payload ()' in state 'Ready'.",
+ "'Acceptor(4)' in state 'Ready' sent event 'eAccepted with payload ()' to 'Learner(2)'.",
+ "'Proposer(8)' in state 'ProposalPhase' sent event 'eAcceptRequest with payload ()' to 'Acceptor(6)'.",
+ "'Proposer(9)' in state 'ProposalPhase' sent event 'eAcceptRequest with payload ()' to 'Acceptor(5)'.",
+ "'Acceptor(6)' dequeued event 'eAcceptRequest with payload ()' in state 'Ready'.",
+ "'Acceptor(5)' dequeued event 'eAcceptRequest with payload ()' in state 'Ready'.",
+ "'Learner(2)' dequeued event 'eAccepted with payload ()' in state 'Learning'."
+ ],
+ "is_test_driver_bug": false,
+ "requires_new_event": false,
+ "requires_multi_file_fix": false
+ },
+ "_checker_root_cause": null,
+ "_checker_suggested_fixes": [],
+ "ensemble_used": 3,
+ "adaptive_retry": "full_regen",
+ "prior_fast_score": 20,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 0
+ },
+ "total": 50,
+ "max": 100
+ },
+ "attempt": 1
+ },
+ {
+ "project_name": "TwoPhaseCommit",
+ "success": true,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Client",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Coordinator",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Participant",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/TwoPhaseCommit_retry2/TwoPhaseCommit_full_retry/TwoPhaseCommit_2026_02_23_16_14_10/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/TwoPhaseCommit_retry2/TwoPhaseCommit_full_retry/TwoPhaseCommit_2026_02_23_16_14_10/PSrc/Client.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/TwoPhaseCommit_retry2/TwoPhaseCommit_full_retry/TwoPhaseCommit_2026_02_23_16_14_10/PSrc/Coordinator.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/TwoPhaseCommit_retry2/TwoPhaseCommit_full_retry/TwoPhaseCommit_2026_02_23_16_14_10/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/TwoPhaseCommit_retry2/TwoPhaseCommit_full_retry/TwoPhaseCommit_2026_02_23_16_14_10/PSrc/Participant.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/TwoPhaseCommit_retry2/TwoPhaseCommit_full_retry/TwoPhaseCommit_2026_02_23_16_14_10/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "no viable alternative at input 'funInitEntry(config:seq[machine],)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "no viable alternative at input 'funHandleWriteTransResp(resp:tWriteTransResp,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 4,
+ "error": "no viable alternative at input 'funHandleInformCoordinator(msg:tInformCoordinator,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 5,
+ "error": "no viable alternative at input 'funHandlePrepareResp(resp:tPrepareResp,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 6,
+ "error": "could not find function 'CreateTimer'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 7,
+ "error": "Module not closed. test module is not closed with respect to created interfaces; interface Timer is created but not implemented inside the module",
+ "fixed": true,
+ "needs_guidance": false
+ }
+ ],
+ "success": true,
+ "total_iterations": 7,
+ "api_version": "1.0",
+ "error_category": null,
+ "retryable": false,
+ "next_actions": [],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "8a28ec14-dd09-45be-95a8-a9dbb5766b02",
+ "timestamp": "2026-02-24T00:16:42.011920Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": true,
+ "failed_tests": [],
+ "passed_tests": [
+ "tcBasicCommit",
+ "tcMultipleClients"
+ ],
+ "error": null
+ },
+ "checker_fixes": [],
+ "errors": [],
+ "timing": {
+ "total_s": 153.6
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "ensemble_used": 3,
+ "adaptive_retry": "full_regen",
+ "prior_fast_score": 20,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 25
+ },
+ "total": 75,
+ "max": 100
+ },
+ "attempt": 2,
+ "retries_used": 1
+ },
+ {
+ "project_name": "MessageBroker",
+ "success": false,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Broker",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Consumer",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Producer",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/MessageBroker_2026_02_23_16_16_44/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/MessageBroker_2026_02_23_16_16_44/PSrc/Broker.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/MessageBroker_2026_02_23_16_16_44/PSrc/Consumer.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/MessageBroker_2026_02_23_16_16_44/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/MessageBroker_2026_02_23_16_16_44/PSrc/Producer.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/MessageBroker_2026_02_23_16_16_44/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "no viable alternative at input 'funHandlePublishAck(ack:tPublishAck,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "no viable alternative at input 'funInitEntry(config:(batchSize:int,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 4,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 5,
+ "error": "no viable alternative at input 'funProcessingEntry(response:tPollResp,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 6,
+ "error": "no viable alternative at input 'consumer=this)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 7,
+ "error": "got type: tPollResp, expected: (consumer:machine,response:tPollResp)",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 8,
+ "error": "could not find foreach iterator variable 'c'",
+ "fixed": true,
+ "needs_guidance": false
+ }
+ ],
+ "success": false,
+ "total_iterations": 8,
+ "api_version": "1.0",
+ "error_category": "internal",
+ "retryable": true,
+ "next_actions": [
+ "Retry the operation or inspect the error details."
+ ],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "d77a6dcb-b45a-4a94-938a-daae9d03053c",
+ "timestamp": "2026-02-24T00:19:06.703775Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": false,
+ "failed_tests": [
+ "tcOneProducerTwoConsumers"
+ ],
+ "passed_tests": [
+ "tcOneProducerOneConsumer",
+ "tcTwoProducersOneConsumer"
+ ],
+ "error": null
+ },
+ "checker_fixes": [
+ {
+ "round": 1,
+ "fixed": false
+ }
+ ],
+ "errors": [
+ "PChecker reported failing tests"
+ ],
+ "timing": {
+ "total_s": 144.2
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "_checker_analysis": {
+ "error_category": "unknown",
+ "error_message": " Assertion Failed: PSpec/Safety.p:107:13 NoMessageLoss violated: offset 0 delivered multiple times",
+ "machine": null,
+ "machine_type": null,
+ "state": null,
+ "event": null,
+ "target_field": null,
+ "execution_steps": 133,
+ "machines_involved": {
+ "Scenario3_OneProducerTwoConsumers": [
+ "Init"
+ ],
+ "Broker": [
+ "Init",
+ "Ready"
+ ],
+ "Producer": [
+ "Init",
+ "Publishing",
+ "Done"
+ ],
+ "Consumer": [
+ "Init",
+ "Registering",
+ "Polling",
+ "Processing"
+ ]
+ },
+ "last_actions": [
+ "Producer(3) is transitioning from state 'Publishing' to state 'PImplementation.Producer.Done'.",
+ "Producer(3) exits state 'Publishing'.",
+ "Producer(3) enters state 'Done'.",
+ "'Broker(2)' dequeued event 'eCommitOffset with payload ()' in state 'Ready'.",
+ "'Broker(2)' in state 'Ready' sent event 'eCommitOffsetAck' to 'Consumer(4)'.",
+ "'Consumer(4)' dequeued event 'eCommitOffsetAck' in state 'Processing'.",
+ "Consumer(4) is transitioning from state 'Processing' to state 'PImplementation.Consumer.Polling'.",
+ "Consumer(4) exits state 'Processing'.",
+ "Consumer(4) enters state 'Polling'.",
+ "'Broker(2)' dequeued event 'ePoll with payload ()' in state 'Ready'."
+ ],
+ "is_test_driver_bug": false,
+ "requires_new_event": false,
+ "requires_multi_file_fix": false
+ },
+ "_checker_root_cause": null,
+ "_checker_suggested_fixes": [],
+ "ensemble_used": 1,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 0
+ },
+ "total": 50,
+ "max": 100
+ },
+ "adaptive_retry": "targeted_test",
+ "prior_fast_score": 50,
+ "regen_count": 1,
+ "attempt": 1
+ },
+ {
+ "project_name": "DistributedLock",
+ "success": true,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Client",
+ "success": true
+ },
+ {
+ "name": "generate_machine:LockServer",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/DistributedLock_2026_02_23_16_19_17/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/DistributedLock_2026_02_23_16_19_17/PSrc/Client.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/DistributedLock_2026_02_23_16_19_17/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/DistributedLock_2026_02_23_16_19_17/PSrc/LockServer.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/DistributedLock_2026_02_23_16_19_17/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "no viable alternative at input 'funHandleLockResponse(resp:tLockResponse,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "no viable alternative at input 'funHandleFirstLockRequest(req:tLockRequest,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "extraneous input ',' expecting ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 4,
+ "error": "Module not closed. test module is not closed with respect to created interfaces; interface Client is created but not implemented inside the module",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 5,
+ "error": "missing '=' at '{'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 6,
+ "error": "missing Iden at ')'",
+ "fixed": true,
+ "needs_guidance": false
+ }
+ ],
+ "success": true,
+ "total_iterations": 6,
+ "api_version": "1.0",
+ "error_category": null,
+ "retryable": false,
+ "next_actions": [],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "93d6c41c-f61d-437d-a78a-6eb2d4afe7db",
+ "timestamp": "2026-02-24T00:21:02.245863Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": true,
+ "failed_tests": [],
+ "passed_tests": [
+ "tc1MultipleClientsSameResource",
+ "tc2MultipleClientsDifferentResources"
+ ],
+ "error": null
+ },
+ "checker_fixes": [],
+ "errors": [],
+ "timing": {
+ "total_s": 106.6
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "ensemble_used": 1,
+ "adaptive_retry": false,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 25
+ },
+ "total": 75,
+ "max": 100
+ },
+ "attempt": 1
+ },
+ {
+ "project_name": "HotelManagement",
+ "success": false,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Client",
+ "success": true
+ },
+ {
+ "name": "generate_machine:FrontDesk",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/HotelManagement_2026_02_23_16_21_03/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/HotelManagement_2026_02_23_16_21_03/PSrc/Client.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/HotelManagement_2026_02_23_16_21_03/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/HotelManagement_2026_02_23_16_21_03/PSrc/FrontDesk.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/HotelManagement_2026_02_23_16_21_03/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "no viable alternative at input 'funInitEntry(config:(numRooms:int,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "no viable alternative at input 'funHandleRoomReservationRequest(request:tRoomReservationRequest,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 4,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 5,
+ "error": "no viable alternative at input 'funHandleReservationResponse(response:tRoomReservationResponse,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 6,
+ "error": "extraneous input ',' expecting ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 7,
+ "error": "extraneous input 'var' expecting {'announce', 'assert', 'break', 'continue', 'foreach', 'goto', 'if', 'new', 'print', 'raise', 'receive', 'return', 'send', 'while', 'assume', '{', '}', ';', Iden}",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 8,
+ "error": "got type: (numRooms:int), expected: int",
+ "fixed": true,
+ "needs_guidance": false
+ }
+ ],
+ "success": false,
+ "total_iterations": 8,
+ "api_version": "1.0",
+ "error_category": "internal",
+ "retryable": true,
+ "next_actions": [
+ "Retry the operation or inspect the error details."
+ ],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "c2c3a546-3de9-425c-a568-80ea093f4c5f",
+ "timestamp": "2026-02-24T00:23:30.581539Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": false,
+ "failed_tests": [
+ "tcScenario1_TwoClientsSuccess",
+ "tcScenario2_ReservationFailure",
+ "tcScenario3_ReserveCancelReserve",
+ "tcScenario4_InvalidCancellation"
+ ],
+ "passed_tests": [],
+ "error": null
+ },
+ "checker_fixes": [
+ {
+ "round": 1,
+ "fixed": false
+ }
+ ],
+ "errors": [
+ "PChecker reported failing tests"
+ ],
+ "timing": {
+ "total_s": 148.6
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "_checker_analysis": {
+ "error_category": "unknown",
+ "error_message": " Exception 'System.ArgumentException' was thrown in FrontDesk(2) (state 'Ready', action '_HandleSpecialRequest'): An item with the same key has already been added. Key: 0",
+ "machine": null,
+ "machine_type": null,
+ "state": null,
+ "event": null,
+ "target_field": null,
+ "execution_steps": 56,
+ "machines_involved": {
+ "Scenario4_InvalidCancellation": [
+ "Init"
+ ],
+ "FrontDesk": [
+ "Init",
+ "Ready"
+ ],
+ "Client": [
+ "Init",
+ "WaitingForReservationResponse",
+ "DecideNextAction",
+ "WaitingForSpecialResponse"
+ ]
+ },
+ "last_actions": [
+ "'Client(3)' dequeued event 'eSpecialResponse with payload ()' in state 'WaitingForSpecialResponse'.",
+ "Client(3) is transitioning from state 'WaitingForSpecialResponse' to state 'PImplementation.Client.DecideNextAction'.",
+ "Client(3) exits state 'WaitingForSpecialResponse'.",
+ "Client(3) enters state 'DecideNextAction'.",
+ "Client(4) enters state 'Init'.",
+ "'Client(3)' in state 'DecideNextAction' sent event 'eSpecialRequest with payload ()' to 'FrontDesk(2)'.",
+ "Client(3) is transitioning from state 'DecideNextAction' to state 'PImplementation.Client.WaitingForSpecialResponse'.",
+ "Client(3) exits state 'DecideNextAction'.",
+ "Client(3) enters state 'WaitingForSpecialResponse'.",
+ "'FrontDesk(2)' dequeued event 'eSpecialRequest with payload ()' in state 'Ready'."
+ ],
+ "is_test_driver_bug": false,
+ "requires_new_event": false,
+ "requires_multi_file_fix": false
+ },
+ "_checker_root_cause": null,
+ "_checker_suggested_fixes": [],
+ "ensemble_used": 1,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 0
+ },
+ "total": 50,
+ "max": 100
+ },
+ "adaptive_retry": "targeted_machine",
+ "prior_fast_score": 50,
+ "regen_count": 2,
+ "attempt": 1
+ },
+ {
+ "project_name": "ClientServer",
+ "success": true,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Client",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Server",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/ClientServer_2026_02_23_16_24_01/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/ClientServer_2026_02_23_16_24_01/PSrc/Client.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/ClientServer_2026_02_23_16_24_01/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/ClientServer_2026_02_23_16_24_01/PSrc/Server.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/ClientServer_2026_02_23_16_24_01/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "no viable alternative at input 'funHandleRequest(req:tRequest,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "no viable alternative at input 'funHandleResponse(resp:tResponse,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "no viable alternative at input 'funTrackRequest(req:tRequest,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ }
+ ],
+ "success": true,
+ "total_iterations": 3,
+ "api_version": "1.0",
+ "error_category": null,
+ "retryable": false,
+ "next_actions": [],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "f46eeb76-6075-49c2-b7cf-7594dd605dd6",
+ "timestamp": "2026-02-24T00:24:51.134750Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": true,
+ "failed_tests": [],
+ "passed_tests": [
+ "tcSingleClient",
+ "tcMultipleClients",
+ "tcDifferentRequestCounts"
+ ],
+ "error": null
+ },
+ "checker_fixes": [],
+ "errors": [],
+ "timing": {
+ "total_s": 51.4
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "ensemble_used": 1,
+ "adaptive_retry": false,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 25
+ },
+ "total": 75,
+ "max": 100
+ },
+ "attempt": 1
+ },
+ {
+ "project_name": "FailureDetector",
+ "success": false,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Client",
+ "success": true
+ },
+ {
+ "name": "generate_machine:FailureDetector",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Node",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/FailureDetector_full_retry/FailureDetector_2026_02_23_16_26_35/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/FailureDetector_full_retry/FailureDetector_2026_02_23_16_26_35/PSrc/Client.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/FailureDetector_full_retry/FailureDetector_2026_02_23_16_26_35/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/FailureDetector_full_retry/FailureDetector_2026_02_23_16_26_35/PSrc/FailureDetector.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/FailureDetector_full_retry/FailureDetector_2026_02_23_16_26_35/PSrc/Node.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/FailureDetector_full_retry/FailureDetector_2026_02_23_16_26_35/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 4,
+ "error": "no viable alternative at input 'funInitEntry(fd:machine,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 5,
+ "error": "no viable alternative at input 'funInitEntry(payload:tMonitorNodes,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 6,
+ "error": "no viable alternative at input 'funInitEntry(fd:machine,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 7,
+ "error": "no viable alternative at input 'funHandleCrash(payload:tCrashNode,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 8,
+ "error": "could not find function 'CreateTimer'",
+ "fixed": false,
+ "needs_guidance": false
+ }
+ ],
+ "success": false,
+ "total_iterations": 8,
+ "api_version": "1.0",
+ "error_category": "internal",
+ "retryable": true,
+ "next_actions": [
+ "Retry the operation or inspect the error details."
+ ],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "65621d04-b887-4c7d-b2bb-274406367d0d",
+ "timestamp": "2026-02-24T00:32:51.904883Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": null,
+ "checker_fixes": [],
+ "errors": [
+ "Compilation failed after fix attempts"
+ ],
+ "timing": {
+ "total_s": 376.3
+ },
+ "compile_after_fix": {
+ "success": false,
+ "error": null
+ },
+ "ensemble_used": 3,
+ "adaptive_retry": "full_regen",
+ "prior_fast_score": 20,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 0,
+ "tests_discovered": 0,
+ "all_tests_pass": 0
+ },
+ "total": 20,
+ "max": 100
+ },
+ "attempt": 1
+ },
+ {
+ "project_name": "EspressoMachine",
+ "success": false,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:CoffeeMaker",
+ "success": true
+ },
+ {
+ "name": "generate_machine:CoffeeMakerControlPanel",
+ "success": true
+ },
+ {
+ "name": "generate_machine:User",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/EspressoMachine_full_retry/EspressoMachine_2026_02_23_16_41_22/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/EspressoMachine_full_retry/EspressoMachine_2026_02_23_16_41_22/PSrc/CoffeeMaker.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/EspressoMachine_full_retry/EspressoMachine_2026_02_23_16_41_22/PSrc/CoffeeMakerControlPanel.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/EspressoMachine_full_retry/EspressoMachine_2026_02_23_16_41_22/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/EspressoMachine_full_retry/EspressoMachine_2026_02_23_16_41_22/PSrc/User.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/EspressoMachine_full_retry/EspressoMachine_2026_02_23_16_41_22/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "no viable alternative at input 'funInitEntry(config:tUserInit,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "no viable alternative at input 'funInitEntry(config:tCoffeeMakerInit,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 4,
+ "error": "no viable alternative at input 'funHandleError(error:tErrorMessage,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 5,
+ "error": "no viable alternative at input 'funHandleError(error:tErrorMessage,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 6,
+ "error": "Method StartNewCoffee is used as a transition function, but might change state here.",
+ "fixed": true,
+ "needs_guidance": false
+ }
+ ],
+ "success": true,
+ "total_iterations": 6,
+ "api_version": "1.0",
+ "error_category": null,
+ "retryable": false,
+ "next_actions": [],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "0fc604bb-1474-4480-bcc4-46a64802bcb7",
+ "timestamp": "2026-02-24T00:43:07.283054Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": false,
+ "failed_tests": [
+ "tcSingleUserSuccess",
+ "tcEmptyWaterRefill",
+ "tcTwoUsersSequential"
+ ],
+ "passed_tests": [],
+ "error": null
+ },
+ "checker_fixes": [
+ {
+ "round": 1,
+ "fixed": false
+ }
+ ],
+ "errors": [
+ "PChecker reported failing tests"
+ ],
+ "timing": {
+ "total_s": 106.3
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "_checker_analysis": {
+ "error_category": "unknown",
+ "error_message": " Assertion Failed: PSpec/Safety.p:17:9 Coffee maker accepted eMakeCoffee while already busy processing a coffee",
+ "machine": null,
+ "machine_type": null,
+ "state": null,
+ "event": null,
+ "target_field": null,
+ "execution_steps": 36,
+ "machines_involved": {
+ "Scenario3_TwoUsersSequential": [
+ "Init"
+ ],
+ "CoffeeMakerControlPanel": [
+ "Init",
+ "Ready"
+ ],
+ "CoffeeMaker": [
+ "Init",
+ "Idle"
+ ],
+ "User": [
+ "Init",
+ "Active"
+ ]
+ },
+ "last_actions": [
+ "CoffeeMaker(3) enters state 'Init'.",
+ "CoffeeMaker(3) is transitioning from state 'Init' to state 'PImplementation.CoffeeMaker.Idle'.",
+ "CoffeeMaker(3) exits state 'Init'.",
+ "CoffeeMaker(3) enters state 'Idle'.",
+ "User(4) enters state 'Init'.",
+ "User(4) is transitioning from state 'Init' to state 'PImplementation.User.Active'.",
+ "User(4) exits state 'Init'.",
+ "User(4) enters state 'Active'.",
+ "'User(4)' in state 'Active' sent event 'eMakeCoffee' to 'CoffeeMakerControlPanel(2)'.",
+ "'CoffeeMakerControlPanel(2)' dequeued event 'eMakeCoffee' in state 'Ready'."
+ ],
+ "is_test_driver_bug": false,
+ "requires_new_event": false,
+ "requires_multi_file_fix": false
+ },
+ "_checker_root_cause": null,
+ "_checker_suggested_fixes": [],
+ "ensemble_used": 3,
+ "adaptive_retry": "full_regen",
+ "prior_fast_score": 20,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 0
+ },
+ "total": 50,
+ "max": 100
+ },
+ "attempt": 1
+ },
+ {
+ "project_name": "RaftLeaderElection",
+ "success": false,
+ "steps": [
+ {
+ "name": "generate_project_structure",
+ "success": true
+ },
+ {
+ "name": "generate_types_events",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Server",
+ "success": true
+ },
+ {
+ "name": "generate_machine:TestDriver",
+ "success": true
+ },
+ {
+ "name": "generate_machine:Timer",
+ "success": true
+ },
+ {
+ "name": "generate_spec",
+ "success": true
+ },
+ {
+ "name": "generate_test",
+ "success": true
+ }
+ ],
+ "generated_files": [
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/RaftLeaderElection_2026_02_23_16_43_08/PSpec/Safety.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/RaftLeaderElection_2026_02_23_16_43_08/PSrc/Enums_Types_Events.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/RaftLeaderElection_2026_02_23_16_43_08/PSrc/Server.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/RaftLeaderElection_2026_02_23_16_43_08/PSrc/TestDriver.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/RaftLeaderElection_2026_02_23_16_43_08/PSrc/Timer.p",
+ "/Users/adesai/workspace/public/P/Src/PeasyAI/generated_code/regression/20260223_160103/RaftLeaderElection_2026_02_23_16_43_08/PTst/TestDriver.p"
+ ],
+ "compile": {
+ "success": false,
+ "error": null
+ },
+ "fix": {
+ "iterations": [
+ {
+ "iteration": 1,
+ "error": "no viable alternative at input 'funInitEntry(owner:machine,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 2,
+ "error": "missing Iden at ')'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 3,
+ "error": "no viable alternative at input 'funInitEntry(config:tServerConfig,)'",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 4,
+ "error": "no viable alternative at input 'funHandleVoteResponse(payload:tVoteResponsePayload,)'",
+ "fixed": false,
+ "needs_guidance": false,
+ "incremental_regen": true
+ },
+ {
+ "iteration": 5,
+ "error": "got type: null, expected: tServerConfig",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 6,
+ "error": "got type: machine, expected: Timer",
+ "fixed": false,
+ "needs_guidance": false
+ },
+ {
+ "iteration": 7,
+ "error": "got type: null, expected: tServerConfig",
+ "fixed": true,
+ "needs_guidance": false
+ }
+ ],
+ "success": true,
+ "total_iterations": 7,
+ "api_version": "1.0",
+ "error_category": null,
+ "retryable": false,
+ "next_actions": [],
+ "metadata": {
+ "tool": "fix_iteratively",
+ "operation_id": "8ccefe4c-842c-45bf-b08c-a01d1063770d",
+ "timestamp": "2026-02-24T00:45:42.927014Z",
+ "provider": "snowflake_cortex",
+ "model": "claude-sonnet-4-5",
+ "token_usage": {}
+ }
+ },
+ "check": {
+ "success": false,
+ "failed_tests": [
+ "tcBasicElection",
+ "tcLeaderFailure",
+ "tcSplitVote"
+ ],
+ "passed_tests": [],
+ "error": null
+ },
+ "checker_fixes": [
+ {
+ "round": 1,
+ "fixed": false
+ }
+ ],
+ "errors": [
+ "PChecker reported failing tests"
+ ],
+ "timing": {
+ "total_s": 355.8
+ },
+ "compile_after_fix": {
+ "success": true,
+ "error": null
+ },
+ "_checker_analysis": {
+ "error_category": "null_target",
+ "error_message": " Target in send cannot be null. Machine Server(5) trying to send event eRequestVote to null target in state Candidate.",
+ "machine": "Server(5)",
+ "machine_type": "Server",
+ "state": "Candidate",
+ "event": "eRequestVote",
+ "target_field": null,
+ "execution_steps": 129,
+ "machines_involved": {
+ "Scenario3_SplitVote": [
+ "Init"
+ ],
+ "Server": [
+ "Init",
+ "Follower",
+ "Candidate"
+ ],
+ "Timer": [
+ "Init",
+ "WaitForTimerRequests",
+ "TimerStarted"
+ ]
+ },
+ "last_actions": [
+ "Timer(10) enters state 'Init'.",
+ "Timer(10) is transitioning from state 'Init' to state 'PImplementation.Timer.WaitForTimerRequests'.",
+ "Timer(10) exits state 'Init'.",
+ "Timer(10) enters state 'WaitForTimerRequests'.",
+ "'Server(5)' dequeued event 'eTimeOut' in state 'Follower'.",
+ "Server(5) is transitioning from state 'Follower' to state 'PImplementation.Server.Candidate'.",
+ "Server(5) exits state 'Follower'.",
+ "'Server(5)' in state 'Follower' sent event 'eCancelTimer' to 'Timer(7)'.",
+ "Server(5) enters state 'Candidate'.",
+ "'Server(5)' in state 'Candidate' sent event 'eRequestVote with payload ()' to 'Server(2)'."
+ ],
+ "is_test_driver_bug": false,
+ "requires_new_event": false,
+ "requires_multi_file_fix": false
+ },
+ "_checker_root_cause": "Machine 'Server' tried to send event 'eRequestVote' to a null machine reference in state 'Candidate'. This typically means a machine field was not initialized before use.",
+ "_checker_suggested_fixes": [],
+ "ensemble_used": 1,
+ "score": {
+ "scores": {
+ "generation_all_ok": 20,
+ "compile_first_try": 0,
+ "compile_after_fix": 15,
+ "tests_discovered": 15,
+ "all_tests_pass": 0
+ },
+ "total": 50,
+ "max": 100
+ },
+ "adaptive_retry": "targeted_machine",
+ "prior_fast_score": 50,
+ "regen_count": 2,
+ "attempt": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Src/PeasyAI/tests/test_config.py b/Src/PeasyAI/tests/test_config.py
new file mode 100644
index 0000000000..4a6935db3a
--- /dev/null
+++ b/Src/PeasyAI/tests/test_config.py
@@ -0,0 +1,280 @@
+"""
+Tests for core/config.py — PeasyAI configuration loader.
+
+Covers: load_settings, apply_settings_to_env, to_env_vars, active_provider_name,
+ active_provider_config, init_settings, malformed config handling.
+"""
+
+import json
+import os
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+PROJECT_ROOT = Path(__file__).parent.parent
+sys.path.insert(0, str(PROJECT_ROOT / "src"))
+
+from core.config import (
+ PeasyAISettings,
+ LLMConfig,
+ ProviderConfig,
+ PCompilerConfig,
+ GenerationConfig,
+ load_settings,
+ apply_settings_to_env,
+ init_settings,
+ _parse_provider,
+)
+
+
+class TestPeasyAISettingsDefaults(unittest.TestCase):
+ """Test default values for settings dataclasses."""
+
+ def test_default_settings(self):
+ s = PeasyAISettings()
+ self.assertEqual(s.llm.provider, "bedrock")
+ self.assertIsNone(s.llm.model)
+ self.assertEqual(s.llm.timeout, 600)
+ self.assertEqual(s.generation.ensemble_size, 3)
+ self.assertEqual(s.generation.output_dir, "./PGenerated")
+ self.assertIsNone(s.p_compiler.path)
+
+ def test_provider_config_defaults(self):
+ pc = ProviderConfig()
+ self.assertIsNone(pc.api_key)
+ self.assertIsNone(pc.base_url)
+ self.assertIsNone(pc.model)
+ self.assertEqual(pc.timeout, 600)
+
+
+class TestActiveProvider(unittest.TestCase):
+ """Test active_provider_name and active_provider_config."""
+
+ def test_default_provider_name(self):
+ s = PeasyAISettings()
+ with patch.dict(os.environ, {}, clear=True):
+ os.environ.pop("LLM_PROVIDER", None)
+ self.assertEqual(s.active_provider_name(), "bedrock")
+
+ def test_env_var_overrides_provider(self):
+ s = PeasyAISettings()
+ with patch.dict(os.environ, {"LLM_PROVIDER": "anthropic"}):
+ self.assertEqual(s.active_provider_name(), "anthropic")
+
+ def test_alias_normalization(self):
+ s = PeasyAISettings(
+ llm=LLMConfig(
+ provider="snowflake_cortex",
+ providers={"snowflake": ProviderConfig(api_key="key123")},
+ )
+ )
+ with patch.dict(os.environ, {}, clear=True):
+ os.environ.pop("LLM_PROVIDER", None)
+ self.assertEqual(s.active_provider_name(), "snowflake_cortex")
+ pc = s.active_provider_config()
+ self.assertEqual(pc.api_key, "key123")
+
+ def test_unknown_provider_returns_empty_config(self):
+ s = PeasyAISettings(llm=LLMConfig(provider="unknown"))
+ with patch.dict(os.environ, {}, clear=True):
+ os.environ.pop("LLM_PROVIDER", None)
+ pc = s.active_provider_config()
+ self.assertIsNone(pc.api_key)
+
+
+class TestToEnvVars(unittest.TestCase):
+ """Test to_env_vars for each provider type."""
+
+ def test_snowflake_env_vars(self):
+ s = PeasyAISettings(
+ llm=LLMConfig(
+ provider="snowflake",
+ model="claude-sonnet-4-5",
+ providers={"snowflake": ProviderConfig(
+ api_key="sk-test",
+ base_url="https://acct.snowflakecomputing.com/api",
+ )},
+ )
+ )
+ with patch.dict(os.environ, {}, clear=True):
+ os.environ.pop("LLM_PROVIDER", None)
+ env = s.to_env_vars()
+ self.assertEqual(env["OPENAI_API_KEY"], "sk-test")
+ self.assertEqual(env["OPENAI_BASE_URL"], "https://acct.snowflakecomputing.com/api")
+ self.assertEqual(env["OPENAI_MODEL_NAME"], "claude-sonnet-4-5")
+ self.assertEqual(env["LLM_PROVIDER"], "snowflake")
+
+ def test_anthropic_env_vars(self):
+ s = PeasyAISettings(
+ llm=LLMConfig(
+ provider="anthropic",
+ providers={"anthropic": ProviderConfig(api_key="ant-key")},
+ )
+ )
+ with patch.dict(os.environ, {}, clear=True):
+ os.environ.pop("LLM_PROVIDER", None)
+ env = s.to_env_vars()
+ self.assertEqual(env["ANTHROPIC_API_KEY"], "ant-key")
+ self.assertEqual(env["ANTHROPIC_MODEL_NAME"], "claude-3-5-sonnet-20241022")
+
+ def test_bedrock_env_vars(self):
+ s = PeasyAISettings(
+ llm=LLMConfig(
+ provider="bedrock",
+ providers={"bedrock": ProviderConfig(region="us-east-1")},
+ )
+ )
+ with patch.dict(os.environ, {}, clear=True):
+ os.environ.pop("LLM_PROVIDER", None)
+ env = s.to_env_vars()
+ self.assertEqual(env["AWS_REGION"], "us-east-1")
+ self.assertEqual(env["BEDROCK_MODEL_ID"], "anthropic.claude-3-5-sonnet-20241022-v2:0")
+
+ def test_model_precedence_llm_level_over_provider(self):
+ """llm.model should take priority over providers.snowflake.model."""
+ s = PeasyAISettings(
+ llm=LLMConfig(
+ provider="snowflake",
+ model="top-level-model",
+ providers={"snowflake": ProviderConfig(
+ api_key="k", base_url="https://x.com", model="provider-model",
+ )},
+ )
+ )
+ with patch.dict(os.environ, {}, clear=True):
+ os.environ.pop("LLM_PROVIDER", None)
+ env = s.to_env_vars()
+ self.assertEqual(env["OPENAI_MODEL_NAME"], "top-level-model")
+
+
+class TestLoadSettings(unittest.TestCase):
+ """Test load_settings with temp files (auto-cleaned)."""
+
+ def _write_temp(self, content: str) -> Path:
+ """Write content to a temp file and return its Path."""
+ fd, path = tempfile.mkstemp(suffix=".json", prefix="peasyai_test_")
+ os.write(fd, content.encode())
+ os.close(fd)
+ self._tmp_files.append(path)
+ return Path(path)
+
+ def setUp(self):
+ self._tmp_files: list = []
+
+ def tearDown(self):
+ for p in self._tmp_files:
+ try:
+ os.unlink(p)
+ except OSError:
+ pass
+
+ def test_missing_file_returns_defaults(self):
+ s = load_settings(Path("/tmp/nonexistent_peasyai_test_xyz.json"))
+ self.assertEqual(s.llm.provider, "bedrock")
+
+ def test_valid_settings_file(self):
+ data = {
+ "llm": {
+ "provider": "snowflake",
+ "model": "test-model",
+ "timeout": 300,
+ "providers": {
+ "snowflake": {
+ "api_key": "test-key",
+ "base_url": "https://test.snowflakecomputing.com",
+ }
+ },
+ },
+ "p_compiler": {"path": "/usr/local/bin/p"},
+ "generation": {"ensemble_size": 5, "output_dir": "/tmp/gen"},
+ }
+ tmp = self._write_temp(json.dumps(data))
+ s = load_settings(tmp)
+ self.assertEqual(s.llm.provider, "snowflake")
+ self.assertEqual(s.llm.model, "test-model")
+ self.assertEqual(s.llm.timeout, 300)
+ self.assertEqual(s.llm.providers["snowflake"].api_key, "test-key")
+ self.assertEqual(s.p_compiler.path, "/usr/local/bin/p")
+ self.assertEqual(s.generation.ensemble_size, 5)
+ self.assertEqual(s.generation.output_dir, "/tmp/gen")
+
+ def test_malformed_json_returns_defaults(self):
+ tmp = self._write_temp("{ this is not valid json !!!")
+ s = load_settings(tmp)
+ self.assertEqual(s.llm.provider, "bedrock")
+
+ def test_empty_json_returns_defaults(self):
+ tmp = self._write_temp("{}")
+ s = load_settings(tmp)
+ self.assertEqual(s.llm.provider, "bedrock")
+ self.assertEqual(s.generation.ensemble_size, 3)
+
+ def test_partial_config_fills_defaults(self):
+ data = {"llm": {"provider": "anthropic"}}
+ tmp = self._write_temp(json.dumps(data))
+ s = load_settings(tmp)
+ self.assertEqual(s.llm.provider, "anthropic")
+ self.assertEqual(s.llm.timeout, 600)
+ self.assertEqual(s.generation.ensemble_size, 3)
+
+
+class TestApplySettingsToEnv(unittest.TestCase):
+ """Test apply_settings_to_env."""
+
+ @patch.dict(os.environ, {}, clear=True)
+ def test_sets_env_vars(self):
+ s = PeasyAISettings(
+ llm=LLMConfig(
+ provider="snowflake",
+ providers={"snowflake": ProviderConfig(
+ api_key="apply-test-key",
+ base_url="https://apply.snowflakecomputing.com",
+ )},
+ )
+ )
+ apply_settings_to_env(s)
+ self.assertEqual(os.environ.get("OPENAI_API_KEY"), "apply-test-key")
+ self.assertEqual(os.environ.get("LLM_PROVIDER"), "snowflake")
+
+ @patch.dict(os.environ, {"OPENAI_API_KEY": "existing-key"}, clear=False)
+ def test_existing_env_not_overwritten(self):
+ s = PeasyAISettings(
+ llm=LLMConfig(
+ provider="snowflake",
+ providers={"snowflake": ProviderConfig(api_key="new-key", base_url="https://x.com")},
+ )
+ )
+ apply_settings_to_env(s)
+ self.assertEqual(os.environ["OPENAI_API_KEY"], "existing-key")
+
+
+class TestParseProvider(unittest.TestCase):
+ """Test _parse_provider helper."""
+
+ def test_full_config(self):
+ raw = {
+ "api_key": "k",
+ "base_url": "https://x.com",
+ "model": "m",
+ "model_id": "mid",
+ "region": "us-west-2",
+ "timeout": 120,
+ }
+ pc = _parse_provider(raw)
+ self.assertEqual(pc.api_key, "k")
+ self.assertEqual(pc.base_url, "https://x.com")
+ self.assertEqual(pc.model, "m")
+ self.assertEqual(pc.model_id, "mid")
+ self.assertEqual(pc.region, "us-west-2")
+ self.assertEqual(pc.timeout, 120)
+
+ def test_empty_config(self):
+ pc = _parse_provider({})
+ self.assertIsNone(pc.api_key)
+ self.assertEqual(pc.timeout, 600)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Src/PeasyAI/tests/test_error_parsers.py b/Src/PeasyAI/tests/test_error_parsers.py
new file mode 100644
index 0000000000..35eea68a58
--- /dev/null
+++ b/Src/PeasyAI/tests/test_error_parsers.py
@@ -0,0 +1,330 @@
+"""
+Tests for core/compilation/error_parser.py and checker_error_parser.py.
+
+Covers: PCompilerErrorParser.parse, error categorization, CompilationResult,
+ parse_compilation_output, PCheckerErrorParser.parse, PCheckerErrorParser.analyze.
+"""
+
+import sys
+from pathlib import Path
+
+import pytest
+
+PROJECT_ROOT = Path(__file__).parent.parent
+sys.path.insert(0, str(PROJECT_ROOT / "src"))
+
+from core.compilation.error_parser import (
+ PCompilerErrorParser,
+ PCompilerError,
+ ErrorType,
+ ErrorCategory,
+ CompilationResult,
+ parse_compilation_output,
+)
+from core.compilation.checker_error_parser import (
+ PCheckerErrorParser,
+ CheckerErrorCategory,
+ CheckerError,
+ MachineState,
+ EventInfo,
+)
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# PCompilerErrorParser
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestPCompilerErrorParser:
+
+ def test_parse_parse_error(self):
+ output = "[Client.p] parse error: line 15:8 mismatched input 'var' expecting ')'"
+ errors = PCompilerErrorParser.parse(output)
+ assert len(errors) == 1
+ e = errors[0]
+ assert e.file == "Client.p"
+ assert e.line == 15
+ assert e.column == 8
+ assert e.error_type == ErrorType.PARSE
+ assert "mismatched" in e.message
+
+ def test_parse_type_error(self):
+ output = "[Server.p] error: line 42:10 undefined event 'eTest'"
+ errors = PCompilerErrorParser.parse(output)
+ assert len(errors) == 1
+ assert errors[0].error_type == ErrorType.TYPE
+ assert errors[0].category == ErrorCategory.UNDEFINED_EVENT
+
+ def test_parse_semantic_error(self):
+ output = "[Error:] [Types.p:5:3] duplicates declaration of 'tConfig'"
+ errors = PCompilerErrorParser.parse(output)
+ assert len(errors) == 1
+ assert errors[0].error_type == ErrorType.SEMANTIC
+ assert errors[0].category == ErrorCategory.DUPLICATE_DECLARATION
+
+ def test_parse_multiple_errors(self):
+ output = """
+[Client.p] parse error: line 10:5 extraneous input 'var' expecting ')'
+[Server.p] error: line 20:3 type 'tFoo' not found
+[Error:] [Types.p:5:3] duplicates declaration of 'tConfig'
+"""
+ errors = PCompilerErrorParser.parse(output)
+ assert len(errors) == 3
+
+ def test_deduplication(self):
+ output = """
+[Client.p] parse error: line 10:5 some error
+[Client.p] parse error: line 10:5 some error
+"""
+ errors = PCompilerErrorParser.parse(output)
+ assert len(errors) == 1
+
+ def test_no_errors(self):
+ output = "Compilation succeeded. Build succeeded."
+ errors = PCompilerErrorParser.parse(output)
+ assert len(errors) == 0
+
+
+class TestErrorCategorization:
+
+ def test_var_declaration_order(self):
+ cat = PCompilerErrorParser._categorize_error("extraneous input 'var' expecting ')'")
+ assert cat == ErrorCategory.VAR_DECLARATION_ORDER
+
+ def test_undefined_event(self):
+ cat = PCompilerErrorParser._categorize_error("event 'eTest' not found in scope")
+ assert cat == ErrorCategory.UNDEFINED_EVENT
+
+ def test_undefined_type(self):
+ cat = PCompilerErrorParser._categorize_error("type 'tConfig' not found")
+ assert cat == ErrorCategory.UNDEFINED_TYPE
+
+ def test_duplicate_declaration(self):
+ cat = PCompilerErrorParser._categorize_error("duplicates declaration of 'eStart'")
+ assert cat == ErrorCategory.DUPLICATE_DECLARATION
+
+ def test_type_mismatch(self):
+ cat = PCompilerErrorParser._categorize_error("got type: int expected: bool")
+ assert cat == ErrorCategory.TYPE_MISMATCH
+
+ def test_missing_semicolon(self):
+ cat = PCompilerErrorParser._categorize_error("expecting ';' at end of statement")
+ assert cat == ErrorCategory.MISSING_SEMICOLON
+
+ def test_unknown_error(self):
+ cat = PCompilerErrorParser._categorize_error("something completely unexpected")
+ assert cat == ErrorCategory.UNKNOWN
+
+
+class TestCompilationResult:
+
+ def test_success_result(self):
+ r = CompilationResult(success=True, errors=[], output="Build succeeded")
+ assert r.success
+ assert r.get_first_error() is None
+ assert r.get_errors_by_file() == {}
+
+ def test_failed_result(self):
+ e1 = PCompilerError(
+ file="a.p", line=1, column=1, error_type=ErrorType.PARSE,
+ category=ErrorCategory.UNKNOWN, message="err1", raw_message="err1",
+ )
+ e2 = PCompilerError(
+ file="a.p", line=5, column=1, error_type=ErrorType.TYPE,
+ category=ErrorCategory.UNKNOWN, message="err2", raw_message="err2",
+ )
+ e3 = PCompilerError(
+ file="b.p", line=1, column=1, error_type=ErrorType.PARSE,
+ category=ErrorCategory.UNKNOWN, message="err3", raw_message="err3",
+ )
+ r = CompilationResult(success=False, errors=[e1, e2, e3])
+ assert r.get_first_error() == e1
+ by_file = r.get_errors_by_file()
+ assert len(by_file["a.p"]) == 2
+ assert len(by_file["b.p"]) == 1
+
+ def test_to_dict(self):
+ r = CompilationResult(success=True, errors=[], warnings=["w1"])
+ d = r.to_dict()
+ assert d["success"] is True
+ assert d["error_count"] == 0
+ assert d["warnings"] == ["w1"]
+
+
+class TestParseCompilationOutput:
+
+ def test_success_output(self):
+ output = "Building...\nCompilation succeeded\n~~ [PTool]: Thanks for using P! ~~"
+ r = parse_compilation_output(output)
+ assert r.success
+ assert len(r.errors) == 0
+
+ def test_failure_output(self):
+ output = """
+[Client.p] parse error: line 10:5 extraneous input 'var'
+Build failed.
+"""
+ r = parse_compilation_output(output)
+ assert not r.success
+ assert len(r.errors) == 1
+
+ def test_warnings_extracted(self):
+ output = "Compilation succeeded\nwarning: unused variable 'x'"
+ r = parse_compilation_output(output)
+ assert r.success
+ assert len(r.warnings) == 1
+
+
+class TestPCompilerErrorToDict:
+
+ def test_to_dict(self):
+ e = PCompilerError(
+ file="test.p", line=10, column=5,
+ error_type=ErrorType.PARSE, category=ErrorCategory.MISSING_SEMICOLON,
+ message="expecting ';'", raw_message="raw",
+ suggestion="Add semicolon",
+ )
+ d = e.to_dict()
+ assert d["file"] == "test.p"
+ assert d["line"] == 10
+ assert d["error_type"] == "parse"
+ assert d["category"] == "missing_semicolon"
+ assert d["suggestion"] == "Add semicolon"
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# PCheckerErrorParser
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestPCheckerErrorParser:
+
+ def test_parse_null_target(self):
+ trace = """
+ 'Coordinator(1)' enters state 'SendCommit'
+ Target in send cannot be null. Machine Coordinator(1) trying to send event eCommit to null target in state SendCommit
+"""
+ parser = PCheckerErrorParser()
+ error = parser.parse(trace)
+ assert error.category == CheckerErrorCategory.NULL_TARGET
+ assert error.machine_type == "Coordinator"
+ assert error.machine_id == "1"
+ assert error.event_name == "eCommit"
+ assert error.machine_state == "SendCommit"
+
+ def test_parse_unhandled_event(self):
+ trace = """
+ 'Server(2)' enters state 'Active'
+ Server(2) received event 'eShutdown' that cannot be handled
+"""
+ parser = PCheckerErrorParser()
+ error = parser.parse(trace)
+ assert error.category == CheckerErrorCategory.UNHANDLED_EVENT
+ assert error.machine_type == "Server"
+ assert error.event_name == "eShutdown"
+
+ def test_parse_unhandled_event_with_namespace(self):
+ trace = " Node(3) received event 'PImplementation.eLearn' that cannot be handled"
+ parser = PCheckerErrorParser()
+ error = parser.parse(trace)
+ assert error.category == CheckerErrorCategory.UNHANDLED_EVENT
+ assert error.event_name == "eLearn"
+
+ def test_parse_assertion_failure(self):
+ trace = " Assertion 'balance >= 0' failed in machine Account(1)"
+ parser = PCheckerErrorParser()
+ error = parser.parse(trace)
+ assert error.category == CheckerErrorCategory.ASSERTION_FAILURE
+
+ def test_parse_deadlock(self):
+ trace = " Deadlock detected. All machines are blocked."
+ parser = PCheckerErrorParser()
+ error = parser.parse(trace)
+ assert error.category == CheckerErrorCategory.DEADLOCK
+
+ def test_parse_liveness_violation(self):
+ trace = " Monitor 'Liveness' detected hot state violation"
+ parser = PCheckerErrorParser()
+ error = parser.parse(trace)
+ assert error.category == CheckerErrorCategory.LIVENESS_VIOLATION
+
+ def test_parse_unknown_error(self):
+ trace = "Some output without any recognizable error pattern"
+ parser = PCheckerErrorParser()
+ error = parser.parse(trace)
+ assert error.category == CheckerErrorCategory.UNKNOWN
+
+ def test_parse_empty_trace(self):
+ parser = PCheckerErrorParser()
+ error = parser.parse("")
+ assert error.category == CheckerErrorCategory.UNKNOWN
+
+
+class TestPCheckerAnalyze:
+
+ def test_analyze_extracts_machines(self):
+ trace = """
+ 'Client(1)' enters state 'Init'
+ 'Server(2)' enters state 'Listening'
+ 'Client(1)' in state 'Init' sent event 'eRequest' to 'Server(2)'
+ 'Client(1)' enters state 'WaitingResponse'
+ Server(2) received event 'eShutdown' that cannot be handled
+"""
+ parser = PCheckerErrorParser()
+ analysis = parser.analyze(trace)
+ assert "Client" in analysis.machines_involved
+ assert "Server" in analysis.machines_involved
+ assert analysis.execution_steps > 0
+ assert len(analysis.last_actions) > 0
+
+ def test_analyze_generates_summary(self):
+ trace = """
+ 'Worker(1)' enters state 'Active'
+ Worker(1) received event 'eStop' that cannot be handled
+"""
+ parser = PCheckerErrorParser()
+ analysis = parser.analyze(trace)
+ summary = analysis.get_summary()
+ assert "PChecker Error Analysis" in summary
+ assert "unhandled_event" in summary
+
+
+class TestMachineState:
+
+ def test_from_log_enters(self):
+ line = " 'Client(1)' enters state 'Init'"
+ ms = MachineState.from_log(line)
+ assert ms is not None
+ assert ms.machine_type == "Client"
+ assert ms.machine_id == "1"
+ assert ms.state == "Init"
+
+ def test_from_log_no_match(self):
+ line = "Some random log line"
+ ms = MachineState.from_log(line)
+ assert ms is None
+
+
+class TestEventInfo:
+
+ def test_from_send_log(self):
+ line = " 'Client(1)' in state 'Active' sent event 'eRequest with payload (42)' to 'Server(2)'"
+ ei = EventInfo.from_log(line)
+ assert ei is not None
+ assert ei.event_name == "eRequest"
+ assert ei.sender == "Client(1)"
+ assert ei.receiver == "Server(2)"
+
+ def test_from_dequeue_log(self):
+ line = " 'Server(2)' dequeued event 'eRequest with payload (42)'"
+ ei = EventInfo.from_log(line)
+ assert ei is not None
+ assert ei.event_name == "eRequest"
+ assert ei.receiver == "Server(2)"
+
+ def test_no_match(self):
+ line = "Just a regular line"
+ ei = EventInfo.from_log(line)
+ assert ei is None
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/Src/PeasyAI/tests/test_mcp_contracts.py b/Src/PeasyAI/tests/test_mcp_contracts.py
new file mode 100644
index 0000000000..bb80e3a894
--- /dev/null
+++ b/Src/PeasyAI/tests/test_mcp_contracts.py
@@ -0,0 +1,119 @@
+import sys
+import unittest
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import patch
+
+
+PROJECT_ROOT = Path(__file__).parent.parent
+SRC_ROOT = PROJECT_ROOT / "src"
+sys.path.insert(0, str(SRC_ROOT))
+sys.path.insert(0, str(PROJECT_ROOT))
+
+from ui.mcp.contracts import with_metadata
+from ui.mcp.tools.generation import register_generation_tools, GenerateTypesEventsParams
+from ui.mcp.tools.compilation import register_compilation_tools, PCompileParams
+
+
+class DummyMCP:
+ def tool(self, *args, **kwargs):
+ def decorator(fn):
+ return fn
+ return decorator
+
+
+def _with_metadata(tool_name, payload, token_usage=None, provider_name=None, model=None):
+ return with_metadata(
+ tool_name=tool_name,
+ payload=payload,
+ token_usage=token_usage,
+ provider_name=provider_name,
+ model=model,
+ )
+
+
+class TestMCPContracts(unittest.TestCase):
+ def test_with_metadata_success_defaults(self):
+ payload = {"success": True, "message": "ok"}
+ response = _with_metadata("demo_tool", payload)
+
+ self.assertEqual(response["api_version"], "1.0")
+ self.assertIsNone(response["error_category"])
+ self.assertFalse(response["retryable"])
+ self.assertEqual(response["next_actions"], [])
+ self.assertIn("metadata", response)
+ self.assertEqual(response["metadata"]["tool"], "demo_tool")
+
+ def test_with_metadata_failure_defaults(self):
+ payload = {"success": False, "error": "boom"}
+ response = _with_metadata("demo_tool", payload)
+
+ self.assertEqual(response["api_version"], "1.0")
+ self.assertEqual(response["error_category"], "internal")
+ self.assertTrue(response["retryable"])
+ self.assertGreater(len(response["next_actions"]), 0)
+ self.assertEqual(response["metadata"]["tool"], "demo_tool")
+
+ @patch("ui.mcp.tools.generation.validate_project_path")
+ def test_generation_tool_contract_shape(self, mock_validate):
+ # Mock path validation to return the path as-is
+ mock_validate.return_value = Path("/tmp/Project")
+
+ mcp = DummyMCP()
+
+ mock_result = SimpleNamespace(
+ success=True,
+ filename="Enums_Types_Events.p",
+ file_path="/tmp/Project/PSrc/Enums_Types_Events.p",
+ code="event eTest;",
+ error=None,
+ token_usage={"inputTokens": 10, "outputTokens": 20},
+ )
+ generation = SimpleNamespace(generate_types_events=lambda **_: mock_result)
+
+ get_services = lambda: {"generation": generation}
+ tools = register_generation_tools(mcp, get_services, _with_metadata)
+
+ response = tools["generate_types_events"](
+ GenerateTypesEventsParams(
+ design_doc="# T\n## Components\n## Interactions\n",
+ project_path="/tmp/Project",
+ )
+ )
+
+ self.assertTrue(response["success"])
+ self.assertTrue(response["preview_only"])
+ self.assertEqual(response["api_version"], "1.0")
+ self.assertIsNone(response["error_category"])
+ self.assertFalse(response["retryable"])
+ self.assertIn("metadata", response)
+
+
+ @patch("ui.mcp.tools.compilation.validate_project_path")
+ def test_compilation_tool_contract_shape(self, mock_validate):
+ # Mock path validation to return the path as-is
+ mock_validate.return_value = Path("/tmp/Project")
+
+ mcp = DummyMCP()
+ compile_result = SimpleNamespace(
+ success=True,
+ stdout="Build succeeded",
+ stderr="",
+ return_code=0,
+ error=None,
+ )
+ compilation = SimpleNamespace(compile=lambda _: compile_result)
+
+ get_services = lambda: {"compilation": compilation}
+ tools = register_compilation_tools(mcp, get_services, _with_metadata)
+
+ response = tools["p_compile"](PCompileParams(path="/tmp/Project"))
+
+ self.assertTrue(response["success"])
+ self.assertEqual(response["api_version"], "1.0")
+ self.assertFalse(response["retryable"])
+ self.assertEqual(response["metadata"]["tool"], "peasy-ai-compile")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Src/PeasyAI/tests/test_phase1_architecture.py b/Src/PeasyAI/tests/test_phase1_architecture.py
new file mode 100644
index 0000000000..8c46900f5a
--- /dev/null
+++ b/Src/PeasyAI/tests/test_phase1_architecture.py
@@ -0,0 +1,348 @@
+#!/usr/bin/env python3
+"""
+Phase 1 Architecture Tests
+
+Tests for the new LLM provider abstraction and services layer.
+"""
+
+import os
+import sys
+import unittest
+from pathlib import Path
+from unittest.mock import Mock, patch, MagicMock
+
+# Add project root to path
+PROJECT_ROOT = Path(__file__).parent.parent
+sys.path.insert(0, str(PROJECT_ROOT / "src"))
+
+from core.llm.base import (
+ LLMProvider,
+ LLMConfig,
+ LLMResponse,
+ Message,
+ MessageRole,
+ Document,
+ TokenUsage,
+)
+from core.llm.factory import LLMProviderFactory, get_default_provider, reset_default_provider
+from core.services.base import EventCallback, ResourceLoader, BaseService
+from core.services.generation import GenerationService, GenerationResult
+from core.services.compilation import CompilationService, CompilationResult, ParsedError
+from core.services.fixer import FixerService, FixResult, FixAttemptTracker
+
+
+class TestLLMDataModels(unittest.TestCase):
+ """Test LLM data model classes"""
+
+ def test_message_creation(self):
+ """Test Message creation and conversion"""
+ msg = Message(role=MessageRole.USER, content="Hello")
+ self.assertEqual(msg.role, MessageRole.USER)
+ self.assertEqual(msg.content, "Hello")
+ self.assertIsNone(msg.documents)
+
+ d = msg.to_dict()
+ self.assertEqual(d["role"], "user")
+ self.assertEqual(d["content"], "Hello")
+
+ def test_message_with_documents(self):
+ """Test Message with document attachments"""
+ doc = Document(name="test", content="Test content")
+ msg = Message(role=MessageRole.USER, content="See attached", documents=[doc])
+
+ full_content = msg.get_full_content()
+ self.assertIn("See attached", full_content)
+ self.assertIn("", full_content)
+ self.assertIn("Test content", full_content)
+
+ def test_llm_config_defaults(self):
+ """Test LLMConfig default values"""
+ config = LLMConfig()
+ self.assertEqual(config.max_tokens, 4096)
+ self.assertEqual(config.temperature, 1.0)
+ self.assertEqual(config.top_p, 0.999)
+ self.assertEqual(config.timeout, 600.0)
+
+ def test_token_usage(self):
+ """Test TokenUsage conversion"""
+ usage = TokenUsage(input_tokens=100, output_tokens=50, total_tokens=150)
+ d = usage.to_dict()
+ self.assertEqual(d["inputTokens"], 100)
+ self.assertEqual(d["outputTokens"], 50)
+ self.assertEqual(d["totalTokens"], 150)
+
+ def test_llm_response(self):
+ """Test LLMResponse conversion to legacy format"""
+ usage = TokenUsage(input_tokens=100, output_tokens=50, total_tokens=150)
+ response = LLMResponse(
+ content="Hello world",
+ usage=usage,
+ finish_reason="stop",
+ latency_ms=500,
+ model="claude-3-5-sonnet",
+ provider="test"
+ )
+
+ d = response.to_dict()
+ self.assertEqual(d["output"]["message"]["content"][0]["text"], "Hello world")
+ self.assertEqual(d["stopReason"], "stop")
+ self.assertEqual(d["usage"]["inputTokens"], 100)
+
+
+class TestLLMProviderFactory(unittest.TestCase):
+ """Test LLM provider factory"""
+
+ def setUp(self):
+ reset_default_provider()
+ # Clear relevant env vars
+ for var in ["LLM_PROVIDER", "OPENAI_BASE_URL", "OPENAI_API_KEY",
+ "ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"]:
+ if var in os.environ:
+ del os.environ[var]
+
+ def tearDown(self):
+ reset_default_provider()
+
+ def test_create_unknown_provider(self):
+ """Test creating unknown provider raises error"""
+ with self.assertRaises(ValueError) as ctx:
+ LLMProviderFactory.create("unknown_provider", {})
+ self.assertIn("Unknown provider", str(ctx.exception))
+
+ @patch.dict(os.environ, {
+ "OPENAI_API_KEY": "test-key",
+ "OPENAI_BASE_URL": "https://test.snowflakecomputing.com/api"
+ })
+ def test_auto_detect_snowflake(self):
+ """Test auto-detection of Snowflake Cortex"""
+ provider = LLMProviderFactory.from_env()
+ self.assertEqual(provider.name, "snowflake_cortex")
+
+ @patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}, clear=True)
+ def test_auto_detect_anthropic(self):
+ """Test auto-detection of Anthropic"""
+ # Clear snowflake vars
+ os.environ.pop("OPENAI_BASE_URL", None)
+ provider = LLMProviderFactory.from_env()
+ self.assertEqual(provider.name, "anthropic")
+
+ def test_explicit_provider_selection(self):
+ """Test explicit provider selection via env var"""
+ with patch.dict(os.environ, {
+ "LLM_PROVIDER": "bedrock",
+ "AWS_REGION": "us-west-2"
+ }):
+ provider = LLMProviderFactory.from_env()
+ self.assertEqual(provider.name, "bedrock")
+
+
+class TestEventCallback(unittest.TestCase):
+ """Test event callback system"""
+
+ def test_default_callbacks_use_logger(self):
+ """Test that default callbacks don't raise exceptions"""
+ cb = EventCallback()
+ # These should not raise
+ cb.status("Test status")
+ cb.progress("Step", 1, 10)
+ cb.error("Test error")
+ cb.warning("Test warning")
+
+ def test_custom_callbacks(self):
+ """Test custom callback functions"""
+ statuses = []
+ errors = []
+
+ cb = EventCallback(
+ on_status=lambda msg: statuses.append(msg),
+ on_error=lambda msg: errors.append(msg)
+ )
+
+ cb.status("Status 1")
+ cb.status("Status 2")
+ cb.error("Error 1")
+
+ self.assertEqual(statuses, ["Status 1", "Status 2"])
+ self.assertEqual(errors, ["Error 1"])
+
+
+class TestResourceLoader(unittest.TestCase):
+ """Test resource loader"""
+
+ def setUp(self):
+ self.loader = ResourceLoader(PROJECT_ROOT / "resources")
+
+ def test_load_existing_file(self):
+ """Test loading an existing resource file"""
+ # This file should exist
+ try:
+ content = self.loader.load_context("about_p.txt")
+ self.assertIsInstance(content, str)
+ self.assertGreater(len(content), 0)
+ except FileNotFoundError:
+ self.skipTest("Resource file not found")
+
+ def test_load_nonexistent_file(self):
+ """Test loading non-existent file raises error"""
+ with self.assertRaises(FileNotFoundError):
+ self.loader.load("nonexistent_file_12345.txt")
+
+ def test_cache_behavior(self):
+ """Test that caching works"""
+ try:
+ # Load twice
+ content1 = self.loader.load_context("about_p.txt")
+ content2 = self.loader.load_context("about_p.txt")
+ self.assertEqual(content1, content2)
+
+ # Clear cache and reload
+ self.loader.clear_cache()
+ content3 = self.loader.load_context("about_p.txt")
+ self.assertEqual(content1, content3)
+ except FileNotFoundError:
+ self.skipTest("Resource file not found")
+
+
+class TestFixAttemptTracker(unittest.TestCase):
+ """Test fix attempt tracking"""
+
+ def test_add_and_count_attempts(self):
+ """Test adding and counting attempts"""
+ tracker = FixAttemptTracker(max_attempts=3)
+
+ self.assertEqual(tracker.get_attempt_count("error1"), 0)
+
+ tracker.add_attempt("error1", "Attempt 1")
+ self.assertEqual(tracker.get_attempt_count("error1"), 1)
+
+ tracker.add_attempt("error1", "Attempt 2")
+ self.assertEqual(tracker.get_attempt_count("error1"), 2)
+
+ def test_should_request_guidance(self):
+ """Test guidance request threshold"""
+ tracker = FixAttemptTracker(max_attempts=2)
+
+ self.assertFalse(tracker.should_request_guidance("error1"))
+
+ tracker.add_attempt("error1", "Attempt 1")
+ self.assertFalse(tracker.should_request_guidance("error1"))
+
+ tracker.add_attempt("error1", "Attempt 2")
+ self.assertTrue(tracker.should_request_guidance("error1"))
+
+ def test_clear_attempts(self):
+ """Test clearing attempts"""
+ tracker = FixAttemptTracker()
+
+ tracker.add_attempt("error1", "Attempt 1")
+ tracker.add_attempt("error1", "Attempt 2")
+ self.assertEqual(tracker.get_attempt_count("error1"), 2)
+
+ tracker.clear("error1")
+ self.assertEqual(tracker.get_attempt_count("error1"), 0)
+
+ def test_get_attempts(self):
+ """Test getting attempt descriptions"""
+ tracker = FixAttemptTracker()
+
+ tracker.add_attempt("error1", "First try")
+ tracker.add_attempt("error1", "Second try")
+
+ attempts = tracker.get_attempts("error1")
+ self.assertEqual(attempts, ["First try", "Second try"])
+
+
+class TestCompilationService(unittest.TestCase):
+ """Test compilation service"""
+
+ def test_parse_error(self):
+ """Test parsing compilation error messages"""
+ service = CompilationService()
+
+ output = """
+Building project...
+/path/to/file.p(10, 5): error: undefined event 'eTest'
+Build failed.
+"""
+
+ error = service.parse_error(output)
+ self.assertIsNotNone(error)
+ self.assertEqual(error.file_path, "/path/to/file.p")
+ self.assertEqual(error.line_number, 10)
+ self.assertEqual(error.column_number, 5)
+ self.assertIn("undefined event", error.message)
+
+ def test_parse_no_error(self):
+ """Test parsing output with no errors"""
+ service = CompilationService()
+
+ output = "Build succeeded."
+ error = service.parse_error(output)
+ self.assertIsNone(error)
+
+ def test_get_all_errors(self):
+ """Test getting all errors from output"""
+ service = CompilationService()
+
+ output = """
+/path/file1.p(10, 5): error: error 1
+/path/file2.p(20, 10): error: error 2
+/path/file3.p(30, 15): warning: warning 1
+"""
+
+ errors = service.get_all_errors(output)
+ self.assertEqual(len(errors), 3)
+ self.assertEqual(errors[0].file_path, "/path/file1.p")
+ self.assertEqual(errors[1].file_path, "/path/file2.p")
+ self.assertEqual(errors[2].error_type, "warning")
+
+
+class TestMockLLMProvider(unittest.TestCase):
+ """Test services with mocked LLM provider"""
+
+ def setUp(self):
+ self.mock_provider = Mock(spec=LLMProvider)
+ self.mock_provider.name = "mock"
+ self.mock_provider.default_model = "mock-model"
+
+ # Setup default response
+ self.mock_response = LLMResponse(
+ content="\nevent eTest;\n",
+ usage=TokenUsage(input_tokens=100, output_tokens=50, total_tokens=150),
+ finish_reason="stop",
+ latency_ms=500,
+ model="mock-model",
+ provider="mock"
+ )
+ self.mock_provider.complete.return_value = self.mock_response
+
+ def test_generation_service_with_mock(self):
+ """Test GenerationService with mocked LLM"""
+ service = GenerationService(llm_provider=self.mock_provider)
+
+ # Verify provider is used
+ self.assertEqual(service.llm, self.mock_provider)
+
+
+class TestServiceIntegration(unittest.TestCase):
+ """Integration tests for services"""
+
+ def test_services_share_provider(self):
+ """Test that services can share an LLM provider"""
+ mock_provider = Mock(spec=LLMProvider)
+ mock_provider.name = "shared"
+
+ gen_service = GenerationService(llm_provider=mock_provider)
+ comp_service = CompilationService(llm_provider=mock_provider)
+ fix_service = FixerService(llm_provider=mock_provider)
+
+ # All should use the same provider
+ self.assertEqual(gen_service.llm, mock_provider)
+ self.assertEqual(comp_service.llm, mock_provider)
+ self.assertEqual(fix_service.llm, mock_provider)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
+
+
diff --git a/Src/PeasyAI/tests/test_security.py b/Src/PeasyAI/tests/test_security.py
new file mode 100644
index 0000000000..d724987470
--- /dev/null
+++ b/Src/PeasyAI/tests/test_security.py
@@ -0,0 +1,190 @@
+"""Tests for core.security path validation and input sanitization."""
+
+import os
+import stat
+import tempfile
+from pathlib import Path
+
+import pytest
+
+from core.security import (
+ PathSecurityError,
+ check_input_size,
+ sanitize_error,
+ validate_file_read_path,
+ validate_file_write_path,
+ validate_project_path,
+)
+
+
+# ---------------------------------------------------------------------------
+# Fixtures
+# ---------------------------------------------------------------------------
+
+
+@pytest.fixture()
+def fake_project(tmp_path: Path):
+ """Create a minimal P project directory structure."""
+ (tmp_path / "PSrc").mkdir()
+ (tmp_path / "PSpec").mkdir()
+ (tmp_path / "PTst").mkdir()
+ (tmp_path / "MyProject.pproj").write_text("")
+ (tmp_path / "PSrc" / "Main.p").write_text("machine Main {}")
+ return tmp_path
+
+
+# ---------------------------------------------------------------------------
+# validate_project_path
+# ---------------------------------------------------------------------------
+
+
+class TestValidateProjectPath:
+ def test_valid_project_accepted(self, fake_project: Path):
+ result = validate_project_path(str(fake_project))
+ assert result == fake_project.resolve()
+
+ def test_rejects_nonexistent(self, tmp_path: Path):
+ with pytest.raises(PathSecurityError, match="does not exist"):
+ validate_project_path(str(tmp_path / "no_such_dir"))
+
+ def test_rejects_non_directory(self, tmp_path: Path):
+ f = tmp_path / "file.txt"
+ f.write_text("hi")
+ with pytest.raises(PathSecurityError, match="not a directory"):
+ validate_project_path(str(f))
+
+ def test_rejects_traversal(self, fake_project: Path):
+ traversal = str(fake_project) + "/../" + fake_project.name
+ with pytest.raises(PathSecurityError, match="traversal"):
+ validate_project_path(traversal)
+
+ def test_rejects_non_p_project(self, tmp_path: Path):
+ (tmp_path / "random").mkdir()
+ with pytest.raises(PathSecurityError, match="does not look like a P project"):
+ validate_project_path(str(tmp_path / "random"))
+
+ def test_accepts_project_with_only_psrc(self, tmp_path: Path):
+ (tmp_path / "PSrc").mkdir()
+ result = validate_project_path(str(tmp_path))
+ assert result == tmp_path.resolve()
+
+ def test_accepts_project_with_only_pproj(self, tmp_path: Path):
+ (tmp_path / "Example.pproj").write_text("")
+ result = validate_project_path(str(tmp_path))
+ assert result == tmp_path.resolve()
+
+
+# ---------------------------------------------------------------------------
+# validate_file_write_path
+# ---------------------------------------------------------------------------
+
+
+class TestValidateFileWritePath:
+ def test_valid_write_accepted(self, fake_project: Path):
+ fp = str(fake_project / "PSrc" / "NewMachine.p")
+ result = validate_file_write_path(fp, str(fake_project))
+ assert result == Path(fp).resolve()
+
+ def test_rejects_escape(self, fake_project: Path):
+ fp = str(fake_project / ".." / "evil.p")
+ with pytest.raises(PathSecurityError, match="traversal"):
+ validate_file_write_path(fp, str(fake_project))
+
+ def test_rejects_outside_project(self, fake_project: Path):
+ # Create a sibling directory that is outside the fake_project
+ outside_dir = fake_project.parent / "elsewhere"
+ outside_dir.mkdir(parents=True, exist_ok=True)
+ outside = outside_dir / "evil.p"
+ with pytest.raises(PathSecurityError, match="inside the project"):
+ validate_file_write_path(str(outside), str(fake_project))
+
+ def test_rejects_non_p_extension(self, fake_project: Path):
+ fp = str(fake_project / "PSrc" / "hack.sh")
+ with pytest.raises(PathSecurityError, match="Only P files"):
+ validate_file_write_path(fp, str(fake_project))
+
+ def test_accepts_pproj_extension(self, fake_project: Path):
+ fp = str(fake_project / "New.pproj")
+ result = validate_file_write_path(fp, str(fake_project))
+ assert result.suffix == ".pproj"
+
+
+# ---------------------------------------------------------------------------
+# validate_file_read_path
+# ---------------------------------------------------------------------------
+
+
+class TestValidateFileReadPath:
+ def test_valid_read_accepted(self, fake_project: Path):
+ fp = str(fake_project / "PSrc" / "Main.p")
+ result = validate_file_read_path(fp, str(fake_project))
+ assert result == Path(fp).resolve()
+
+ def test_rejects_traversal(self, fake_project: Path):
+ fp = str(fake_project / ".." / "etc" / "passwd")
+ with pytest.raises(PathSecurityError, match="traversal"):
+ validate_file_read_path(fp, str(fake_project))
+
+ def test_rejects_outside_project(self, fake_project: Path):
+ # Create a sibling directory that is outside the fake_project
+ outside_dir = fake_project.parent / "other"
+ outside_dir.mkdir(parents=True, exist_ok=True)
+ outside = outside_dir / "secret.p"
+ with pytest.raises(PathSecurityError, match="inside the project"):
+ validate_file_read_path(str(outside), str(fake_project))
+
+ def test_accepts_without_project_constraint(self, tmp_path: Path):
+ f = tmp_path / "anything.txt"
+ f.write_text("data")
+ result = validate_file_read_path(str(f))
+ assert result == f.resolve()
+
+
+# ---------------------------------------------------------------------------
+# check_input_size
+# ---------------------------------------------------------------------------
+
+
+class TestCheckInputSize:
+ def test_accepts_within_limit(self):
+ check_input_size("hello", "test_field", 100)
+
+ def test_rejects_oversized(self):
+ with pytest.raises(ValueError, match="too large"):
+ check_input_size("x" * 200, "test_field", 100)
+
+ def test_counts_utf8_bytes(self):
+ emoji = "\U0001f600" # 4 bytes in UTF-8
+ check_input_size(emoji, "emoji", 4)
+ with pytest.raises(ValueError):
+ check_input_size(emoji, "emoji", 3)
+
+
+# ---------------------------------------------------------------------------
+# sanitize_error
+# ---------------------------------------------------------------------------
+
+
+class TestSanitizeError:
+ def test_redacts_long_paths(self):
+ err = FileNotFoundError("/very/long/absolute/path/to/secret/file.txt not found")
+ result = sanitize_error(err, "test")
+ assert "/very/long" not in result
+ assert "file.txt" in result
+ assert "[test]" in result
+
+ def test_keeps_short_paths(self):
+ err = ValueError("port 8080 is busy")
+ result = sanitize_error(err)
+ assert "8080" in result
+
+ def test_includes_context(self):
+ err = RuntimeError("boom")
+ result = sanitize_error(err, "compile")
+ assert "[compile]" in result
+ assert "RuntimeError" in result
+
+ def test_no_context(self):
+ err = RuntimeError("boom")
+ result = sanitize_error(err)
+ assert result.startswith("RuntimeError")
diff --git a/Src/PeasyAI/tests/test_snowflake_latest_model.py b/Src/PeasyAI/tests/test_snowflake_latest_model.py
new file mode 100644
index 0000000000..a5b7e0bf2c
--- /dev/null
+++ b/Src/PeasyAI/tests/test_snowflake_latest_model.py
@@ -0,0 +1,35 @@
+import sys
+import unittest
+from pathlib import Path
+
+
+PROJECT_ROOT = Path(__file__).parent.parent
+SRC_ROOT = PROJECT_ROOT / "src"
+sys.path.insert(0, str(SRC_ROOT))
+
+from core.llm.snowflake import SnowflakeCortexProvider
+
+
+class TestSnowflakeModelSelection(unittest.TestCase):
+ def test_uses_fixed_default_model(self):
+ provider = SnowflakeCortexProvider(
+ {
+ "api_key": "test-key",
+ "base_url": "https://snowflake.invalid/v1",
+ }
+ )
+ self.assertEqual(provider.default_model, "claude-opus-4-6")
+
+ def test_explicit_config_model_overrides_default(self):
+ provider = SnowflakeCortexProvider(
+ {
+ "api_key": "test-key",
+ "base_url": "https://snowflake.invalid/v1",
+ "model": "claude-opus-4-5",
+ }
+ )
+ self.assertEqual(provider.default_model, "claude-opus-4-5")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Src/PeasyAI/tests/test_validation.py b/Src/PeasyAI/tests/test_validation.py
new file mode 100644
index 0000000000..44873ab3c1
--- /dev/null
+++ b/Src/PeasyAI/tests/test_validation.py
@@ -0,0 +1,875 @@
+"""
+Tests for the Validation Pipeline.
+
+Tests the validators and pipeline for P code validation.
+"""
+
+import pytest
+import sys
+from pathlib import Path
+
+# Add project paths
+PROJECT_ROOT = Path(__file__).parent.parent
+SRC_ROOT = PROJECT_ROOT / "src"
+sys.path.insert(0, str(SRC_ROOT))
+
+from core.validation import (
+ ValidationPipeline,
+ SyntaxValidator,
+ TypeDeclarationValidator,
+ EventDeclarationValidator,
+ MachineStructureValidator,
+ DesignDocValidator,
+ ProjectPathValidator,
+ IssueSeverity,
+ NamedTupleConstructionValidator,
+)
+
+
+class TestSyntaxValidator:
+ """Tests for SyntaxValidator."""
+
+ def test_valid_machine(self):
+ """Test validation of valid machine code."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry {
+ goto Running;
+ }
+ }
+
+ state Running {
+ on eStop do {
+ goto Init;
+ }
+ }
+ }
+ """
+ validator = SyntaxValidator()
+ result = validator.validate(code)
+
+ assert result.is_valid
+ assert len(result.errors) == 0
+
+ def test_unbalanced_braces(self):
+ """Test detection of unbalanced braces."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry {
+ goto Running;
+ }
+ }
+ """ # Missing closing brace
+
+ validator = SyntaxValidator()
+ result = validator.validate(code)
+
+ assert not result.is_valid
+ assert any("brace" in issue.message.lower() for issue in result.errors)
+
+ def test_unbalanced_parentheses(self):
+ """Test detection of unbalanced parentheses."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry {
+ if (x > 0 {
+ goto Running;
+ }
+ }
+ }
+ }
+ """
+
+ validator = SyntaxValidator()
+ result = validator.validate(code)
+
+ assert not result.is_valid
+ assert any("parenthes" in issue.message.lower() for issue in result.errors)
+
+
+class TestTypeDeclarationValidator:
+ """Tests for TypeDeclarationValidator."""
+
+ def test_builtin_types(self):
+ """Test that built-in types are recognized."""
+ code = """
+ var x: int;
+ var y: bool;
+ var z: string;
+ """
+
+ validator = TypeDeclarationValidator()
+ result = validator.validate(code)
+
+ # Built-in types should not cause warnings
+ assert result.is_valid
+
+ def test_declared_type(self):
+ """Test that declared types are recognized."""
+ code = """
+ type MyType = (x: int, y: int);
+ var point: MyType;
+ """
+
+ validator = TypeDeclarationValidator()
+ result = validator.validate(code)
+
+ assert result.is_valid
+
+ def test_undeclared_type_warning(self):
+ """Test warning for potentially undeclared types."""
+ code = """
+ var point: UndeclaredType;
+ """
+
+ validator = TypeDeclarationValidator()
+ result = validator.validate(code)
+
+ # Should have a warning (not error) for undeclared type
+ assert any("UndeclaredType" in issue.message for issue in result.warnings)
+
+ def test_type_from_context(self):
+ """Test that types from context files are recognized."""
+ code = """
+ var point: SharedType;
+ """
+ context = {
+ "types.p": "type SharedType = (x: int, y: int);"
+ }
+
+ validator = TypeDeclarationValidator()
+ result = validator.validate(code, context)
+
+ # Should not warn about SharedType since it's in context
+ assert not any("SharedType" in issue.message for issue in result.issues)
+
+
+class TestEventDeclarationValidator:
+ """Tests for EventDeclarationValidator."""
+
+ def test_declared_event(self):
+ """Test that declared events are recognized."""
+ code = """
+ event eStart;
+
+ machine TestMachine {
+ start state Init {
+ entry {
+ send eStart, this;
+ }
+ }
+ }
+ """
+
+ validator = EventDeclarationValidator()
+ result = validator.validate(code)
+
+ assert result.is_valid
+
+ def test_undeclared_event_warning(self):
+ """Test warning for potentially undeclared events."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry {
+ raise eUndeclared;
+ }
+ }
+ }
+ """
+
+ validator = EventDeclarationValidator()
+ result = validator.validate(code)
+
+ assert any("eUndeclared" in issue.message for issue in result.warnings)
+
+ def test_event_from_context(self):
+ """Test that events from context files are recognized."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry {
+ send eSharedEvent, this;
+ }
+ }
+ }
+ """
+ context = {
+ "events.p": "event eSharedEvent;"
+ }
+
+ validator = EventDeclarationValidator()
+ result = validator.validate(code, context)
+
+ assert not any("eSharedEvent" in issue.message for issue in result.issues)
+
+
+class TestMachineStructureValidator:
+ """Tests for MachineStructureValidator."""
+
+ def test_valid_machine(self):
+ """Test validation of valid machine structure."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry {
+ goto Running;
+ }
+ }
+
+ state Running {
+ on eStop do {
+ goto Init;
+ }
+ }
+ }
+ """
+
+ validator = MachineStructureValidator()
+ result = validator.validate(code)
+
+ assert result.is_valid
+
+ def test_missing_start_state(self):
+ """Test detection of missing start state."""
+ code = """
+ machine TestMachine {
+ state Running {
+ on eStop do {
+ goto Init;
+ }
+ }
+ }
+ """
+
+ validator = MachineStructureValidator()
+ result = validator.validate(code)
+
+ assert not result.is_valid
+ assert any("start state" in issue.message.lower() for issue in result.errors)
+
+ def test_no_states(self):
+ """Test detection of machine with no states."""
+ code = """
+ machine TestMachine {
+ }
+ """
+
+ validator = MachineStructureValidator()
+ result = validator.validate(code)
+
+ assert not result.is_valid
+ assert any("start state" in issue.message.lower() for issue in result.errors)
+
+ def test_non_machine_file(self):
+ """Test that non-machine files pass validation."""
+ code = """
+ type Point = (x: int, y: int);
+ event eMove: Point;
+ """
+
+ validator = MachineStructureValidator()
+ result = validator.validate(code)
+
+ # Non-machine files should pass
+ assert result.is_valid
+
+
+class TestValidationPipeline:
+ """Tests for ValidationPipeline."""
+
+ def test_pipeline_runs_all_validators(self):
+ """Test that pipeline runs all validators."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry {
+ goto Running;
+ }
+ }
+
+ state Running {
+ }
+ }
+ """
+
+ pipeline = ValidationPipeline()
+ result = pipeline.validate(code)
+
+ assert len(result.validators_run) >= 4
+ assert "SyntaxValidator" in result.validators_run
+ assert "TypeDeclarationValidator" in result.validators_run
+ assert "EventDeclarationValidator" in result.validators_run
+ assert "MachineStructureValidator" in result.validators_run
+
+ def test_pipeline_collects_all_issues(self):
+ """Test that pipeline collects issues from all validators."""
+ code = """
+ machine TestMachine {
+ state Running {
+ entry {
+ send eUndeclared, this;
+ var x: UndeclaredType;
+ }
+ }
+ }
+ """
+
+ pipeline = ValidationPipeline()
+ result = pipeline.validate(code)
+
+ # Should have issues from multiple validators
+ assert len(result.issues) > 0
+
+ def test_pipeline_summary(self):
+ """Test pipeline result summary."""
+ code = """
+ machine TestMachine {
+ start state Init {
+ entry { }
+ }
+ }
+ """
+
+ pipeline = ValidationPipeline()
+ result = pipeline.validate(code)
+
+ summary = result.summary()
+ assert "Validation" in summary
+ assert "Validators run" in summary
+
+
+class TestDesignDocValidator:
+ """Tests for DesignDocValidator."""
+
+ def test_valid_design_doc(self):
+ """Test validation of valid design document."""
+ doc = """
+# Test System
+
+## Introduction
+This is a test system with two components.
+
+## Components
+- Client: Sends requests
+- Server: Handles requests
+
+## Interactions
+Client sends eRequest to Server.
+Server responds with eResponse.
+ """
+
+ validator = DesignDocValidator()
+ result = validator.validate(doc)
+
+ assert result.is_valid
+
+ def test_too_short(self):
+ """Test rejection of too-short documents."""
+ doc = "Short doc"
+
+ validator = DesignDocValidator()
+ result = validator.validate(doc)
+
+ assert not result.is_valid
+ assert any("too short" in error.lower() for error in result.errors)
+
+ def test_missing_sections_warning(self):
+ """Test warning for missing sections."""
+ doc = """
+ This is a document without proper sections.
+ It describes a system but doesn't use the expected format.
+ """ * 10 # Make it long enough
+
+ validator = DesignDocValidator()
+ result = validator.validate(doc)
+
+ # Should have warnings about missing sections
+ assert len(result.warnings) > 0
+
+ def test_extract_metadata(self):
+ """Test metadata extraction."""
+ doc = """
+# My System
+
+## Components
+
+#### 1. Client
+- **Role:** Sends requests
+
+#### 2. Server
+- **Role:** Handles requests
+ """
+
+ validator = DesignDocValidator()
+ metadata = validator.extract_metadata(doc)
+
+ assert metadata["title"] == "My System"
+ assert "Client" in metadata["components"]
+ assert "Server" in metadata["components"]
+
+
+class TestProjectPathValidator:
+ """Tests for ProjectPathValidator."""
+
+ def test_nonexistent_path(self, tmp_path):
+ """Test validation of nonexistent path."""
+ validator = ProjectPathValidator()
+ result = validator.validate_existing_project(str(tmp_path / "nonexistent"))
+
+ assert not result.is_valid
+
+ def test_valid_project_structure(self, tmp_path):
+ """Test validation of valid project structure."""
+ # Create project structure
+ (tmp_path / "PSrc").mkdir()
+ (tmp_path / "PSpec").mkdir()
+ (tmp_path / "PTst").mkdir()
+ (tmp_path / "test.pproj").touch()
+ (tmp_path / "PSrc" / "main.p").write_text("machine Main { }")
+
+ validator = ProjectPathValidator()
+ result = validator.validate_existing_project(str(tmp_path))
+
+ assert result.is_valid
+
+ def test_missing_directories_warning(self, tmp_path):
+ """Test warning for missing directories."""
+ # Create only PSrc
+ (tmp_path / "PSrc").mkdir()
+ (tmp_path / "PSrc" / "main.p").write_text("machine Main { }")
+
+ validator = ProjectPathValidator()
+ result = validator.validate_existing_project(str(tmp_path))
+
+ # Should have warnings about missing directories
+ assert any("PSpec" in w or "PTst" in w for w in result.warnings)
+
+ def test_output_path_validation(self, tmp_path):
+ """Test validation of output path."""
+ output_path = tmp_path / "new_project"
+
+ validator = ProjectPathValidator()
+ result = validator.validate_output_path(str(output_path))
+
+ assert result.is_valid
+
+
+class TestNamedTupleConstructionValidator:
+ """Tests for NamedTupleConstructionValidator."""
+
+ def test_correct_new_with_named_tuple(self):
+ """Correct named-tuple constructor should pass."""
+ code = """
+ machine Client {
+ start state Init {
+ entry InitEntry;
+ }
+ fun InitEntry(config: tClientConfig) {
+ goto Active;
+ }
+ state Active { }
+ }
+ machine TestScenario {
+ start state Init {
+ entry {
+ var c: machine;
+ c = new Client((server = this,));
+ }
+ }
+ }
+ """
+ context = {
+ "types.p": "type tClientConfig = (server: machine);"
+ }
+ validator = NamedTupleConstructionValidator()
+ result = validator.validate(code, context)
+ assert result.is_valid
+ assert not result.errors
+
+ def test_bare_value_in_new(self):
+ """Bare value instead of named tuple should be flagged as ERROR."""
+ code = """
+ machine FailureDetector {
+ start state Init {
+ entry InitEntry;
+ }
+ fun InitEntry(config: tFDConfig) {
+ goto Monitoring;
+ }
+ state Monitoring { }
+ }
+ machine TestScenario {
+ start state Init {
+ entry {
+ var nodes: seq[machine];
+ var fd: machine;
+ fd = new FailureDetector(nodes);
+ }
+ }
+ }
+ """
+ context = {
+ "types.p": "type tFDConfig = (nodes: seq[machine]);"
+ }
+ validator = NamedTupleConstructionValidator()
+ result = validator.validate(code, context)
+ assert not result.is_valid
+ assert any("bare value" in i.message for i in result.errors)
+
+ def test_wrong_field_name_in_new(self):
+ """Wrong field name should produce a warning."""
+ code = """
+ machine Node {
+ start state Init {
+ entry InitEntry;
+ }
+ fun InitEntry(config: tNodeConfig) {
+ goto Alive;
+ }
+ state Alive { }
+ }
+ machine TestScenario {
+ start state Init {
+ entry {
+ var fd: machine;
+ var n: machine;
+ n = new Node((detector = fd,));
+ }
+ }
+ }
+ """
+ context = {
+ "types.p": "type tNodeConfig = (failureDetector: machine);"
+ }
+ validator = NamedTupleConstructionValidator()
+ result = validator.validate(code, context)
+ assert any("missing field" in i.message for i in result.warnings)
+ assert any("unexpected field" in i.message for i in result.warnings)
+
+ def test_bare_value_in_send(self):
+ """Bare value in send payload should be flagged."""
+ code = """
+ machine Sender {
+ start state Init {
+ entry {
+ var target: machine;
+ send target, eRegister, (this);
+ }
+ }
+ }
+ """
+ context = {
+ "types.p": (
+ "type tRegPayload = (client: machine);\n"
+ "event eRegister: tRegPayload;"
+ )
+ }
+ validator = NamedTupleConstructionValidator()
+ result = validator.validate(code, context)
+ assert not result.is_valid
+ assert any("bare value" in i.message for i in result.errors)
+
+ def test_correct_send_with_named_tuple(self):
+ """Correct named-tuple send should pass."""
+ code = """
+ machine Sender {
+ start state Init {
+ entry {
+ var target: machine;
+ send target, eRegister, (client = this,);
+ }
+ }
+ }
+ """
+ context = {
+ "types.p": (
+ "type tRegPayload = (client: machine);\n"
+ "event eRegister: tRegPayload;"
+ )
+ }
+ validator = NamedTupleConstructionValidator()
+ result = validator.validate(code, context)
+ assert result.is_valid
+
+ def test_no_config_type_skipped(self):
+ """Machines without a typed entry param should be skipped."""
+ code = """
+ machine Simple {
+ start state Init {
+ entry { }
+ }
+ }
+ machine Test {
+ start state Init {
+ entry {
+ var s: machine;
+ s = new Simple();
+ }
+ }
+ }
+ """
+ validator = NamedTupleConstructionValidator()
+ result = validator.validate(code)
+ assert result.is_valid
+
+ def test_inline_entry_param(self):
+ """Machines with inline entry (param: Type) should be detected."""
+ code = """
+ machine Worker {
+ start state Init {
+ entry (config: tWorkerConfig) {
+ goto Working;
+ }
+ }
+ state Working { }
+ }
+ machine Test {
+ start state Init {
+ entry {
+ var w: machine;
+ w = new Worker(42);
+ }
+ }
+ }
+ """
+ context = {
+ "types.p": "type tWorkerConfig = (id: int);"
+ }
+ validator = NamedTupleConstructionValidator()
+ result = validator.validate(code, context)
+ assert not result.is_valid
+ assert any("bare value" in i.message for i in result.errors)
+
+
+class TestPostProcessorTrailingComma:
+ """Tests for trailing comma removal in parameter lists."""
+
+ def _process(self, code):
+ from core.compilation.p_post_processor import PCodePostProcessor
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_fun_trailing_comma(self):
+ code = "fun InitEntry(config: tConfig,) {\n goto Active;\n}"
+ result = self._process(code)
+ assert ",)" not in result.code
+ assert "config: tConfig)" in result.code
+ assert len(result.fixes_applied) > 0
+
+ def test_entry_trailing_comma(self):
+ code = "entry (payload: tPayload,) {\n goto Active;\n}"
+ result = self._process(code)
+ assert ",)" not in result.code
+ assert "payload: tPayload)" in result.code
+
+ def test_on_do_trailing_comma(self):
+ code = "on eRequest do (req: tRequest,) {\n goto Active;\n}"
+ result = self._process(code)
+ assert ",)" not in result.code
+ assert "req: tRequest)" in result.code
+
+ def test_no_false_positive_on_tuple_values(self):
+ """Trailing comma in tuple values should NOT be removed."""
+ code = "send target, eEvent, (value,);"
+ result = self._process(code)
+ assert "(value,)" in result.code
+
+ def test_multi_param_trailing_comma(self):
+ code = "fun Foo(a: int, b: string,) {\n return;\n}"
+ result = self._process(code)
+ assert ",)" not in result.code
+ assert "b: string)" in result.code
+
+ def test_no_trailing_comma_untouched(self):
+ code = "fun Bar(x: int) {\n return;\n}"
+ result = self._process(code)
+ assert "x: int)" in result.code
+ assert not any("trailing comma" in f.lower() for f in result.fixes_applied)
+
+
+class TestPostProcessorTestDeclUnion:
+ """Tests for (union { ... }) fix in test declarations."""
+
+ def _process(self, code):
+ from core.compilation.p_post_processor import PCodePostProcessor
+ proc = PCodePostProcessor()
+ return proc.process(code, is_test_file=True)
+
+ def test_union_syntax_removed(self):
+ code = (
+ 'test tcBasic [main=TestDriver]:\n'
+ ' assert Safety in\n'
+ ' (union { Server, Client, TestDriver });'
+ )
+ result = self._process(code)
+ assert "(union" not in result.code
+ assert "{ Server, Client, TestDriver }" in result.code
+ assert len(result.fixes_applied) > 0
+
+ def test_union_without_assert(self):
+ code = 'test tcBasic [main=TestDriver]: (union { Server, Client });'
+ result = self._process(code)
+ assert "(union" not in result.code
+ assert "{ Server, Client }" in result.code
+
+ def test_missing_semicolon_added(self):
+ code = (
+ 'test tcBasic [main=TestDriver]:\n'
+ ' assert Safety in\n'
+ ' { Server, Client, TestDriver }\n'
+ )
+ result = self._process(code)
+ assert "TestDriver };" in result.code
+
+ def test_existing_semicolon_untouched(self):
+ code = (
+ 'test tcBasic [main=TestDriver]:\n'
+ ' assert Safety in\n'
+ ' { Server, Client, TestDriver };\n'
+ )
+ result = self._process(code)
+ assert result.code.count("};") == 0 or result.code.count(";") == code.count(";")
+
+ def test_multiple_test_decls(self):
+ code = (
+ 'test tc1 [main=D1]: assert S in (union { A, B, D1 });\n'
+ 'test tc2 [main=D2]: assert S in (union { A, C, D2 });\n'
+ )
+ result = self._process(code)
+ assert "(union" not in result.code
+ assert "{ A, B, D1 }" in result.code
+ assert "{ A, C, D2 }" in result.code
+
+ def test_multiline_missing_semicolon(self):
+ """Multi-line test declaration missing semicolon should be fixed."""
+ code = (
+ 'test tcBasic [main=Scenario1]:\n'
+ ' assert Safety, Liveness in\n'
+ ' { Server, Client, Scenario1 }\n'
+ )
+ result = self._process(code)
+ assert "Scenario1 };" in result.code
+
+
+class TestDocumentationReviewParser:
+ """Tests for the LLM documentation review response parser."""
+
+ def test_parse_valid_response(self):
+ from core.services.generation import GenerationService
+ original = "machine Coordinator {\n start state Init {}\n}\n"
+ response = (
+ "Some analysis text.\n\n"
+ "\n"
+ "// Coordinator for the Two Phase Commit protocol\n"
+ "machine Coordinator {\n"
+ " start state Init {}\n"
+ "}\n"
+ ""
+ )
+ result = GenerationService._parse_documentation_review_response(response, original)
+ assert result["status"] == "success"
+ assert "// Coordinator for the Two Phase Commit protocol" in result["code"]
+ assert "machine Coordinator" in result["code"]
+
+ def test_parse_missing_tags(self):
+ from core.services.generation import GenerationService
+ original = "machine Coordinator {\n start state Init {}\n}\n"
+ response = "Here is the code with comments:\nmachine Coordinator {\n start state Init {}\n}\n"
+ result = GenerationService._parse_documentation_review_response(response, original)
+ assert result["status"] == "parse_error"
+ assert result["code"] == original
+
+ def test_parse_empty_documented_code(self):
+ from core.services.generation import GenerationService
+ original = "machine Coordinator {\n start state Init {}\n}\n"
+ response = "\n"
+ result = GenerationService._parse_documentation_review_response(response, original)
+ assert result["status"] == "parse_error"
+ assert result["code"] == original
+
+ def test_parse_rejects_dropped_declarations(self):
+ from core.services.generation import GenerationService
+ original = "machine Coordinator {\n start state Init {}\n}\n"
+ response = (
+ "\n"
+ "// Only comments, no machine declaration\n"
+ "// The coordinator handles transactions\n"
+ ""
+ )
+ result = GenerationService._parse_documentation_review_response(response, original)
+ assert result["status"] == "declarations_dropped"
+ assert result["code"] == original
+
+ def test_parse_accepts_matching_declarations(self):
+ from core.services.generation import GenerationService
+ original = (
+ "machine Coordinator {\n start state Init {}\n}\n"
+ "machine Participant {\n start state Init {}\n}\n"
+ )
+ response = (
+ "\n"
+ "// Coordinator orchestrates 2PC\n"
+ "machine Coordinator {\n start state Init {}\n}\n"
+ "// Participant votes on transactions\n"
+ "machine Participant {\n start state Init {}\n}\n"
+ ""
+ )
+ result = GenerationService._parse_documentation_review_response(response, original)
+ assert result["status"] == "success"
+ assert "machine Coordinator" in result["code"]
+ assert "machine Participant" in result["code"]
+
+ def test_parse_spec_declarations_preserved(self):
+ from core.services.generation import GenerationService
+ original = "spec Atomicity observes eCommit {\n start state Init {}\n}\n"
+ response = (
+ "\n"
+ "// Atomicity: ensures all-or-nothing commit semantics\n"
+ "spec Atomicity observes eCommit {\n start state Init {}\n}\n"
+ ""
+ )
+ result = GenerationService._parse_documentation_review_response(response, original)
+ assert result["status"] == "success"
+ assert "spec Atomicity" in result["code"]
+
+ def test_parse_truncated_response(self):
+ from core.services.generation import GenerationService
+ original = "machine Coordinator {\n start state Init {}\n}\n"
+ response = (
+ "\n"
+ "// Coordinator for 2PC\n"
+ "machine Coordinator {\n"
+ " start state Init {}\n"
+ "}\n"
+ )
+ result = GenerationService._parse_documentation_review_response(
+ response, original, finish_reason="length"
+ )
+ assert result["status"] == "truncated"
+ assert result["code"] == original
+ assert "truncated" in result["reason"].lower()
+
+ def test_parse_truncated_before_open_tag(self):
+ from core.services.generation import GenerationService
+ original = "machine Coordinator {\n start state Init {}\n}\n"
+ response = "Here is the documented code with insightful comments explaining"
+ result = GenerationService._parse_documentation_review_response(
+ response, original, finish_reason="length"
+ )
+ assert result["status"] == "truncated"
+ assert result["code"] == original
+
+ def test_post_processor_no_design_doc_param(self):
+ """Verify PCodePostProcessor.process() no longer accepts design_doc."""
+ from core.compilation.p_post_processor import PCodePostProcessor
+ import inspect
+ sig = inspect.signature(PCodePostProcessor.process)
+ assert "design_doc" not in sig.parameters
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/Src/PeasyAI/tests/test_validators_extended.py b/Src/PeasyAI/tests/test_validators_extended.py
new file mode 100644
index 0000000000..e0fe24f3a0
--- /dev/null
+++ b/Src/PeasyAI/tests/test_validators_extended.py
@@ -0,0 +1,750 @@
+"""
+Extended validator tests — covers the 7 validators and PCodePostProcessor fixes
+that were previously untested.
+
+Validators: InlineInitValidator, VarDeclarationOrderValidator, CollectionOpsValidator,
+ SpecObservesConsistencyValidator, DuplicateDeclarationValidator,
+ SpecForbiddenKeywordValidator, PayloadFieldValidator, TestFileValidator
+
+PostProcessor: var reordering, enum dot-access, entry function syntax, bare halt,
+ forbidden keywords in spec, missing semicolons after return
+"""
+
+import sys
+from pathlib import Path
+
+import pytest
+
+PROJECT_ROOT = Path(__file__).parent.parent
+sys.path.insert(0, str(PROJECT_ROOT / "src"))
+
+from core.validation.validators import (
+ InlineInitValidator,
+ VarDeclarationOrderValidator,
+ CollectionOpsValidator,
+ SpecObservesConsistencyValidator,
+ DuplicateDeclarationValidator,
+ SpecForbiddenKeywordValidator,
+ PayloadFieldValidator,
+ TestFileValidator,
+ IssueSeverity,
+)
+from core.compilation.p_post_processor import PCodePostProcessor
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# InlineInitValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestInlineInitValidator:
+
+ def test_detects_inline_init(self):
+ code = " var x: int = 0;"
+ v = InlineInitValidator()
+ r = v.validate(code)
+ assert not r.is_valid
+ assert any("Inline initialization" in i.message for i in r.issues)
+
+ def test_auto_fixes_inline_init(self):
+ code = " var x: int = 0;"
+ v = InlineInitValidator()
+ r = v.validate(code)
+ fixed = code
+ for issue in r.issues:
+ if issue.auto_fixable:
+ fixed = issue.apply_fix(fixed)
+ assert "var x: int;" in fixed
+ assert "x = 0;" in fixed
+
+ def test_plain_declaration_passes(self):
+ code = "var x: int;\nx = 0;"
+ v = InlineInitValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+ def test_multiple_inline_inits(self):
+ code = "var a: int = 1;\nvar b: string = \"hello\";"
+ v = InlineInitValidator()
+ r = v.validate(code)
+ assert len(r.issues) == 2
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# VarDeclarationOrderValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestVarDeclarationOrderValidator:
+
+ def test_all_vars_after_statements_flagged(self):
+ """All vars after all statements should be flagged."""
+ code = (
+ "machine M {\n"
+ " start state Init {\n"
+ " entry {\n"
+ " x = 1;\n"
+ " var y: int;\n"
+ " y = 2;\n"
+ " }\n"
+ " }\n"
+ "}\n"
+ )
+ v = VarDeclarationOrderValidator()
+ r = v.validate(code)
+ assert any("Variable declaration after statement" in i.message for i in r.issues)
+
+ def test_interleaved_vars_flagged(self):
+ """Vars interleaved with statements (var, stmt, var) should be flagged."""
+ code = (
+ "machine M {\n"
+ " start state Init {\n"
+ " entry {\n"
+ " var x: int;\n"
+ " x = 1;\n"
+ " var y: int;\n"
+ " y = 2;\n"
+ " }\n"
+ " }\n"
+ "}\n"
+ )
+ v = VarDeclarationOrderValidator()
+ r = v.validate(code)
+ assert any("Variable declaration after statement" in i.message for i in r.issues)
+
+ def test_vars_at_top_no_error(self):
+ """Properly ordered vars (all at top) should not trigger errors."""
+ code = (
+ "machine M {\n"
+ " start state Init {\n"
+ " entry {\n"
+ " var x: int;\n"
+ " var y: int;\n"
+ " x = 1;\n"
+ " y = 2;\n"
+ " }\n"
+ " }\n"
+ "}\n"
+ )
+ v = VarDeclarationOrderValidator()
+ r = v.validate(code)
+ var_order_errors = [i for i in r.issues if "Variable declaration after statement" in i.message]
+ assert len(var_order_errors) == 0
+
+ def test_vars_before_statements_pass(self):
+ code = (
+ "fun DoWork() {\n"
+ " var x: int;\n"
+ " var y: int;\n"
+ " x = 1;\n"
+ " y = 2;\n"
+ "}\n"
+ )
+ v = VarDeclarationOrderValidator()
+ r = v.validate(code)
+ errors = [i for i in r.issues if i.severity == IssueSeverity.ERROR]
+ assert len(errors) == 0
+
+ def test_var_inside_loop_flagged(self):
+ code = (
+ "fun Process() {\n"
+ " var i: int;\n"
+ " while (i < 10) {\n"
+ " var temp: int;\n"
+ " temp = i;\n"
+ " }\n"
+ "}\n"
+ )
+ v = VarDeclarationOrderValidator()
+ r = v.validate(code)
+ assert any("inside a loop" in i.message for i in r.issues)
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# CollectionOpsValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestCollectionOpsValidator:
+
+ def test_append_function_flagged(self):
+ code = "append(mySeq, value);"
+ v = CollectionOpsValidator()
+ r = v.validate(code)
+ assert not r.is_valid
+ assert any("append()" in i.message for i in r.issues)
+
+ def test_receive_function_flagged(self):
+ code = "var msg: int;\nmsg = receive();"
+ v = CollectionOpsValidator()
+ r = v.validate(code)
+ assert not r.is_valid
+ assert any("receive()" in i.message for i in r.issues)
+
+ def test_wrong_seq_concat_warned(self):
+ code = "mySeq = mySeq + (elem,);"
+ v = CollectionOpsValidator()
+ r = v.validate(code)
+ assert any("concatenation" in i.message.lower() for i in r.issues)
+
+ def test_valid_seq_append_passes(self):
+ code = "mySeq += (sizeof(mySeq), elem);"
+ v = CollectionOpsValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# SpecObservesConsistencyValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestSpecObservesConsistencyValidator:
+
+ def test_handled_but_not_observed_flagged(self):
+ code = """
+event eStart;
+event eStop;
+spec Safety observes eStart {
+ start state Init {
+ on eStart goto Running;
+ }
+ state Running {
+ on eStop goto Init;
+ }
+}
+"""
+ v = SpecObservesConsistencyValidator()
+ r = v.validate(code)
+ assert not r.is_valid
+ assert any("eStop" in i.message and "does not list" in i.message for i in r.issues)
+
+ def test_consistent_spec_passes(self):
+ code = """
+event eStart;
+event eStop;
+spec Safety observes eStart, eStop {
+ start state Init {
+ on eStart goto Running;
+ }
+ state Running {
+ on eStop goto Init;
+ }
+}
+"""
+ v = SpecObservesConsistencyValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+ def test_observes_undefined_event_warned(self):
+ code = """
+spec Safety observes eNonExistent {
+ start state Init { }
+}
+"""
+ v = SpecObservesConsistencyValidator()
+ r = v.validate(code)
+ assert any("undefined event" in i.message.lower() for i in r.issues)
+
+ def test_event_from_context_recognized(self):
+ code = """
+spec Safety observes eStart {
+ start state Init {
+ on eStart goto Done;
+ }
+ state Done { }
+}
+"""
+ context = {"types.p": "event eStart;"}
+ v = SpecObservesConsistencyValidator()
+ r = v.validate(code, context)
+ assert r.is_valid
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# DuplicateDeclarationValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestDuplicateDeclarationValidator:
+
+ def test_duplicate_type_flagged(self):
+ code = "type tConfig = (x: int);"
+ context = {"other.p": "type tConfig = (y: int);"}
+ v = DuplicateDeclarationValidator()
+ r = v.validate(code, context)
+ assert not r.is_valid
+ assert any("tConfig" in i.message for i in r.issues)
+
+ def test_duplicate_event_flagged(self):
+ code = "event eStart;"
+ context = {"types.p": "event eStart;"}
+ v = DuplicateDeclarationValidator()
+ r = v.validate(code, context)
+ assert not r.is_valid
+
+ def test_duplicate_machine_flagged(self):
+ code = "machine Server {"
+ context = {"server.p": "machine Server {"}
+ v = DuplicateDeclarationValidator()
+ r = v.validate(code, context)
+ assert not r.is_valid
+
+ def test_no_duplicates_passes(self):
+ code = "type tFoo = (x: int);"
+ context = {"other.p": "type tBar = (y: int);"}
+ v = DuplicateDeclarationValidator()
+ r = v.validate(code, context)
+ assert r.is_valid
+
+ def test_no_context_passes(self):
+ code = "type tFoo = (x: int);\nevent eFoo;"
+ v = DuplicateDeclarationValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# SpecForbiddenKeywordValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestSpecForbiddenKeywordValidator:
+
+ def test_this_in_spec_flagged(self):
+ code = """
+spec Monitor observes eEvent {
+ start state Init {
+ entry {
+ var m: machine;
+ m = this;
+ }
+ }
+}
+"""
+ v = SpecForbiddenKeywordValidator()
+ r = v.validate(code)
+ assert not r.is_valid
+ assert any("this" in i.message for i in r.issues)
+
+ def test_send_in_spec_flagged(self):
+ code = """
+spec Monitor observes eEvent {
+ start state Init {
+ on eEvent do {
+ send target, eOther;
+ }
+ }
+}
+"""
+ v = SpecForbiddenKeywordValidator()
+ r = v.validate(code)
+ assert not r.is_valid
+ assert any("send" in i.message for i in r.issues)
+
+ def test_new_in_spec_flagged(self):
+ code = """
+spec Monitor observes eEvent {
+ start state Init {
+ entry {
+ var m: machine;
+ m = new Helper();
+ }
+ }
+}
+"""
+ v = SpecForbiddenKeywordValidator()
+ r = v.validate(code)
+ assert not r.is_valid
+ assert any("new" in i.message for i in r.issues)
+
+ def test_clean_spec_passes(self):
+ code = """
+event eCommit;
+event eAbort;
+spec Atomicity observes eCommit, eAbort {
+ var committed: bool;
+ start state Init {
+ on eCommit goto Committed;
+ on eAbort goto Aborted;
+ }
+ state Committed { }
+ state Aborted { }
+}
+"""
+ v = SpecForbiddenKeywordValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+ def test_non_spec_code_passes(self):
+ code = """
+machine Worker {
+ start state Init {
+ entry {
+ send coordinator, eReady;
+ }
+ }
+}
+"""
+ v = SpecForbiddenKeywordValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# PayloadFieldValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestPayloadFieldValidator:
+
+ def test_wrong_field_name_warned(self):
+ code = """
+fun HandleRequest(req: tRequest) {
+ var x: int;
+ x = req.wrongField;
+}
+"""
+ context = {"types.p": "type tRequest = (sender: machine, amount: int);"}
+ v = PayloadFieldValidator()
+ r = v.validate(code, context)
+ assert any("wrongField" in i.message for i in r.issues)
+
+ def test_correct_field_passes(self):
+ code = """
+fun HandleRequest(req: tRequest) {
+ var x: int;
+ x = req.amount;
+}
+"""
+ context = {"types.p": "type tRequest = (sender: machine, amount: int);"}
+ v = PayloadFieldValidator()
+ r = v.validate(code, context)
+ assert not any("amount" in i.message for i in r.issues)
+
+ def test_entry_param_checked(self):
+ code = """
+machine Server {
+ start state Init {
+ entry (config: tServerConfig) {
+ var x: machine;
+ x = config.badField;
+ }
+ }
+}
+"""
+ context = {"types.p": "type tServerConfig = (coordinator: machine);"}
+ v = PayloadFieldValidator()
+ r = v.validate(code, context)
+ assert any("badField" in i.message for i in r.issues)
+
+ def test_on_do_param_checked(self):
+ code = """
+on eRequest do (payload: tReqPayload) {
+ var s: machine;
+ s = payload.client;
+}
+"""
+ context = {"types.p": "type tReqPayload = (sender: machine, data: int);"}
+ v = PayloadFieldValidator()
+ r = v.validate(code, context)
+ assert any("client" in i.message for i in r.issues)
+
+ def test_no_context_skips(self):
+ code = "fun Foo(x: tBar) { var y: int; y = x.field; }"
+ v = PayloadFieldValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# TestFileValidator
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestTestFileValidator:
+
+ def test_missing_test_decl_warned(self):
+ code = """
+machine Scenario {
+ start state Init {
+ entry {
+ var s: machine;
+ s = new Server();
+ }
+ }
+}
+"""
+ v = TestFileValidator()
+ r = v.validate(code)
+ assert any("no test declarations" in i.message.lower() for i in r.issues)
+
+ def test_with_test_decl_passes(self):
+ code = """
+machine Scenario {
+ start state Init { entry { } }
+}
+test tcBasic [main=Scenario]: assert Safety in { Server, Scenario };
+"""
+ context = {"spec.p": "spec Safety observes eStart { start state Init { } }"}
+ v = TestFileValidator()
+ r = v.validate(code, context)
+ missing_decl = [i for i in r.issues if "no test declarations" in i.message.lower()]
+ assert len(missing_decl) == 0
+
+ def test_missing_spec_assertion_warned(self):
+ code = """
+machine Scenario {
+ start state Init { entry { } }
+}
+test tcBasic [main=Scenario]: { Server, Scenario };
+"""
+ context = {"spec.p": "spec Safety observes eStart { start state Init { } }"}
+ v = TestFileValidator()
+ r = v.validate(code, context)
+ assert any("Safety" in i.message for i in r.issues)
+
+ def test_no_machines_no_warning(self):
+ code = "// Just a comment file\ntype tFoo = (x: int);"
+ v = TestFileValidator()
+ r = v.validate(code)
+ assert r.is_valid
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# PCodePostProcessor — extended fix tests
+# ═══════════════════════════════════════════════════════════════════════
+
+class TestPostProcessorVarReorder:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_vars_hoisted_to_top(self):
+ """Post-processor hoists vars when they appear after statements."""
+ code = (
+ "fun Work() {\n"
+ " x = 1;\n"
+ " var y: int;\n"
+ " y = 2;\n"
+ "}\n"
+ )
+ result = self._process(code)
+ lines = result.code.strip().splitlines()
+ var_y_idx = None
+ assign_x_idx = None
+ for i, line in enumerate(lines):
+ if "var y: int" in line and var_y_idx is None:
+ var_y_idx = i
+ if "x = 1" in line and assign_x_idx is None:
+ assign_x_idx = i
+ assert var_y_idx is not None
+ assert assign_x_idx is not None
+ assert var_y_idx < assign_x_idx
+
+ def test_interleaved_vars_hoisted(self):
+ """Post-processor hoists interleaved vars (var, stmt, var)."""
+ code = (
+ "fun Work() {\n"
+ " var x: int;\n"
+ " x = 1;\n"
+ " var y: int;\n"
+ " y = 2;\n"
+ "}\n"
+ )
+ result = self._process(code)
+ lines = result.code.strip().splitlines()
+ var_y_idx = None
+ assign_x_idx = None
+ for i, line in enumerate(lines):
+ if "var y: int" in line and var_y_idx is None:
+ var_y_idx = i
+ if "x = 1" in line and assign_x_idx is None:
+ assign_x_idx = i
+ assert var_y_idx is not None
+ assert assign_x_idx is not None
+ assert var_y_idx < assign_x_idx
+
+ def test_already_ordered_vars_untouched(self):
+ """Vars already at top of function should not be reordered."""
+ code = (
+ "fun Work() {\n"
+ " var x: int;\n"
+ " var y: int;\n"
+ " x = 1;\n"
+ " y = 2;\n"
+ "}\n"
+ )
+ result = self._process(code)
+ assert "var x: int" in result.code
+ assert "var y: int" in result.code
+ assert not any("variable declaration" in f.lower() for f in result.fixes_applied)
+
+ def test_already_ordered_untouched(self):
+ code = (
+ "fun Work() {\n"
+ " var x: int;\n"
+ " x = 1;\n"
+ "}\n"
+ )
+ result = self._process(code)
+ assert "var x: int" in result.code
+ assert not any("variable declaration" in f.lower() for f in result.fixes_applied)
+
+
+class TestPostProcessorEnumDotAccess:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_enum_dot_removed(self):
+ code = "var x: tColor;\nx = tColor.RED;"
+ result = self._process(code)
+ assert "tColor.RED" not in result.code
+ assert "RED" in result.code
+
+ def test_no_false_positive_on_field_access(self):
+ code = "var x: int;\nx = config.timeout;"
+ result = self._process(code)
+ assert "config.timeout" in result.code
+
+
+class TestPostProcessorEntryFunctionSyntax:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_entry_parens_removed(self):
+ code = "entry InitEntry();"
+ result = self._process(code)
+ assert "entry InitEntry;" in result.code
+ assert "()" not in result.code
+
+ def test_entry_with_param_untouched(self):
+ code = "entry (config: tConfig) {"
+ result = self._process(code)
+ assert "entry (config: tConfig) {" in result.code
+
+ def test_entry_block_untouched(self):
+ code = "entry { goto Running; }"
+ result = self._process(code)
+ assert "entry { goto Running; }" in result.code
+
+
+class TestPostProcessorBareHalt:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_bare_halt_fixed(self):
+ code = "halt;"
+ result = self._process(code)
+ assert "raise halt;" in result.code
+
+ def test_raise_halt_untouched(self):
+ code = "raise halt;"
+ result = self._process(code)
+ assert result.code.count("raise halt;") == 1
+
+
+class TestPostProcessorForbiddenInMonitors:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_this_as_machine_removed(self):
+ code = """
+spec Monitor observes eEvent {
+ var selfRef: machine;
+ start state Init {
+ entry {
+ selfRef = this as machine;
+ }
+ }
+}
+"""
+ result = self._process(code)
+ assert "this as machine" not in result.code
+
+ def test_forbidden_keyword_warned(self):
+ code = """
+spec Monitor observes eEvent {
+ start state Init {
+ entry {
+ send target, eOther;
+ }
+ }
+}
+"""
+ result = self._process(code)
+ assert any("send" in w.lower() for w in result.warnings)
+
+
+class TestPostProcessorMissingSemicolons:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_return_missing_semicolon(self):
+ code = "fun Foo() {\n return true\n}"
+ result = self._process(code)
+ assert "return true;" in result.code
+
+ def test_return_with_semicolon_untouched(self):
+ code = "fun Foo() {\n return true;\n}"
+ result = self._process(code)
+ assert result.code.count("return true;") == 1
+
+
+class TestPostProcessorNamedFieldTuple:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code)
+
+ def test_named_fields_stripped(self):
+ code = "send target, eConfig, (server = this, count = 3,);"
+ result = self._process(code)
+ assert "server =" not in result.code
+ assert "count =" not in result.code
+ assert "(this, 3,)" in result.code
+
+ def test_positional_untouched(self):
+ code = "send target, eConfig, (this, 3,);"
+ result = self._process(code)
+ assert "(this, 3,)" in result.code
+
+
+class TestPostProcessorTestDeclarations:
+
+ def _process(self, code):
+ proc = PCodePostProcessor()
+ return proc.process(code, filename="TestDriver.p", is_test_file=True)
+
+ def test_auto_generates_test_decl(self):
+ code = """
+machine Scenario {
+ start state Init {
+ entry {
+ var s: machine;
+ s = new Server();
+ send s, eStart;
+ }
+ }
+}
+"""
+ result = self._process(code)
+ assert "test " in result.code
+ assert "[main=Scenario]" in result.code
+
+ def test_existing_test_decl_not_duplicated(self):
+ code = """
+machine Scenario {
+ start state Init { entry { } }
+}
+test tcBasic [main=Scenario]: { Scenario };
+"""
+ result = self._process(code)
+ assert result.code.count("test ") == 1
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/Src/PeasyAI/tests/test_workflow_persistence.py b/Src/PeasyAI/tests/test_workflow_persistence.py
new file mode 100644
index 0000000000..69ab8cb0b1
--- /dev/null
+++ b/Src/PeasyAI/tests/test_workflow_persistence.py
@@ -0,0 +1,84 @@
+import sys
+import unittest
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+PROJECT_ROOT = Path(__file__).parent.parent
+SRC_ROOT = PROJECT_ROOT / "src"
+sys.path.insert(0, str(SRC_ROOT))
+
+from core.workflow.engine import WorkflowEngine, WorkflowDefinition
+from core.workflow.events import EventEmitter
+from core.workflow.steps import WorkflowStep, StepResult
+
+
+class NeedsGuidanceStep(WorkflowStep):
+ name = "needs_guidance"
+ description = "Pause until user guidance is provided"
+ max_retries = 1
+
+ def execute(self, context):
+ if context.get("user_guidance"):
+ return StepResult.success({"guided_value": context["user_guidance"]})
+ return StepResult.needs_guidance("Please provide guidance")
+
+ def can_skip(self, context):
+ return False
+
+
+class FinishStep(WorkflowStep):
+ name = "finish"
+ description = "Finalize workflow"
+ max_retries = 1
+
+ def execute(self, context):
+ return StepResult.success({"done": True})
+
+ def can_skip(self, context):
+ return False
+
+
+class TestWorkflowPersistence(unittest.TestCase):
+ def test_pause_resume_persists_state(self):
+ with TemporaryDirectory() as tmpdir:
+ state_file = str(Path(tmpdir) / "workflow_state.json")
+
+ emitter = EventEmitter()
+ engine = WorkflowEngine(emitter, state_store_path=state_file)
+ workflow = WorkflowDefinition(
+ name="test_flow",
+ steps=[NeedsGuidanceStep(), FinishStep()],
+ continue_on_failure=False,
+ )
+ engine.register_workflow(workflow)
+
+ paused_result = engine.execute("test_flow", {"project_path": "/tmp/project"})
+ self.assertTrue(paused_result.get("needs_guidance"))
+ self.assertIn("_workflow_id", paused_result)
+ workflow_id = paused_result["_workflow_id"]
+
+ self.assertTrue(Path(state_file).exists())
+ persisted = Path(state_file).read_text(encoding="utf-8")
+ self.assertIn(workflow_id, persisted)
+ persistence_status = engine.get_persistence_status()
+ self.assertTrue(persistence_status["enabled"])
+ self.assertEqual(persistence_status["state_store_path"], state_file)
+ self.assertIn(workflow_id, persistence_status["persisted_workflow_ids"])
+
+ # Simulate restart by creating a new engine bound to same state file.
+ new_engine = WorkflowEngine(EventEmitter(), state_store_path=state_file)
+ new_engine.register_workflow(workflow)
+ resumed = new_engine.resume(workflow_id, "approved-guidance")
+
+ self.assertTrue(resumed.get("success"))
+ self.assertEqual(resumed.get("guided_value"), "approved-guidance")
+ self.assertTrue(resumed.get("done"))
+
+ # Ensure completed workflow no longer stays active.
+ self.assertEqual(len(new_engine.get_active_workflows()), 0)
+ final_status = new_engine.get_persistence_status()
+ self.assertEqual(final_status["persisted_workflow_ids"], [])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/BasicPaxos.pproj b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/BasicPaxos.pproj
new file mode 100644
index 0000000000..01ef474706
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/BasicPaxos.pproj
@@ -0,0 +1,10 @@
+
+
+BasicPaxos
+
+ ./PSrc/
+ ./PSpec/
+ ./PTst/
+
+./PGenerated/
+
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSpec/OnlyOneValueChosen.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSpec/OnlyOneValueChosen.p
new file mode 100644
index 0000000000..228eb00a89
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSpec/OnlyOneValueChosen.p
@@ -0,0 +1,19 @@
+spec OnlyOneValueChosen observes eLearn {
+ var chosenValue: int;
+
+ start state Init {
+ entry {
+ chosenValue = -1;
+ }
+
+ on eLearn do (payload: tLearnPayload) {
+ assert(payload.learnedValue != -1);
+ if (chosenValue != -1) {
+ assert chosenValue == payload.learnedValue,
+ format("Safety violation: previously chose {0} but now learning {1}",
+ chosenValue, payload.learnedValue);
+ }
+ chosenValue = payload.learnedValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Acceptor.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Acceptor.p
new file mode 100644
index 0000000000..c84d0a2193
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Acceptor.p
@@ -0,0 +1,51 @@
+machine Acceptor {
+ var highestProposalSeen: int;
+ var acceptedProposal: int;
+ var acceptedValue: int;
+ var learners: seq[machine];
+
+ start state Init {
+ entry (config: tAcceptorConfig) {
+ learners = config.learners;
+ highestProposalSeen = -1;
+ acceptedProposal = -1;
+ acceptedValue = -1;
+ goto Ready;
+ }
+ }
+
+ state Ready {
+ on ePropose do (payload: tProposePayload) {
+ if (payload.proposalNumber > highestProposalSeen) {
+ highestProposalSeen = payload.proposalNumber;
+ send payload.proposer, ePromise, (
+ proposer = payload.proposer,
+ highestProposalSeen = highestProposalSeen,
+ acceptedValue = acceptedValue,
+ acceptedProposal = acceptedProposal
+ );
+ }
+ }
+
+ on eAcceptRequest do (payload: tAcceptRequestPayload) {
+ if (payload.proposalNumber >= highestProposalSeen) {
+ highestProposalSeen = payload.proposalNumber;
+ acceptedProposal = payload.proposalNumber;
+ acceptedValue = payload.proposedValue;
+ NotifyLearners(payload.proposalNumber, payload.proposedValue);
+ }
+ }
+ }
+
+ fun NotifyLearners(propNum: int, propVal: int) {
+ var i: int;
+ i = 0;
+ while (i < sizeof(learners)) {
+ send learners[i], eAccepted, (
+ proposalNumber = propNum,
+ acceptedValue = propVal
+ );
+ i = i + 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Enums_Types_Events.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Enums_Types_Events.p
new file mode 100644
index 0000000000..8ba25234c3
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Enums_Types_Events.p
@@ -0,0 +1,17 @@
+type tProposePayload = (proposer: machine, proposalNumber: int, proposedValue: int);
+type tPromisePayload = (proposer: machine, highestProposalSeen: int, acceptedValue: int, acceptedProposal: int);
+type tAcceptRequestPayload = (proposer: machine, proposalNumber: int, proposedValue: int);
+type tAcceptedPayload = (proposalNumber: int, acceptedValue: int);
+type tLearnPayload = (learnedValue: int);
+
+type tProposerConfig = (acceptors: seq[machine], learners: seq[machine], proposerId: int, valueToPropose: int);
+type tAcceptorConfig = (learners: seq[machine]);
+type tLearnerConfig = (majoritySize: int);
+type tClientConfig = (proposer: machine, valueToPropose: int);
+
+event ePropose: tProposePayload;
+event ePromise: tPromisePayload;
+event eAcceptRequest: tAcceptRequestPayload;
+event eAccepted: tAcceptedPayload;
+event eLearn: tLearnPayload;
+event eStartConsensus: int;
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Learner.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Learner.p
new file mode 100644
index 0000000000..b1a9a4144c
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Learner.p
@@ -0,0 +1,34 @@
+machine Learner {
+ var acceptCount: map[int, int];
+ var acceptedValues: map[int, int];
+ var majoritySize: int;
+ var chosenValue: int;
+ var hasLearned: bool;
+
+ start state Init {
+ entry (config: tLearnerConfig) {
+ majoritySize = config.majoritySize;
+ chosenValue = -1;
+ hasLearned = false;
+ goto Learning;
+ }
+ }
+
+ state Learning {
+ on eAccepted do (payload: tAcceptedPayload) {
+ if (!hasLearned) {
+ if (!(payload.proposalNumber in acceptCount)) {
+ acceptCount[payload.proposalNumber] = 0;
+ acceptedValues[payload.proposalNumber] = payload.acceptedValue;
+ }
+ acceptCount[payload.proposalNumber] = acceptCount[payload.proposalNumber] + 1;
+
+ if (acceptCount[payload.proposalNumber] >= majoritySize) {
+ chosenValue = acceptedValues[payload.proposalNumber];
+ hasLearned = true;
+ announce eLearn, (learnedValue = chosenValue,);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Proposer.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Proposer.p
new file mode 100644
index 0000000000..b78b8a0e9b
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PSrc/Proposer.p
@@ -0,0 +1,64 @@
+machine Proposer {
+ var acceptors: seq[machine];
+ var learners: seq[machine];
+ var proposalNumber: int;
+ var proposedValue: int;
+ var promiseCount: int;
+ var majoritySize: int;
+ var highestAcceptedProposal: int;
+
+ start state Init {
+ entry (config: tProposerConfig) {
+ acceptors = config.acceptors;
+ learners = config.learners;
+ proposalNumber = config.proposerId;
+ proposedValue = config.valueToPropose;
+ majoritySize = sizeof(acceptors) / 2 + 1;
+ highestAcceptedProposal = -1;
+ goto ProposalPhase;
+ }
+ }
+
+ state ProposalPhase {
+ entry {
+ var i: int;
+ promiseCount = 0;
+ highestAcceptedProposal = -1;
+ i = 0;
+ while (i < sizeof(acceptors)) {
+ send acceptors[i], ePropose, (proposer = this, proposalNumber = proposalNumber, proposedValue = proposedValue);
+ i = i + 1;
+ }
+ }
+
+ on ePromise do (payload: tPromisePayload) {
+ if (payload.highestProposalSeen == proposalNumber) {
+ if (payload.acceptedProposal > highestAcceptedProposal) {
+ highestAcceptedProposal = payload.acceptedProposal;
+ if (payload.acceptedValue != -1) {
+ proposedValue = payload.acceptedValue;
+ }
+ }
+ promiseCount = promiseCount + 1;
+ if (promiseCount >= majoritySize) {
+ goto AcceptPhase;
+ }
+ }
+ }
+ }
+
+ state AcceptPhase {
+ entry {
+ var i: int;
+ i = 0;
+ while (i < sizeof(acceptors)) {
+ send acceptors[i], eAcceptRequest, (proposer = this, proposalNumber = proposalNumber, proposedValue = proposedValue);
+ i = i + 1;
+ }
+ }
+
+ ignore ePromise;
+ }
+}
+
+module PaxosModule = { Proposer, Acceptor, Learner };
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PTst/TestDriver.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PTst/TestDriver.p
new file mode 100644
index 0000000000..8bc2ad7d2c
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_19_03_15/PTst/TestDriver.p
@@ -0,0 +1,61 @@
+type tPaxosTestConfig = (numProposers: int, numAcceptors: int, numLearners: int);
+
+fun SetupPaxos(config: tPaxosTestConfig) {
+ var i: int;
+ var acceptors: seq[machine];
+ var learners: seq[machine];
+ var majoritySize: int;
+
+ majoritySize = config.numAcceptors / 2 + 1;
+
+ i = 0;
+ while (i < config.numLearners) {
+ learners += (sizeof(learners), new Learner((majoritySize = majoritySize,)));
+ i = i + 1;
+ }
+
+ i = 0;
+ while (i < config.numAcceptors) {
+ acceptors += (sizeof(acceptors), new Acceptor((learners = learners,)));
+ i = i + 1;
+ }
+
+ i = 0;
+ while (i < config.numProposers) {
+ new Proposer((acceptors = acceptors, learners = learners, proposerId = i + 1, valueToPropose = (i + 1) * 100));
+ i = i + 1;
+ }
+}
+
+machine TestBasicConsensus {
+ start state Init {
+ entry {
+ SetupPaxos((numProposers = 1, numAcceptors = 3, numLearners = 1));
+ }
+ }
+}
+
+machine TestMultipleProposers {
+ start state Init {
+ entry {
+ SetupPaxos((numProposers = 2, numAcceptors = 5, numLearners = 1));
+ }
+ }
+}
+
+machine TestThreeProposers {
+ start state Init {
+ entry {
+ SetupPaxos((numProposers = 3, numAcceptors = 3, numLearners = 1));
+ }
+ }
+}
+
+test testBasicConsensus [main = TestBasicConsensus]:
+ assert OnlyOneValueChosen in (union PaxosModule, { TestBasicConsensus });
+
+test testMultipleProposers [main = TestMultipleProposers]:
+ assert OnlyOneValueChosen in (union PaxosModule, { TestMultipleProposers });
+
+test testThreeProposers [main = TestThreeProposers]:
+ assert OnlyOneValueChosen in (union PaxosModule, { TestThreeProposers });
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/BasicPaxos.pproj b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/BasicPaxos.pproj
new file mode 100644
index 0000000000..01ef474706
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/BasicPaxos.pproj
@@ -0,0 +1,10 @@
+
+
+BasicPaxos
+
+ ./PSrc/
+ ./PSpec/
+ ./PTst/
+
+./PGenerated/
+
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSpec/OnlyOneValueChosen.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSpec/OnlyOneValueChosen.p
new file mode 100644
index 0000000000..88b93aba8f
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSpec/OnlyOneValueChosen.p
@@ -0,0 +1,27 @@
+spec OnlyOneValueChosen observes eLearn {
+ var chosenValue: int;
+ var hasChosenValue: bool;
+
+ start state Init {
+ entry {
+ hasChosenValue = false;
+ chosenValue = 0;
+ goto Monitoring;
+ }
+ }
+
+ state Monitoring {
+ on eLearn do CheckSingleValueChosen;
+ }
+
+ fun CheckSingleValueChosen(payload: tLearnPayload) {
+ if (hasChosenValue) {
+ assert chosenValue == payload.learnedValue,
+ format("Safety violation: Multiple values chosen. Previously chosen: {0}, Now received: {1}",
+ chosenValue, payload.learnedValue);
+ } else {
+ chosenValue = payload.learnedValue;
+ hasChosenValue = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Acceptor.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Acceptor.p
new file mode 100644
index 0000000000..2b694c5f61
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Acceptor.p
@@ -0,0 +1,45 @@
+machine Acceptor {
+ var highestProposalSeen: int;
+ var acceptedProposal: int;
+ var acceptedValue: int;
+ var learners: seq[machine];
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ state Ready {
+ on ePropose do HandlePropose;
+ on eAcceptRequest do HandleAcceptRequest;
+ ignore eStartConsensus;
+ }
+
+ fun InitEntry(config: tAcceptorConfig) {
+ learners = config.learners;
+ highestProposalSeen = -1;
+ acceptedProposal = -1;
+ acceptedValue = -1;
+ goto Ready;
+ }
+
+ fun HandlePropose(msg: tProposePayload) {
+ if (msg.proposalNumber > highestProposalSeen) {
+ highestProposalSeen = msg.proposalNumber;
+ send msg.proposer, ePromise, (proposer = this, highestProposalSeen = highestProposalSeen, acceptedValue = acceptedValue, acceptedProposal = acceptedProposal);
+ }
+ }
+
+ fun HandleAcceptRequest(msg: tAcceptRequestPayload) {
+ var i: int;
+ if (msg.proposalNumber >= highestProposalSeen) {
+ acceptedProposal = msg.proposalNumber;
+ acceptedValue = msg.proposedValue;
+ highestProposalSeen = msg.proposalNumber;
+ i = 0;
+ while (i < sizeof(learners)) {
+ send learners[i], eAccepted, (proposalNumber = msg.proposalNumber, acceptedValue = msg.proposedValue);
+ i = i + 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Enums_Types_Events.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Enums_Types_Events.p
new file mode 100644
index 0000000000..8acf122f14
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Enums_Types_Events.p
@@ -0,0 +1,20 @@
+// Types for machine configurations
+type tProposerConfig = (acceptors: seq[machine], learners: seq[machine], proposerId: int, valueToPropose: int);
+type tAcceptorConfig = (learners: seq[machine]);
+type tLearnerConfig = (majoritySize: int);
+type tClientConfig = (proposer: machine, valueToPropose: int);
+
+// Types for event payloads
+type tProposePayload = (proposer: machine, proposalNumber: int, proposedValue: int);
+type tPromisePayload = (proposer: machine, highestProposalSeen: int, acceptedValue: int, acceptedProposal: int);
+type tAcceptRequestPayload = (proposer: machine, proposalNumber: int, proposedValue: int);
+type tAcceptedPayload = (proposalNumber: int, acceptedValue: int);
+type tLearnPayload = (learnedValue: int);
+
+// Events
+event ePropose: tProposePayload;
+event ePromise: tPromisePayload;
+event eAcceptRequest: tAcceptRequestPayload;
+event eAccepted: tAcceptedPayload;
+event eLearn: tLearnPayload;
+event eStartConsensus: int;
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Learner.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Learner.p
new file mode 100644
index 0000000000..1a4eeb2021
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Learner.p
@@ -0,0 +1,51 @@
+machine Learner {
+ var acceptedValues: map[int, int];
+ var acceptCount: map[int, int];
+ var majoritySize: int;
+ var chosenValue: int;
+ var hasLearned: bool;
+
+ start state Init {
+ entry InitEntry;
+ }
+
+ state Learning {
+ on eAccepted do HandleAccepted;
+ ignore ePropose, ePromise, eAcceptRequest, eStartConsensus;
+ }
+
+ fun InitEntry(config: tLearnerConfig) {
+ majoritySize = config.majoritySize;
+ acceptedValues = default(map[int, int]);
+ acceptCount = default(map[int, int]);
+ chosenValue = -1;
+ hasLearned = false;
+ goto Learning;
+ }
+
+ fun HandleAccepted(msg: tAcceptedPayload) {
+ var propNum: int;
+ var accValue: int;
+ var currentCount: int;
+
+ propNum = msg.proposalNumber;
+ accValue = msg.acceptedValue;
+
+ acceptedValues[propNum] = accValue;
+
+ if (propNum in acceptCount) {
+ currentCount = acceptCount[propNum];
+ currentCount = currentCount + 1;
+ acceptCount[propNum] = currentCount;
+ } else {
+ acceptCount[propNum] = 1;
+ currentCount = 1;
+ }
+
+ if (currentCount >= majoritySize && !hasLearned) {
+ chosenValue = accValue;
+ hasLearned = true;
+ announce eLearn, (learnedValue = chosenValue,);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Proposer.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Proposer.p
new file mode 100644
index 0000000000..1e9cbd3e42
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PSrc/Proposer.p
@@ -0,0 +1,96 @@
+machine Proposer {
+ var acceptors: seq[machine];
+ var learners: seq[machine];
+ var proposerId: int;
+ var valueToPropose: int;
+ var proposalNumber: int;
+ var proposedValue: int;
+ var promiseCount: int;
+ var majoritySize: int;
+ var highestAcceptedProposal: int;
+ var highestAcceptedValue: int;
+
+ start state Init {
+ entry InitEntry;
+ on eStartConsensus goto ProposalPhase with StartProposal;
+ }
+
+ state ProposalPhase {
+ entry ProposalPhaseEntry;
+ on ePromise do HandlePromise;
+ defer eStartConsensus;
+ ignore eAcceptRequest;
+ }
+
+ state AcceptPhase {
+ entry AcceptPhaseEntry;
+ defer ePromise;
+ defer eStartConsensus;
+ ignore eAcceptRequest;
+ }
+
+ fun InitEntry(config: tProposerConfig) {
+ acceptors = config.acceptors;
+ learners = config.learners;
+ proposerId = config.proposerId;
+ valueToPropose = config.valueToPropose;
+ majoritySize = sizeof(acceptors) / 2 + 1;
+ proposalNumber = proposerId;
+ highestAcceptedProposal = -1;
+ highestAcceptedValue = -1;
+ promiseCount = 0;
+ }
+
+ fun StartProposal(value: int) {
+ proposedValue = value;
+ promiseCount = 0;
+ highestAcceptedProposal = -1;
+ highestAcceptedValue = -1;
+ }
+
+ fun ProposalPhaseEntry() {
+ var i: int;
+ var payload: tProposePayload;
+
+ payload = (proposer = this, proposalNumber = proposalNumber, proposedValue = proposedValue);
+
+ i = 0;
+ while (i < sizeof(acceptors)) {
+ send acceptors[i], ePropose, payload;
+ i = i + 1;
+ }
+ }
+
+ fun HandlePromise(msg: tPromisePayload) {
+ promiseCount = promiseCount + 1;
+
+ if (msg.acceptedProposal > highestAcceptedProposal) {
+ highestAcceptedProposal = msg.acceptedProposal;
+ highestAcceptedValue = msg.acceptedValue;
+ }
+
+ if (promiseCount >= majoritySize) {
+ goto AcceptPhase;
+ }
+ }
+
+ fun AcceptPhaseEntry() {
+ var i: int;
+ var valueToAccept: int;
+ var payload: tAcceptRequestPayload;
+
+ if (highestAcceptedValue != -1) {
+ valueToAccept = highestAcceptedValue;
+ } else {
+ valueToAccept = proposedValue;
+ }
+
+ payload = (proposer = this, proposalNumber = proposalNumber, proposedValue = valueToAccept);
+
+ i = 0;
+ while (i < sizeof(acceptors)) {
+ send acceptors[i], eAcceptRequest, payload;
+ i = i + 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PTst/TestDriver.p b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PTst/TestDriver.p
new file mode 100644
index 0000000000..e93b1d686d
--- /dev/null
+++ b/Tutorial/BasicPaxos/BasicPaxos_2026_02_23_20_05_17/PTst/TestDriver.p
@@ -0,0 +1,61 @@
+// Scenario 1: 3 acceptors, 1 proposer, 1 learner
+machine Scenario1_BasicConsensus {
+ start state Init {
+ entry {
+ var learner: machine;
+ var learners: seq[machine];
+ var acceptors: seq[machine];
+ var proposer: machine;
+ var i: int;
+
+ learner = new Learner((majoritySize = 2,));
+ learners += (0, learner);
+
+ i = 0;
+ while (i < 3) {
+ acceptors += (i, new Acceptor((learners = learners,)));
+ i = i + 1;
+ }
+
+ proposer = new Proposer((acceptors = acceptors, learners = learners, proposerId = 1, valueToPropose = 100));
+ send proposer, eStartConsensus, 100;
+ }
+ }
+}
+
+// Scenario 2: 5 acceptors, 2 proposers, 1 learner (competing proposals)
+machine Scenario2_CompetingProposals {
+ start state Init {
+ entry {
+ var learner: machine;
+ var learners: seq[machine];
+ var acceptors: seq[machine];
+ var proposer1: machine;
+ var proposer2: machine;
+ var i: int;
+
+ learner = new Learner((majoritySize = 3,));
+ learners += (0, learner);
+
+ i = 0;
+ while (i < 5) {
+ acceptors += (i, new Acceptor((learners = learners,)));
+ i = i + 1;
+ }
+
+ proposer1 = new Proposer((acceptors = acceptors, learners = learners, proposerId = 1, valueToPropose = 200));
+ proposer2 = new Proposer((acceptors = acceptors, learners = learners, proposerId = 2, valueToPropose = 300));
+
+ send proposer1, eStartConsensus, 200;
+ send proposer2, eStartConsensus, 300;
+ }
+ }
+}
+
+test tcBasicConsensus [main=Scenario1_BasicConsensus]:
+ assert OnlyOneValueChosen in
+ { Proposer, Acceptor, Learner, Scenario1_BasicConsensus };
+
+test tcCompetingProposals [main=Scenario2_CompetingProposals]:
+ assert OnlyOneValueChosen in
+ { Proposer, Acceptor, Learner, Scenario2_CompetingProposals };
\ No newline at end of file