From ccfeb73c1ed4195103ada7363856c4803f899fde Mon Sep 17 00:00:00 2001 From: Ankush Desai Date: Wed, 27 May 2026 19:39:03 -0700 Subject: [PATCH] PeasyAI: default to collecting-mode compilation in subprocess.run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PeasyAI's compile path shells out to `p compile` via three subprocess.run sites: CompilationService (the canonical entry, used by peasy-ai-compile and the fix-all loop), compile_utils.try_compile (legacy helper), and a candidate-scoring path in GenerationService. None of them previously propagated `P_COMPILER_COLLECT_ERRORS`. With P compiler 3.0+'s new multi-error mode (PRs #957/#963/#965), enabling the env var means the fix-all LLM loop converges in O(N/k) round trips instead of O(N) where k is the average errors-per-iteration — exactly the motivating use case for the whole multi-error initiative. ## Change At each subprocess.run site: ```python env = os.environ.copy() env.setdefault("P_COMPILER_COLLECT_ERRORS", "1") result = subprocess.run([...], ..., env=env) ``` `setdefault` (not direct assignment) means an explicit override in the parent shell still wins: - Unset → child sees "1" (collecting mode, the new default) - Set to "0" / "false" → child sees the user's value (strict mode) - Set to anything else truthy → child sees the user's value This is the right precedence: the user's explicit choice always overrides PeasyAI's preferred default. ## Sites changed - `Src/PeasyAI/src/core/services/compilation.py` (the canonical compile entry point; used by peasy-ai-compile MCP tool + the fix-iteratively loop) - `Src/PeasyAI/src/utils/compile_utils.py` (legacy helper used by evaluation pipelines) - `Src/PeasyAI/src/core/services/generation.py:1445` (candidate-scoring compile during code generation) `os` import added to `compilation.py`; the other two files already had it. ## Not changed - `Src/PeasyAI/src/utils/checker_utils.py` — these invoke `p check`, not `p compile`. The env var has no effect on `p check`. - `Src/PeasyAI/src/core/compilation/environment.py` — these are toolchain-validation calls (e.g. `dotnet --list-sdks`), not P compilation. - The error parser already supports multiple errors via `compilation.get_all_errors` and `error_parser.parse` (both iterate all matches with re.finditer). No parser changes needed. ## Follow-up (separate PR) Audit recommended updating `fix_iteratively` (`fixer.py`) to call `get_all_errors` instead of `parse_error` and send all errors to the LLM in one batch. That's a behavioral change to the fix loop; this PR is the prerequisite (env var must propagate first) but the actual batching work is its own PR with its own testing. Co-Authored-By: Claude Opus 4.7 (1M context) --- Src/PeasyAI/src/core/services/compilation.py | 10 +++++++++- Src/PeasyAI/src/core/services/generation.py | 5 +++++ Src/PeasyAI/src/utils/compile_utils.py | 11 ++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Src/PeasyAI/src/core/services/compilation.py b/Src/PeasyAI/src/core/services/compilation.py index 8b83c9e92..049c456cd 100644 --- a/Src/PeasyAI/src/core/services/compilation.py +++ b/Src/PeasyAI/src/core/services/compilation.py @@ -5,6 +5,7 @@ This service is UI-agnostic. """ +import os import subprocess import logging import traceback @@ -85,13 +86,20 @@ def compile(self, project_path: str) -> CompilationResult: return_code=-1, ) - # Run P compiler + # Run P compiler. Default to collecting-mode (`P_COMPILER_COLLECT_ERRORS=1`) + # so a project with N independent type errors produces all N diagnostics + # in one compile, letting PeasyAI's fix loop converge in N/k iterations + # instead of N. setdefault preserves an explicit user override (set the + # env var to "0" or "false" in the parent shell to keep strict mode). + env = os.environ.copy() + env.setdefault("P_COMPILER_COLLECT_ERRORS", "1") result = subprocess.run( ['p', 'compile'], capture_output=True, cwd=project_path, text=True, timeout=300, # 5 minute timeout + env=env, ) # Check for success diff --git a/Src/PeasyAI/src/core/services/generation.py b/Src/PeasyAI/src/core/services/generation.py index 02ddca39c..2c6a4026f 100644 --- a/Src/PeasyAI/src/core/services/generation.py +++ b/Src/PeasyAI/src/core/services/generation.py @@ -1442,12 +1442,17 @@ def _compile_check_candidates( env = ensure_environment() if env.is_valid and env.p_compiler_path: import subprocess + # Default to collecting-mode compilation (see + # core/services/compilation.py for full rationale). + subprocess_env = os.environ.copy() + subprocess_env.setdefault("P_COMPILER_COLLECT_ERRORS", "1") result = subprocess.run( [env.p_compiler_path, "compile"], cwd=project_path, capture_output=True, text=True, timeout=30, + env=subprocess_env, ) if result.returncode == 0: logger.info(f" Candidate compiles successfully (+{COMPILE_BONUS} bonus)") diff --git a/Src/PeasyAI/src/utils/compile_utils.py b/Src/PeasyAI/src/utils/compile_utils.py index 0bf566316..ac7920d6b 100644 --- a/Src/PeasyAI/src/utils/compile_utils.py +++ b/Src/PeasyAI/src/utils/compile_utils.py @@ -36,7 +36,16 @@ def try_compile(ppath, captured_streams_output_dir): 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) + # Default to collecting-mode compilation (see compilation.py for full rationale). + # setdefault preserves an explicit user override. + env = os.environ.copy() + env.setdefault("P_COMPILER_COLLECT_ERRORS", "1") + result = subprocess.run( + final_cmd_arr, + capture_output=True, + cwd=ppath if not p.is_file() else None, + env=env, + ) out_dir = f"{captured_streams_output_dir}/compile" os.makedirs(out_dir, exist_ok=True)