From cd8a4c42d4c8e451f22d4de89b30eecbe6fad981 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 07:44:56 -0400 Subject: [PATCH 01/47] Add OptiNLP prompt assets --- tools/optiNLP/README.md | 71 ++++ tools/optiNLP/eval/code_to_script_cases.md | 96 +++++ tools/optiNLP/eval/command_to_script_cases.md | 106 ++++++ tools/optiNLP/eval/target_cases.md | 360 ++++++++++++++++++ tools/optiNLP/eval/target_prompt_smoke.md | 55 +++ tools/optiNLP/knowledge/optilambda.md | 30 ++ tools/optiNLP/knowledge/script_patterns.md | 74 ++++ tools/optiNLP/knowledge/targets.md | 153 ++++++++ tools/optiNLP/knowledge/transformations.md | 47 +++ tools/optiNLP/prompts/01_target_generator.md | 238 ++++++++++++ tools/optiNLP/prompts/02_command_to_script.md | 121 ++++++ .../prompts/03_code_to_candidate_script.md | 81 ++++ 12 files changed, 1432 insertions(+) create mode 100644 tools/optiNLP/README.md create mode 100644 tools/optiNLP/eval/code_to_script_cases.md create mode 100644 tools/optiNLP/eval/command_to_script_cases.md create mode 100644 tools/optiNLP/eval/target_cases.md create mode 100644 tools/optiNLP/eval/target_prompt_smoke.md create mode 100644 tools/optiNLP/knowledge/optilambda.md create mode 100644 tools/optiNLP/knowledge/script_patterns.md create mode 100644 tools/optiNLP/knowledge/targets.md create mode 100644 tools/optiNLP/knowledge/transformations.md create mode 100644 tools/optiNLP/prompts/01_target_generator.md create mode 100644 tools/optiNLP/prompts/02_command_to_script.md create mode 100644 tools/optiNLP/prompts/03_code_to_candidate_script.md diff --git a/tools/optiNLP/README.md b/tools/optiNLP/README.md new file mode 100644 index 000000000..0424ad801 --- /dev/null +++ b/tools/optiNLP/README.md @@ -0,0 +1,71 @@ +# OptiNLP Prompt Kit + +OptiNLP is a prompt-engineering layer for helping an AI assistant produce +OptiTrust transformation scripts. It is not an NLP model, parser, or runtime +integration yet. The first goal is to build reliable prompts that understand +OptiTrust targets, transformation commands, and the script style used in the +repository. + +This directory is the tool-facing home for OptiNLP prompt assets and possible +future integrations. Private internship notes and reports under `practice/` may +be used as background while designing the prompts, but generated OptiNLP +artifacts must not be written there or copy private text from there. + +## Milestones + +1. Target generation: convert natural-language references to program locations + into valid OptiTrust target syntax. +2. Command-to-script generation: convert explicit user commands into OptiTrust + scripts using known transformation APIs. +3. Code-to-candidate-script generation: inspect input C/C++ or OptiLambda text + and propose candidate transformations with assumptions and validation steps. + +## Current Scope + +Implemented in this first pass: + +- knowledge notes for target syntax, script patterns, transformations, and + OptiLambda context; +- three engineered prompts: + - `prompts/01_target_generator.md`; + - `prompts/02_command_to_script.md`; + - `prompts/03_code_to_candidate_script.md`; +- separate manual evaluation cases for each prompt. + +Not implemented yet: + +- a CLI; +- VS Code UI integration; +- calls to an AI provider; +- automatic script validation; +- `.opti` parsing. OptiLambda is currently a printer-oriented textual view; + prompt outputs must not assume that `Run.script_opti` exists. + +## How To Use + +Give an AI assistant: + +1. the relevant prompt from `prompts/`; +2. the knowledge files in `knowledge/`; +3. the user request; +4. the source C/C++ code or printed OptiLambda text; +5. any existing script, trace, diff, or error output if available. + +The assistant should return structured reasoning, valid target or script syntax, +and validation steps. When it cannot disambiguate a target, it should ask a +focused clarification instead of guessing. + +## Prompt And Evaluation Split + +Prompts define assistant behavior. Evaluation files test whether that behavior +works on concrete cases. + +```text +prompts/01_target_generator.md target request -> target syntax +prompts/02_command_to_script.md user command -> transformation script +prompts/03_code_to_candidate_script.md input code -> candidate scripts + +eval/target_cases.md +eval/command_to_script_cases.md +eval/code_to_script_cases.md +``` diff --git a/tools/optiNLP/eval/code_to_script_cases.md b/tools/optiNLP/eval/code_to_script_cases.md new file mode 100644 index 000000000..3ac1b79f9 --- /dev/null +++ b/tools/optiNLP/eval/code_to_script_cases.md @@ -0,0 +1,96 @@ +# Code To Candidate Script Evaluation Cases + +Use these cases to manually test `prompts/03_code_to_candidate_script.md`. + +## Case 1: Simple Loop + +Input: + +```c +void f(int n) { + for (int i = 0; i < n; i++) { + work(i); + } +} +``` + +Acceptable suggestions: + +- `Loop.unroll [cFor "i"]` as a candidate only if the goal is reducing loop + overhead or exposing straight-line code. +- `Omp.parallel_for [cFor "i"]` only if independence is plausible and validation + is required. + +Required behavior: + +- Rank confidence. +- State that independence is not proven from the snippet alone. + +## Case 2: Function Call In Hot Loop + +Input: + +```c +void f(int n) { + for (int i = 0; i < n; i++) { + y[i] = helper(x[i]); + } +} +``` + +Acceptable suggestion: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Function.inline [cFor "i"; cCall "helper"]; +) +``` + +Required behavior: + +- Explain that inlining may expose further simplifications. +- Require validation with diff/trace or tests. + +## Case 3: Adjacent Loops + +Input: + +```c +void f(int n) { + for (int i = 0; i < n; i++) A[i] = i; + for (int i = 0; i < n; i++) B[i] = A[i] + 1; +} +``` + +Acceptable suggestion: + +- Candidate loop fusion targeting the repeated `i` loops. + +Required behavior: + +- Mark as medium confidence. +- Mention dependency and resource checks. +- Do not claim semantic safety without validation. + +## Case 4: Printed OptiLambda + +Input: + +```optilambda +fun main(n: int): int { + for i in 0..n { + x = x + i; + } + x +} +``` + +Required behavior: + +- Use the printed loop to reason about targets such as `[cFor "i"]`. +- Do not generate `Run.script_opti`. +- If a script is proposed, state that it must be applied through an existing + C/C++ script workflow until parser support exists. diff --git a/tools/optiNLP/eval/command_to_script_cases.md b/tools/optiNLP/eval/command_to_script_cases.md new file mode 100644 index 000000000..9709f3f72 --- /dev/null +++ b/tools/optiNLP/eval/command_to_script_cases.md @@ -0,0 +1,106 @@ +# Command To Script Evaluation Cases + +Use these cases to manually test `prompts/02_command_to_script.md`. + +## Case 1: Unroll Loop + +Request: + +```text +unroll the loop i +``` + +Expected script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Loop.unroll [cFor "i"]; +) +``` + +## Case 2: Inline Function Call + +Request: + +```text +inline calls to f +``` + +Expected script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Function.inline [cCall "f"]; +) +``` + +## Case 3: Inline Call In Function + +Request: + +```text +inline the call to g inside main +``` + +Expected script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Function.inline [cTopFunDef "main"; cCall "g"]; +) +``` + +## Case 4: Insert Statement Before Variable + +Request: + +```text +insert a++; before variable c +``` + +Expected script: + +```ocaml +open Optitrust +open Prelude + +let _ = Run.script_cpp (fun _ -> + !! Sequence_basic.insert ~reparse:true (stmt "a++;") [tBefore; cVarDef "c"]; +) +``` + +## Case 5: Missing Tile Size + +Request: + +```text +tile loop i +``` + +Expected behavior: + +- Ask for the tile size and desired tile index name, unless provided elsewhere. +- Do not invent a tile size. + +## Case 6: Unsupported `.opti` Execution + +Request: + +```text +run this transformation directly on the .opti file +``` + +Expected behavior: + +- Explain that OptiLambda is currently printer-oriented and no `Run.script_opti` + workflow should be generated. +- Ask for the C/C++ source or an existing supported script context. diff --git a/tools/optiNLP/eval/target_cases.md b/tools/optiNLP/eval/target_cases.md new file mode 100644 index 000000000..ceef62788 --- /dev/null +++ b/tools/optiNLP/eval/target_cases.md @@ -0,0 +1,360 @@ +# Target Generator Evaluation Cases + +Use these cases to manually test `prompts/01_target_generator.md`. + +Each case gives a user request, a source snippet when needed, and the expected +target output. A prompt run passes when it returns the expected target or an +equally specific accepted variant, and when it asks for clarification in the +cases marked ambiguous. + +## Case 1: Function Definition + +Request: + +```text +target the function main +``` + +Expected target: + +```ocaml +[cFunDef "main"] +``` + +Acceptable narrower top-level variant: + +```ocaml +[cTopFunDef "main"] +``` + +## Case 2: Loop By Index + +Source: + +```c +void kernel(int n) { + for (int i = 0; i < n; i++) { + work(i); + } +} +``` + +Request: + +```text +target the loop i +``` + +Expected target: + +```ocaml +[cFor "i"] +``` + +## Case 3: Loop Inside Function + +Source: + +```c +void init(int n) { + for (int i = 0; i < n; i++) clear(i); +} + +void main_loop(int n) { + for (int i = 0; i < n; i++) update(i); +} +``` + +Request: + +```text +target the loop i inside function main_loop +``` + +Expected target: + +```ocaml +[cFunBody "main_loop"; cFor "i"] +``` + +## Case 4: Multiple Calls + +Source: + +```c +void step() { + update(0); + update(1); +} +``` + +Request: + +```text +target every call to update +``` + +Expected target: + +```ocaml +[nbMulti; cCall "update"] +``` + +## Case 5: Occurrence Selection + +Source: + +```c +void two_loops(int n) { + for (int i = 0; i < n; i++) a(i); + for (int i = 0; i < n; i++) b(i); +} +``` + +Request: + +```text +target the second loop named i +``` + +Expected target: + +```ocaml +[occIndex 1; cFor "i"] +``` + +## Case 6: Insertion Position + +Source: + +```c +void f() { + int a = 0; + int c = 1; +} +``` + +Request: + +```text +target the position before variable c is declared +``` + +Expected target: + +```ocaml +[tBefore; cVarDef "c"] +``` + +## Case 7: Array Write + +Source: + +```c +void fill(int n, int* A) { + for (int i = 0; i < n; i++) { + A[i] = i; + } +} +``` + +Request: + +```text +target writes to A +``` + +Expected target: + +```ocaml +[nbMulti; cArrayWrite "A"] +``` + +## Case 8: Ambiguous Loop + +Request: + +```text +target the loop on line 10 +``` + +Expected behavior: + +- If source code with line numbers is available, map line 10 to a semantic + target such as `[cFor "i"]` or `[occIndex 1; cFor "i"]`. +- If source code is not available, ask for the code or the loop index/name. + +## Case 9: Call Inside Function + +Source: + +```c +void helper() { + foo(); +} + +void main() { + foo(); +} +``` + +Request: + +```text +target the call to foo inside main +``` + +Expected target: + +```ocaml +[cTopFunDef "main"; cCall "foo"] +``` + +Accepted variant: + +```ocaml +[cFunBody "main"; cCall "foo"] +``` + +## Case 10: Position After Loop + +Source: + +```c +void f(int n) { + for (int i = 0; i < n; i++) { + work(i); + } + finish(); +} +``` + +Request: + +```text +target the position after the loop i +``` + +Expected target: + +```ocaml +[cFor "i"; tAfter] +``` + +Accepted variant: + +```ocaml +[tAfter; cFor "i"] +``` + +## Case 11: Loop With Array Write In Body + +Source: + +```c +void harris(int n, int* out, int* tmp) { + for (int y = 0; y < n; y++) { + tmp[y] = y; + } + for (int y = 0; y < n; y++) { + out[y] = tmp[y]; + } +} +``` + +Request: + +```text +target the y loop that writes to out +``` + +Expected target: + +```ocaml +[cFor "y" ~body:[cArrayWrite "out"]] +``` + +## Case 12: Multiple Named Variables + +Source: + +```c +void f() { + int gray = 0; + int ix = 0; + int iy = 0; +} +``` + +Request: + +```text +target the variable definitions gray, ix, and iy +``` + +Expected target: + +```ocaml +[multi cVarDef ["gray"; "ix"; "iy"]] +``` + +## Case 13: Ambiguous Named Loop Without Context + +Source: + +```c +void a(int n) { + for (int i = 0; i < n; i++) work_a(i); +} + +void b(int n) { + for (int i = 0; i < n; i++) work_b(i); +} +``` + +Request: + +```text +target the loop i +``` + +Expected behavior: + +- Do not claim a unique target. +- Ask which enclosing function is intended. +- Good alternatives to show: + +```ocaml +[cFunBody "a"; cFor "i"] +[cFunBody "b"; cFor "i"] +``` + +## Case 14: OptiLambda Printed Loop + +Source: + +```optilambda +fun main(n: int): int { + for i in 0..n { + x = x + i; + } + x +} +``` + +Request: + +```text +target the OptiLambda loop over i +``` + +Expected target: + +```ocaml +[cFor "i"] +``` + +Expected note: + +- The `.opti` text is used only for target reasoning. +- Do not generate `Run.script_opti`. diff --git a/tools/optiNLP/eval/target_prompt_smoke.md b/tools/optiNLP/eval/target_prompt_smoke.md new file mode 100644 index 000000000..b133bd139 --- /dev/null +++ b/tools/optiNLP/eval/target_prompt_smoke.md @@ -0,0 +1,55 @@ +# Target Generator Smoke Test Notes + +This file records the first manual smoke test for +`prompts/01_target_generator.md`. + +## Purpose + +Step 4 of the OptiNLP plan is to test Prompt 1 on concrete target-generation +examples and refine the prompt before building command-to-script generation. + +## Coverage Added + +The evaluation set now covers: + +- named function targets; +- named loop targets; +- loop targets inside a specific function; +- repeated calls with `nbMulti`; +- ordinal selection with `occIndex`; +- insertion positions with `tBefore`; +- after-loop positions with `tAfter`; +- array writes with `cArrayWrite`; +- loop body constraints such as `cFor "y" ~body:[cArrayWrite "out"]`; +- multiple named alternatives using `multi`; +- ambiguous repeated loops that should trigger clarification; +- printed OptiLambda used only as readable structure. + +## Refinements Made + +Prompt 1 now explicitly says: + +- convert line references to semantic targets when source code is available; +- use stable names instead of line numbers in final target syntax; +- add enclosing context when the same target name appears in several scopes; +- use occurrence selectors for ordinal requests; +- ask for source code when only a line number is given. + +## Manual Pass Criteria + +A target-generation response passes when it: + +- uses existing `Target` constructors only; +- returns the expected target or an equally specific accepted variant; +- asks for clarification for ambiguous cases; +- does not generate a transformation script; +- does not claim `.opti` text is runnable input. + +## Next Prompt Gaps To Watch + +- Whether `tBefore` and `tAfter` should be placed before or after the structural + selector may depend on the transformation. Prompt 2 should learn this from the + transformation examples rather than forcing one global convention. +- Source line targeting will need a convention for line-numbered snippets. A + future tool integration can provide AST, trace, or string-representation data + to reduce ambiguity. diff --git a/tools/optiNLP/knowledge/optilambda.md b/tools/optiNLP/knowledge/optilambda.md new file mode 100644 index 000000000..26a2e9cdc --- /dev/null +++ b/tools/optiNLP/knowledge/optilambda.md @@ -0,0 +1,30 @@ +# OptiLambda Context For OptiNLP + +OptiLambda is the textual language used to display OptiTrust internal AST terms +without going through the C/C++ printer. It is useful for traces, diffs, target +reasoning, and future internal-language workflows. + +Current status: + +- OptiLambda is implemented as a printer over `Ast.trm`. +- A parser is planned, but not implemented. +- The framework can print three synchronized representations: `surface`, + `internal`, and `typed`. +- All three representations describe the same AST; switching representation + changes the printed view, not the transformation step. + +Prompt implications: + +- The AI may inspect printed `.opti` code to understand functions, loops, + assignments, calls, marks, and contracts. +- The AI must not claim that `.opti` can currently be used as runnable input. +- The AI must not generate `Run.script_opti` unless the repository later adds + that API. +- For now, generated runnable scripts should use `Run.script_cpp`. + +Important source: + +- `lib/optilambda/optilambda_syntax.md` +- `lib/optilambda/optilambda_style.ml` +- `lib/optilambda/optilambda_printer.ml` +- `tests_infra/optilambda/` diff --git a/tools/optiNLP/knowledge/script_patterns.md b/tools/optiNLP/knowledge/script_patterns.md new file mode 100644 index 000000000..11a70f9e6 --- /dev/null +++ b/tools/optiNLP/knowledge/script_patterns.md @@ -0,0 +1,74 @@ +# OptiTrust Script Patterns + +Most generated scripts should follow the existing test and case-study style: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Transformation.name [target]; +) +``` + +Some examples use `open Prelude` instead of or in addition to `open Target` +when helper constructors such as `lit`, `int`, `expr`, `stmt`, or `ty` are +needed. The prompt should include the opens required by the generated code. + +## Common Shapes + +Inline a call: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Function.inline [cCall "f"]; +) +``` + +Unroll a loop: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Loop.unroll [cFor "i"]; +) +``` + +Tile a loop: + +```ocaml +open Optitrust +open Prelude + +let _ = Run.script_cpp (fun _ -> + !! Loop_basic.tile (lit "32") ~index:"bi" ~bound:TileDivides [cFor "i"]; +) +``` + +Insert before an instruction: + +```ocaml +open Optitrust +open Prelude + +let _ = Run.script_cpp (fun _ -> + !! Sequence_basic.insert ~reparse:true (stmt "a++;") [tBefore; cVarDef "c"]; +) +``` + +## Generated Script Rules + +- Use `!!` for transformations in `Run.script_cpp`. +- Keep the first generated script minimal. +- Include only transformations that exist in `lib/transfo/`. +- Prefer examples from `tests/**/**_doc.ml` when choosing module names and + argument order. +- Do not generate `Run.script_opti`; OptiLambda parsing is not implemented. +- If the user gives `.opti` text, use it for inspection and target reasoning, + but generate a C/C++ script workflow unless the repository later adds a real + OptiLambda parser path. diff --git a/tools/optiNLP/knowledge/targets.md b/tools/optiNLP/knowledge/targets.md new file mode 100644 index 000000000..0f8f33a12 --- /dev/null +++ b/tools/optiNLP/knowledge/targets.md @@ -0,0 +1,153 @@ +# OptiTrust Target Knowledge + +An OptiTrust target is an OCaml list of constraints used to locate one or more +nodes in the current AST. In scripts, targets are usually written with smart +constructors from `Target`, for example: + +```ocaml +[cFor "i"] +[nbMulti; cCall "foo"] +[cTopFunDef "main"; cCall "bar"] +[tBefore; cVarDef "x"] +``` + +The target generator prompt should produce this current script syntax, not older +or paper-only notation. + +## Core Model + +- A target is a `constr list`. +- Constraints are resolved left to right. +- A target may identify exactly one node, several nodes, no nodes, or an + interstitial position such as before or after an instruction. +- Transformations often expect either one target or explicitly multiple targets. + Use occurrence constraints when multiplicity matters. + +## Occurrence Constraints + +Use these when the same structural pattern can match several nodes: + +```ocaml +nbMulti (* one or more matches *) +nbAny (* zero or more matches *) +nbExact 2 (* exactly two matches *) +occIndex 0 (* first match, zero-based *) +occIndex 1 (* second match *) +occIndex (-1) (* last match *) +occFirst +occLast +``` + +Examples: + +```ocaml +[nbMulti; cFor "i"] +[occIndex 1; cFor "i"] +[occFirst; cCall "foo"] +[occLast; cVarDef "tmp"] +``` + +If the user says "all", "each", or "every", prefer `nbMulti` when at least one +match is expected. If the user says "second", "third", or "last", use an +occurrence selector. If the prompt cannot determine which occurrence is meant, +return alternatives and ask for clarification. + +## Relative And Sequence Positions + +Use relative constraints for insertion, movement, spans, and transformations +that operate at a position rather than directly on a node: + +```ocaml +tBefore +tAfter +tFirst +tLast +tBetweenAll +tSpan [START_TARGET] [STOP_TARGET] +``` + +Examples: + +```ocaml +[tBefore; cVarDef "x"] +[tAfter; cCall "init"] +[cFunBody "main"; tFirst] +[cForBody "i"; tBetweenAll] +``` + +Do not use `tBefore` or `tAfter` unless the operation needs a position, such as +inserting, moving, fissioning, or selecting a boundary. + +## Structural Selectors + +Common selectors: + +```ocaml +cFor "i" +cFor_c "i" +cWhile () +cIf () +cFunDef "foo" +cTopFunDef "foo" +cFunBody "foo" +cTopFunBody "foo" +cCall "foo" +cVarDef "x" +cVarsDef "x" +cVar "x" +cReadVar "x" +cWriteVar "x" +cArrayRead "a" +cArrayWrite "a" +cFieldRead ~field:"x" () +cFieldWrite ~field:"x" () +cSeq () +cReturn () +cLabel "name" +cMark "mark" +``` + +Nested constraints narrow the match by context: + +```ocaml +[cTopFunDef "main"; cCall "foo"] +[cFunBody "main"; cFor "i"] +[cFor "i"; cArrayWrite "A"] +[cIf ~cond:[sExpr "x < n"] (); dThen] +``` + +Use empty names intentionally only when the repository examples do so and the +target is clearly broad, for example `[cFunDef ""]` or `[cFor ""]`. + +## String Selectors + +String-based selectors are useful when a more semantic selector is unavailable: + +```ocaml +sInstr "x++;" +sExpr "i + 1" +sInstrRegexp "A\\[.*\\]" +sExprRegexp "MINDEX.*" +``` + +Prefer semantic constructors such as `cFor`, `cCall`, `cVarDef`, `cArrayRead`, +and `cArrayWrite` before falling back to string matching. + +## Prompt Policy + +The target generator should: + +- quote exact identifiers as OCaml strings; +- use current OptiTrust target constructors only; +- prefer semantic targets over line-number-only targets; +- turn line references into structural targets when source code is available; +- ask for clarification when two plausible targets remain; +- mention why a target may match multiple nodes; +- avoid inventing selectors not present in `Target`. + +Sources to refresh when syntax changes: + +- `lib/framework/target/target.ml` +- `lib/framework/target/constr.ml` +- `doc/target.md` +- `tests/**/**_doc.ml` diff --git a/tools/optiNLP/knowledge/transformations.md b/tools/optiNLP/knowledge/transformations.md new file mode 100644 index 000000000..2d713d02b --- /dev/null +++ b/tools/optiNLP/knowledge/transformations.md @@ -0,0 +1,47 @@ +# OptiTrust Transformation Knowledge + +This file is a compact orientation map for prompt generation. It is not a full +API reference. The prompt should prefer exact signatures from `lib/transfo/` and +usage examples from `tests/**/**_doc.ml` before emitting code. + +## Common Modules + +- `Loop`: loop-level transformations such as unrolling, tiling, fission, + fusion, hoisting, shifting ranges, reordering, and parallelization helpers. +- `Loop_basic`: lower-level loop transformations used directly in some tests. +- `Function`: function inlining and related function transformations. +- `Variable`: variable folding, unfolding, inlining, insertion, renaming, and + binding. +- `Sequence` and `Sequence_basic`: sequence introduction, insertion, deletion, + and grouping of instructions. +- `Instr`: instruction movement, copy, read-last-write, and accumulation. +- `Matrix` and `Matrix_basic`: matrix/local storage transformations, + delocalization, tiling, storage folding, and simplifications. +- `Omp` and `Omp_basic`: OpenMP pragmas such as parallel, parallel_for, simd, + task, target, target_data, and related clauses. +- `Cleanup`: cleanup passes usually applied after larger transformations. + +## Prompt Policy + +For command-to-script and code-to-script prompts: + +- map user words to a known module/function only when the mapping is clear; +- use examples from `tests/` to choose exact function names; +- include required non-target arguments such as tile sizes, names, clauses, or + destination targets; +- ask for missing parameters when no safe default exists; +- do not invent transformations from compiler terminology alone; +- state when an output is a candidate script rather than a proven optimization. + +## Useful Example Families + +- `tests/function/inline_simple/*_doc.ml` +- `tests/loop/unroll/*_doc.ml` +- `tests/loop/tile/*_doc.ml` +- `tests/loop/fusion/*_doc.ml` +- `tests/loop/fission/*_doc.ml` +- `tests/sequence/insert/*_doc.ml` +- `tests/variable/inline/*_doc.ml` +- `tests/omp/*` +- `case_studies/matmul/` +- `case_studies/harris/` diff --git a/tools/optiNLP/prompts/01_target_generator.md b/tools/optiNLP/prompts/01_target_generator.md new file mode 100644 index 000000000..10c2542b9 --- /dev/null +++ b/tools/optiNLP/prompts/01_target_generator.md @@ -0,0 +1,238 @@ +# Prompt 1: OptiTrust Target Generator + +You are OptiNLP Target Generator, an assistant specialized in converting +natural-language references to program locations into valid OptiTrust target +syntax. + +Your output must use the current OCaml target syntax used by OptiTrust scripts. +Do not invent target constructors. If the request is ambiguous, ask a focused +clarification instead of guessing. + +## Inputs + +You may receive: + +- a user request in natural language; +- C/C++ source code; +- printed OptiLambda text; +- existing OptiTrust script fragments; +- trace, diff, or error output; +- the OptiNLP knowledge files about targets and script patterns. + +## Hard Rules + +- Use only known `Target` constructors. +- Prefer semantic targets over line-number-only reasoning. +- Convert line references into structural targets when source code is available. +- When a line contains a node with a stable name, use that name instead of the + line number in the final target. +- Use occurrence selectors when a target may match multiple nodes. +- Do not pretend a target is unique if the code contains several matches. +- Do not generate a full transformation script unless asked; this prompt only + generates targets. +- Do not assume `.opti` is runnable input. Use `.opti` only as readable program + structure. +- Always include a short validation suggestion. + +## Reasoning Procedure + +1. Identify the requested program entity: function, loop, call, variable + definition, assignment, read/write, statement, mark, sequence, or position. +2. Locate all matching candidates in the provided code. +3. Choose the narrowest stable semantic target. +4. Add context constraints when needed, such as enclosing function or loop. +5. Add occurrence constraints when the same selector still matches more than + one node. +6. If ambiguity remains, ask a clarification and show the competing candidates. + +## Disambiguation Policy + +- If the request names an enclosing function, include `cFunBody` or + `cTopFunDef` context. +- If the request names all matching nodes, use `nbMulti`. +- If the request names one occurrence by ordinal, use `occIndex` with a + zero-based index. +- If the request names a position before or after a node, include `tBefore` or + `tAfter`. +- If a loop variable is unique in the visible code, `[cFor "i"]` is enough. +- If a loop variable repeats in different functions or scopes, add the enclosing + function or loop context. +- If the only clue is a line number and no source is available, ask for the + source code. + +## Target Construction Guide + +Functions: + +```ocaml +[cFunDef "foo"] +[cTopFunDef "foo"] +[cFunBody "foo"] +[cTopFunBody "foo"] +``` + +Loops: + +```ocaml +[cFor "i"] +[cFor_c "i"] +[cWhile ()] +[cFunBody "main"; cFor "i"] +[occIndex 1; cFor "i"] +``` + +Calls: + +```ocaml +[cCall "foo"] +[nbMulti; cCall "foo"] +[cTopFunDef "main"; cCall "foo"] +``` + +Variables and statements: + +```ocaml +[cVarDef "x"] +[cVar "x"] +[cReadVar "x"] +[cWriteVar "x"] +[sInstr "x++;"] +[sExpr "i + 1"] +``` + +Array, field, and assignment-like targets: + +```ocaml +[cArrayRead "A"] +[cArrayWrite "A"] +[cWrite ~lhs:[cVar "x"] ()] +[cFieldRead ~field:"next" ()] +[cFieldWrite ~field:"next" ()] +``` + +Positions: + +```ocaml +[tBefore; cVarDef "x"] +[tAfter; cCall "init"] +[cFunBody "main"; tFirst] +[cForBody "i"; tBetweenAll] +[cFor "i"; tAfter] +``` + +Marks: + +```ocaml +[cMark "target"] +[nbMulti; cMark "to_inline"] +``` + +Multiple named alternatives: + +```ocaml +[multi cFor ["i"; "j"]] +[multi cVarDef ["x"; "y"]] +[any cArrayWrite ["A"; "B"]] +``` + +## Output Format + +Use this exact structure: + +````markdown +## Intent +One sentence describing the requested location. + +## Candidate Nodes +- Candidate 1: ... +- Candidate 2: ... + +## Recommended Target +```ocaml +[...] +``` + +## Why This Target +Short explanation of why the target is stable and what it matches. + +## Ambiguities +State "None." or ask one focused clarification question. + +## Alternatives +```ocaml +[...] +``` + +## Validation +Show how to inspect the target, usually with `Show.target`, or explain what +context is needed before validation is possible. +```` + +If no valid target can be produced, omit `Recommended Target` and return: + +```markdown +## Missing Information +Ask for the smallest extra detail needed, such as function name, loop index, +occurrence number, or surrounding statement. +``` + +## Examples + +User request: + +```text +target the loop i inside main +``` + +Recommended target: + +```ocaml +[cFunBody "main"; cFor "i"] +``` + +User request: + +```text +target every call to vect_mul +``` + +Recommended target: + +```ocaml +[nbMulti; cCall "vect_mul"] +``` + +User request: + +```text +target the second loop named i +``` + +Recommended target: + +```ocaml +[occIndex 1; cFor "i"] +``` + +User request: + +```text +insert before variable c +``` + +Recommended target: + +```ocaml +[tBefore; cVarDef "c"] +``` + +Validation: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Show.target [tBefore; cVarDef "c"]; +) +``` diff --git a/tools/optiNLP/prompts/02_command_to_script.md b/tools/optiNLP/prompts/02_command_to_script.md new file mode 100644 index 000000000..4725b7009 --- /dev/null +++ b/tools/optiNLP/prompts/02_command_to_script.md @@ -0,0 +1,121 @@ +# Prompt 2: OptiTrust Command To Script + +You are OptiNLP Command To Script, an assistant specialized in converting +explicit user transformation commands into valid OptiTrust scripts. + +Your job is not to invent optimizations. Your job is to understand a user +command such as "unroll the loop i" or "inline function f in main", resolve the +target, choose a real OptiTrust transformation API, and output a minimal script. + +## Inputs + +You may receive: + +- a user command in natural language; +- C/C++ source code; +- printed OptiLambda text for inspection only; +- a target produced by Prompt 1; +- existing script examples; +- the OptiNLP knowledge files. + +## Hard Rules + +- Use only transformations that exist in `lib/transfo/`. +- Use only target constructors that exist in `Target`. +- Do not invent `.opti` parser support or `Run.script_opti`. +- Ask for missing parameters when no safe default exists. +- Prefer examples from `tests/**/**_doc.ml` for module names and argument order. +- Always include validation commands. + +## Reasoning Procedure + +1. Restate the transformation intent. +2. Identify the transformation API and required arguments. +3. Resolve the target using Prompt 1 target-generation rules. +4. Generate the smallest valid `Run.script_cpp` script. +5. State assumptions and missing information. +6. Provide validation commands. + +## Common Mappings + +User command: + +```text +unroll the loop i +``` + +Script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Loop.unroll [cFor "i"]; +) +``` + +User command: + +```text +inline calls to f +``` + +Script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Function.inline [cCall "f"]; +) +``` + +User command: + +```text +insert a++; before variable c +``` + +Script: + +```ocaml +open Optitrust +open Prelude + +let _ = Run.script_cpp (fun _ -> + !! Sequence_basic.insert ~reparse:true (stmt "a++;") [tBefore; cVarDef "c"]; +) +``` + +## Output Format + +````markdown +## Intent +One sentence describing the command. + +## Transformation API +Name the selected OptiTrust function and why it fits. + +## Target +```ocaml +[...] +``` + +## Generated Script +```ocaml +... +``` + +## Assumptions +List assumptions or state "None." + +## Validation +```bash +dune exec ... +``` +```` + +If the command is underspecified, ask one focused clarification instead of +emitting a guessed script. diff --git a/tools/optiNLP/prompts/03_code_to_candidate_script.md b/tools/optiNLP/prompts/03_code_to_candidate_script.md new file mode 100644 index 000000000..dd1405049 --- /dev/null +++ b/tools/optiNLP/prompts/03_code_to_candidate_script.md @@ -0,0 +1,81 @@ +# Prompt 3: OptiTrust Code To Candidate Script + +You are OptiNLP Code To Candidate Script, an assistant specialized in reading +input C/C++ or printed OptiLambda code and proposing possible OptiTrust +transformation scripts. + +This prompt is intentionally conservative. It should suggest candidates, not +pretend to know the globally optimal transformation sequence. + +## Inputs + +You may receive: + +- C/C++ source code; +- printed OptiLambda text for inspection only; +- performance or optimization goals; +- existing scripts, traces, diffs, or error output; +- the OptiNLP knowledge files. + +## Hard Rules + +- Suggest only transformations that exist in `lib/transfo/`. +- Do not invent target constructors. +- Do not invent `.opti` parser support or `Run.script_opti`. +- Separate facts from hypotheses. +- Rank candidate transformations by confidence. +- Provide validation commands for every candidate script. +- Ask for clarification when the optimization goal or target workload is + unclear. + +## Reasoning Procedure + +1. Summarize the visible program structure: functions, loops, calls, arrays, + writes, reductions, and obvious kernels. +2. Identify candidate targets using Prompt 1 rules. +3. Map visible opportunities to known OptiTrust transformations. +4. Rank candidates as high, medium, or low confidence. +5. Generate one or more candidate scripts. +6. Explain why each candidate may apply and what could make it invalid. +7. Provide validation commands and expected evidence. + +## Candidate Quality Rules + +- High confidence: direct user goal or common local transformation with clear + target, such as unroll loop `i`, inline call `f`, or add OpenMP to a named + loop. +- Medium confidence: plausible transformation requiring workload or semantic + validation, such as tiling a loop nest or fusing adjacent loops. +- Low confidence: optimization idea that needs more information, such as + changing memory layout or introducing GPU transformations. + +## Output Format + +````markdown +## Code Summary +Short structural summary of the input code. + +## Candidate Transformations +| Rank | Transformation | Target | Why it may apply | Risk | +| --- | --- | --- | --- | --- | + +## Recommended First Candidate +Explain which candidate should be tried first. + +## Candidate Script +```ocaml +... +``` + +## Validation +```bash +dune exec ... +``` + +## Missing Information +State "None." or ask focused questions. +```` + +If the input is `.opti`, say clearly that the text is used for inspection and +that runnable script generation still targets the existing C/C++ pipeline unless +OptiLambda parser support is added later. From c5adb4f2ddce3bb6ed02c3638f97dfb99ebbf359 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 07:45:09 -0400 Subject: [PATCH 02/47] Add OptiNLP provider layer --- .../src/optinlp/geminiProvider.ts | 189 ++++++++++++++++++ .../src/optinlp/generation.ts | 14 ++ .../src/optinlp/mockProvider.ts | 141 +++++++++++++ tools/vscode-optitrust/src/optinlp/modes.ts | 79 ++++++++ .../src/optinlp/providerErrors.ts | 26 +++ .../src/optinlp/providerFactory.ts | 33 +++ .../src/optinlp/providerTypes.ts | 37 ++++ 7 files changed, 519 insertions(+) create mode 100644 tools/vscode-optitrust/src/optinlp/geminiProvider.ts create mode 100644 tools/vscode-optitrust/src/optinlp/generation.ts create mode 100644 tools/vscode-optitrust/src/optinlp/mockProvider.ts create mode 100644 tools/vscode-optitrust/src/optinlp/modes.ts create mode 100644 tools/vscode-optitrust/src/optinlp/providerErrors.ts create mode 100644 tools/vscode-optitrust/src/optinlp/providerFactory.ts create mode 100644 tools/vscode-optitrust/src/optinlp/providerTypes.ts diff --git a/tools/vscode-optitrust/src/optinlp/geminiProvider.ts b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts new file mode 100644 index 000000000..837c2a513 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts @@ -0,0 +1,189 @@ +// Gemini implementation of the OptiNLP provider interface. This file owns the +// Gemini wire format and converts responses back into provider-neutral results. +import { OptiNlpProviderError, technicalDetailFrom } from "./providerErrors"; +import { OptiNlpProvider, OptiNlpProviderRequest, OptiNlpProviderResult, requestWithMode } from "./providerTypes"; +import { OptiNlpSchemaError, parseOptiNlpMarkdownResult } from "./resultSchemas"; + +export const DEFAULT_GEMINI_MODEL = "gemini-3.5-flash"; + +export interface GeminiProviderOptions { + readonly model?: string; + readonly apiKey?: string; + readonly apiKeyProvider?: () => Promise | string | undefined; + readonly endpointBase?: string; + readonly fetchImpl?: typeof fetch; +} + +interface GeminiTextPart { + readonly text?: string; +} + +interface GeminiResponse { + readonly candidates?: readonly { + readonly content?: { + readonly parts?: readonly GeminiTextPart[]; + }; + }[]; + readonly error?: { + readonly message?: string; + }; +} + +export class GeminiProvider implements OptiNlpProvider { + readonly name = "gemini"; + readonly model: string; + private readonly apiKey?: string; + private readonly apiKeyProvider?: GeminiProviderOptions["apiKeyProvider"]; + private readonly endpointBase: string; + private readonly fetchImpl: typeof fetch; + + constructor(options: GeminiProviderOptions = {}) { + this.model = options.model ?? DEFAULT_GEMINI_MODEL; + this.apiKey = options.apiKey; + this.apiKeyProvider = options.apiKeyProvider; + this.endpointBase = options.endpointBase ?? "https://generativelanguage.googleapis.com/v1beta"; + this.fetchImpl = options.fetchImpl ?? fetch; + } + + async generateTarget(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "target")); + } + + async generateScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "command_to_script")); + } + + async generateCandidateScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_candidate_script")); + } + + buildPromptForTest(request: OptiNlpProviderRequest): string { + return buildGeminiPrompt(request); + } + + private async generate(request: OptiNlpProviderRequest): Promise { + const apiKey = await this.resolveApiKey(); + if (!apiKey) { + throw new OptiNlpProviderError(this.name, "Set Gemini API key before using OptiNLP.", "Missing Gemini API key."); + } + + const url = `${this.endpointBase}/models/${encodeURIComponent(this.model)}:generateContent?key=${encodeURIComponent(apiKey)}`; + const body = { + contents: [ + { + role: "user", + parts: [{ text: buildGeminiPrompt(request) }] + } + ], + generationConfig: { + temperature: 0.2 + } + }; + + let response: Response; + try { + response = await this.fetchImpl(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + } catch (error) { + throw new OptiNlpProviderError( + this.name, + "Gemini request failed. Check your network connection and API key.", + technicalDetailFrom(error), + error + ); + } + + let rawResponse: GeminiResponse; + try { + rawResponse = (await response.json()) as GeminiResponse; + } catch (error) { + throw new OptiNlpProviderError(this.name, "Gemini returned an unreadable response.", technicalDetailFrom(error), error); + } + + if (!response.ok) { + const detail = rawResponse.error?.message ?? `HTTP ${response.status}`; + throw new OptiNlpProviderError(this.name, "Gemini request failed. Check your network connection and API key.", detail); + } + + const markdownOutput = extractGeminiText(rawResponse); + if (markdownOutput.trim().length === 0) { + throw new OptiNlpProviderError(this.name, "Gemini returned an empty OptiNLP response.", "No candidate text parts found."); + } + + let structured; + try { + structured = parseOptiNlpMarkdownResult(request.mode, markdownOutput); + } catch (error) { + if (error instanceof OptiNlpSchemaError) { + throw new OptiNlpProviderError(this.name, "Gemini returned an invalid OptiNLP response.", error.message, error); + } + throw error; + } + + return { + provider: this.name, + model: this.model, + markdownOutput, + structured, + rawResponse + }; + } + + private async resolveApiKey(): Promise { + if (this.apiKey && this.apiKey.trim().length > 0) { + return this.apiKey.trim(); + } + const provided = await this.apiKeyProvider?.(); + if (provided && provided.trim().length > 0) { + return provided.trim(); + } + if (process.env.GEMINI_API_KEY && process.env.GEMINI_API_KEY.trim().length > 0) { + return process.env.GEMINI_API_KEY.trim(); + } + return undefined; + } +} + +function buildGeminiPrompt(request: OptiNlpProviderRequest): string { + const session = request.sessionSummary?.trim(); + return [ + "# OptiNLP Task", + `Mode: ${request.mode}`, + "", + "# System Prompt", + request.promptText.trim(), + "", + "# Knowledge", + request.knowledgeText.trim(), + "", + "# Context", + `File: ${request.filePath}`, + `Language: ${request.language}`, + session ? `Session summary:\n${session}` : "Session summary: None.", + "", + "# Source", + "```", + request.sourceText, + "```", + "", + "# User Request", + request.userRequest, + "", + "# Output Contract", + "Return only the markdown format required by the selected OptiNLP prompt.", + "Include every required section from that prompt.", + "Do not add provider notes, apologies, or extra sections." + ].join("\n"); +} + +function extractGeminiText(response: GeminiResponse): string { + return ( + response.candidates?.[0]?.content?.parts + ?.map(part => part.text ?? "") + .join("") + .trim() ?? "" + ); +} diff --git a/tools/vscode-optitrust/src/optinlp/generation.ts b/tools/vscode-optitrust/src/optinlp/generation.ts new file mode 100644 index 000000000..35de1ed80 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/generation.ts @@ -0,0 +1,14 @@ +// Provider-neutral generation helpers shared by CLI, commands, and webviews. +// Keeping dispatch here avoids repeating mode-specific provider calls. +import { OptiNlpProvider, OptiNlpProviderRequest, OptiNlpProviderResult } from "./providerTypes"; + +export async function generateOptiNlp(provider: OptiNlpProvider, request: OptiNlpProviderRequest): Promise { + switch (request.mode) { + case "target": + return provider.generateTarget(request); + case "command_to_script": + return provider.generateScript(request); + case "code_to_candidate_script": + return provider.generateCandidateScript(request); + } +} diff --git a/tools/vscode-optitrust/src/optinlp/mockProvider.ts b/tools/vscode-optitrust/src/optinlp/mockProvider.ts new file mode 100644 index 000000000..71c162e2b --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/mockProvider.ts @@ -0,0 +1,141 @@ +// Deterministic provider used for tests and UI development without network +// access or API keys. +import { OptiNlpProvider, OptiNlpProviderRequest, OptiNlpProviderResult, requestWithMode } from "./providerTypes"; +import { parseOptiNlpMarkdownResult } from "./resultSchemas"; + +export interface MockProviderOptions { + readonly model?: string; + readonly markdownOutput?: string; +} + +export class MockProvider implements OptiNlpProvider { + readonly name = "mock"; + readonly model: string; + private readonly markdownOutput: string; + + constructor(options: MockProviderOptions = {}) { + this.model = options.model ?? "mock-model"; + this.markdownOutput = options.markdownOutput ?? DEFAULT_MOCK_OUTPUT; + } + + async generateTarget(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "target")); + } + + async generateScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "command_to_script")); + } + + async generateCandidateScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_candidate_script")); + } + + private async generate(request: OptiNlpProviderRequest): Promise { + const markdownOutput = this.markdownOutput === DEFAULT_MOCK_OUTPUT ? mockOutputForMode(request.mode) : this.markdownOutput; + return { + provider: this.name, + model: this.model, + markdownOutput, + structured: parseOptiNlpMarkdownResult(request.mode, markdownOutput), + rawResponse: { provider: this.name, mode: request.mode } + }; + } +} + +const DEFAULT_MOCK_OUTPUT = "__default__"; + +function mockOutputForMode(mode: OptiNlpProviderRequest["mode"]): string { + switch (mode) { + case "target": + return [ + "## Intent", + "Mock target intent.", + "", + "## Candidate Nodes", + "- Candidate 1: mock node", + "", + "## Recommended Target", + "```ocaml", + "[cFor \"i\"]", + "```", + "", + "## Why This Target", + "Mock explanation.", + "", + "## Ambiguities", + "None.", + "", + "## Alternatives", + "```ocaml", + "[cFunBody \"f\"; cFor \"i\"]", + "```", + "", + "## Validation", + "```ocaml", + "!! Show.target [cFor \"i\"];", + "```" + ].join("\n"); + case "command_to_script": + return [ + "## Intent", + "Mock script intent.", + "", + "## Transformation API", + "Loop.unroll fits the mock command.", + "", + "## Target", + "```ocaml", + "[cFor \"i\"]", + "```", + "", + "## Generated Script", + "```ocaml", + "open Optitrust", + "open Target", + "", + "let _ = Run.script_cpp (fun _ ->", + " !! Loop.unroll [cFor \"i\"];", + ")", + "```", + "", + "## Assumptions", + "None.", + "", + "## Validation", + "```bash", + "dune exec -- ./mock.exe", + "```" + ].join("\n"); + case "code_to_candidate_script": + return [ + "## Code Summary", + "Mock code summary.", + "", + "## Candidate Transformations", + "| Rank | Transformation | Target | Why it may apply | Risk |", + "| --- | --- | --- | --- | --- |", + "| High | Loop.unroll | `[cFor \"i\"]` | Mock reason | Mock risk |", + "", + "## Recommended First Candidate", + "Try the high-confidence mock candidate first.", + "", + "## Candidate Script", + "```ocaml", + "open Optitrust", + "open Target", + "", + "let _ = Run.script_cpp (fun _ ->", + " !! Loop.unroll [cFor \"i\"];", + ")", + "```", + "", + "## Validation", + "```bash", + "dune exec -- ./mock.exe", + "```", + "", + "## Missing Information", + "None." + ].join("\n"); + } +} diff --git a/tools/vscode-optitrust/src/optinlp/modes.ts b/tools/vscode-optitrust/src/optinlp/modes.ts new file mode 100644 index 000000000..346ac3242 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/modes.ts @@ -0,0 +1,79 @@ +// Central registry for OptiNLP workflows. Add or change workflows here first so +// prompt loading, CLI routing, UI labels, and auto-routing stay in sync. +import { OptiNlpMode } from "./providerTypes"; + +export type OptiNlpCliCommand = "target" | "script" | "candidates"; +export type OptiNlpUiMode = OptiNlpMode | "auto"; + +export interface OptiNlpModeDefinition { + readonly id: OptiNlpMode; + readonly cliCommand: OptiNlpCliCommand; + readonly label: string; + readonly shortLabel: string; + readonly placeholder: string; + readonly promptFile: string; + readonly knowledgeFiles: readonly string[]; +} + +export const OPTINLP_MODE_DEFINITIONS: readonly OptiNlpModeDefinition[] = [ + { + id: "target", + cliCommand: "target", + label: "Generate Target", + shortLabel: "Target", + placeholder: "target the second loop named i", + promptFile: "01_target_generator.md", + knowledgeFiles: ["targets.md"] + }, + { + id: "command_to_script", + cliCommand: "script", + label: "Generate Script", + shortLabel: "Script", + placeholder: "unroll the loop i", + promptFile: "02_command_to_script.md", + knowledgeFiles: ["targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] + }, + { + id: "code_to_candidate_script", + cliCommand: "candidates", + label: "Suggest Candidate Script", + shortLabel: "Candidates", + placeholder: "suggest a first transformation", + promptFile: "03_code_to_candidate_script.md", + knowledgeFiles: ["targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] + } +] as const; + +export const OPTINLP_MODE_BY_ID = new Map(OPTINLP_MODE_DEFINITIONS.map(definition => [definition.id, definition])); +export const OPTINLP_MODE_BY_CLI_COMMAND = new Map(OPTINLP_MODE_DEFINITIONS.map(definition => [definition.cliCommand, definition])); + +export function modeDefinition(mode: OptiNlpMode): OptiNlpModeDefinition { + const definition = OPTINLP_MODE_BY_ID.get(mode); + if (!definition) { + throw new Error(`Unknown OptiNLP mode '${mode}'.`); + } + return definition; +} + +export function modeFromCliCommand(command: string): OptiNlpMode | undefined { + return OPTINLP_MODE_BY_CLI_COMMAND.get(command as OptiNlpCliCommand)?.id; +} + +export function isOptiNlpCliCommand(command: string): command is OptiNlpCliCommand { + return OPTINLP_MODE_BY_CLI_COMMAND.has(command as OptiNlpCliCommand); +} + +export function resolveAutoMode(mode: OptiNlpUiMode, request: string): OptiNlpMode { + if (mode !== "auto") { + return mode; + } + const text = request.toLowerCase(); + if (/\b(target|position|before|after|loop|call|variable)\b/u.test(text) && !/\b(unroll|inline|insert|tile|fuse|split|parallel|transform)\b/u.test(text)) { + return "target"; + } + if (/\b(suggest|candidate|optimi[sz]e|opportunity|what can)\b/u.test(text)) { + return "code_to_candidate_script"; + } + return "command_to_script"; +} diff --git a/tools/vscode-optitrust/src/optinlp/providerErrors.ts b/tools/vscode-optitrust/src/optinlp/providerErrors.ts new file mode 100644 index 000000000..1c9147558 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/providerErrors.ts @@ -0,0 +1,26 @@ +// Normalized provider error type. UI and CLI code can show userMessage while +// keeping provider-specific details separate and non-secret. +export class OptiNlpProviderError extends Error { + readonly provider: string; + readonly userMessage: string; + readonly technicalDetail?: string; + + constructor(provider: string, userMessage: string, technicalDetail?: string, cause?: unknown) { + super(userMessage); + this.name = "OptiNlpProviderError"; + this.provider = provider; + this.userMessage = userMessage; + this.technicalDetail = technicalDetail; + + if (cause !== undefined) { + (this as Error & { cause?: unknown }).cause = cause; + } + } +} + +export function technicalDetailFrom(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} diff --git a/tools/vscode-optitrust/src/optinlp/providerFactory.ts b/tools/vscode-optitrust/src/optinlp/providerFactory.ts new file mode 100644 index 000000000..cffca4f89 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/providerFactory.ts @@ -0,0 +1,33 @@ +// Factory for OptiNLP model providers. New providers should be registered here +// without leaking provider-specific options into the rest of the OptiNLP code. +import { GeminiProvider, GeminiProviderOptions } from "./geminiProvider"; +import { MockProvider, MockProviderOptions } from "./mockProvider"; +import { OptiNlpProvider } from "./providerTypes"; + +export type OptiNlpProviderId = "gemini" | "mock" | "openai" | "ollama"; +export const DEFAULT_OPTINLP_PROVIDER: OptiNlpProviderId = "gemini"; +export const OPTINLP_PROVIDER_IDS: readonly OptiNlpProviderId[] = ["gemini", "mock", "openai", "ollama"]; +export const IMPLEMENTED_OPTINLP_PROVIDER_IDS: readonly OptiNlpProviderId[] = ["gemini", "mock"]; + +export interface OptiNlpProviderFactoryOptions { + readonly provider?: OptiNlpProviderId; + readonly gemini?: GeminiProviderOptions; + readonly mock?: MockProviderOptions; +} + +export function createOptiNlpProvider(options: OptiNlpProviderFactoryOptions = {}): OptiNlpProvider { + const provider = options.provider ?? DEFAULT_OPTINLP_PROVIDER; + switch (provider) { + case "gemini": + return new GeminiProvider(options.gemini); + case "mock": + return new MockProvider(options.mock); + case "openai": + case "ollama": + throw new Error(`OptiNLP provider '${provider}' is not implemented yet.`); + } +} + +export function parseOptiNlpProviderId(value: string): OptiNlpProviderId | undefined { + return OPTINLP_PROVIDER_IDS.includes(value as OptiNlpProviderId) ? (value as OptiNlpProviderId) : undefined; +} diff --git a/tools/vscode-optitrust/src/optinlp/providerTypes.ts b/tools/vscode-optitrust/src/optinlp/providerTypes.ts new file mode 100644 index 000000000..48e0ccc50 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/providerTypes.ts @@ -0,0 +1,37 @@ +// Provider-neutral OptiNLP request/result contracts. CLI, VS Code commands, +// panels, and provider implementations should communicate through these types. +import type { OptiNlpStructuredResult } from "./resultSchemas"; + +export type OptiNlpMode = "target" | "command_to_script" | "code_to_candidate_script"; + +export interface OptiNlpProviderRequest { + readonly mode: OptiNlpMode; + readonly userRequest: string; + readonly sourceText: string; + readonly filePath: string; + readonly language: string; + readonly promptText: string; + readonly knowledgeText: string; + readonly sessionSummary?: string; +} + +export interface OptiNlpProviderResult { + readonly provider: string; + readonly model: string; + readonly markdownOutput: string; + readonly structured?: OptiNlpStructuredResult; + readonly rawResponse?: unknown; +} + +export interface OptiNlpProvider { + readonly name: string; + readonly model: string; + + generateTarget(request: OptiNlpProviderRequest): Promise; + generateScript(request: OptiNlpProviderRequest): Promise; + generateCandidateScript(request: OptiNlpProviderRequest): Promise; +} + +export function requestWithMode(request: OptiNlpProviderRequest, mode: OptiNlpMode): OptiNlpProviderRequest { + return { ...request, mode }; +} From 53e3d713dd93118f0fd86731ab7f520d349a6353 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:17:22 -0400 Subject: [PATCH 03/47] Make Prompt 3 generate full transformation scripts --- tools/optiNLP/eval/code_to_script_cases.md | 52 ++++++++++-- .../prompts/03_code_to_candidate_script.md | 83 +++++++++++++++---- 2 files changed, 111 insertions(+), 24 deletions(-) diff --git a/tools/optiNLP/eval/code_to_script_cases.md b/tools/optiNLP/eval/code_to_script_cases.md index 3ac1b79f9..2ef3a91ef 100644 --- a/tools/optiNLP/eval/code_to_script_cases.md +++ b/tools/optiNLP/eval/code_to_script_cases.md @@ -1,4 +1,4 @@ -# Code To Candidate Script Evaluation Cases +# Code To Full Script Evaluation Cases Use these cases to manually test `prompts/03_code_to_candidate_script.md`. @@ -14,12 +14,13 @@ void f(int n) { } ``` -Acceptable suggestions: +Acceptable full-script behavior: -- `Loop.unroll [cFor "i"]` as a candidate only if the goal is reducing loop - overhead or exposing straight-line code. -- `Omp.parallel_for [cFor "i"]` only if independence is plausible and validation - is required. +- Emit a complete OCaml script with `open Optitrust`, target-related opens, and + `Run.script_cpp`. +- Include at least one conservative candidate transformation in the table. +- The `Candidate Script` section must contain a complete `.ml` script, not just + `[cFor "i"]`. Required behavior: @@ -53,6 +54,7 @@ Required behavior: - Explain that inlining may expose further simplifications. - Require validation with diff/trace or tests. +- Emit the complete script in the `Candidate Script` section. ## Case 3: Adjacent Loops @@ -65,7 +67,7 @@ void f(int n) { } ``` -Acceptable suggestion: +Acceptable full-script behavior: - Candidate loop fusion targeting the repeated `i` loops. @@ -74,6 +76,7 @@ Required behavior: - Mark as medium confidence. - Mention dependency and resource checks. - Do not claim semantic safety without validation. +- Emit the complete script in the `Candidate Script` section. ## Case 4: Printed OptiLambda @@ -94,3 +97,38 @@ Required behavior: - Do not generate `Run.script_opti`. - If a script is proposed, state that it must be applied through an existing C/C++ script workflow until parser support exists. + +## Case 5: Matrix Multiplication Full File + +Input: + +```c +void mm(int n, double* A, double* B, double* C) { + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + double sum = 0.; + for (int k = 0; k < n; k++) { + sum += A[i*n+k] * B[k*n+j]; + } + C[i*n+j] = sum; + } + } +} +``` + +Request: + +```text +generate a complete transformation script for the whole file +``` + +Required behavior: + +- Treat this as Prompt 3 / full-script generation, not Prompt 2. +- Emit a complete OCaml script in the style of `matmul.ml`. +- Include `open Optitrust` and the needed target/prelude opens. +- Use `Run.script_cpp`. +- Prefer a coherent matrix-multiplication strategy such as tiling `i`, `j`, and + `k`, loop reordering, optional SIMD/parallelism when targets are clear, and + `Cleanup.std ()`. +- Rank transformations and state which assumptions need validation. diff --git a/tools/optiNLP/prompts/03_code_to_candidate_script.md b/tools/optiNLP/prompts/03_code_to_candidate_script.md index dd1405049..4efe8eccf 100644 --- a/tools/optiNLP/prompts/03_code_to_candidate_script.md +++ b/tools/optiNLP/prompts/03_code_to_candidate_script.md @@ -1,11 +1,17 @@ -# Prompt 3: OptiTrust Code To Candidate Script +# Prompt 3: OptiTrust Code To Full Script -You are OptiNLP Code To Candidate Script, an assistant specialized in reading -input C/C++ or printed OptiLambda code and proposing possible OptiTrust -transformation scripts. +You are OptiNLP Code To Full Script, an assistant specialized in reading a +complete input C/C++ file, identifying a plausible OptiTrust transformation +strategy, and generating the full OCaml OptiTrust script for that file. -This prompt is intentionally conservative. It should suggest candidates, not -pretend to know the globally optimal transformation sequence. +For example, when given `matmul.cpp`, your job is to produce the corresponding +`matmul.ml` style transformation script: complete opens, flags when useful, +helper definitions when useful, a `Run.script_cpp` block, transformation calls, +and validation commands. + +This prompt is allowed to choose a reasonable first transformation strategy from +the code and goal. It must still be honest: separate confident transformations +from hypotheses, use only known OptiTrust APIs, and include validation. ## Inputs @@ -19,14 +25,25 @@ You may receive: ## Hard Rules -- Suggest only transformations that exist in `lib/transfo/`. +- Generate a complete OCaml OptiTrust script, not just a target and not just a + list of ideas. +- The script must be directly usable as a `.ml` transformation script for the + full input file. +- Use `Run.script_cpp`; do not generate `Run.script_opti`. +- Include the needed `open` statements, usually `open Optitrust` and either + `open Target` or `open Prelude`. +- Use `!!` or `!!!` consistently with repository examples. +- Prefer semantic targets over line numbers. +- Use only transformations that exist in `lib/transfo/`. - Do not invent target constructors. - Do not invent `.opti` parser support or `Run.script_opti`. - Separate facts from hypotheses. -- Rank candidate transformations by confidence. -- Provide validation commands for every candidate script. -- Ask for clarification when the optimization goal or target workload is - unclear. +- Rank the main transformations by confidence before the script. +- Provide validation commands for the generated script. +- If a full optimization strategy is unsafe, still emit a conservative runnable + script using safe inspection or cleanup transformations, and state what is + missing. +- Ask for clarification only when no runnable script can be produced at all. ## Reasoning Procedure @@ -35,19 +52,28 @@ You may receive: 2. Identify candidate targets using Prompt 1 rules. 3. Map visible opportunities to known OptiTrust transformations. 4. Rank candidates as high, medium, or low confidence. -5. Generate one or more candidate scripts. -6. Explain why each candidate may apply and what could make it invalid. -7. Provide validation commands and expected evidence. +5. Choose one coherent script plan for the full file. +6. Generate the complete OCaml script. +7. Explain what could make the script invalid. +8. Provide validation commands and expected evidence. ## Candidate Quality Rules - High confidence: direct user goal or common local transformation with clear - target, such as unroll loop `i`, inline call `f`, or add OpenMP to a named - loop. + target, such as inline a called helper, unroll a named loop, clean up after a + transformation, or expose a function body. - Medium confidence: plausible transformation requiring workload or semantic validation, such as tiling a loop nest or fusing adjacent loops. - Low confidence: optimization idea that needs more information, such as changing memory layout or introducing GPU transformations. +- If the code resembles a known kernel such as matrix multiplication, generate a + script in the style of existing case studies: inline helper kernels, tile loop + nests, reorder loops, optionally hoist/copy repeated data, add SIMD/parallel + annotations when targets are clear, then run cleanup. +- If the code is too small or the optimization goal is vague, generate a + conservative starter script for the full file, such as a script that performs + the safest applicable local transformation plus `Cleanup.std ()`, and list + stronger transformations as medium/low confidence. ## Output Format @@ -60,7 +86,7 @@ Short structural summary of the input code. | --- | --- | --- | --- | --- | ## Recommended First Candidate -Explain which candidate should be tried first. +Explain the chosen full-file script strategy. ## Candidate Script ```ocaml @@ -79,3 +105,26 @@ State "None." or ask focused questions. If the input is `.opti`, say clearly that the text is used for inspection and that runnable script generation still targets the existing C/C++ pipeline unless OptiLambda parser support is added later. + +## Example Shape + +For a matrix multiplication file, prefer a complete script shape such as: + +```ocaml +open Optitrust +open Prelude + +let int = trm_int + +let _ = Run.script_cpp (fun () -> + !! Function.inline_def [cFunDef "mm"]; + !! Loop.tile (int 32) ~index:"bi" ~bound:TileDivides [cFor "i"]; + !! Loop.tile (int 32) ~index:"bj" ~bound:TileDivides [cFor "j"]; + !! Loop.tile (int 4) ~index:"bk" ~bound:TileDivides [cFor "k"]; + !! Loop.reorder_at ~order:["bi"; "bj"; "bk"; "i"; "k"; "j"] [cPlusEq ~lhs:[cVar "sum"] ()]; + !! Cleanup.std (); +) +``` + +Adapt names, targets, tile sizes, and transformation sequence to the actual +source. Do not copy this blindly when the code is not matrix multiplication. From 8a7e690860189935fa2ebc607abf68c944f2d4e5 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:18:20 -0400 Subject: [PATCH 04/47] Add OptiNLP result schema parsing --- .../src/optinlp/resultSchemas.ts | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 tools/vscode-optitrust/src/optinlp/resultSchemas.ts diff --git a/tools/vscode-optitrust/src/optinlp/resultSchemas.ts b/tools/vscode-optitrust/src/optinlp/resultSchemas.ts new file mode 100644 index 000000000..9e8a21e02 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/resultSchemas.ts @@ -0,0 +1,252 @@ +// Structured OptiNLP result types and markdown parsers for the three prompt +// outputs. This is the bridge between prompt-facing markdown and UI actions. +import type { OptiNlpMode } from "./providerTypes"; + +export interface TargetResult { + readonly kind: "target"; + readonly intent: string; + readonly candidateNodes: readonly string[]; + readonly recommendedTarget?: string; + readonly whyThisTarget?: string; + readonly ambiguities?: string; + readonly alternatives: readonly string[]; + readonly validation?: string; + readonly missingInformation?: string; +} + +export interface ScriptResult { + readonly kind: "command_to_script"; + readonly intent: string; + readonly transformationApi: string; + readonly target: string; + readonly generatedScript: string; + readonly assumptions: readonly string[]; + readonly validation: string; +} + +export interface CandidateTransformation { + readonly rank: string; + readonly transformation: string; + readonly target: string; + readonly whyItMayApply: string; + readonly risk: string; +} + +export interface CandidateScriptResult { + readonly kind: "code_to_candidate_script"; + readonly codeSummary: string; + readonly candidateTransformations: readonly CandidateTransformation[]; + readonly recommendedFirstCandidate: string; + readonly candidateScript: string; + readonly validation: string; + readonly missingInformation?: string; +} + +export type OptiNlpStructuredResult = TargetResult | ScriptResult | CandidateScriptResult; + +export const targetResultSchema = { + type: "object", + required: ["kind", "intent", "candidateNodes", "alternatives"], + properties: { + kind: { const: "target" }, + intent: { type: "string" }, + candidateNodes: { type: "array", items: { type: "string" } }, + recommendedTarget: { type: "string" }, + whyThisTarget: { type: "string" }, + ambiguities: { type: "string" }, + alternatives: { type: "array", items: { type: "string" } }, + validation: { type: "string" }, + missingInformation: { type: "string" } + } +} as const; + +export const scriptResultSchema = { + type: "object", + required: ["kind", "intent", "transformationApi", "target", "generatedScript", "assumptions", "validation"], + properties: { + kind: { const: "command_to_script" }, + intent: { type: "string" }, + transformationApi: { type: "string" }, + target: { type: "string" }, + generatedScript: { type: "string" }, + assumptions: { type: "array", items: { type: "string" } }, + validation: { type: "string" } + } +} as const; + +export const candidateScriptResultSchema = { + type: "object", + required: ["kind", "codeSummary", "candidateTransformations", "recommendedFirstCandidate", "candidateScript", "validation"], + properties: { + kind: { const: "code_to_candidate_script" }, + codeSummary: { type: "string" }, + candidateTransformations: { + type: "array", + items: { + type: "object", + required: ["rank", "transformation", "target", "whyItMayApply", "risk"], + properties: { + rank: { type: "string" }, + transformation: { type: "string" }, + target: { type: "string" }, + whyItMayApply: { type: "string" }, + risk: { type: "string" } + } + } + }, + recommendedFirstCandidate: { type: "string" }, + candidateScript: { type: "string" }, + validation: { type: "string" }, + missingInformation: { type: "string" } + } +} as const; + +export class OptiNlpSchemaError extends Error { + readonly mode: OptiNlpMode; + + constructor(mode: OptiNlpMode, message: string) { + super(message); + this.name = "OptiNlpSchemaError"; + this.mode = mode; + } +} + +export function parseOptiNlpMarkdownResult(mode: OptiNlpMode, markdown: string): OptiNlpStructuredResult { + switch (mode) { + case "target": + return parseTargetResult(markdown); + case "command_to_script": + return parseScriptResult(markdown); + case "code_to_candidate_script": + return parseCandidateScriptResult(markdown); + } +} + +function parseTargetResult(markdown: string): TargetResult { + const missingInformation = section(markdown, "Missing Information"); + if (missingInformation) { + return { + kind: "target", + intent: "", + candidateNodes: [], + alternatives: [], + missingInformation + }; + } + + const intent = requiredSection(markdown, "Intent", "target"); + const recommendedTarget = firstCodeBlock(requiredSection(markdown, "Recommended Target", "target")); + const candidateNodes = bulletLines(section(markdown, "Candidate Nodes") ?? ""); + const alternatives = codeBlocks(section(markdown, "Alternatives") ?? ""); + + if (!recommendedTarget) { + throw new OptiNlpSchemaError("target", "Target result is missing a Recommended Target code block."); + } + + return { + kind: "target", + intent, + candidateNodes, + recommendedTarget, + whyThisTarget: section(markdown, "Why This Target"), + ambiguities: section(markdown, "Ambiguities"), + alternatives, + validation: firstCodeBlock(section(markdown, "Validation") ?? "") ?? section(markdown, "Validation") + }; +} + +function parseScriptResult(markdown: string): ScriptResult { + return { + kind: "command_to_script", + intent: requiredSection(markdown, "Intent", "command_to_script"), + transformationApi: requiredSection(markdown, "Transformation API", "command_to_script"), + target: requiredCodeBlock(markdown, "Target", "command_to_script"), + generatedScript: requiredCodeBlock(markdown, "Generated Script", "command_to_script"), + assumptions: linesOrNone(requiredSection(markdown, "Assumptions", "command_to_script")), + validation: requiredCodeBlock(markdown, "Validation", "command_to_script") + }; +} + +function parseCandidateScriptResult(markdown: string): CandidateScriptResult { + return { + kind: "code_to_candidate_script", + codeSummary: requiredSection(markdown, "Code Summary", "code_to_candidate_script"), + candidateTransformations: parseCandidateTable(requiredSection(markdown, "Candidate Transformations", "code_to_candidate_script")), + recommendedFirstCandidate: requiredSection(markdown, "Recommended First Candidate", "code_to_candidate_script"), + candidateScript: requiredCodeBlock(markdown, "Candidate Script", "code_to_candidate_script"), + validation: requiredCodeBlock(markdown, "Validation", "code_to_candidate_script"), + missingInformation: section(markdown, "Missing Information") + }; +} + +function section(markdown: string, title: string): string | undefined { + const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(`^##\\s+${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, "im"); + const match = regex.exec(markdown); + const value = match?.[1]?.trim(); + return value && value.length > 0 ? value : undefined; +} + +function requiredSection(markdown: string, title: string, mode: OptiNlpMode): string { + const value = section(markdown, title); + if (!value) { + throw new OptiNlpSchemaError(mode, `${mode} result is missing required section '${title}'.`); + } + return value; +} + +function codeBlocks(markdown: string): string[] { + const blocks: string[] = []; + const regex = /```[A-Za-z0-9_-]*\n([\s\S]*?)```/g; + let match: RegExpExecArray | null; + while ((match = regex.exec(markdown)) !== null) { + blocks.push(match[1].trim()); + } + return blocks; +} + +function firstCodeBlock(markdown: string): string | undefined { + return codeBlocks(markdown)[0]; +} + +function requiredCodeBlock(markdown: string, title: string, mode: OptiNlpMode): string { + const block = firstCodeBlock(requiredSection(markdown, title, mode)); + if (!block) { + throw new OptiNlpSchemaError(mode, `${mode} result section '${title}' is missing a code block.`); + } + return block; +} + +function bulletLines(markdown: string): string[] { + return markdown + .split(/\r?\n/u) + .map(line => line.trim()) + .filter(line => /^[-*]\s+/u.test(line)) + .map(line => line.replace(/^[-*]\s+/u, "").trim()); +} + +function linesOrNone(markdown: string): string[] { + const bullets = bulletLines(markdown); + if (bullets.length > 0) { + return bullets; + } + const trimmed = markdown.trim(); + return trimmed.length > 0 && trimmed !== "None." ? [trimmed] : []; +} + +function parseCandidateTable(markdown: string): CandidateTransformation[] { + return markdown + .split(/\r?\n/u) + .map(line => line.trim()) + .filter(line => line.startsWith("|") && !/^\|\s*-+/u.test(line)) + .slice(1) + .map(line => line.split("|").slice(1, -1).map(cell => cell.trim())) + .filter(cells => cells.length >= 5) + .map(cells => ({ + rank: cells[0], + transformation: cells[1], + target: cells[2], + whyItMayApply: cells[3], + risk: cells[4] + })); +} From 8f85405e481a6882efa8c4ab771cd8f95a3fa41c Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:19:36 -0400 Subject: [PATCH 05/47] Add OpenAI OptiNLP provider --- .../src/optinlp/openaiProvider.ts | 190 ++++++++++++++++++ .../src/optinlp/providerFactory.ts | 5 +- 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tools/vscode-optitrust/src/optinlp/openaiProvider.ts diff --git a/tools/vscode-optitrust/src/optinlp/openaiProvider.ts b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts new file mode 100644 index 000000000..028ef52c8 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts @@ -0,0 +1,190 @@ +// OpenAI implementation of the OptiNLP provider interface. This file owns the +// Responses API wire format and keeps OpenAI-specific fields out of callers. +import { OptiNlpProviderError, technicalDetailFrom } from "./providerErrors"; +import { OptiNlpProvider, OptiNlpProviderRequest, OptiNlpProviderResult, requestWithMode } from "./providerTypes"; +import { OptiNlpSchemaError, parseOptiNlpMarkdownResult } from "./resultSchemas"; + +export const DEFAULT_OPENAI_MODEL = "gpt-5.5"; + +export interface OpenAiProviderOptions { + readonly model?: string; + readonly apiKey?: string; + readonly apiKeyProvider?: () => Promise | string | undefined; + readonly endpoint?: string; + readonly fetchImpl?: typeof fetch; +} + +interface OpenAiTextPart { + readonly type?: string; + readonly text?: string; +} + +interface OpenAiOutputItem { + readonly type?: string; + readonly content?: readonly OpenAiTextPart[]; +} + +interface OpenAiResponse { + readonly output?: readonly OpenAiOutputItem[]; + readonly output_text?: string; + readonly error?: { + readonly message?: string; + }; +} + +export class OpenAiProvider implements OptiNlpProvider { + readonly name = "openai"; + readonly model: string; + private readonly apiKey?: string; + private readonly apiKeyProvider?: OpenAiProviderOptions["apiKeyProvider"]; + private readonly endpoint: string; + private readonly fetchImpl: typeof fetch; + + constructor(options: OpenAiProviderOptions = {}) { + this.model = options.model ?? DEFAULT_OPENAI_MODEL; + this.apiKey = options.apiKey; + this.apiKeyProvider = options.apiKeyProvider; + this.endpoint = options.endpoint ?? "https://api.openai.com/v1/responses"; + this.fetchImpl = options.fetchImpl ?? fetch; + } + + async generateTarget(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "target")); + } + + async generateScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "command_to_script")); + } + + async generateCandidateScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_candidate_script")); + } + + buildPromptForTest(request: OptiNlpProviderRequest): string { + return buildOpenAiPrompt(request); + } + + private async generate(request: OptiNlpProviderRequest): Promise { + const apiKey = await this.resolveApiKey(); + if (!apiKey) { + throw new OptiNlpProviderError(this.name, "Set OpenAI API key before using OptiNLP.", "Missing OpenAI API key."); + } + + const body = { + model: this.model, + instructions: [ + "You are an OptiNLP provider.", + "Return only the markdown format required by the selected OptiNLP prompt.", + "Include every required section from that prompt.", + "Do not add provider notes, apologies, or extra sections." + ].join("\n"), + input: buildOpenAiPrompt(request), + store: false + }; + + let response: Response; + try { + response = await this.fetchImpl(this.endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify(body) + }); + } catch (error) { + throw new OptiNlpProviderError(this.name, "OpenAI request failed. Check your network connection and API key.", technicalDetailFrom(error), error); + } + + let rawResponse: OpenAiResponse; + try { + rawResponse = (await response.json()) as OpenAiResponse; + } catch (error) { + throw new OptiNlpProviderError(this.name, "OpenAI returned an unreadable response.", technicalDetailFrom(error), error); + } + + if (!response.ok) { + const detail = rawResponse.error?.message ?? `HTTP ${response.status}`; + throw new OptiNlpProviderError(this.name, "OpenAI request failed. Check your network connection and API key.", detail); + } + + const markdownOutput = extractOpenAiText(rawResponse); + if (markdownOutput.trim().length === 0) { + throw new OptiNlpProviderError(this.name, "OpenAI returned an empty OptiNLP response.", "No output text found."); + } + + let structured; + try { + structured = parseOptiNlpMarkdownResult(request.mode, markdownOutput); + } catch (error) { + if (error instanceof OptiNlpSchemaError) { + throw new OptiNlpProviderError(this.name, "OpenAI returned an invalid OptiNLP response.", error.message, error); + } + throw error; + } + + return { + provider: this.name, + model: this.model, + markdownOutput, + structured, + rawResponse + }; + } + + private async resolveApiKey(): Promise { + if (this.apiKey && this.apiKey.trim().length > 0) { + return this.apiKey.trim(); + } + const provided = await this.apiKeyProvider?.(); + if (provided && provided.trim().length > 0) { + return provided.trim(); + } + if (process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.trim().length > 0) { + return process.env.OPENAI_API_KEY.trim(); + } + return undefined; + } +} + +function buildOpenAiPrompt(request: OptiNlpProviderRequest): string { + const session = request.sessionSummary?.trim(); + return [ + "# OptiNLP Task", + `Mode: ${request.mode}`, + "", + "# System Prompt", + request.promptText.trim(), + "", + "# Knowledge", + request.knowledgeText.trim(), + "", + "# Context", + `File: ${request.filePath}`, + `Language: ${request.language}`, + session ? `Session summary:\n${session}` : "Session summary: None.", + "", + "# Source", + "```", + request.sourceText, + "```", + "", + "# User Request", + request.userRequest + ].join("\n"); +} + +function extractOpenAiText(response: OpenAiResponse): string { + if (response.output_text && response.output_text.trim().length > 0) { + return response.output_text.trim(); + } + + return ( + response.output + ?.flatMap(item => item.content ?? []) + .filter(part => part.type === "output_text" || part.text !== undefined) + .map(part => part.text ?? "") + .join("") + .trim() ?? "" + ); +} diff --git a/tools/vscode-optitrust/src/optinlp/providerFactory.ts b/tools/vscode-optitrust/src/optinlp/providerFactory.ts index cffca4f89..11bddc435 100644 --- a/tools/vscode-optitrust/src/optinlp/providerFactory.ts +++ b/tools/vscode-optitrust/src/optinlp/providerFactory.ts @@ -2,17 +2,19 @@ // without leaking provider-specific options into the rest of the OptiNLP code. import { GeminiProvider, GeminiProviderOptions } from "./geminiProvider"; import { MockProvider, MockProviderOptions } from "./mockProvider"; +import { OpenAiProvider, OpenAiProviderOptions } from "./openaiProvider"; import { OptiNlpProvider } from "./providerTypes"; export type OptiNlpProviderId = "gemini" | "mock" | "openai" | "ollama"; export const DEFAULT_OPTINLP_PROVIDER: OptiNlpProviderId = "gemini"; export const OPTINLP_PROVIDER_IDS: readonly OptiNlpProviderId[] = ["gemini", "mock", "openai", "ollama"]; -export const IMPLEMENTED_OPTINLP_PROVIDER_IDS: readonly OptiNlpProviderId[] = ["gemini", "mock"]; +export const IMPLEMENTED_OPTINLP_PROVIDER_IDS: readonly OptiNlpProviderId[] = ["gemini", "mock", "openai"]; export interface OptiNlpProviderFactoryOptions { readonly provider?: OptiNlpProviderId; readonly gemini?: GeminiProviderOptions; readonly mock?: MockProviderOptions; + readonly openai?: OpenAiProviderOptions; } export function createOptiNlpProvider(options: OptiNlpProviderFactoryOptions = {}): OptiNlpProvider { @@ -23,6 +25,7 @@ export function createOptiNlpProvider(options: OptiNlpProviderFactoryOptions = { case "mock": return new MockProvider(options.mock); case "openai": + return new OpenAiProvider(options.openai); case "ollama": throw new Error(`OptiNLP provider '${provider}' is not implemented yet.`); } From 7eb95d214f6f65ad2118349ec93956b101e411f9 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:20:17 -0400 Subject: [PATCH 06/47] Add OptiNLP provider tests --- .../src/optinlp/provider.test.ts | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 tools/vscode-optitrust/src/optinlp/provider.test.ts diff --git a/tools/vscode-optitrust/src/optinlp/provider.test.ts b/tools/vscode-optitrust/src/optinlp/provider.test.ts new file mode 100644 index 000000000..aa5196ac7 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/provider.test.ts @@ -0,0 +1,374 @@ +import * as assert from "assert"; +import { GeminiProvider } from "./geminiProvider"; +import { MockProvider } from "./mockProvider"; +import { OpenAiProvider } from "./openaiProvider"; +import { OptiNlpProviderError } from "./providerErrors"; +import { createOptiNlpProvider } from "./providerFactory"; +import { OptiNlpProviderRequest } from "./providerTypes"; +import { parseOptiNlpMarkdownResult } from "./resultSchemas"; + +const sampleRequest: OptiNlpProviderRequest = { + mode: "target", + userRequest: "target the loop i", + sourceText: "void f(int n) { for (int i = 0; i < n; i++) work(i); }", + filePath: "tests/demo.cpp", + language: "cpp", + promptText: "# Prompt\nReturn target output.", + knowledgeText: "# Knowledge\nUse cFor for loops.", + sessionSummary: "Previous target was [cFor \"j\"]." +}; + +async function testProviderFactory(): Promise { + assert.strictEqual(createOptiNlpProvider().name, "gemini"); + assert.strictEqual(createOptiNlpProvider({ provider: "mock" }).name, "mock"); + assert.strictEqual(createOptiNlpProvider({ provider: "openai" }).name, "openai"); +} + +async function testMockProvider(): Promise { + const provider = new MockProvider(); + const result = await provider.generateTarget(sampleRequest); + + assert.strictEqual(result.provider, "mock"); + assert.strictEqual(result.model, "mock-model"); + assert.strictEqual(result.structured?.kind, "target"); + assert.strictEqual(result.structured?.recommendedTarget, "[cFor \"i\"]"); + + const scriptResult = await provider.generateScript(sampleRequest); + assert.strictEqual(scriptResult.structured?.kind, "command_to_script"); + + const candidateResult = await provider.generateCandidateScript(sampleRequest); + assert.strictEqual(candidateResult.structured?.kind, "code_to_candidate_script"); +} + +async function testGeminiPromptConstruction(): Promise { + const provider = new GeminiProvider({ apiKey: "test-key", model: "test-model" }); + const prompt = provider.buildPromptForTest(sampleRequest); + + assert.match(prompt, /Mode: target/); + assert.match(prompt, /# System Prompt/); + assert.match(prompt, /# Knowledge/); + assert.match(prompt, /File: tests\/demo.cpp/); + assert.match(prompt, /target the loop i/); + assert.match(prompt, /Return only the markdown format required/); + assert.match(prompt, /Include every required section/); +} + +async function testGeminiMissingApiKey(): Promise { + const previous = process.env.GEMINI_API_KEY; + delete process.env.GEMINI_API_KEY; + + try { + const provider = new GeminiProvider({ apiKeyProvider: () => undefined }); + await assert.rejects( + () => provider.generateTarget(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && + error.provider === "gemini" && + error.userMessage === "Set Gemini API key before using OptiNLP." + ); + } finally { + if (previous !== undefined) { + process.env.GEMINI_API_KEY = previous; + } + } +} + +async function testGeminiEmptyResponse(): Promise { + const fetchImpl: typeof fetch = async () => + new Response(JSON.stringify({ candidates: [{ content: { parts: [{ text: " " }] } }] }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + + const provider = new GeminiProvider({ apiKey: "test-key", fetchImpl }); + await assert.rejects( + () => provider.generateTarget(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && + error.provider === "gemini" && + error.userMessage === "Gemini returned an empty OptiNLP response." + ); +} + +async function testGeminiProviderException(): Promise { + const fetchImpl: typeof fetch = async () => { + throw new Error("network unavailable"); + }; + + const provider = new GeminiProvider({ apiKey: "test-key", fetchImpl }); + await assert.rejects( + () => provider.generateScript(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && + error.provider === "gemini" && + error.userMessage === "Gemini request failed. Check your network connection and API key." && + error.technicalDetail === "network unavailable" + ); +} + +async function testGeminiSuccessfulResponse(): Promise { + const fetchImpl: typeof fetch = async () => + new Response(JSON.stringify({ candidates: [{ content: { parts: [{ text: validCandidateMarkdown }] } }] }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + + const provider = new GeminiProvider({ apiKey: "test-key", model: "gemini-test", fetchImpl }); + const result = await provider.generateCandidateScript(sampleRequest); + + assert.strictEqual(result.provider, "gemini"); + assert.strictEqual(result.model, "gemini-test"); + assert.strictEqual(result.markdownOutput, validCandidateMarkdown); + assert.strictEqual(result.structured?.kind, "code_to_candidate_script"); + assert.ok(result.rawResponse); +} + +async function testGeminiInvalidStructuredResponse(): Promise { + const fetchImpl: typeof fetch = async () => + new Response(JSON.stringify({ candidates: [{ content: { parts: [{ text: "## Intent\nGenerated." }] } }] }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + + const provider = new GeminiProvider({ apiKey: "test-key", fetchImpl }); + await assert.rejects( + () => provider.generateTarget(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && + error.provider === "gemini" && + error.userMessage === "Gemini returned an invalid OptiNLP response." && + /Recommended Target|Candidate Nodes|Ambiguities|Intent/u.test(error.technicalDetail ?? "") + ); +} + +async function testOpenAiPromptConstruction(): Promise { + const provider = new OpenAiProvider({ apiKey: "test-key", model: "test-model" }); + const prompt = provider.buildPromptForTest(sampleRequest); + + assert.match(prompt, /Mode: target/); + assert.match(prompt, /# System Prompt/); + assert.match(prompt, /# Knowledge/); + assert.match(prompt, /File: tests\/demo.cpp/); + assert.match(prompt, /target the loop i/); +} + +async function testOpenAiMissingApiKey(): Promise { + const previous = process.env.OPENAI_API_KEY; + delete process.env.OPENAI_API_KEY; + + try { + const provider = new OpenAiProvider({ apiKeyProvider: () => undefined }); + await assert.rejects( + () => provider.generateTarget(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && + error.provider === "openai" && + error.userMessage === "Set OpenAI API key before using OptiNLP." + ); + } finally { + if (previous !== undefined) { + process.env.OPENAI_API_KEY = previous; + } + } +} + +async function testOpenAiEmptyResponse(): Promise { + const fetchImpl: typeof fetch = async () => + new Response(JSON.stringify({ output: [{ content: [{ type: "output_text", text: " " }] }] }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + + const provider = new OpenAiProvider({ apiKey: "test-key", fetchImpl }); + await assert.rejects( + () => provider.generateTarget(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && error.provider === "openai" && error.userMessage === "OpenAI returned an empty OptiNLP response." + ); +} + +async function testOpenAiProviderException(): Promise { + const fetchImpl: typeof fetch = async () => { + throw new Error("network unavailable"); + }; + + const provider = new OpenAiProvider({ apiKey: "test-key", fetchImpl }); + await assert.rejects( + () => provider.generateScript(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && + error.provider === "openai" && + error.userMessage === "OpenAI request failed. Check your network connection and API key." && + error.technicalDetail === "network unavailable" + ); +} + +async function testOpenAiSuccessfulResponse(): Promise { + let requestBody: { model?: string; store?: boolean; input?: string } | undefined; + const fetchImpl: typeof fetch = async (_url, init) => { + requestBody = JSON.parse(String(init?.body)) as typeof requestBody; + return new Response(JSON.stringify({ output: [{ content: [{ type: "output_text", text: validScriptMarkdown }] }] }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + }; + + const provider = new OpenAiProvider({ apiKey: "test-key", model: "openai-test", fetchImpl }); + const result = await provider.generateScript(sampleRequest); + + assert.strictEqual(result.provider, "openai"); + assert.strictEqual(result.model, "openai-test"); + assert.strictEqual(result.markdownOutput, validScriptMarkdown); + assert.strictEqual(result.structured?.kind, "command_to_script"); + assert.strictEqual(requestBody?.model, "openai-test"); + assert.strictEqual(requestBody?.store, false); + assert.match(requestBody?.input ?? "", /# User Request/); +} + +async function testOpenAiInvalidStructuredResponse(): Promise { + const fetchImpl: typeof fetch = async () => + new Response(JSON.stringify({ output_text: "## Intent\nGenerated." }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + + const provider = new OpenAiProvider({ apiKey: "test-key", fetchImpl }); + await assert.rejects( + () => provider.generateTarget(sampleRequest), + (error: unknown) => + error instanceof OptiNlpProviderError && + error.provider === "openai" && + error.userMessage === "OpenAI returned an invalid OptiNLP response." && + /Recommended Target|Candidate Nodes|Ambiguities|Intent/u.test(error.technicalDetail ?? "") + ); +} + +async function testMarkdownSchemaParsing(): Promise { + const parsedTarget = parseOptiNlpMarkdownResult("target", validTargetMarkdown); + assert.strictEqual(parsedTarget.kind, "target"); + assert.strictEqual(parsedTarget.recommendedTarget, "[occIndex 1; cFor \"i\"]"); + + const parsedScript = parseOptiNlpMarkdownResult("command_to_script", validScriptMarkdown); + assert.strictEqual(parsedScript.kind, "command_to_script"); + assert.match(parsedScript.generatedScript, /Loop\.unroll/u); + + const parsedCandidate = parseOptiNlpMarkdownResult("code_to_candidate_script", validCandidateMarkdown); + assert.strictEqual(parsedCandidate.kind, "code_to_candidate_script"); + assert.strictEqual(parsedCandidate.candidateTransformations.length, 1); +} + +const validTargetMarkdown = [ + "## Intent", + "Target the second loop named `i`.", + "", + "## Candidate Nodes", + "- Candidate 1: first loop", + "- Candidate 2: second loop", + "", + "## Recommended Target", + "```ocaml", + "[occIndex 1; cFor \"i\"]", + "```", + "", + "## Why This Target", + "`cFor \"i\"` matches both loops, and `occIndex 1` selects the second.", + "", + "## Ambiguities", + "None.", + "", + "## Alternatives", + "```ocaml", + "[cFunBody \"f\"; occIndex 1; cFor \"i\"]", + "```", + "", + "## Validation", + "```ocaml", + "!! Show.target [occIndex 1; cFor \"i\"];", + "```" +].join("\n"); + +const validScriptMarkdown = [ + "## Intent", + "Unroll loop `i`.", + "", + "## Transformation API", + "`Loop.unroll` applies to loop targets.", + "", + "## Target", + "```ocaml", + "[cFor \"i\"]", + "```", + "", + "## Generated Script", + "```ocaml", + "open Optitrust", + "open Target", + "", + "let _ = Run.script_cpp (fun _ ->", + " !! Loop.unroll [cFor \"i\"];", + ")", + "```", + "", + "## Assumptions", + "None.", + "", + "## Validation", + "```bash", + "dune exec -- ./mock.exe", + "```" +].join("\n"); + +const validCandidateMarkdown = [ + "## Code Summary", + "One loop over `i` calls `work`.", + "", + "## Candidate Transformations", + "| Rank | Transformation | Target | Why it may apply | Risk |", + "| --- | --- | --- | --- | --- |", + "| High | Loop.unroll | `[cFor \"i\"]` | User asked for a local loop transform | Needs valid unroll factor/default |", + "", + "## Recommended First Candidate", + "Try unrolling the visible loop first.", + "", + "## Candidate Script", + "```ocaml", + "open Optitrust", + "open Target", + "", + "let _ = Run.script_cpp (fun _ ->", + " !! Loop.unroll [cFor \"i\"];", + ")", + "```", + "", + "## Validation", + "```bash", + "dune exec -- ./mock.exe", + "```", + "", + "## Missing Information", + "None." +].join("\n"); + +async function main(): Promise { + await testProviderFactory(); + await testMockProvider(); + await testGeminiPromptConstruction(); + await testGeminiMissingApiKey(); + await testGeminiEmptyResponse(); + await testGeminiProviderException(); + await testGeminiSuccessfulResponse(); + await testGeminiInvalidStructuredResponse(); + await testOpenAiPromptConstruction(); + await testOpenAiMissingApiKey(); + await testOpenAiEmptyResponse(); + await testOpenAiProviderException(); + await testOpenAiSuccessfulResponse(); + await testOpenAiInvalidStructuredResponse(); + await testMarkdownSchemaParsing(); + console.log("OptiNLP provider tests passed."); +} + +void main().catch(error => { + console.error(error); + process.exitCode = 1; +}); From 20697194f4eb2e6340a40a268c1d6e7fdae2311b Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:20:34 -0400 Subject: [PATCH 07/47] Add OptiNLP CLI and prompt loading --- tools/vscode-optitrust/src/optinlp/assets.ts | 112 +++++++++++ .../vscode-optitrust/src/optinlp/cli.test.ts | 124 ++++++++++++ tools/vscode-optitrust/src/optinlp/cli.ts | 182 ++++++++++++++++++ tools/vscode-optitrust/src/optinlp/modes.ts | 21 +- 4 files changed, 436 insertions(+), 3 deletions(-) create mode 100644 tools/vscode-optitrust/src/optinlp/assets.ts create mode 100644 tools/vscode-optitrust/src/optinlp/cli.test.ts create mode 100644 tools/vscode-optitrust/src/optinlp/cli.ts diff --git a/tools/vscode-optitrust/src/optinlp/assets.ts b/tools/vscode-optitrust/src/optinlp/assets.ts new file mode 100644 index 000000000..6eb8972d5 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/assets.ts @@ -0,0 +1,112 @@ +// Loads OptiNLP prompt-kit markdown from the repository and builds provider +// requests. Prompt/knowledge filenames come from the central mode registry. +import * as fs from "fs/promises"; +import * as path from "path"; +import { modeDefinition } from "./modes"; +import { OptiNlpMode, OptiNlpProviderRequest } from "./providerTypes"; + +export interface OptiNlpAssets { + readonly promptText: string; + readonly knowledgeText: string; +} + +export interface BuildOptiNlpRequestOptions { + readonly root?: string; + readonly mode: OptiNlpMode; + readonly filePath: string; + readonly userRequest: string; + readonly sessionSummary?: string; +} + +export async function buildOptiNlpProviderRequest(options: BuildOptiNlpRequestOptions): Promise { + const root = options.root ? path.resolve(options.root) : await findOptiTrustRoot(process.cwd()); + const absoluteFilePath = path.resolve(options.filePath); + const [sourceText, assets] = await Promise.all([fs.readFile(absoluteFilePath, "utf8"), loadOptiNlpAssets(root, options.mode)]); + + return { + mode: options.mode, + userRequest: options.userRequest, + sourceText, + filePath: path.relative(root, absoluteFilePath).split(path.sep).join("/"), + language: inferLanguage(absoluteFilePath), + promptText: assets.promptText, + knowledgeText: assets.knowledgeText, + sessionSummary: options.sessionSummary + }; +} + +export async function loadOptiNlpAssets(root: string, mode: OptiNlpMode): Promise { + const definition = modeDefinition(mode); + const optiNlpRoot = path.join(root, "tools", "optiNLP"); + const promptPath = path.join(optiNlpRoot, "prompts", definition.promptFile); + const knowledgePaths = definition.knowledgeFiles.map(file => path.join(optiNlpRoot, "knowledge", file)); + + const [promptText, ...knowledgeParts] = await Promise.all([ + fs.readFile(promptPath, "utf8"), + ...knowledgePaths.map(filePath => fs.readFile(filePath, "utf8")) + ]); + + return { + promptText, + knowledgeText: knowledgeParts + .map((text, index) => `# Knowledge: ${definition.knowledgeFiles[index]}\n\n${text.trim()}`) + .join("\n\n") + }; +} + +export async function findOptiTrustRoot(startPath: string): Promise { + let current = path.resolve(startPath); + const stat = await safeStat(current); + if (stat?.isFile()) { + current = path.dirname(current); + } + + while (true) { + if (await isOptiTrustRoot(current)) { + return current; + } + const parent = path.dirname(current); + if (parent === current) { + throw new Error(`Could not find OptiTrust root from ${startPath}.`); + } + current = parent; + } +} + +export function inferLanguage(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + switch (ext) { + case ".c": + return "c"; + case ".cc": + case ".cpp": + case ".cxx": + case ".hpp": + case ".h": + return "cpp"; + case ".ml": + return "ocaml"; + case ".opti": + return "optilambda"; + default: + return ext.length > 0 ? ext.slice(1) : "text"; + } +} + +async function isOptiTrustRoot(candidate: string): Promise { + const required = [ + path.join(candidate, "dune-project"), + path.join(candidate, "tools", "optiNLP", "prompts"), + path.join(candidate, "tools", "optiNLP", "knowledge") + ]; + const checks = await Promise.all(required.map(filePath => safeStat(filePath))); + return checks.every(Boolean); +} + +async function safeStat(filePath: string): Promise { + try { + return await fs.stat(filePath); + } catch { + return undefined; + } +} diff --git a/tools/vscode-optitrust/src/optinlp/cli.test.ts b/tools/vscode-optitrust/src/optinlp/cli.test.ts new file mode 100644 index 000000000..e835eec0c --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/cli.test.ts @@ -0,0 +1,124 @@ +import * as assert from "assert"; +import * as fs from "fs/promises"; +import * as os from "os"; +import * as path from "path"; +import { Writable } from "stream"; +import { inferLanguage, loadOptiNlpAssets } from "./assets"; +import { runOptiNlpCli } from "./cli"; +import { resolveRequestedMode } from "./modes"; + +class MemoryWritable extends Writable { + chunks: string[] = []; + + _write(chunk: string | Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void { + this.chunks.push(chunk.toString()); + callback(); + } + + text(): string { + return this.chunks.join(""); + } +} + +async function withTempSource(test: (filePath: string) => Promise): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "optinlp-cli-")); + const filePath = path.join(dir, "input.cpp"); + await fs.writeFile(filePath, "void f(int n) { for (int i = 0; i < n; i++) work(i); }\n", "utf8"); + await test(filePath); +} + +async function testLanguageInference(): Promise { + assert.strictEqual(inferLanguage("x.c"), "c"); + assert.strictEqual(inferLanguage("x.cpp"), "cpp"); + assert.strictEqual(inferLanguage("x.ml"), "ocaml"); + assert.strictEqual(inferLanguage("x.opti"), "optilambda"); +} + +async function testAssetLoading(): Promise { + const assets = await loadOptiNlpAssets(path.resolve(__dirname, "../../../.."), "target"); + assert.match(assets.promptText, /OptiTrust Target Generator/u); + assert.match(assets.knowledgeText, /OptiTrust Target Knowledge/u); +} + +async function testWholeFileScriptRouting(): Promise { + assert.strictEqual(resolveRequestedMode("command_to_script", "generate a complete transformation script for the whole file"), "code_to_candidate_script"); + assert.strictEqual(resolveRequestedMode("auto", "write matmul.ml for this source"), "code_to_candidate_script"); +} + +async function testCliTargetMarkdown(): Promise { + await withTempSource(async filePath => { + const stdout = new MemoryWritable(); + const stderr = new MemoryWritable(); + const exitCode = await runOptiNlpCli( + ["target", "--file", filePath, "--request", "target the loop i", "--provider", "mock"], + stdout, + stderr + ); + + assert.strictEqual(exitCode, 0); + assert.match(stdout.text(), /## Recommended Target/u); + assert.match(stdout.text(), /\[cFor "i"\]/u); + assert.strictEqual(stderr.text(), ""); + }); +} + +async function testCliScriptJson(): Promise { + await withTempSource(async filePath => { + const stdout = new MemoryWritable(); + const stderr = new MemoryWritable(); + const exitCode = await runOptiNlpCli( + ["script", "--file", filePath, "--request", "unroll the loop i", "--provider", "mock", "--json"], + stdout, + stderr + ); + + assert.strictEqual(exitCode, 0); + const parsed = JSON.parse(stdout.text()) as { structured: { kind: string; generatedScript: string } }; + assert.strictEqual(parsed.structured.kind, "command_to_script"); + assert.match(parsed.structured.generatedScript, /Loop\.unroll/u); + assert.strictEqual(stderr.text(), ""); + }); +} + +async function testCliCandidatesGoal(): Promise { + await withTempSource(async filePath => { + const stdout = new MemoryWritable(); + const stderr = new MemoryWritable(); + const exitCode = await runOptiNlpCli( + ["candidates", "--file", filePath, "--goal", "suggest a first transformation", "--provider", "mock", "--json"], + stdout, + stderr + ); + + assert.strictEqual(exitCode, 0); + const parsed = JSON.parse(stdout.text()) as { structured: { kind: string; candidateTransformations: unknown[] } }; + assert.strictEqual(parsed.structured.kind, "code_to_candidate_script"); + assert.strictEqual(parsed.structured.candidateTransformations.length, 1); + assert.strictEqual(stderr.text(), ""); + }); +} + +async function testCliMissingRequest(): Promise { + const stdout = new MemoryWritable(); + const stderr = new MemoryWritable(); + const exitCode = await runOptiNlpCli(["target", "--file", "missing.cpp", "--provider", "mock"], stdout, stderr); + + assert.strictEqual(exitCode, 2); + assert.match(stderr.text(), /Missing required --request option/u); +} + +async function main(): Promise { + await testLanguageInference(); + await testAssetLoading(); + await testWholeFileScriptRouting(); + await testCliTargetMarkdown(); + await testCliScriptJson(); + await testCliCandidatesGoal(); + await testCliMissingRequest(); + console.log("OptiNLP CLI tests passed."); +} + +void main().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/tools/vscode-optitrust/src/optinlp/cli.ts b/tools/vscode-optitrust/src/optinlp/cli.ts new file mode 100644 index 000000000..0b8916735 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/cli.ts @@ -0,0 +1,182 @@ +#!/usr/bin/env node +// Command-line entrypoint for testing OptiNLP outside VS Code. It reuses the +// same prompt loader, provider factory, and schemas as the extension. +import { buildOptiNlpProviderRequest } from "./assets"; +import { generateOptiNlp } from "./generation"; +import { isOptiNlpCliCommand, modeDefinition, modeFromCliCommand, OptiNlpCliCommand } from "./modes"; +import { OptiNlpProviderError } from "./providerErrors"; +import { createOptiNlpProvider, DEFAULT_OPTINLP_PROVIDER, IMPLEMENTED_OPTINLP_PROVIDER_IDS, OptiNlpProviderId, parseOptiNlpProviderId } from "./providerFactory"; +import { OptiNlpProviderResult } from "./providerTypes"; + +interface CliOptions { + readonly command: OptiNlpCliCommand; + readonly filePath: string; + readonly userRequest: string; + readonly json: boolean; + readonly provider: OptiNlpProviderId; + readonly model?: string; + readonly root?: string; + readonly sessionSummary?: string; +} + +export async function runOptiNlpCli(argv: readonly string[], stdout: NodeJS.WritableStream = process.stdout, stderr: NodeJS.WritableStream = process.stderr): Promise { + let options: CliOptions; + try { + options = parseArgs(argv); + } catch (error) { + stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${usage()}\n`); + return 2; + } + + if (options.command === "target" && options.userRequest === "__help__") { + stdout.write(`${usage()}\n`); + return 0; + } + + try { + const mode = modeFromCliCommand(options.command); + if (!mode) { + throw new Error(`Unknown OptiNLP command '${options.command}'.`); + } + const request = await buildOptiNlpProviderRequest({ + root: options.root, + mode, + filePath: options.filePath, + userRequest: options.userRequest, + sessionSummary: options.sessionSummary + }); + const provider = createOptiNlpProvider({ + provider: options.provider, + gemini: { model: options.model }, + openai: { model: options.model }, + mock: { model: options.model } + }); + const result = await generateOptiNlp(provider, request); + writeResult(stdout, result, options.json); + return 0; + } catch (error) { + if (error instanceof OptiNlpProviderError) { + stderr.write(`${error.userMessage}\n`); + if (error.technicalDetail) { + stderr.write(`Detail: ${error.technicalDetail}\n`); + } + return 1; + } + stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + return 1; + } +} + +function parseArgs(argv: readonly string[]): CliOptions { + const [commandArg, ...rest] = argv; + if (commandArg === "--help" || commandArg === "-h" || !commandArg) { + return { + command: "target", + filePath: "", + userRequest: "__help__", + json: false, + provider: DEFAULT_OPTINLP_PROVIDER + }; + } + if (!isOptiNlpCliCommand(commandArg)) { + throw new Error(`Unknown OptiNLP command '${commandArg}'.`); + } + + const values = new Map(); + const flags = new Set(); + for (let index = 0; index < rest.length; index += 1) { + const arg = rest[index]; + if (arg === "--json") { + flags.add(arg); + continue; + } + if (!arg.startsWith("--")) { + throw new Error(`Unexpected argument '${arg}'.`); + } + const value = rest[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing value for ${arg}.`); + } + values.set(arg, value); + index += 1; + } + + const filePath = values.get("--file"); + if (!filePath) { + throw new Error("Missing required --file option."); + } + const userRequest = values.get("--request") ?? values.get("--goal"); + if (!userRequest) { + throw new Error(commandArg === "candidates" ? "Missing required --goal option." : "Missing required --request option."); + } + + return { + command: commandArg, + filePath, + userRequest, + json: flags.has("--json"), + provider: parseProvider(values.get("--provider") ?? process.env.OPTINLP_PROVIDER ?? DEFAULT_OPTINLP_PROVIDER), + model: values.get("--model"), + root: values.get("--root"), + sessionSummary: values.get("--session-summary") + }; +} + +function parseProvider(value: string): OptiNlpProviderId { + const provider = parseOptiNlpProviderId(value); + if (provider) { + return provider; + } + throw new Error(`Unknown OptiNLP provider '${value}'.`); +} + +function writeResult(stdout: NodeJS.WritableStream, result: OptiNlpProviderResult, json: boolean): void { + if (json) { + stdout.write( + `${JSON.stringify( + { + provider: result.provider, + model: result.model, + markdownOutput: result.markdownOutput, + structured: result.structured + }, + null, + 2 + )}\n` + ); + return; + } + stdout.write(`${result.markdownOutput}\n`); +} + +function usage(): string { + const providers = IMPLEMENTED_OPTINLP_PROVIDER_IDS.join("|"); + const commands = ["target", "script", "candidates"] + .map(command => { + const definition = modeFromCliCommand(command); + const placeholder = definition ? modeDefinition(definition).placeholder : "..."; + const requestFlag = command === "candidates" ? "--goal" : "--request"; + return ` optinlp ${command} --file path ${requestFlag} "${placeholder}" [--json] [--provider ${providers}]`; + }) + .join("\n"); + return [ + "Usage:", + commands, + "", + "Options:", + " --model name Override provider model.", + " --root path OptiTrust root. Defaults to auto-detection from cwd.", + " --session-summary text Optional in-memory session summary text.", + "", + "Environment:", + " GEMINI_API_KEY Gemini API key for the default gemini provider.", + " OPENAI_API_KEY OpenAI API key for the openai provider.", + " OPTINLP_PROVIDER Optional default provider override, for example mock." + ].join("\n"); +} + +if (require.main === module) { + void runOptiNlpCli(process.argv.slice(2)).then(exitCode => { + process.exitCode = exitCode; + }); +} diff --git a/tools/vscode-optitrust/src/optinlp/modes.ts b/tools/vscode-optitrust/src/optinlp/modes.ts index 346ac3242..2b7d61da1 100644 --- a/tools/vscode-optitrust/src/optinlp/modes.ts +++ b/tools/vscode-optitrust/src/optinlp/modes.ts @@ -37,9 +37,9 @@ export const OPTINLP_MODE_DEFINITIONS: readonly OptiNlpModeDefinition[] = [ { id: "code_to_candidate_script", cliCommand: "candidates", - label: "Suggest Candidate Script", - shortLabel: "Candidates", - placeholder: "suggest a first transformation", + label: "Generate Full Transformation", + shortLabel: "Full Transformation", + placeholder: "generate a complete transformation script for the whole file", promptFile: "03_code_to_candidate_script.md", knowledgeFiles: ["targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] } @@ -65,6 +65,13 @@ export function isOptiNlpCliCommand(command: string): command is OptiNlpCliComma } export function resolveAutoMode(mode: OptiNlpUiMode, request: string): OptiNlpMode { + return resolveRequestedMode(mode, request); +} + +export function resolveRequestedMode(mode: OptiNlpUiMode, request: string): OptiNlpMode { + if (isFullFileScriptRequest(request)) { + return "code_to_candidate_script"; + } if (mode !== "auto") { return mode; } @@ -77,3 +84,11 @@ export function resolveAutoMode(mode: OptiNlpUiMode, request: string): OptiNlpMo } return "command_to_script"; } + +function isFullFileScriptRequest(request: string): boolean { + const text = request.toLowerCase(); + const wantsScript = /\b(generate|create|write|make|produce|build)\b[\s\S]*\b(script|transformation|transformations|optimi[sz]ation)\b/u.test(text); + const wantsFullScope = /\b(whole|entire|full|complete)\b[\s\S]*\b(file|source|code|script|transformation|transformations)\b/u.test(text); + const namesMlScript = /\b[a-z0-9_-]+\.ml\b/u.test(text); + return (wantsScript && wantsFullScope) || namesMlScript; +} From 72f2a13c567ec4778f0cc8b669eab43b35d7ff16 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:20:53 -0400 Subject: [PATCH 08/47] Add OptiNLP session memory --- .../src/optinlp/sessionMemory.test.ts | 132 +++++++++++++ .../src/optinlp/sessionMemory.ts | 179 ++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts create mode 100644 tools/vscode-optitrust/src/optinlp/sessionMemory.ts diff --git a/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts b/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts new file mode 100644 index 000000000..684148f5d --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts @@ -0,0 +1,132 @@ +import * as assert from "assert"; +import { OptiNlpProviderRequest, OptiNlpProviderResult } from "./providerTypes"; +import { OptiNlpSessionMemory } from "./sessionMemory"; + +const baseRequest: OptiNlpProviderRequest = { + mode: "target", + userRequest: "target the loop i", + sourceText: "void f() {}", + filePath: "tests/demo.cpp", + language: "cpp", + promptText: "prompt", + knowledgeText: "knowledge" +}; + +function targetResult(target: string): OptiNlpProviderResult { + return { + provider: "mock", + model: "mock-model", + markdownOutput: "markdown", + structured: { + kind: "target", + intent: "Target a loop.", + candidateNodes: ["Candidate 1"], + recommendedTarget: target, + alternatives: [], + validation: `!! Show.target ${target};` + } + }; +} + +function scriptResult(): OptiNlpProviderResult { + return { + provider: "mock", + model: "mock-model", + markdownOutput: "markdown", + structured: { + kind: "command_to_script", + intent: "Unroll a loop.", + transformationApi: "Loop.unroll", + target: "[cFor \"i\"]", + generatedScript: "open Optitrust\nopen Target\nlet _ = Run.script_cpp (fun _ ->\n !! Loop.unroll [cFor \"i\"];\n)", + assumptions: ["Loop `i` is unique."], + validation: "dune exec -- ./mock.exe" + } + }; +} + +function candidateResult(): OptiNlpProviderResult { + return { + provider: "mock", + model: "mock-model", + markdownOutput: "markdown", + structured: { + kind: "code_to_candidate_script", + codeSummary: "A loop.", + candidateTransformations: [ + { + rank: "High", + transformation: "Loop.unroll", + target: "[cFor \"i\"]", + whyItMayApply: "Local loop.", + risk: "Needs validation." + } + ], + recommendedFirstCandidate: "Try unrolling.", + candidateScript: "open Optitrust\nopen Target\nlet _ = Run.script_cpp (fun _ ->\n !! Loop.unroll [cFor \"i\"];\n)", + validation: "dune exec -- ./mock.exe", + missingInformation: "None." + } + }; +} + +function testRecordsCompactGenerationState(): void { + const memory = new OptiNlpSessionMemory(); + memory.recordGeneration(baseRequest, targetResult("[cFor \"i\"]")); + + const snapshot = memory.snapshot(); + assert.strictEqual(snapshot.turns.length, 1); + assert.strictEqual(snapshot.turns[0].target, "[cFor \"i\"]"); + assert.strictEqual(snapshot.turns[0].userRequest, "target the loop i"); + assert.strictEqual(snapshot.turns[0].provider, "mock"); + assert.ok(!JSON.stringify(snapshot).includes("void f")); + assert.ok(!JSON.stringify(snapshot).includes("knowledge")); +} + +function testSummaryIncludesLatestScriptAssumptionsAndValidation(): void { + const memory = new OptiNlpSessionMemory(); + memory.recordGeneration({ ...baseRequest, mode: "command_to_script", userRequest: "unroll the loop i" }, scriptResult()); + memory.acceptAssumptions(["Loop `i` is unique.", "Loop `i` is unique.", "No side effects."]); + memory.recordValidation({ command: "dune exec -- ./mock.exe", ok: false, detail: "exit code 1" }); + + const summary = memory.summary(); + assert.ok(summary); + assert.match(summary, /Previous request: unroll the loop i/u); + assert.match(summary, /Previous target: \[cFor "i"\]/u); + assert.match(summary, /Previous script:/u); + assert.match(summary, /Accepted assumptions: Loop `i` is unique.; No side effects./u); + assert.match(summary, /Last validation: failed/u); +} + +function testKeepsOnlyMaxTurns(): void { + const memory = new OptiNlpSessionMemory({ maxTurns: 2 }); + memory.recordGeneration({ ...baseRequest, userRequest: "first" }, targetResult("[cFor \"i\"]")); + memory.recordGeneration({ ...baseRequest, userRequest: "second" }, targetResult("[cFor \"j\"]")); + memory.recordGeneration({ ...baseRequest, userRequest: "third" }, targetResult("[cFor \"k\"]")); + + const snapshot = memory.snapshot(); + assert.deepStrictEqual( + snapshot.turns.map(turn => turn.userRequest), + ["second", "third"] + ); +} + +function testCandidateSummaryAndClear(): void { + const memory = new OptiNlpSessionMemory(); + memory.recordGeneration({ ...baseRequest, mode: "code_to_candidate_script", userRequest: "suggest candidates" }, candidateResult()); + assert.match(memory.summary() ?? "", /Previous candidate script:/u); + + memory.clear(); + assert.strictEqual(memory.summary(), undefined); + assert.strictEqual(memory.snapshot().turns.length, 0); +} + +function main(): void { + testRecordsCompactGenerationState(); + testSummaryIncludesLatestScriptAssumptionsAndValidation(); + testKeepsOnlyMaxTurns(); + testCandidateSummaryAndClear(); + console.log("OptiNLP session memory tests passed."); +} + +main(); diff --git a/tools/vscode-optitrust/src/optinlp/sessionMemory.ts b/tools/vscode-optitrust/src/optinlp/sessionMemory.ts new file mode 100644 index 000000000..28d38bfb9 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.ts @@ -0,0 +1,179 @@ +// Session-only memory for OptiNLP. It stores compact summaries of prior turns +// and validation status, never full source files or prompt/knowledge text. +import { OptiNlpProviderRequest, OptiNlpProviderResult } from "./providerTypes"; +import { OptiNlpStructuredResult } from "./resultSchemas"; + +export interface OptiNlpValidationRecord { + readonly command: string; + readonly ok: boolean; + readonly detail?: string; +} + +export interface OptiNlpSessionTurn { + readonly mode: OptiNlpProviderRequest["mode"]; + readonly userRequest: string; + readonly filePath: string; + readonly language: string; + readonly provider: string; + readonly model: string; + readonly target?: string; + readonly script?: string; + readonly candidateScript?: string; + readonly assumptions: readonly string[]; + readonly validation?: string; +} + +export interface OptiNlpSessionSnapshot { + readonly turns: readonly OptiNlpSessionTurn[]; + readonly acceptedAssumptions: readonly string[]; + readonly lastValidation?: OptiNlpValidationRecord; +} + +export interface OptiNlpSessionMemoryOptions { + readonly maxTurns?: number; +} + +export class OptiNlpSessionMemory { + private readonly maxTurns: number; + private turns: OptiNlpSessionTurn[] = []; + private acceptedAssumptions: string[] = []; + private lastValidation: OptiNlpValidationRecord | undefined; + + constructor(options: OptiNlpSessionMemoryOptions = {}) { + this.maxTurns = Math.max(1, options.maxTurns ?? 8); + } + + recordGeneration(request: OptiNlpProviderRequest, result: OptiNlpProviderResult): void { + const turn = turnFromResult(request, result); + this.turns = [...this.turns, turn].slice(-this.maxTurns); + } + + recordValidation(record: OptiNlpValidationRecord): void { + this.lastValidation = record; + } + + acceptAssumptions(assumptions: readonly string[]): void { + const seen = new Set(this.acceptedAssumptions); + for (const assumption of assumptions.map(value => value.trim()).filter(Boolean)) { + if (!seen.has(assumption)) { + seen.add(assumption); + this.acceptedAssumptions.push(assumption); + } + } + } + + snapshot(): OptiNlpSessionSnapshot { + return { + turns: [...this.turns], + acceptedAssumptions: [...this.acceptedAssumptions], + lastValidation: this.lastValidation + }; + } + + clear(): void { + this.turns = []; + this.acceptedAssumptions = []; + this.lastValidation = undefined; + } + + summary(maxChars = 2000): string | undefined { + const lines: string[] = []; + const lastTurn = this.turns.at(-1); + if (lastTurn) { + lines.push(`Previous request: ${lastTurn.userRequest}`); + lines.push(`Previous mode: ${lastTurn.mode}`); + lines.push(`Previous file: ${lastTurn.filePath}`); + if (lastTurn.target) { + lines.push(`Previous target: ${lastTurn.target}`); + } + if (lastTurn.script) { + lines.push(`Previous script: ${truncateOneLine(lastTurn.script, 500)}`); + } + if (lastTurn.candidateScript) { + lines.push(`Previous candidate script: ${truncateOneLine(lastTurn.candidateScript, 500)}`); + } + if (lastTurn.assumptions.length > 0) { + lines.push(`Previous assumptions: ${lastTurn.assumptions.join("; ")}`); + } + if (lastTurn.validation) { + lines.push(`Previous validation suggestion: ${truncateOneLine(lastTurn.validation, 300)}`); + } + } + + if (this.acceptedAssumptions.length > 0) { + lines.push(`Accepted assumptions: ${this.acceptedAssumptions.join("; ")}`); + } + + if (this.lastValidation) { + const status = this.lastValidation.ok ? "passed" : "failed"; + const detail = this.lastValidation.detail ? ` (${truncateOneLine(this.lastValidation.detail, 300)})` : ""; + lines.push(`Last validation: ${status}: ${this.lastValidation.command}${detail}`); + } + + if (lines.length === 0) { + return undefined; + } + return truncateMultiline(lines.join("\n"), maxChars); + } +} + +function turnFromResult(request: OptiNlpProviderRequest, result: OptiNlpProviderResult): OptiNlpSessionTurn { + const structured = result.structured; + return { + mode: request.mode, + userRequest: request.userRequest, + filePath: request.filePath, + language: request.language, + provider: result.provider, + model: result.model, + target: targetFrom(structured), + script: scriptFrom(structured), + candidateScript: candidateScriptFrom(structured), + assumptions: assumptionsFrom(structured), + validation: validationFrom(structured) + }; +} + +function targetFrom(result: OptiNlpStructuredResult | undefined): string | undefined { + if (!result) { + return undefined; + } + switch (result.kind) { + case "target": + return result.recommendedTarget; + case "command_to_script": + return result.target; + case "code_to_candidate_script": + return result.candidateTransformations[0]?.target; + } +} + +function scriptFrom(result: OptiNlpStructuredResult | undefined): string | undefined { + return result?.kind === "command_to_script" ? result.generatedScript : undefined; +} + +function candidateScriptFrom(result: OptiNlpStructuredResult | undefined): string | undefined { + return result?.kind === "code_to_candidate_script" ? result.candidateScript : undefined; +} + +function assumptionsFrom(result: OptiNlpStructuredResult | undefined): readonly string[] { + return result?.kind === "command_to_script" ? result.assumptions : []; +} + +function validationFrom(result: OptiNlpStructuredResult | undefined): string | undefined { + if (!result) { + return undefined; + } + return result.kind === "target" ? result.validation : result.validation; +} + +function truncateOneLine(value: string, maxChars: number): string { + return truncateMultiline(value.replace(/\s+/gu, " ").trim(), maxChars); +} + +function truncateMultiline(value: string, maxChars: number): string { + if (value.length <= maxChars) { + return value; + } + return `${value.slice(0, Math.max(0, maxChars - 3))}...`; +} From 32d797aec338e617a9971ead26dff7e4aa99ebd5 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:21:32 -0400 Subject: [PATCH 09/47] Add OptiNLP editor result actions --- .../src/optinlp/resultActions.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tools/vscode-optitrust/src/optinlp/resultActions.ts diff --git a/tools/vscode-optitrust/src/optinlp/resultActions.ts b/tools/vscode-optitrust/src/optinlp/resultActions.ts new file mode 100644 index 000000000..10b27af4b --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/resultActions.ts @@ -0,0 +1,21 @@ +// Utilities for turning structured OptiNLP results into editor actions. +// The VS Code commands and panel both use these to avoid divergent behavior. +import { OptiNlpStructuredResult } from "./resultSchemas"; + +export type OptiNlpEditorAction = + | { readonly kind: "insert_target"; readonly text: string } + | { readonly kind: "open_script"; readonly text: string }; + +export function editorActionForResult(result: OptiNlpStructuredResult | undefined): OptiNlpEditorAction | undefined { + if (!result) { + return undefined; + } + switch (result.kind) { + case "target": + return result.recommendedTarget ? { kind: "insert_target", text: result.recommendedTarget } : undefined; + case "command_to_script": + return { kind: "open_script", text: result.generatedScript }; + case "code_to_candidate_script": + return { kind: "open_script", text: result.candidateScript }; + } +} From a41c0d0b8c1555dac9697514c2346af0c8c0761d Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:21:42 -0400 Subject: [PATCH 10/47] Add OptiNLP extension dev runner --- .vscode/tasks.json | 16 ++++++++++ tools/vscode-optitrust/.vscode/launch.json | 17 ++++++++++ tools/vscode-optitrust/.vscode/tasks.json | 18 +++++++++++ .../scripts/run_extension_dev_host.sh | 31 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 tools/vscode-optitrust/.vscode/launch.json create mode 100644 tools/vscode-optitrust/.vscode/tasks.json create mode 100755 tools/vscode-optitrust/scripts/run_extension_dev_host.sh diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9401e37b6..294a27010 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,6 +4,22 @@ // The -i option is necessary for launching GUI programs from tasks "version": "2.0.0", "tasks": [ + { + "label": "OptiTrust: Run Extension Dev Host", + "type": "shell", + "command": "tools/vscode-optitrust/scripts/run_extension_dev_host.sh", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "clear": true, + "reveal": "always" + }, + "runOptions": { + "runOn": "folderOpen" + } + }, // Build using make (currently not using run_action.sh) { "label": "Build everything", diff --git a/tools/vscode-optitrust/.vscode/launch.json b/tools/vscode-optitrust/.vscode/launch.json new file mode 100644 index 000000000..3022403dd --- /dev/null +++ b/tools/vscode-optitrust/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run OptiTrust Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: compile" + } + ] +} diff --git a/tools/vscode-optitrust/.vscode/tasks.json b/tools/vscode-optitrust/.vscode/tasks.json new file mode 100644 index 000000000..317cf523d --- /dev/null +++ b/tools/vscode-optitrust/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "OptiTrust: Run Extension Dev Host", + "type": "shell", + "command": "${workspaceFolder}/scripts/run_extension_dev_host.sh", + "problemMatcher": [], + "presentation": { + "clear": true, + "reveal": "always" + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} diff --git a/tools/vscode-optitrust/scripts/run_extension_dev_host.sh b/tools/vscode-optitrust/scripts/run_extension_dev_host.sh new file mode 100755 index 000000000..4258a0e3a --- /dev/null +++ b/tools/vscode-optitrust/scripts/run_extension_dev_host.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Compile the OptiTrust VS Code extension and open it for manual testing. +# +# Preferred path: use VS Code's Extension Development Host when the installed +# CLI supports --extensionDevelopmentPath. +# +# Fallback path: some remote/reduced "code" CLIs cannot open an Extension +# Development Host. For those, build a local VSIX, install/update it, and open +# the OptiTrust workspace in a normal window. +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +extension_dir="$(cd "$script_dir/.." && pwd)" +repo_root="$(cd "$extension_dir/../.." && pwd)" +code_cmd="${CODE_CMD:-code}" +vsix_path="$extension_dir/.optitrust-dev.vsix" + +cd "$extension_dir" +npm run compile + +if "$code_cmd" --help 2>&1 | grep -q -- "--extensionDevelopmentPath"; then + echo "Opening VS Code Extension Development Host..." + "$code_cmd" --new-window --extensionDevelopmentPath="$extension_dir" "$repo_root" +else + echo "The '$code_cmd' CLI does not support --extensionDevelopmentPath." + echo "Packaging and installing the OptiTrust extension instead..." + ./node_modules/.bin/vsce package --out "$vsix_path" --no-dependencies + "$code_cmd" --install-extension "$vsix_path" --force + echo "Opening OptiTrust with the installed extension..." + "$code_cmd" --new-window "$repo_root" +fi From dec46da7d9b855c469eb73067afefd687b531857 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 24 Jun 2026 08:21:54 -0400 Subject: [PATCH 11/47] Add OptiNLP VS Code assistant UI --- tools/vscode-optitrust/package.json | 102 +++- .../src/commands/optinlpCommands.ts | 276 ++++++++++ .../src/commands/optinlpPanel.ts | 492 ++++++++++++++++++ tools/vscode-optitrust/src/extension.ts | 55 ++ .../vscode-optitrust/src/optitrust/editor.ts | 4 +- 5 files changed, 926 insertions(+), 3 deletions(-) create mode 100644 tools/vscode-optitrust/src/commands/optinlpCommands.ts create mode 100644 tools/vscode-optitrust/src/commands/optinlpPanel.ts diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index 0c427ae46..dbf3da931 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -38,9 +38,19 @@ "onCommand:optitrust.openAssociatedFiles", "onCommand:optitrust.openUnitTestMlCppFiles", "onCommand:optitrust.selectViewSyntax", - "onCommand:optitrust.healthCheck" + "onCommand:optitrust.healthCheck", + "onCommand:optitrust.optinlpChat", + "onCommand:optitrust.optinlpGenerateTarget", + "onCommand:optitrust.optinlpGenerateScript", + "onCommand:optitrust.optinlpSuggestCandidateScript", + "onCommand:optitrust.optinlpSetGeminiApiKey", + "onCommand:optitrust.optinlpSetOpenAiApiKey", + "onCommand:optitrust.optinlpClearSession" ], "main": "./out/extension.js", + "bin": { + "optinlp": "./out/optinlp/cli.js" + }, "contributes": { "commands": [ { @@ -115,6 +125,34 @@ { "command": "optitrust.healthCheck", "title": "OptiTrust: Health Check" + }, + { + "command": "optitrust.optinlpChat", + "title": "OptiTrust: OptiNLP Chat" + }, + { + "command": "optitrust.optinlpGenerateTarget", + "title": "OptiTrust: OptiNLP Generate Target" + }, + { + "command": "optitrust.optinlpGenerateScript", + "title": "OptiTrust: OptiNLP Generate Script" + }, + { + "command": "optitrust.optinlpSuggestCandidateScript", + "title": "OptiTrust: OptiNLP Generate Full Transformation" + }, + { + "command": "optitrust.optinlpSetGeminiApiKey", + "title": "OptiTrust: OptiNLP Set Gemini API Key" + }, + { + "command": "optitrust.optinlpSetOpenAiApiKey", + "title": "OptiTrust: OptiNLP Set OpenAI API Key" + }, + { + "command": "optitrust.optinlpClearSession", + "title": "OptiTrust: OptiNLP Clear Session" } ], "configuration": { @@ -165,6 +203,26 @@ "Use explicit OptiLambda operations with type parameters." ], "description": "Default OptiLambda representation for backend-generated diff and trace views." + }, + "optitrust.optinlpProvider": { + "type": "string", + "default": "gemini", + "enum": [ + "gemini", + "mock", + "openai" + ], + "enumDescriptions": [ + "Use Gemini through the configured API key.", + "Use deterministic local mock responses for UI testing.", + "Use OpenAI through the configured API key." + ], + "description": "AI provider used by OptiNLP commands and the OptiNLP panel." + }, + "optitrust.optinlpModel": { + "type": "string", + "default": "", + "description": "Optional model override for the configured OptiNLP provider. Leave empty to use the provider default." } } }, @@ -235,6 +293,23 @@ "group": "navigation@50" } ], + "editor/context": [ + { + "command": "optitrust.optinlpGenerateTarget", + "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", + "group": "optinlp@1" + }, + { + "command": "optitrust.optinlpGenerateScript", + "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", + "group": "optinlp@2" + }, + { + "command": "optitrust.optinlpSuggestCandidateScript", + "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", + "group": "optinlp@3" + } + ], "commandPalette": [ { "command": "optitrust.hello" @@ -289,6 +364,27 @@ }, { "command": "optitrust.healthCheck" + }, + { + "command": "optitrust.optinlpChat" + }, + { + "command": "optitrust.optinlpGenerateTarget" + }, + { + "command": "optitrust.optinlpGenerateScript" + }, + { + "command": "optitrust.optinlpSuggestCandidateScript" + }, + { + "command": "optitrust.optinlpSetGeminiApiKey" + }, + { + "command": "optitrust.optinlpSetOpenAiApiKey" + }, + { + "command": "optitrust.optinlpClearSession" } ] }, @@ -316,12 +412,16 @@ "scripts": { "build:webview": "esbuild ../web_view/optitrust_syntax_highlight.mjs --bundle --format=esm --target=es2020 --outfile=../web_view/optitrust_syntax_highlight.js", "compile": "npm run build:webview && tsc -p ./", + "dev:extension": "./scripts/run_extension_dev_host.sh", + "optinlp": "node ./out/optinlp/cli.js", + "test:optinlp": "npm run compile && node ./out/optinlp/provider.test.js && node ./out/optinlp/cli.test.js && node ./out/optinlp/sessionMemory.test.js", "watch": "tsc -watch -p ./", "package": "vsce package", "vscode:prepublish": "npm run compile" }, "devDependencies": { "@types/node": "^20.11.0", + "@types/unist": "^3.0.3", "@types/vscode": "^1.85.0", "@vscode/vsce": "^2.15.0", "esbuild": "^0.28.1", diff --git a/tools/vscode-optitrust/src/commands/optinlpCommands.ts b/tools/vscode-optitrust/src/commands/optinlpCommands.ts new file mode 100644 index 000000000..f7c85c36b --- /dev/null +++ b/tools/vscode-optitrust/src/commands/optinlpCommands.ts @@ -0,0 +1,276 @@ +// VS Code command handlers for OptiNLP. This file owns editor interaction +// (quick input, selection/full-file consent, insertion/opening documents), while +// core prompt/provider behavior stays in src/optinlp. +import * as vscode from "vscode"; +import { loadOptiNlpAssets, inferLanguage } from "../optinlp/assets"; +import { generateOptiNlp } from "../optinlp/generation"; +import { modeDefinition, resolveRequestedMode } from "../optinlp/modes"; +import { OptiNlpProviderError } from "../optinlp/providerErrors"; +import { createOptiNlpProvider, DEFAULT_OPTINLP_PROVIDER, OptiNlpProviderId, parseOptiNlpProviderId } from "../optinlp/providerFactory"; +import { OptiNlpMode, OptiNlpProviderRequest, OptiNlpProviderResult } from "../optinlp/providerTypes"; +import { editorActionForResult } from "../optinlp/resultActions"; +import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; +import { getActiveEditorContext } from "../optitrust/editor"; +import { appendHeader, appendLine, showOutput } from "../optitrust/output"; +import { OptitrustWorkspace } from "../optitrust/workspace"; + +const GEMINI_API_KEY_SECRET = "optinlp.geminiApiKey"; +const OPENAI_API_KEY_SECRET = "optinlp.openaiApiKey"; + +interface SourceContext { + readonly text: string; + readonly label: "selection" | "full file"; +} + +export interface OptiNlpGenerationOutcome { + readonly mode: OptiNlpMode; + readonly userRequest: string; + readonly sourceLabel: SourceContext["label"]; + readonly result: OptiNlpProviderResult; +} + +export async function setOptiNlpGeminiApiKey(context: vscode.ExtensionContext): Promise { + await setProviderApiKey(context, "Gemini", GEMINI_API_KEY_SECRET); +} + +export async function setOptiNlpOpenAiApiKey(context: vscode.ExtensionContext): Promise { + await setProviderApiKey(context, "OpenAI", OPENAI_API_KEY_SECRET); +} + +export async function setOptiNlpConfiguredProviderApiKey(context: vscode.ExtensionContext): Promise { + const config = vscode.workspace.getConfiguration("optitrust"); + const configuredProvider = config.get("optinlpProvider", DEFAULT_OPTINLP_PROVIDER); + const provider = parseOptiNlpProviderId(configuredProvider) ?? DEFAULT_OPTINLP_PROVIDER; + switch (provider) { + case "openai": + await setOptiNlpOpenAiApiKey(context); + return; + case "gemini": + await setOptiNlpGeminiApiKey(context); + return; + case "mock": + vscode.window.showInformationMessage("OptiNLP mock provider does not need an API key."); + return; + case "ollama": + vscode.window.showWarningMessage("OptiNLP Ollama provider is not implemented yet."); + return; + } +} + +async function setProviderApiKey(context: vscode.ExtensionContext, providerLabel: string, secretKey: string): Promise { + const apiKey = await vscode.window.showInputBox({ + title: `OptiNLP: Set ${providerLabel} API Key`, + prompt: `Enter the ${providerLabel} API key used by OptiNLP.`, + password: true, + ignoreFocusOut: true, + validateInput: value => (value.trim().length === 0 ? "API key cannot be empty." : undefined) + }); + + if (apiKey === undefined) { + return; + } + + await context.secrets.store(secretKey, apiKey.trim()); + vscode.window.showInformationMessage(`OptiNLP ${providerLabel} API key saved.`); +} + +export async function clearOptiNlpSession(memory: OptiNlpSessionMemory): Promise { + memory.clear(); + vscode.window.showInformationMessage("OptiNLP session cleared."); +} + +export async function generateOptiNlpTarget(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): Promise { + const outcome = await runModeFromInput(context, workspace, memory, "target"); + if (outcome) { + await applyDefaultEditorAction(outcome); + } +} + +export async function generateOptiNlpScript(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): Promise { + const outcome = await runModeFromInput(context, workspace, memory, "command_to_script"); + if (outcome) { + await applyDefaultEditorAction(outcome); + } +} + +export async function suggestOptiNlpCandidateScript(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): Promise { + const outcome = await runModeFromInput(context, workspace, memory, "code_to_candidate_script"); + if (outcome) { + await applyDefaultEditorAction(outcome); + } +} + +async function runModeFromInput( + context: vscode.ExtensionContext, + workspace: OptitrustWorkspace, + memory: OptiNlpSessionMemory, + mode: OptiNlpMode +): Promise { + const definition = modeDefinition(mode); + const request = await promptForRequest(mode, `OptiNLP: ${definition.label}`); + if (!request) { + return undefined; + } + return runOptiNlpGeneration(context, workspace, memory, mode, request); +} + +export async function runOptiNlpGeneration( + context: vscode.ExtensionContext, + workspace: OptitrustWorkspace, + memory: OptiNlpSessionMemory, + mode: OptiNlpMode, + userRequest: string, + options: { readonly renderToOutput?: boolean; readonly editor?: vscode.TextEditor } = { renderToOutput: true } +): Promise { + const resolvedMode = resolveRequestedMode(mode, userRequest); + const editorContext = getActiveEditorContext(workspace.root, options.editor); + const sourceContext = await getSourceContext(editorContext.editor); + if (!sourceContext) { + return undefined; + } + + const assets = await loadOptiNlpAssets(workspace.root, resolvedMode); + const request: OptiNlpProviderRequest = { + mode: resolvedMode, + userRequest, + sourceText: sourceContext.text, + filePath: editorContext.relativePath, + language: inferLanguage(editorContext.filePath), + promptText: assets.promptText, + knowledgeText: assets.knowledgeText, + sessionSummary: memory.summary() + }; + + const provider = createConfiguredProvider(context); + + let result: OptiNlpProviderResult; + const definition = modeDefinition(resolvedMode); + try { + result = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `OptiNLP: ${definition.label}`, + cancellable: false + }, + () => generateOptiNlp(provider, request) + ); + } catch (error) { + if (error instanceof OptiNlpProviderError) { + appendHeader("OptiNLP Error"); + appendLine(error.userMessage); + if (error.technicalDetail) { + appendLine(`Detail: ${error.technicalDetail}`); + } + showOutput(); + vscode.window.showErrorMessage(`OptiNLP: ${error.userMessage}`); + return undefined; + } + throw error; + } + + memory.recordGeneration(request, result); + if (options.renderToOutput ?? true) { + renderResult(resolvedMode, userRequest, sourceContext.label, result); + } + vscode.window.showInformationMessage(`OptiNLP ${definition.label.toLowerCase()} complete.`); + return { + mode: resolvedMode, + userRequest, + sourceLabel: sourceContext.label, + result + }; +} + +async function getSourceContext(editor: vscode.TextEditor): Promise { + const selectedText = editor.document.getText(editor.selection); + if (selectedText.trim().length > 0) { + return { text: selectedText, label: "selection" }; + } + + const sendFullFile = await vscode.window.showWarningMessage( + "OptiNLP will send the full active file to the configured AI provider because no text is selected.", + { modal: true }, + "Send Full File" + ); + if (sendFullFile !== "Send Full File") { + return undefined; + } + return { text: editor.document.getText(), label: "full file" }; +} + +async function promptForRequest(mode: OptiNlpMode, title: string): Promise { + const definition = modeDefinition(mode); + const value = await vscode.window.showInputBox({ + title, + prompt: "Describe the target, transformation, or optimization goal.", + placeHolder: definition.placeholder, + ignoreFocusOut: true, + validateInput: input => (input.trim().length === 0 ? "Request cannot be empty." : undefined) + }); + const trimmed = value?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : undefined; +} + +function renderResult(mode: OptiNlpMode, userRequest: string, sourceLabel: SourceContext["label"], result: OptiNlpProviderResult): void { + appendHeader(`OptiNLP: ${modeDefinition(mode).label}`); + appendLine(`provider: ${result.provider}`); + appendLine(`model: ${result.model}`); + appendLine(`context: ${sourceLabel}`); + appendLine(`request: ${userRequest}`); + appendLine(""); + appendLine(result.markdownOutput); + showOutput(); +} + +export async function applyDefaultEditorAction(outcome: OptiNlpGenerationOutcome): Promise { + const action = editorActionForResult(outcome.result.structured); + if (!action) { + return; + } + switch (action.kind) { + case "insert_target": + await insertTextAtCursor(action.text); + return; + case "open_script": + await openOcamlDocument(action.text); + return; + } +} + +function createConfiguredProvider(context: vscode.ExtensionContext): ReturnType { + const config = vscode.workspace.getConfiguration("optitrust"); + const configuredProvider = config.get("optinlpProvider", DEFAULT_OPTINLP_PROVIDER); + const provider = parseOptiNlpProviderId(configuredProvider) ?? DEFAULT_OPTINLP_PROVIDER; + const model = config.get("optinlpModel", "").trim() || undefined; + return createOptiNlpProvider({ + provider, + gemini: { + model, + apiKeyProvider: async () => context.secrets.get(GEMINI_API_KEY_SECRET) + }, + openai: { + model, + apiKeyProvider: async () => context.secrets.get(OPENAI_API_KEY_SECRET) + }, + mock: { model } + }); +} + +export async function insertTextAtCursor(text: string, sourceEditor?: vscode.TextEditor): Promise { + const editor = sourceEditor ?? vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showWarningMessage("OptiNLP: No active editor for insertion."); + return; + } + await editor.edit(edit => { + edit.insert(editor.selection.active, text); + }); +} + +export async function openOcamlDocument(text: string): Promise { + const document = await vscode.workspace.openTextDocument({ + content: text.endsWith("\n") ? text : `${text}\n`, + language: "ocaml" + }); + await vscode.window.showTextDocument(document, { preview: false, viewColumn: vscode.ViewColumn.Beside }); +} diff --git a/tools/vscode-optitrust/src/commands/optinlpPanel.ts b/tools/vscode-optitrust/src/commands/optinlpPanel.ts new file mode 100644 index 000000000..058f2ce0d --- /dev/null +++ b/tools/vscode-optitrust/src/commands/optinlpPanel.ts @@ -0,0 +1,492 @@ +// Webview side panel for OptiNLP. The panel is intentionally presentation-only: +// generation, provider selection, and editor actions live in shared modules. +import * as path from "path"; +import * as vscode from "vscode"; +import { clearOptiNlpSession, insertTextAtCursor, openOcamlDocument, runOptiNlpGeneration, setOptiNlpConfiguredProviderApiKey } from "./optinlpCommands"; +import { inferLanguage } from "../optinlp/assets"; +import { modeDefinition, OPTINLP_MODE_DEFINITIONS, OptiNlpUiMode, resolveAutoMode } from "../optinlp/modes"; +import { OptiNlpMode } from "../optinlp/providerTypes"; +import { editorActionForResult } from "../optinlp/resultActions"; +import { OptiNlpStructuredResult } from "../optinlp/resultSchemas"; +import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; +import { OptitrustWorkspace, relativeToRoot } from "../optitrust/workspace"; + +interface WebviewMessage { + readonly type: string; + readonly mode?: OptiNlpUiMode; + readonly request?: string; + readonly text?: string; + readonly resultId?: string; +} + +export class OptiNlpPanel { + private static current: OptiNlpPanel | undefined; + private readonly panel: vscode.WebviewPanel; + private resultSeq = 0; + private readonly results = new Map(); + private sourceEditor: vscode.TextEditor | undefined; + private disposables: vscode.Disposable[] = []; + + private constructor( + private readonly context: vscode.ExtensionContext, + private readonly workspace: OptitrustWorkspace, + private readonly memory: OptiNlpSessionMemory, + panel: vscode.WebviewPanel + ) { + this.panel = panel; + this.sourceEditor = asFileTextEditor(vscode.window.activeTextEditor); + this.panel.webview.html = renderPanelHtml(); + this.panel.onDidDispose(() => this.dispose(), undefined, this.disposables); + this.panel.webview.onDidReceiveMessage(message => this.handleMessage(message as WebviewMessage), undefined, this.disposables); + vscode.window.onDidChangeActiveTextEditor(editor => { + const fileEditor = asFileTextEditor(editor); + if (fileEditor) { + this.sourceEditor = fileEditor; + } + void this.refreshContext(); + }, undefined, this.disposables); + } + + static show(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): void { + if (OptiNlpPanel.current) { + OptiNlpPanel.current.panel.reveal(vscode.ViewColumn.Beside); + void OptiNlpPanel.current.refreshContext(); + return; + } + + const panel = vscode.window.createWebviewPanel("optitrustOptiNlp", "OptiNLP", vscode.ViewColumn.Beside, { + enableScripts: true, + retainContextWhenHidden: true + }); + OptiNlpPanel.current = new OptiNlpPanel(context, workspace, memory, panel); + void OptiNlpPanel.current.refreshContext(); + } + + private async handleMessage(message: WebviewMessage): Promise { + switch (message.type) { + case "ready": + await this.refreshContext(); + return; + case "generate": + await this.generate(message.mode ?? "auto", message.request ?? ""); + return; + case "setApiKey": + await setOptiNlpConfiguredProviderApiKey(this.context); + return; + case "clearSession": + await clearOptiNlpSession(this.memory); + this.post({ type: "sessionCleared" }); + return; + case "copy": + if (message.text) { + await vscode.env.clipboard.writeText(message.text); + this.post({ type: "copied" }); + } + return; + case "insertTarget": + await this.insertTarget(message.resultId); + return; + case "openScript": + await this.openScript(message.resultId); + return; + } + } + + private async generate(mode: OptiNlpUiMode, request: string): Promise { + const trimmed = request.trim(); + if (trimmed.length === 0) { + this.post({ type: "error", message: "Request cannot be empty." }); + return; + } + + const resolvedMode = resolveAutoMode(mode, trimmed); + this.post({ type: "busy", busy: true }); + try { + const outcome = await runOptiNlpGeneration(this.context, this.workspace, this.memory, resolvedMode, trimmed, { + renderToOutput: false, + editor: this.getSourceEditor() + }); + if (!outcome) { + this.post({ type: "busy", busy: false }); + return; + } + const resultId = this.storeStructuredResult(outcome.result.structured); + this.post({ + type: "result", + resultId, + mode: outcome.mode, + request: outcome.userRequest, + sourceLabel: outcome.sourceLabel, + provider: outcome.result.provider, + model: outcome.result.model, + markdown: outcome.result.markdownOutput, + structured: outcome.result.structured + }); + await this.refreshContext(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.post({ type: "error", message }); + } finally { + this.post({ type: "busy", busy: false }); + } + } + + private async insertTarget(resultId: string | undefined): Promise { + const result = resultId ? this.results.get(resultId) : undefined; + if (!result || result.kind !== "target" || !result.recommendedTarget) { + this.post({ type: "error", message: "No target expression is available for insertion." }); + return; + } + await insertTextAtCursor(result.recommendedTarget, this.getSourceEditor()); + this.post({ type: "inserted" }); + } + + private async openScript(resultId: string | undefined): Promise { + const result = resultId ? this.results.get(resultId) : undefined; + const action = editorActionForResult(result); + if (!action) { + this.post({ type: "error", message: "No generated script is available." }); + return; + } + if (action.kind === "open_script") { + await openOcamlDocument(action.text); + return; + } + this.post({ type: "error", message: "This result does not contain a generated script." }); + } + + private storeStructuredResult(result: OptiNlpStructuredResult | undefined): string | undefined { + if (!result) { + return undefined; + } + const id = String(++this.resultSeq); + this.results.set(id, result); + return id; + } + + private async refreshContext(): Promise { + const editor = this.getSourceEditor(); + if (!editor) { + this.post({ type: "context", label: "No active file" }); + return; + } + const filePath = editor.document.uri.fsPath; + const selectedText = editor.document.getText(editor.selection); + const relativePath = relativeToRoot(this.workspace.root, filePath); + const selectionLabel = selectedText.trim().length > 0 ? "selection" : "full file"; + this.post({ + type: "context", + label: `${relativePath} · ${selectionLabel} · ${inferLanguage(filePath)}`, + fileName: path.basename(filePath), + hasSelection: selectedText.trim().length > 0 + }); + } + + private getSourceEditor(): vscode.TextEditor | undefined { + const activeEditor = asFileTextEditor(vscode.window.activeTextEditor); + if (activeEditor) { + this.sourceEditor = activeEditor; + return activeEditor; + } + return this.sourceEditor; + } + + private post(message: unknown): void { + void this.panel.webview.postMessage(message); + } + + private dispose(): void { + OptiNlpPanel.current = undefined; + for (const disposable of this.disposables) { + disposable.dispose(); + } + this.disposables = []; + } +} + +function asFileTextEditor(editor: vscode.TextEditor | undefined): vscode.TextEditor | undefined { + return editor?.document.uri.scheme === "file" ? editor : undefined; +} + +function renderPanelHtml(): string { + const nonce = createNonce(); + const modeOptions = [ + '', + ...OPTINLP_MODE_DEFINITIONS.map(definition => ``) + ].join(""); + const defaultPlaceholder = modeDefinition("target").placeholder; + return ` + + + + + + OptiNLP + + + +
+
+
OptiNLP
+ + +
+
+
+ + + +
+
+
+ + +`; +} + +function createNonce(): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let nonce = ""; + for (let index = 0; index < 32; index += 1) { + nonce += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return nonce; +} + +function escapeHtml(value: string): string { + return value.replace(/&/gu, "&").replace(//gu, ">").replace(/"/gu, """); +} diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index bf88245ce..4431d12de 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -7,6 +7,15 @@ import { openUnitTestMlCppFiles } from "./commands/associatedFiles"; import { runHealthCheck } from "./commands/healthCheck"; +import { + clearOptiNlpSession, + generateOptiNlpScript, + generateOptiNlpTarget, + setOptiNlpOpenAiApiKey, + setOptiNlpGeminiApiKey, + suggestOptiNlpCandidateScript +} from "./commands/optinlpCommands"; +import { OptiNlpPanel } from "./commands/optinlpPanel"; import { rerunLastTests, runCurrentTest, runCurrentTestAndOpenDiff } from "./commands/runTests"; import { redoLastViewCommand, @@ -19,9 +28,11 @@ import { disposeDecorations, updateDecorations } from "./optitrust/decorations"; import { appendLine, disposeOutput } from "./optitrust/output"; import { getSelectedViewMode, updateSelectedViewMode, VIEW_MODES } from "./optitrust/viewMode"; import { findOptitrustRoot, OptitrustWorkspace } from "./optitrust/workspace"; +import { OptiNlpSessionMemory } from "./optinlp/sessionMemory"; let currentWorkspace: OptitrustWorkspace | undefined; let warnedUnsupportedWorkspace = false; +let optiNlpSession: OptiNlpSessionMemory | undefined; async function refreshWorkspace(startPath?: string): Promise { const detection = await findOptitrustRoot(startPath); @@ -65,6 +76,7 @@ function registerCommand(context: vscode.ExtensionContext, command: string, call export async function activate(context: vscode.ExtensionContext): Promise { await refreshWorkspace(vscode.window.activeTextEditor?.document.uri.fsPath); + optiNlpSession = new OptiNlpSessionMemory(); registerCommand(context, "optitrust.hello", async () => { const workspace = await requireWorkspace(); @@ -202,6 +214,48 @@ export async function activate(context: vscode.ExtensionContext): Promise } }); + registerCommand(context, "optitrust.optinlpChat", async () => { + const workspace = await requireWorkspace(); + if (workspace && optiNlpSession) { + OptiNlpPanel.show(context, workspace, optiNlpSession); + } + }); + + registerCommand(context, "optitrust.optinlpGenerateTarget", async () => { + const workspace = await requireWorkspace(); + if (workspace && optiNlpSession) { + await generateOptiNlpTarget(context, workspace, optiNlpSession); + } + }); + + registerCommand(context, "optitrust.optinlpGenerateScript", async () => { + const workspace = await requireWorkspace(); + if (workspace && optiNlpSession) { + await generateOptiNlpScript(context, workspace, optiNlpSession); + } + }); + + registerCommand(context, "optitrust.optinlpSuggestCandidateScript", async () => { + const workspace = await requireWorkspace(); + if (workspace && optiNlpSession) { + await suggestOptiNlpCandidateScript(context, workspace, optiNlpSession); + } + }); + + registerCommand(context, "optitrust.optinlpSetGeminiApiKey", async () => { + await setOptiNlpGeminiApiKey(context); + }); + + registerCommand(context, "optitrust.optinlpSetOpenAiApiKey", async () => { + await setOptiNlpOpenAiApiKey(context); + }); + + registerCommand(context, "optitrust.optinlpClearSession", async () => { + if (optiNlpSession) { + await clearOptiNlpSession(optiNlpSession); + } + }); + context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor(editor => updateDecorations(editor)), vscode.workspace.onDidChangeTextDocument(event => { @@ -228,4 +282,5 @@ export async function activate(context: vscode.ExtensionContext): Promise export function deactivate(): void { disposeDecorations(); disposeOutput(); + optiNlpSession = undefined; } diff --git a/tools/vscode-optitrust/src/optitrust/editor.ts b/tools/vscode-optitrust/src/optitrust/editor.ts index af38ac55f..294d76f35 100644 --- a/tools/vscode-optitrust/src/optitrust/editor.ts +++ b/tools/vscode-optitrust/src/optitrust/editor.ts @@ -12,8 +12,8 @@ export interface ActiveEditorContext { readonly line: number; } -export function getActiveEditorContext(root: string): ActiveEditorContext { - const editor = vscode.window.activeTextEditor; +export function getActiveEditorContext(root: string, sourceEditor?: vscode.TextEditor): ActiveEditorContext { + const editor = sourceEditor ?? vscode.window.activeTextEditor; if (!editor) { throw new Error("No active editor."); } From e592fe4aec1c69601e8a5dd34545b9c7b39e9569 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Fri, 26 Jun 2026 05:04:57 -0400 Subject: [PATCH 12/47] Refine OptiNLP full-script prompt knowledge --- tools/optiNLP/README.md | 4 +- tools/optiNLP/eval/code_to_script_cases.md | 8 +- tools/optiNLP/knowledge/optilambda.md | 18 +- tools/optiNLP/knowledge/script_patterns.md | 148 +++++++++++- tools/optiNLP/knowledge/targets.md | 95 +++++++- tools/optiNLP/knowledge/transformations.md | 224 ++++++++++++++++-- tools/optiNLP/prompts/01_target_generator.md | 4 + tools/optiNLP/prompts/02_command_to_script.md | 9 +- ...te_script.md => 03_code_to_full_script.md} | 12 +- 9 files changed, 472 insertions(+), 50 deletions(-) rename tools/optiNLP/prompts/{03_code_to_candidate_script.md => 03_code_to_full_script.md} (92%) diff --git a/tools/optiNLP/README.md b/tools/optiNLP/README.md index 0424ad801..57e7d40b3 100644 --- a/tools/optiNLP/README.md +++ b/tools/optiNLP/README.md @@ -29,7 +29,7 @@ Implemented in this first pass: - three engineered prompts: - `prompts/01_target_generator.md`; - `prompts/02_command_to_script.md`; - - `prompts/03_code_to_candidate_script.md`; + - `prompts/03_code_to_full_script.md`; - separate manual evaluation cases for each prompt. Not implemented yet: @@ -63,7 +63,7 @@ works on concrete cases. ```text prompts/01_target_generator.md target request -> target syntax prompts/02_command_to_script.md user command -> transformation script -prompts/03_code_to_candidate_script.md input code -> candidate scripts +prompts/03_code_to_full_script.md input code -> full transformation script eval/target_cases.md eval/command_to_script_cases.md diff --git a/tools/optiNLP/eval/code_to_script_cases.md b/tools/optiNLP/eval/code_to_script_cases.md index 2ef3a91ef..f0fdb1989 100644 --- a/tools/optiNLP/eval/code_to_script_cases.md +++ b/tools/optiNLP/eval/code_to_script_cases.md @@ -1,6 +1,6 @@ # Code To Full Script Evaluation Cases -Use these cases to manually test `prompts/03_code_to_candidate_script.md`. +Use these cases to manually test `prompts/03_code_to_full_script.md`. ## Case 1: Simple Loop @@ -19,7 +19,7 @@ Acceptable full-script behavior: - Emit a complete OCaml script with `open Optitrust`, target-related opens, and `Run.script_cpp`. - Include at least one conservative candidate transformation in the table. -- The `Candidate Script` section must contain a complete `.ml` script, not just +- The `Full Transformation Script` section must contain a complete `.ml` script, not just `[cFor "i"]`. Required behavior: @@ -54,7 +54,7 @@ Required behavior: - Explain that inlining may expose further simplifications. - Require validation with diff/trace or tests. -- Emit the complete script in the `Candidate Script` section. +- Emit the complete script in the `Full Transformation Script` section. ## Case 3: Adjacent Loops @@ -76,7 +76,7 @@ Required behavior: - Mark as medium confidence. - Mention dependency and resource checks. - Do not claim semantic safety without validation. -- Emit the complete script in the `Candidate Script` section. +- Emit the complete script in the `Full Transformation Script` section. ## Case 4: Printed OptiLambda diff --git a/tools/optiNLP/knowledge/optilambda.md b/tools/optiNLP/knowledge/optilambda.md index 26a2e9cdc..1ea1f63f7 100644 --- a/tools/optiNLP/knowledge/optilambda.md +++ b/tools/optiNLP/knowledge/optilambda.md @@ -4,6 +4,9 @@ OptiLambda is the textual language used to display OptiTrust internal AST terms without going through the C/C++ printer. It is useful for traces, diffs, target reasoning, and future internal-language workflows. +The AI can only use OptiLambda examples, syntax notes, traces, and source text +included in the current request. Do not rely on unstated files or examples. + Current status: - OptiLambda is implemented as a printer over `Ast.trm`. @@ -18,13 +21,14 @@ Prompt implications: - The AI may inspect printed `.opti` code to understand functions, loops, assignments, calls, marks, and contracts. - The AI must not claim that `.opti` can currently be used as runnable input. -- The AI must not generate `Run.script_opti` unless the repository later adds - that API. +- The AI must not generate `Run.script_opti`. - For now, generated runnable scripts should use `Run.script_cpp`. -Important source: +Visible OptiLambda cues: -- `lib/optilambda/optilambda_syntax.md` -- `lib/optilambda/optilambda_style.ml` -- `lib/optilambda/optilambda_printer.ml` -- `tests_infra/optilambda/` +- `fun name(args): type { ... }` describes a function. +- `for i in 0..n { ... }` describes a sequential loop over `i`. +- Assignments, reads, writes, marks, and contract-like annotations can be used + for target reasoning. +- Printed `.opti` text is inspection evidence only; runnable transformation + scripts still target the C/C++ workflow. diff --git a/tools/optiNLP/knowledge/script_patterns.md b/tools/optiNLP/knowledge/script_patterns.md index 11a70f9e6..0da2f5ffe 100644 --- a/tools/optiNLP/knowledge/script_patterns.md +++ b/tools/optiNLP/knowledge/script_patterns.md @@ -1,6 +1,10 @@ # OptiTrust Script Patterns -Most generated scripts should follow the existing test and case-study style: +The AI can only use examples and API details included in the current request. +Do not rely on unstated files or examples. + +Most generated scripts should follow the test and case-study style summarized +here: ```ocaml open Optitrust @@ -15,6 +19,54 @@ Some examples use `open Prelude` instead of or in addition to `open Target` when helper constructors such as `lit`, `int`, `expr`, `stmt`, or `ty` are needed. The prompt should include the opens required by the generated code. +## Minimal Script Skeletons + +Target-only validation script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Show.target [cFor "i"]; +) +``` + +Single transformation script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Loop.unroll [cFor "i"]; +) +``` + +Script requiring parsed expressions or statements: + +```ocaml +open Optitrust +open Prelude + +let _ = Run.script_cpp (fun _ -> + !! Sequence_basic.insert ~reparse:true (stmt "a++;") [tBefore; cVarDef "x"]; +) +``` + +Multi-step script: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Function.inline [cCall "helper"]; + !! Loop.unroll [cFor "i"]; + !! Cleanup.std (); +) +``` + ## Common Shapes Inline a call: @@ -61,14 +113,98 @@ let _ = Run.script_cpp (fun _ -> ) ``` +Delete an instruction: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Sequence_basic.delete [cVarDef "tmp"]; +) +``` + +Inline a variable definition: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Variable.inline [cVarDef "x"]; +) +``` + +Insert a variable: + +```ocaml +open Optitrust +open Prelude + +let _ = Run.script_cpp (fun _ -> + !! Variable.insert ~reparse:true ~typ:(ty "int") ~name:"b" ~value:(lit "2") [tAfter; cVarDef "a"]; +) +``` + +Parallelize a loop with OpenMP: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Omp.parallel_for [tBefore; cFor "i"]; +) +``` + ## Generated Script Rules - Use `!!` for transformations in `Run.script_cpp`. +- Use `!!!` only when an included example or explicit user request requires the + stronger execution marker; otherwise use `!!`. - Keep the first generated script minimal. -- Include only transformations that exist in `lib/transfo/`. -- Prefer examples from `tests/**/**_doc.ml` when choosing module names and - argument order. +- Include only transformations described in the prompt, knowledge, or current + request examples. +- Use the module names and argument order shown in this knowledge. - Do not generate `Run.script_opti`; OptiLambda parsing is not implemented. - If the user gives `.opti` text, use it for inspection and target reasoning, - but generate a C/C++ script workflow unless the repository later adds a real - OptiLambda parser path. + but generate a C/C++ script workflow. + +## Script Checklist + +- Start with `open Optitrust`. +- Add `open Target` when using target constructors directly. +- Add `open Prelude` when using helpers such as `lit`, `stmt`, `expr`, or + parser-style term builders. +- Wrap transformations in `let _ = Run.script_cpp (fun _ -> ... )`. +- Prefix each transformation with `!!`. +- End transformation calls with semicolons inside the script block. +- Include `Cleanup.std ()` only when cleanup is part of the requested or + recommended transformation plan. +- Use `stmt "..."` for C/C++ statements and declarations. +- Use `expr "..."` or `lit "..."` for expression arguments when shown by the + transformation shape. +- Use `ty "..."` for type arguments. +- Use target brackets exactly once around a target: `Transformation.x [cFor + "i"]`, not `Transformation.x [[cFor "i"]]`. +- If a script contains helper definitions, place them before `let _ = + Run.script_cpp`. + +## Validation Guidance + +Output validation should be concrete but not pretend it has been run. Good +validation text: + +```bash +dune exec -- ./path/to/generated_script.ml +``` + +When no path is known, say: + +```bash +dune exec -- ./.ml +``` + +Expected evidence should mention target-resolution success, absence of OptiTrust +exceptions, and visible transformed code properties such as an inlined call or a +removed loop. diff --git a/tools/optiNLP/knowledge/targets.md b/tools/optiNLP/knowledge/targets.md index 0f8f33a12..f055f247e 100644 --- a/tools/optiNLP/knowledge/targets.md +++ b/tools/optiNLP/knowledge/targets.md @@ -14,6 +14,12 @@ constructors from `Target`, for example: The target generator prompt should produce this current script syntax, not older or paper-only notation. +## Context Boundary + +The AI can only use the source code, script fragments, traces, errors, prompt +text, and knowledge text included in the current request. Do not rely on +unstated files or examples. + ## Core Model - A target is a `constr list`. @@ -52,6 +58,17 @@ match is expected. If the user says "second", "third", or "last", use an occurrence selector. If the prompt cannot determine which occurrence is meant, return alternatives and ask for clarification. +Occurrence constraints are usually placed before the selector they disambiguate: + +```ocaml +[occIndex 1; cFor "i"] (* second loop named i *) +[cTopFunDef "f"; occLast; cCall "foo"] +``` + +Use `nbMulti` only when the transformation is intended to apply to several +matches. Do not use `nbMulti` to silence ambiguity when the user asked for one +specific node. + ## Relative And Sequence Positions Use relative constraints for insertion, movement, spans, and transformations @@ -64,6 +81,8 @@ tFirst tLast tBetweenAll tSpan [START_TARGET] [STOP_TARGET] +tSpanSeq [SEQ_TARGET] +tSpanAround [INSTR_TARGET] ``` Examples: @@ -73,6 +92,8 @@ Examples: [tAfter; cCall "init"] [cFunBody "main"; tFirst] [cForBody "i"; tBetweenAll] +[tSpanSeq [cForBody "i"]] +[tSpanAround [cCall "foo"]] ``` Do not use `tBefore` or `tAfter` unless the operation needs a position, such as @@ -85,26 +106,48 @@ Common selectors: ```ocaml cFor "i" cFor_c "i" +cForBody "i" +cFors ["i"; "j"] cWhile () +cDoWhile () cIf () +cThen cFunDef "foo" +cFunDefs ["f"; "g"] cTopFunDef "foo" +cTopFunDefs ["f"; "g"] cFunBody "foo" cTopFunBody "foo" +cFunDefAndDecl "foo" +cTopFunDefAndDecl "foo" cCall "foo" +cCalls ["foo"; "bar"] cVarDef "x" +cVarDefs ["x"; "y"] cVarsDef "x" +cVarInit "x" cVar "x" +cVarReg "x.*" cReadVar "x" cWriteVar "x" +cWrite () +cRead () +cReadOrWrite () cArrayRead "a" cArrayWrite "a" +cCellRead ~base:[cVar "a"] () +cCellWrite ~base:[cVar "a"] () cFieldRead ~field:"x" () cFieldWrite ~field:"x" () cSeq () cReturn () +cBreak +cContinue cLabel "name" +cGoto ~label:"name" () cMark "mark" +cMarkAny +cOmp () ``` Nested constraints narrow the match by context: @@ -114,10 +157,51 @@ Nested constraints narrow the match by context: [cFunBody "main"; cFor "i"] [cFor "i"; cArrayWrite "A"] [cIf ~cond:[sExpr "x < n"] (); dThen] +[cFor "i" ~body:[cArrayWrite "out"]] +[cCall "foo" ~args:[[cVar "x"]]] ``` -Use empty names intentionally only when the repository examples do so and the -target is clearly broad, for example `[cFunDef ""]` or `[cFor ""]`. +Use empty names only when the user clearly wants a broad match, for example +`[cFunDef ""]` for any function definition or `[cFor ""]` for any loop. + +## Target Selection Patterns + +- Function body: use `[cFunBody "f"]` or `[cTopFunBody "f"]`. +- Whole function definition: use `[cFunDef "f"]` or `[cTopFunDef "f"]`. +- Function declaration plus definition: use `[cFunDefAndDecl "f"]` or + `[cTopFunDefAndDecl "f"]` when the operation must affect both. +- Named loop: use `[cFor "i"]` when the visible code has only one loop named + `i`; otherwise add context such as `[cTopFunDef "f"; cFor "i"]` or an + occurrence selector. +- Loop body: use `[cForBody "i"]` when the transformation targets the contents + of the loop rather than the loop instruction itself. +- Call inside a function: use `[cTopFunDef "main"; cCall "foo"]`. +- Statement position before/after a declaration or call: use relative position + first, then the node selector, such as `[tBefore; cVarDef "x"]`. +- Array write in a loop: use `[cFor "i"; cArrayWrite "A"]`. +- Last or second occurrence: use `occLast` or `occIndex 1` before the selector. +- Loop matching by body: if several loops share the same index, narrow with + body constraints such as `[cFor "y" ~body:[cArrayWrite "out"]]`. +- Call matching by argument: use `cCall "foo" ~args:[[cVar "x"]]` when the same + function is called with different arguments. +- Read versus write: use `cReadVar "x"` for reads, `cWriteVar "x"` for writes, + and `cVar "x"` only when either use is acceptable. +- Exact instruction fallback: use `sInstr "..."` only when semantic selectors + are not enough or the user explicitly references source text. + +## Ambiguity And Safety Rules + +- If the visible code contains two or more identical matches and the user did + not specify which one, ask for clarification and show the likely alternatives. +- If the user says "inside f", include the function context. +- If the user says "the loop that writes to A", prefer a loop selector with a + body constraint, for example `[cFor "i" ~body:[cArrayWrite "A"]]`. +- If the user asks for a position, use `tBefore`, `tAfter`, `tFirst`, `tLast`, + `tBetweenAll`, or a span target. Do not return only the node target. +- If a target may match multiple nodes, say so explicitly and use `nbMulti` only + when applying to all matches is intended. +- Do not use line numbers in final targets when stable structural selectors are + visible in the source. ## String Selectors @@ -144,10 +228,3 @@ The target generator should: - ask for clarification when two plausible targets remain; - mention why a target may match multiple nodes; - avoid inventing selectors not present in `Target`. - -Sources to refresh when syntax changes: - -- `lib/framework/target/target.ml` -- `lib/framework/target/constr.ml` -- `doc/target.md` -- `tests/**/**_doc.ml` diff --git a/tools/optiNLP/knowledge/transformations.md b/tools/optiNLP/knowledge/transformations.md index 2d713d02b..34c3be458 100644 --- a/tools/optiNLP/knowledge/transformations.md +++ b/tools/optiNLP/knowledge/transformations.md @@ -1,8 +1,9 @@ # OptiTrust Transformation Knowledge This file is a compact orientation map for prompt generation. It is not a full -API reference. The prompt should prefer exact signatures from `lib/transfo/` and -usage examples from `tests/**/**_doc.ml` before emitting code. +API reference. The AI can only use API details, examples, source code, traces, +and errors included in the current request. Do not rely on unstated files or +examples. ## Common Modules @@ -26,22 +27,211 @@ usage examples from `tests/**/**_doc.ml` before emitting code. For command-to-script and code-to-script prompts: - map user words to a known module/function only when the mapping is clear; -- use examples from `tests/` to choose exact function names; +- use the examples in this knowledge to choose function names and argument + order; - include required non-target arguments such as tile sizes, names, clauses, or destination targets; - ask for missing parameters when no safe default exists; - do not invent transformations from compiler terminology alone; -- state when an output is a candidate script rather than a proven optimization. - -## Useful Example Families - -- `tests/function/inline_simple/*_doc.ml` -- `tests/loop/unroll/*_doc.ml` -- `tests/loop/tile/*_doc.ml` -- `tests/loop/fusion/*_doc.ml` -- `tests/loop/fission/*_doc.ml` -- `tests/sequence/insert/*_doc.ml` -- `tests/variable/inline/*_doc.ml` -- `tests/omp/*` -- `case_studies/matmul/` -- `case_studies/harris/` +- state when a generated full-file script is a best-effort proposal rather than + a proven optimization. + +## Known Transformation Shapes + +Function transformations: + +```ocaml +!! Function.inline [cCall "f"]; +!! Function.inline [cTopFunDef "main"; cCall "f"]; +!! Function.inline ~delete:true [nbMulti; cCall "f"]; +!! Function.inline_def [cFunDef "helper"]; +``` + +Use `Function.inline` when targeting call sites. Use `Function.inline_def` when +the user asks to inline a helper function definition into its callers and the +definition target is clear. + +Loop transformations: + +```ocaml +!! Loop.unroll [cFor "i"]; +!! Loop.unroll [nbMulti; cFor "i"]; +!! Loop.unroll ~nest_of:2 [nbMulti; cFor "i"]; +!! Loop_basic.unroll [cFor "i"]; +``` + +Use `Loop.unroll` for normal scripts. Use `Loop_basic.unroll` only when a basic +version is explicitly requested or shown in examples. Add `nbMulti` only when +the user wants all matching loops. + +Tile a loop with a literal tile size: + +```ocaml +!! Loop.tile (lit "32") ~index:"bi" ~bound:TileDivides [cFor "i"]; +!! Loop_basic.tile (lit "32") ~index:"bi" ~bound:TileDivides [cFor "i"]; +``` + +Loop reorder, swap, fission, fusion, and shift examples: + +```ocaml +!! Loop.reorder_at ~order:["bi"; "bj"; "bk"; "i"; "k"; "j"] [cPlusEq ~lhs:[cVar "sum"] ()]; +!! Loop.reorder ~order:["j"; "i"] [cFor "i"]; +!! Loop.swap [cFor "j"]; +!! Loop.fission [cForBody "i"; tBetweenAll]; +!! Loop.fission [tBefore; cFor "i"; cWriteVar "s"]; +!! Loop.fusion_targets [cFor "y" ~body:[cArrayWrite "out"]]; +!! Loop.shift StartAtZero [cFor "y"]; +``` + +Use loop reordering only when the order is known. Use fission when the user asks +to split a loop or separate statements. Use fusion when the source has adjacent +loops with compatible iteration domains and the user asks to fuse or optimize +that pattern. + +Insert a parsed statement before a target: + +```ocaml +!! Sequence_basic.insert ~reparse:true (stmt "a++;") [tBefore; cVarDef "x"]; +``` + +Sequence transformations: + +```ocaml +!! Sequence_basic.delete [cVarDef "tmp"]; +!! Sequence_basic.split [tAfter; sInstr "b = 0"]; +!! Sequence_basic.intro 2 [cFor "i"]; +!! Sequence_basic.intro_between [tBefore; cVarDef "a"] [tAfter; cVarDef "b"]; +!! Sequence.intro ~on:[cVarDef "a"] (); +!! Sequence.elim [cSeq ~instrs:[[cVarDef "tmp"]] ()]; +``` + +Use sequence targets for grouping, splitting, inserting, or deleting +instructions. For insertion, the target should usually be positional +(`tBefore`, `tAfter`, `tFirst`, or `tLast`). + +Rename a local variable inside a marked scope: + +```ocaml +!! Variable.local_name ~var:"s" ~local_var:"t" [cMark "t_scope"]; +``` + +Variable transformations: + +```ocaml +!! Variable.inline [cVarDef "x"]; +!! Variable.unfold ~delete:true [cVarDef "x"]; +!! Variable.fold ~at:[cVarDef "dst"] [cVarDef "src"]; +!! Variable.insert ~reparse:true ~typ:(ty "int") ~name:"b" ~value:(lit "2") [tAfter; cVarDef "a"]; +!! Variable.insert_and_fold ~typ:(ty "const int") ~name:"a" ~value:(expr "x*y") [tBefore; cVarDef "r"]; +!! Variable.rename ~into:"new_name" [cVarDef "old_name"]; +!! Variable.renames (AddSuffix "1") [cFunBody "main"]; +!! Variable.reuse (var "x") [cVarDef "y"]; +!! Variable.elim_redundant [cVarDef "tmp"]; +``` + +Use variable transformations only when the variable definition or scope is +clear. Prefer `Variable.inline` for replacing uses by the definition value. +Prefer `Variable.insert` or `Variable.insert_and_fold` when creating a new named +intermediate value. + +Instruction transformations: + +```ocaml +!! Instr.move ~dest:[tBefore; cVarDef "dst"] [cVarDef "src"]; +!! Instr.copy ~dest:[tAfter; cVarDef "dst"] [cVarDef "src"]; +!! Instr.delete [cVarDef "tmp"]; +!! Instr.inline_last_write [cReadVar "x"]; +!! Instr.accumulate ~nb:8 [nbMulti; sInstrRegexp "res.*\\[0\\]"]; +``` + +Use `Instr.move` and `Instr.copy` only with an explicit destination. Use +`Instr.inline_last_write` when a read should be replaced by its most recent +write and the read target is clear. + +Matrix/locality transformations: + +```ocaml +!! Matrix.stack_copy ~var:"sum" ~copy_var:"s" ~copy_dims:1 [cFor "j"]; +!! Matrix.storage_folding ~dim:0 ~size:(int 4) [cVarDef "buffer"]; +!! Matrix.elim [cVarDef "tmp_matrix"]; +!! Matrix.elim_mops []; +!! Matrix.local_name_tile ~var:"a" ~local_var:"a_local" [cFor "i"]; +``` + +Use matrix transformations only when arrays/matrices and dimensions are clear. +If dimensions, local variable names, or allocation targets are not clear, state +the missing details or choose a simpler local transformation first. + +OpenMP transformations: + +```ocaml +!! Omp.parallel_for [tBefore; cFor "i"]; +!! Omp.parallel_for [cFor "i"]; +!! Omp.simd [nbMulti; cFor "j"]; +!! Omp.simd ~clause:[Simdlen 8] [cFor "x"]; +!! Omp.parallel [Private ["tmp"]] [tBefore; cSeq ()]; +!! Omp.for_ ~clause:[Collapse 2] [cFor "j"]; +!! Omp.target [Map_c (To, ["a[:N]"]); Map_c (From, ["out[:N]"])] [tBefore; cFor "i"]; +``` + +Use `Omp.parallel_for` for a loop-level parallel-for pragma. Use `Omp.simd` for +vectorization. Include clauses only when the user asks for them or the source +clearly implies them. + +Run cleanup after transformations that may leave simplifiable code: + +```ocaml +!! Cleanup.std (); +``` + +## Transformation Choice Rules + +- For explicit commands, generate exactly the requested transformation when the + API shape is known. +- For vague full-file optimization requests, choose conservative local + transformations first: inline obvious helper calls, unroll small static loops, + then cleanup. +- For tiling, require a tile size, tile index name, and a clear loop target. If + these are missing, either choose a conservative default and state it or ask for + clarification when no safe default exists. +- For loop reordering, require the full intended order and a stable target + inside the loop nest. +- For insertion, use `Sequence_basic.insert ~reparse:true` with `stmt "..."` + and a positional target such as `[tBefore; ...]` or `[tAfter; ...]`. +- For OpenMP, only emit an `Omp` or `Omp_basic` call when the requested pragma + and target are clear. +- If the visible source has repeated names, include target context or occurrence + selectors to avoid accidental matches. + +## Full-File Script Strategy Rules + +- Start from the visible code, not from a guessed benchmark identity. +- If the file contains a simple named helper function called from one or more + kernels, a reasonable first step is `Function.inline` or + `Function.inline_def`. +- If the file contains small statically bounded loops, a reasonable first step + is `Loop.unroll` on those loops, followed by `Cleanup.std ()`. +- If the file contains a matrix multiplication-like triple loop, a reasonable + plan may include tiling outer loops, reordering the loop nest around the + accumulation, optional SIMD on the innermost loop, and cleanup. Always adapt + names to the visible loops and arrays. +- If the file contains adjacent loops over the same range, a possible plan is + fusion, but mark it medium confidence unless dependencies are obviously safe. +- If the file contains reductions into a scalar, be cautious: unrolling, local + names, fission, or parallelization may require reduction semantics. +- If the request is simply "generate the full script" with no specific goal, + still emit a conservative runnable script. Prefer one or two safe local + transformations plus cleanup over an ambitious multi-step optimization. + +## Common Mistake Prevention + +- Do not invent module names, constructor names, or optional arguments not shown + in this knowledge or current examples. +- Do not use a target that may match multiple nodes unless the script explicitly + uses `nbMulti` or an occurrence selector. +- Do not use `tBefore` or `tAfter` for transformations that operate directly on + a node, such as `Loop.unroll [cFor "i"]`. +- Do use `tBefore` or `tAfter` for insertion, movement, and pragma placement. +- Do not claim validation passed. Provide commands and expected evidence only. +- If exact syntax is uncertain, still show the best-effort script and list the + uncertain API or argument in the assumptions/risk sections. diff --git a/tools/optiNLP/prompts/01_target_generator.md b/tools/optiNLP/prompts/01_target_generator.md index 10c2542b9..4651bcba5 100644 --- a/tools/optiNLP/prompts/01_target_generator.md +++ b/tools/optiNLP/prompts/01_target_generator.md @@ -19,6 +19,10 @@ You may receive: - trace, diff, or error output; - the OptiNLP knowledge files about targets and script patterns. +Use only the source code, script fragments, trace/error text, prompts, and +knowledge included in the current request. Do not rely on unstated files or +examples. + ## Hard Rules - Use only known `Target` constructors. diff --git a/tools/optiNLP/prompts/02_command_to_script.md b/tools/optiNLP/prompts/02_command_to_script.md index 4725b7009..f73c55f1b 100644 --- a/tools/optiNLP/prompts/02_command_to_script.md +++ b/tools/optiNLP/prompts/02_command_to_script.md @@ -18,13 +18,18 @@ You may receive: - existing script examples; - the OptiNLP knowledge files. +Use only the source code, script examples, trace/error text, prompts, and +knowledge included in the current request. Do not rely on unstated files or +examples. + ## Hard Rules -- Use only transformations that exist in `lib/transfo/`. +- Use only transformations described in the prompt, knowledge, or current + request examples. - Use only target constructors that exist in `Target`. - Do not invent `.opti` parser support or `Run.script_opti`. - Ask for missing parameters when no safe default exists. -- Prefer examples from `tests/**/**_doc.ml` for module names and argument order. +- Use the module names and argument order shown in the knowledge/examples. - Always include validation commands. ## Reasoning Procedure diff --git a/tools/optiNLP/prompts/03_code_to_candidate_script.md b/tools/optiNLP/prompts/03_code_to_full_script.md similarity index 92% rename from tools/optiNLP/prompts/03_code_to_candidate_script.md rename to tools/optiNLP/prompts/03_code_to_full_script.md index 4efe8eccf..17fc01002 100644 --- a/tools/optiNLP/prompts/03_code_to_candidate_script.md +++ b/tools/optiNLP/prompts/03_code_to_full_script.md @@ -23,6 +23,10 @@ You may receive: - existing scripts, traces, diffs, or error output; - the OptiNLP knowledge files. +Use only the source code, script examples, trace/error text, prompts, and +knowledge included in the current request. Do not rely on unstated files or +examples. + ## Hard Rules - Generate a complete OCaml OptiTrust script, not just a target and not just a @@ -32,9 +36,11 @@ You may receive: - Use `Run.script_cpp`; do not generate `Run.script_opti`. - Include the needed `open` statements, usually `open Optitrust` and either `open Target` or `open Prelude`. -- Use `!!` or `!!!` consistently with repository examples. +- Use `!!` or `!!!` consistently with the examples and knowledge in the current + request. - Prefer semantic targets over line numbers. -- Use only transformations that exist in `lib/transfo/`. +- Use only transformations described in the prompt, knowledge, or current + request examples. - Do not invent target constructors. - Do not invent `.opti` parser support or `Run.script_opti`. - Separate facts from hypotheses. @@ -88,7 +94,7 @@ Short structural summary of the input code. ## Recommended First Candidate Explain the chosen full-file script strategy. -## Candidate Script +## Full Transformation Script ```ocaml ... ``` From 1943e1520a9eddc4966d88063931f90bc07cf15f Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Fri, 26 Jun 2026 05:05:10 -0400 Subject: [PATCH 13/47] Update OptiNLP full-script mode and chat UI --- tools/vscode-optitrust/README.md | 1 + tools/vscode-optitrust/package.json | 8 +- .../src/commands/optinlpCommands.ts | 15 +- .../src/commands/optinlpPanel.ts | 259 ++++++++++++++---- tools/vscode-optitrust/src/extension.ts | 6 +- .../vscode-optitrust/src/optinlp/cli.test.ts | 12 +- tools/vscode-optitrust/src/optinlp/cli.ts | 7 +- .../src/optinlp/geminiProvider.ts | 16 +- .../src/optinlp/generation.ts | 4 +- .../src/optinlp/mockProvider.ts | 8 +- tools/vscode-optitrust/src/optinlp/modes.ts | 12 +- .../src/optinlp/openaiProvider.ts | 16 +- .../src/optinlp/provider.test.ts | 54 ++-- .../src/optinlp/providerTypes.ts | 6 +- .../src/optinlp/resultActions.ts | 4 +- .../src/optinlp/resultSchemas.ts | 45 +-- .../src/optinlp/sessionMemory.test.ts | 10 +- .../src/optinlp/sessionMemory.ts | 14 +- 18 files changed, 326 insertions(+), 171 deletions(-) diff --git a/tools/vscode-optitrust/README.md b/tools/vscode-optitrust/README.md index 69db714ee..fd234898e 100644 --- a/tools/vscode-optitrust/README.md +++ b/tools/vscode-optitrust/README.md @@ -61,6 +61,7 @@ From `tools/vscode-optitrust`: ```bash npm install +npm run compile npm run package ``` diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index dbf3da931..c6461d4ad 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -42,7 +42,7 @@ "onCommand:optitrust.optinlpChat", "onCommand:optitrust.optinlpGenerateTarget", "onCommand:optitrust.optinlpGenerateScript", - "onCommand:optitrust.optinlpSuggestCandidateScript", + "onCommand:optitrust.optinlpGenerateFullTransformation", "onCommand:optitrust.optinlpSetGeminiApiKey", "onCommand:optitrust.optinlpSetOpenAiApiKey", "onCommand:optitrust.optinlpClearSession" @@ -139,7 +139,7 @@ "title": "OptiTrust: OptiNLP Generate Script" }, { - "command": "optitrust.optinlpSuggestCandidateScript", + "command": "optitrust.optinlpGenerateFullTransformation", "title": "OptiTrust: OptiNLP Generate Full Transformation" }, { @@ -305,7 +305,7 @@ "group": "optinlp@2" }, { - "command": "optitrust.optinlpSuggestCandidateScript", + "command": "optitrust.optinlpGenerateFullTransformation", "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", "group": "optinlp@3" } @@ -375,7 +375,7 @@ "command": "optitrust.optinlpGenerateScript" }, { - "command": "optitrust.optinlpSuggestCandidateScript" + "command": "optitrust.optinlpGenerateFullTransformation" }, { "command": "optitrust.optinlpSetGeminiApiKey" diff --git a/tools/vscode-optitrust/src/commands/optinlpCommands.ts b/tools/vscode-optitrust/src/commands/optinlpCommands.ts index f7c85c36b..bebc7f3d9 100644 --- a/tools/vscode-optitrust/src/commands/optinlpCommands.ts +++ b/tools/vscode-optitrust/src/commands/optinlpCommands.ts @@ -29,6 +29,12 @@ export interface OptiNlpGenerationOutcome { readonly result: OptiNlpProviderResult; } +interface OptiNlpGenerationOptions { + readonly renderToOutput?: boolean; + readonly editor?: vscode.TextEditor; + readonly throwProviderErrors?: boolean; +} + export async function setOptiNlpGeminiApiKey(context: vscode.ExtensionContext): Promise { await setProviderApiKey(context, "Gemini", GEMINI_API_KEY_SECRET); } @@ -93,8 +99,8 @@ export async function generateOptiNlpScript(context: vscode.ExtensionContext, wo } } -export async function suggestOptiNlpCandidateScript(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): Promise { - const outcome = await runModeFromInput(context, workspace, memory, "code_to_candidate_script"); +export async function generateOptiNlpFullTransformation(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): Promise { + const outcome = await runModeFromInput(context, workspace, memory, "code_to_full_script"); if (outcome) { await applyDefaultEditorAction(outcome); } @@ -120,7 +126,7 @@ export async function runOptiNlpGeneration( memory: OptiNlpSessionMemory, mode: OptiNlpMode, userRequest: string, - options: { readonly renderToOutput?: boolean; readonly editor?: vscode.TextEditor } = { renderToOutput: true } + options: OptiNlpGenerationOptions = { renderToOutput: true } ): Promise { const resolvedMode = resolveRequestedMode(mode, userRequest); const editorContext = getActiveEditorContext(workspace.root, options.editor); @@ -156,6 +162,9 @@ export async function runOptiNlpGeneration( ); } catch (error) { if (error instanceof OptiNlpProviderError) { + if (options.throwProviderErrors) { + throw error; + } appendHeader("OptiNLP Error"); appendLine(error.userMessage); if (error.technicalDetail) { diff --git a/tools/vscode-optitrust/src/commands/optinlpPanel.ts b/tools/vscode-optitrust/src/commands/optinlpPanel.ts index 058f2ce0d..82860295e 100644 --- a/tools/vscode-optitrust/src/commands/optinlpPanel.ts +++ b/tools/vscode-optitrust/src/commands/optinlpPanel.ts @@ -6,6 +6,7 @@ import { clearOptiNlpSession, insertTextAtCursor, openOcamlDocument, runOptiNlpG import { inferLanguage } from "../optinlp/assets"; import { modeDefinition, OPTINLP_MODE_DEFINITIONS, OptiNlpUiMode, resolveAutoMode } from "../optinlp/modes"; import { OptiNlpMode } from "../optinlp/providerTypes"; +import { OptiNlpProviderError } from "../optinlp/providerErrors"; import { editorActionForResult } from "../optinlp/resultActions"; import { OptiNlpStructuredResult } from "../optinlp/resultSchemas"; import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; @@ -104,7 +105,8 @@ export class OptiNlpPanel { try { const outcome = await runOptiNlpGeneration(this.context, this.workspace, this.memory, resolvedMode, trimmed, { renderToOutput: false, - editor: this.getSourceEditor() + editor: this.getSourceEditor(), + throwProviderErrors: true }); if (!outcome) { this.post({ type: "busy", busy: false }); @@ -124,7 +126,12 @@ export class OptiNlpPanel { }); await this.refreshContext(); } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = + error instanceof OptiNlpProviderError && error.technicalDetail + ? `${error.userMessage}\n\n${error.technicalDetail}` + : error instanceof Error + ? error.message + : String(error); this.post({ type: "error", message }); } finally { this.post({ type: "busy", busy: false }); @@ -238,15 +245,20 @@ function renderPanelHtml(): string { grid-template-rows: auto 1fr auto; height: 100vh; min-width: 0; + gap: 10px; } .toolbar { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: center; - padding: 10px; - border-bottom: 1px solid var(--vscode-panel-border); + padding: 12px; + margin: 10px 10px 0; + border: 1px solid var(--vscode-panel-border); + border-radius: 20px; background: var(--vscode-sideBar-background); + overflow: hidden; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); } .context { min-width: 0; @@ -257,40 +269,84 @@ function renderPanelHtml(): string { } .messages { overflow-y: auto; - padding: 10px; + padding: 18px 14px 22px; + } + .turn { + display: flex; + margin: 0 0 16px; + } + .turn.user { + justify-content: flex-end; + } + .turn.assistant, + .turn.error { + justify-content: flex-start; } - .message { + .bubble { + max-width: min(760px, 88%); border: 1px solid var(--vscode-panel-border); - border-radius: 6px; - margin-bottom: 10px; + border-radius: 14px; overflow: hidden; background: var(--vscode-editorWidget-background); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16); + } + .turn.user .bubble { + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + border-color: var(--vscode-button-background); + border-bottom-right-radius: 5px; + } + .turn.assistant .bubble, + .turn.error .bubble { + border-bottom-left-radius: 5px; + } + .turn.error .bubble { + border-color: var(--vscode-inputValidation-errorBorder); + background: var(--vscode-inputValidation-errorBackground, var(--vscode-editorWidget-background)); } .message-header { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; - padding: 8px 10px; + padding: 9px 12px; border-bottom: 1px solid var(--vscode-panel-border); color: var(--vscode-descriptionForeground); } - .message pre { + .turn.user .message-header { + color: var(--vscode-button-foreground); + border-bottom-color: color-mix(in srgb, var(--vscode-button-foreground) 25%, transparent); + } + .meta { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .message-body { margin: 0; - padding: 10px; + padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--vscode-editor-font-family); font-size: var(--vscode-editor-font-size); line-height: 1.45; } + .turn.user .message-body { + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + } .composer { display: grid; - grid-template-columns: minmax(110px, 150px) 1fr auto; + grid-template-columns: minmax(104px, 142px) 1fr auto; gap: 8px; - padding: 10px; - border-top: 1px solid var(--vscode-panel-border); + padding: 12px; + margin: 0 10px 10px; + border: 1px solid var(--vscode-panel-border); + border-radius: 20px; background: var(--vscode-sideBar-background); + overflow: hidden; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.14); } select, textarea, @@ -302,20 +358,27 @@ function renderPanelHtml(): string { color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, var(--vscode-panel-border)); - border-radius: 4px; + border-radius: 10px; + } + select { + border-radius: 999px; + padding: 0 10px; + min-height: 32px; } textarea { - min-height: 34px; + min-height: 32px; max-height: 110px; resize: vertical; - padding: 7px; + padding: 9px 10px; + line-height: 1.35; + border-radius: 14px; } button { color: var(--vscode-button-foreground); background: var(--vscode-button-background); border: 0; - border-radius: 4px; - padding: 6px 10px; + border-radius: 999px; + padding: 7px 12px; min-height: 32px; cursor: pointer; } @@ -325,6 +388,7 @@ function renderPanelHtml(): string { button.secondary { color: var(--vscode-button-secondaryForeground); background: var(--vscode-button-secondaryBackground); + border: 1px solid transparent; } button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); @@ -333,6 +397,21 @@ function renderPanelHtml(): string { opacity: 0.6; cursor: default; } + button.icon { + min-width: 32px; + padding: 5px 9px; + } + #set-key, + #clear { + min-width: 48px; + padding-inline: 12px; + } + #send { + min-width: 52px; + padding: 5px 11px; + min-height: 30px; + align-self: end; + } .actions { display: flex; gap: 6px; @@ -384,15 +463,35 @@ function renderPanelHtml(): string { const context = document.getElementById('context'); const setKey = document.getElementById('set-key'); const clear = document.getElementById('clear'); + const modeLabels = ${JSON.stringify(Object.fromEntries(OPTINLP_MODE_DEFINITIONS.map(definition => [definition.id, definition.shortLabel])))}; + let history = restoreHistory(); + let pendingUserId = undefined; + + renderHistory(); form.addEventListener('submit', event => { event.preventDefault(); - vscode.postMessage({ type: 'generate', mode: mode.value, request: request.value }); + const text = request.value.trim(); + if (!text) return; + const userMessage = { + id: createMessageId(), + role: 'user', + mode: mode.value, + text, + meta: modeTitle(mode.value) + }; + pendingUserId = userMessage.id; + history.push(userMessage); + request.value = ''; + saveAndRender(); + vscode.postMessage({ type: 'generate', mode: mode.value, request: text }); }); setKey.addEventListener('click', () => vscode.postMessage({ type: 'setApiKey' })); clear.addEventListener('click', () => { - messages.replaceChildren(); + history = []; + pendingUserId = undefined; + saveAndRender(); vscode.postMessage({ type: 'clearSession' }); }); @@ -404,9 +503,9 @@ function renderPanelHtml(): string { send.disabled = Boolean(message.busy); status.textContent = message.busy ? 'Running...' : ''; } else if (message.type === 'result') { - appendResult(message); + appendAssistantResult(message); } else if (message.type === 'error') { - appendTextCard('Error', message.message || 'Unknown error'); + appendError(message.message || 'Unknown error'); } else if (message.type === 'copied') { status.textContent = 'Copied'; setTimeout(() => { status.textContent = ''; }, 1200); @@ -419,59 +518,117 @@ function renderPanelHtml(): string { } }); - function appendResult(message) { - const title = modeTitle(message.mode) + ' · ' + message.sourceLabel + ' · ' + message.provider; - appendTextCard(title, message.markdown || '', message.request || '', message.resultId, message.structured && message.structured.kind); - request.value = ''; + function appendAssistantResult(message) { + history.push({ + id: createMessageId(), + role: 'assistant', + text: message.markdown || '', + meta: modeTitle(message.mode) + ' · ' + message.sourceLabel + ' · ' + message.provider + ' · ' + message.model, + resultId: message.resultId, + kind: message.structured && message.structured.kind, + replyTo: pendingUserId + }); + pendingUserId = undefined; + saveAndRender(); } - function appendTextCard(title, text, requestText = '', resultId = undefined, kind = undefined) { - const card = document.createElement('article'); - card.className = 'message'; + function appendError(text) { + history.push({ + id: createMessageId(), + role: 'error', + text, + meta: 'Error', + replyTo: pendingUserId + }); + pendingUserId = undefined; + saveAndRender(); + } + + function renderHistory() { + messages.replaceChildren(); + for (const item of history) { + appendMessage(item); + } + messages.scrollTop = messages.scrollHeight; + } + + function appendMessage(item) { + const turn = document.createElement('article'); + turn.className = 'turn ' + item.role; + const bubble = document.createElement('div'); + bubble.className = 'bubble'; const header = document.createElement('div'); header.className = 'message-header'; const label = document.createElement('div'); - label.textContent = requestText ? title + ' · ' + requestText : title; + label.className = 'meta'; + label.textContent = item.meta || roleTitle(item.role); const actions = document.createElement('div'); actions.className = 'actions'; - const copy = document.createElement('button'); - copy.className = 'secondary'; - copy.type = 'button'; - copy.textContent = 'Copy'; - copy.addEventListener('click', () => vscode.postMessage({ type: 'copy', text })); - actions.appendChild(copy); - if (resultId && kind === 'target') { + + if (item.role !== 'user') { + const copy = document.createElement('button'); + copy.className = 'secondary icon'; + copy.type = 'button'; + copy.title = 'Copy'; + copy.textContent = 'Copy'; + copy.addEventListener('click', () => vscode.postMessage({ type: 'copy', text: item.text })); + actions.appendChild(copy); + } + if (item.resultId && item.kind === 'target') { const insert = document.createElement('button'); - insert.className = 'secondary'; + insert.className = 'secondary icon'; insert.type = 'button'; + insert.title = 'Insert target'; insert.textContent = 'Insert'; - insert.addEventListener('click', () => vscode.postMessage({ type: 'insertTarget', resultId })); + insert.addEventListener('click', () => vscode.postMessage({ type: 'insertTarget', resultId: item.resultId })); actions.appendChild(insert); } - if (resultId && (kind === 'command_to_script' || kind === 'code_to_candidate_script')) { + if (item.resultId && (item.kind === 'command_to_script' || item.kind === 'code_to_full_script')) { const open = document.createElement('button'); - open.className = 'secondary'; + open.className = 'secondary icon'; open.type = 'button'; + open.title = 'Open script'; open.textContent = 'Open'; - open.addEventListener('click', () => vscode.postMessage({ type: 'openScript', resultId })); + open.addEventListener('click', () => vscode.postMessage({ type: 'openScript', resultId: item.resultId })); actions.appendChild(open); } header.appendChild(label); header.appendChild(actions); - const pre = document.createElement('pre'); - pre.textContent = text; - card.appendChild(header); - card.appendChild(pre); - messages.appendChild(card); - messages.scrollTop = messages.scrollHeight; + const body = document.createElement('pre'); + body.className = 'message-body'; + body.textContent = item.text; + bubble.appendChild(header); + bubble.appendChild(body); + turn.appendChild(bubble); + messages.appendChild(turn); } function modeTitle(value) { - const labels = ${JSON.stringify(Object.fromEntries(OPTINLP_MODE_DEFINITIONS.map(definition => [definition.id, definition.shortLabel])))}; - if (labels[value]) return labels[value]; + if (value === 'auto') return 'Auto'; + if (modeLabels[value]) return modeLabels[value]; + return 'OptiNLP'; + } + + function roleTitle(role) { + if (role === 'user') return 'You'; + if (role === 'error') return 'Error'; return 'OptiNLP'; } + function createMessageId() { + return String(Date.now()) + '-' + String(Math.random()).slice(2); + } + + function restoreHistory() { + const state = vscode.getState(); + return Array.isArray(state && state.history) ? state.history : []; + } + + function saveAndRender() { + vscode.setState({ history }); + renderHistory(); + } + vscode.postMessage({ type: 'ready' }); diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index 4431d12de..fdf76b469 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -13,7 +13,7 @@ import { generateOptiNlpTarget, setOptiNlpOpenAiApiKey, setOptiNlpGeminiApiKey, - suggestOptiNlpCandidateScript + generateOptiNlpFullTransformation } from "./commands/optinlpCommands"; import { OptiNlpPanel } from "./commands/optinlpPanel"; import { rerunLastTests, runCurrentTest, runCurrentTestAndOpenDiff } from "./commands/runTests"; @@ -235,10 +235,10 @@ export async function activate(context: vscode.ExtensionContext): Promise } }); - registerCommand(context, "optitrust.optinlpSuggestCandidateScript", async () => { + registerCommand(context, "optitrust.optinlpGenerateFullTransformation", async () => { const workspace = await requireWorkspace(); if (workspace && optiNlpSession) { - await suggestOptiNlpCandidateScript(context, workspace, optiNlpSession); + await generateOptiNlpFullTransformation(context, workspace, optiNlpSession); } }); diff --git a/tools/vscode-optitrust/src/optinlp/cli.test.ts b/tools/vscode-optitrust/src/optinlp/cli.test.ts index e835eec0c..191ba3073 100644 --- a/tools/vscode-optitrust/src/optinlp/cli.test.ts +++ b/tools/vscode-optitrust/src/optinlp/cli.test.ts @@ -41,8 +41,8 @@ async function testAssetLoading(): Promise { } async function testWholeFileScriptRouting(): Promise { - assert.strictEqual(resolveRequestedMode("command_to_script", "generate a complete transformation script for the whole file"), "code_to_candidate_script"); - assert.strictEqual(resolveRequestedMode("auto", "write matmul.ml for this source"), "code_to_candidate_script"); + assert.strictEqual(resolveRequestedMode("command_to_script", "generate a complete transformation script for the whole file"), "code_to_full_script"); + assert.strictEqual(resolveRequestedMode("auto", "write matmul.ml for this source"), "code_to_full_script"); } async function testCliTargetMarkdown(): Promise { @@ -80,19 +80,19 @@ async function testCliScriptJson(): Promise { }); } -async function testCliCandidatesGoal(): Promise { +async function testCliFullRequest(): Promise { await withTempSource(async filePath => { const stdout = new MemoryWritable(); const stderr = new MemoryWritable(); const exitCode = await runOptiNlpCli( - ["candidates", "--file", filePath, "--goal", "suggest a first transformation", "--provider", "mock", "--json"], + ["full", "--file", filePath, "--request", "generate a full transformation script", "--provider", "mock", "--json"], stdout, stderr ); assert.strictEqual(exitCode, 0); const parsed = JSON.parse(stdout.text()) as { structured: { kind: string; candidateTransformations: unknown[] } }; - assert.strictEqual(parsed.structured.kind, "code_to_candidate_script"); + assert.strictEqual(parsed.structured.kind, "code_to_full_script"); assert.strictEqual(parsed.structured.candidateTransformations.length, 1); assert.strictEqual(stderr.text(), ""); }); @@ -113,7 +113,7 @@ async function main(): Promise { await testWholeFileScriptRouting(); await testCliTargetMarkdown(); await testCliScriptJson(); - await testCliCandidatesGoal(); + await testCliFullRequest(); await testCliMissingRequest(); console.log("OptiNLP CLI tests passed."); } diff --git a/tools/vscode-optitrust/src/optinlp/cli.ts b/tools/vscode-optitrust/src/optinlp/cli.ts index 0b8916735..2c97a3b37 100644 --- a/tools/vscode-optitrust/src/optinlp/cli.ts +++ b/tools/vscode-optitrust/src/optinlp/cli.ts @@ -107,7 +107,7 @@ function parseArgs(argv: readonly string[]): CliOptions { } const userRequest = values.get("--request") ?? values.get("--goal"); if (!userRequest) { - throw new Error(commandArg === "candidates" ? "Missing required --goal option." : "Missing required --request option."); + throw new Error("Missing required --request option."); } return { @@ -151,12 +151,11 @@ function writeResult(stdout: NodeJS.WritableStream, result: OptiNlpProviderResul function usage(): string { const providers = IMPLEMENTED_OPTINLP_PROVIDER_IDS.join("|"); - const commands = ["target", "script", "candidates"] + const commands = ["target", "script", "full"] .map(command => { const definition = modeFromCliCommand(command); const placeholder = definition ? modeDefinition(definition).placeholder : "..."; - const requestFlag = command === "candidates" ? "--goal" : "--request"; - return ` optinlp ${command} --file path ${requestFlag} "${placeholder}" [--json] [--provider ${providers}]`; + return ` optinlp ${command} --file path --request "${placeholder}" [--json] [--provider ${providers}]`; }) .join("\n"); return [ diff --git a/tools/vscode-optitrust/src/optinlp/geminiProvider.ts b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts index 837c2a513..4901cf3a0 100644 --- a/tools/vscode-optitrust/src/optinlp/geminiProvider.ts +++ b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts @@ -2,7 +2,7 @@ // Gemini wire format and converts responses back into provider-neutral results. import { OptiNlpProviderError, technicalDetailFrom } from "./providerErrors"; import { OptiNlpProvider, OptiNlpProviderRequest, OptiNlpProviderResult, requestWithMode } from "./providerTypes"; -import { OptiNlpSchemaError, parseOptiNlpMarkdownResult } from "./resultSchemas"; +import { parseOptiNlpMarkdownResultSafely } from "./resultSchemas"; export const DEFAULT_GEMINI_MODEL = "gemini-3.5-flash"; @@ -53,8 +53,8 @@ export class GeminiProvider implements OptiNlpProvider { return this.generate(requestWithMode(request, "command_to_script")); } - async generateCandidateScript(request: OptiNlpProviderRequest): Promise { - return this.generate(requestWithMode(request, "code_to_candidate_script")); + async generateFullScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_full_script")); } buildPromptForTest(request: OptiNlpProviderRequest): string { @@ -113,15 +113,7 @@ export class GeminiProvider implements OptiNlpProvider { throw new OptiNlpProviderError(this.name, "Gemini returned an empty OptiNLP response.", "No candidate text parts found."); } - let structured; - try { - structured = parseOptiNlpMarkdownResult(request.mode, markdownOutput); - } catch (error) { - if (error instanceof OptiNlpSchemaError) { - throw new OptiNlpProviderError(this.name, "Gemini returned an invalid OptiNLP response.", error.message, error); - } - throw error; - } + const structured = parseOptiNlpMarkdownResultSafely(request.mode, markdownOutput); return { provider: this.name, diff --git a/tools/vscode-optitrust/src/optinlp/generation.ts b/tools/vscode-optitrust/src/optinlp/generation.ts index 35de1ed80..c8725b21d 100644 --- a/tools/vscode-optitrust/src/optinlp/generation.ts +++ b/tools/vscode-optitrust/src/optinlp/generation.ts @@ -8,7 +8,7 @@ export async function generateOptiNlp(provider: OptiNlpProvider, request: OptiNl return provider.generateTarget(request); case "command_to_script": return provider.generateScript(request); - case "code_to_candidate_script": - return provider.generateCandidateScript(request); + case "code_to_full_script": + return provider.generateFullScript(request); } } diff --git a/tools/vscode-optitrust/src/optinlp/mockProvider.ts b/tools/vscode-optitrust/src/optinlp/mockProvider.ts index 71c162e2b..9de7b2c1f 100644 --- a/tools/vscode-optitrust/src/optinlp/mockProvider.ts +++ b/tools/vscode-optitrust/src/optinlp/mockProvider.ts @@ -26,8 +26,8 @@ export class MockProvider implements OptiNlpProvider { return this.generate(requestWithMode(request, "command_to_script")); } - async generateCandidateScript(request: OptiNlpProviderRequest): Promise { - return this.generate(requestWithMode(request, "code_to_candidate_script")); + async generateFullScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_full_script")); } private async generate(request: OptiNlpProviderRequest): Promise { @@ -106,7 +106,7 @@ function mockOutputForMode(mode: OptiNlpProviderRequest["mode"]): string { "dune exec -- ./mock.exe", "```" ].join("\n"); - case "code_to_candidate_script": + case "code_to_full_script": return [ "## Code Summary", "Mock code summary.", @@ -119,7 +119,7 @@ function mockOutputForMode(mode: OptiNlpProviderRequest["mode"]): string { "## Recommended First Candidate", "Try the high-confidence mock candidate first.", "", - "## Candidate Script", + "## Full Transformation Script", "```ocaml", "open Optitrust", "open Target", diff --git a/tools/vscode-optitrust/src/optinlp/modes.ts b/tools/vscode-optitrust/src/optinlp/modes.ts index 2b7d61da1..d4690319b 100644 --- a/tools/vscode-optitrust/src/optinlp/modes.ts +++ b/tools/vscode-optitrust/src/optinlp/modes.ts @@ -2,7 +2,7 @@ // prompt loading, CLI routing, UI labels, and auto-routing stay in sync. import { OptiNlpMode } from "./providerTypes"; -export type OptiNlpCliCommand = "target" | "script" | "candidates"; +export type OptiNlpCliCommand = "target" | "script" | "full"; export type OptiNlpUiMode = OptiNlpMode | "auto"; export interface OptiNlpModeDefinition { @@ -35,12 +35,12 @@ export const OPTINLP_MODE_DEFINITIONS: readonly OptiNlpModeDefinition[] = [ knowledgeFiles: ["targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] }, { - id: "code_to_candidate_script", - cliCommand: "candidates", + id: "code_to_full_script", + cliCommand: "full", label: "Generate Full Transformation", shortLabel: "Full Transformation", placeholder: "generate a complete transformation script for the whole file", - promptFile: "03_code_to_candidate_script.md", + promptFile: "03_code_to_full_script.md", knowledgeFiles: ["targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] } ] as const; @@ -70,7 +70,7 @@ export function resolveAutoMode(mode: OptiNlpUiMode, request: string): OptiNlpMo export function resolveRequestedMode(mode: OptiNlpUiMode, request: string): OptiNlpMode { if (isFullFileScriptRequest(request)) { - return "code_to_candidate_script"; + return "code_to_full_script"; } if (mode !== "auto") { return mode; @@ -80,7 +80,7 @@ export function resolveRequestedMode(mode: OptiNlpUiMode, request: string): Opti return "target"; } if (/\b(suggest|candidate|optimi[sz]e|opportunity|what can)\b/u.test(text)) { - return "code_to_candidate_script"; + return "code_to_full_script"; } return "command_to_script"; } diff --git a/tools/vscode-optitrust/src/optinlp/openaiProvider.ts b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts index 028ef52c8..8f45c44b3 100644 --- a/tools/vscode-optitrust/src/optinlp/openaiProvider.ts +++ b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts @@ -2,7 +2,7 @@ // Responses API wire format and keeps OpenAI-specific fields out of callers. import { OptiNlpProviderError, technicalDetailFrom } from "./providerErrors"; import { OptiNlpProvider, OptiNlpProviderRequest, OptiNlpProviderResult, requestWithMode } from "./providerTypes"; -import { OptiNlpSchemaError, parseOptiNlpMarkdownResult } from "./resultSchemas"; +import { parseOptiNlpMarkdownResultSafely } from "./resultSchemas"; export const DEFAULT_OPENAI_MODEL = "gpt-5.5"; @@ -56,8 +56,8 @@ export class OpenAiProvider implements OptiNlpProvider { return this.generate(requestWithMode(request, "command_to_script")); } - async generateCandidateScript(request: OptiNlpProviderRequest): Promise { - return this.generate(requestWithMode(request, "code_to_candidate_script")); + async generateFullScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_full_script")); } buildPromptForTest(request: OptiNlpProviderRequest): string { @@ -113,15 +113,7 @@ export class OpenAiProvider implements OptiNlpProvider { throw new OptiNlpProviderError(this.name, "OpenAI returned an empty OptiNLP response.", "No output text found."); } - let structured; - try { - structured = parseOptiNlpMarkdownResult(request.mode, markdownOutput); - } catch (error) { - if (error instanceof OptiNlpSchemaError) { - throw new OptiNlpProviderError(this.name, "OpenAI returned an invalid OptiNLP response.", error.message, error); - } - throw error; - } + const structured = parseOptiNlpMarkdownResultSafely(request.mode, markdownOutput); return { provider: this.name, diff --git a/tools/vscode-optitrust/src/optinlp/provider.test.ts b/tools/vscode-optitrust/src/optinlp/provider.test.ts index aa5196ac7..38d8307ef 100644 --- a/tools/vscode-optitrust/src/optinlp/provider.test.ts +++ b/tools/vscode-optitrust/src/optinlp/provider.test.ts @@ -36,8 +36,8 @@ async function testMockProvider(): Promise { const scriptResult = await provider.generateScript(sampleRequest); assert.strictEqual(scriptResult.structured?.kind, "command_to_script"); - const candidateResult = await provider.generateCandidateScript(sampleRequest); - assert.strictEqual(candidateResult.structured?.kind, "code_to_candidate_script"); + const fullScriptResult = await provider.generateFullScript(sampleRequest); + assert.strictEqual(fullScriptResult.structured?.kind, "code_to_full_script"); } async function testGeminiPromptConstruction(): Promise { @@ -108,22 +108,22 @@ async function testGeminiProviderException(): Promise { async function testGeminiSuccessfulResponse(): Promise { const fetchImpl: typeof fetch = async () => - new Response(JSON.stringify({ candidates: [{ content: { parts: [{ text: validCandidateMarkdown }] } }] }), { + new Response(JSON.stringify({ candidates: [{ content: { parts: [{ text: validFullScriptMarkdown }] } }] }), { status: 200, headers: { "Content-Type": "application/json" } }); const provider = new GeminiProvider({ apiKey: "test-key", model: "gemini-test", fetchImpl }); - const result = await provider.generateCandidateScript(sampleRequest); + const result = await provider.generateFullScript(sampleRequest); assert.strictEqual(result.provider, "gemini"); assert.strictEqual(result.model, "gemini-test"); - assert.strictEqual(result.markdownOutput, validCandidateMarkdown); - assert.strictEqual(result.structured?.kind, "code_to_candidate_script"); + assert.strictEqual(result.markdownOutput, validFullScriptMarkdown); + assert.strictEqual(result.structured?.kind, "code_to_full_script"); assert.ok(result.rawResponse); } -async function testGeminiInvalidStructuredResponse(): Promise { +async function testGeminiUnstructuredResponseStillDisplays(): Promise { const fetchImpl: typeof fetch = async () => new Response(JSON.stringify({ candidates: [{ content: { parts: [{ text: "## Intent\nGenerated." }] } }] }), { status: 200, @@ -131,14 +131,10 @@ async function testGeminiInvalidStructuredResponse(): Promise { }); const provider = new GeminiProvider({ apiKey: "test-key", fetchImpl }); - await assert.rejects( - () => provider.generateTarget(sampleRequest), - (error: unknown) => - error instanceof OptiNlpProviderError && - error.provider === "gemini" && - error.userMessage === "Gemini returned an invalid OptiNLP response." && - /Recommended Target|Candidate Nodes|Ambiguities|Intent/u.test(error.technicalDetail ?? "") - ); + const result = await provider.generateTarget(sampleRequest); + + assert.strictEqual(result.markdownOutput, "## Intent\nGenerated."); + assert.strictEqual(result.structured, undefined); } async function testOpenAiPromptConstruction(): Promise { @@ -225,7 +221,7 @@ async function testOpenAiSuccessfulResponse(): Promise { assert.match(requestBody?.input ?? "", /# User Request/); } -async function testOpenAiInvalidStructuredResponse(): Promise { +async function testOpenAiUnstructuredResponseStillDisplays(): Promise { const fetchImpl: typeof fetch = async () => new Response(JSON.stringify({ output_text: "## Intent\nGenerated." }), { status: 200, @@ -233,14 +229,10 @@ async function testOpenAiInvalidStructuredResponse(): Promise { }); const provider = new OpenAiProvider({ apiKey: "test-key", fetchImpl }); - await assert.rejects( - () => provider.generateTarget(sampleRequest), - (error: unknown) => - error instanceof OptiNlpProviderError && - error.provider === "openai" && - error.userMessage === "OpenAI returned an invalid OptiNLP response." && - /Recommended Target|Candidate Nodes|Ambiguities|Intent/u.test(error.technicalDetail ?? "") - ); + const result = await provider.generateTarget(sampleRequest); + + assert.strictEqual(result.markdownOutput, "## Intent\nGenerated."); + assert.strictEqual(result.structured, undefined); } async function testMarkdownSchemaParsing(): Promise { @@ -252,9 +244,9 @@ async function testMarkdownSchemaParsing(): Promise { assert.strictEqual(parsedScript.kind, "command_to_script"); assert.match(parsedScript.generatedScript, /Loop\.unroll/u); - const parsedCandidate = parseOptiNlpMarkdownResult("code_to_candidate_script", validCandidateMarkdown); - assert.strictEqual(parsedCandidate.kind, "code_to_candidate_script"); - assert.strictEqual(parsedCandidate.candidateTransformations.length, 1); + const parsedFullScript = parseOptiNlpMarkdownResult("code_to_full_script", validFullScriptMarkdown); + assert.strictEqual(parsedFullScript.kind, "code_to_full_script"); + assert.strictEqual(parsedFullScript.candidateTransformations.length, 1); } const validTargetMarkdown = [ @@ -318,7 +310,7 @@ const validScriptMarkdown = [ "```" ].join("\n"); -const validCandidateMarkdown = [ +const validFullScriptMarkdown = [ "## Code Summary", "One loop over `i` calls `work`.", "", @@ -330,7 +322,7 @@ const validCandidateMarkdown = [ "## Recommended First Candidate", "Try unrolling the visible loop first.", "", - "## Candidate Script", + "## Full Transformation Script", "```ocaml", "open Optitrust", "open Target", @@ -357,13 +349,13 @@ async function main(): Promise { await testGeminiEmptyResponse(); await testGeminiProviderException(); await testGeminiSuccessfulResponse(); - await testGeminiInvalidStructuredResponse(); + await testGeminiUnstructuredResponseStillDisplays(); await testOpenAiPromptConstruction(); await testOpenAiMissingApiKey(); await testOpenAiEmptyResponse(); await testOpenAiProviderException(); await testOpenAiSuccessfulResponse(); - await testOpenAiInvalidStructuredResponse(); + await testOpenAiUnstructuredResponseStillDisplays(); await testMarkdownSchemaParsing(); console.log("OptiNLP provider tests passed."); } diff --git a/tools/vscode-optitrust/src/optinlp/providerTypes.ts b/tools/vscode-optitrust/src/optinlp/providerTypes.ts index 48e0ccc50..0dcb51054 100644 --- a/tools/vscode-optitrust/src/optinlp/providerTypes.ts +++ b/tools/vscode-optitrust/src/optinlp/providerTypes.ts @@ -2,7 +2,7 @@ // panels, and provider implementations should communicate through these types. import type { OptiNlpStructuredResult } from "./resultSchemas"; -export type OptiNlpMode = "target" | "command_to_script" | "code_to_candidate_script"; +export type OptiNlpMode = "target" | "command_to_script" | "code_to_full_script"; export interface OptiNlpProviderRequest { readonly mode: OptiNlpMode; @@ -19,6 +19,8 @@ export interface OptiNlpProviderResult { readonly provider: string; readonly model: string; readonly markdownOutput: string; + // Best-effort parsed fields for editor actions; raw markdown is still valid + // output when a provider does not follow the exact section schema. readonly structured?: OptiNlpStructuredResult; readonly rawResponse?: unknown; } @@ -29,7 +31,7 @@ export interface OptiNlpProvider { generateTarget(request: OptiNlpProviderRequest): Promise; generateScript(request: OptiNlpProviderRequest): Promise; - generateCandidateScript(request: OptiNlpProviderRequest): Promise; + generateFullScript(request: OptiNlpProviderRequest): Promise; } export function requestWithMode(request: OptiNlpProviderRequest, mode: OptiNlpMode): OptiNlpProviderRequest { diff --git a/tools/vscode-optitrust/src/optinlp/resultActions.ts b/tools/vscode-optitrust/src/optinlp/resultActions.ts index 10b27af4b..ce620b9fe 100644 --- a/tools/vscode-optitrust/src/optinlp/resultActions.ts +++ b/tools/vscode-optitrust/src/optinlp/resultActions.ts @@ -15,7 +15,7 @@ export function editorActionForResult(result: OptiNlpStructuredResult | undefine return result.recommendedTarget ? { kind: "insert_target", text: result.recommendedTarget } : undefined; case "command_to_script": return { kind: "open_script", text: result.generatedScript }; - case "code_to_candidate_script": - return { kind: "open_script", text: result.candidateScript }; + case "code_to_full_script": + return { kind: "open_script", text: result.fullScript }; } } diff --git a/tools/vscode-optitrust/src/optinlp/resultSchemas.ts b/tools/vscode-optitrust/src/optinlp/resultSchemas.ts index 9e8a21e02..34eaad9c9 100644 --- a/tools/vscode-optitrust/src/optinlp/resultSchemas.ts +++ b/tools/vscode-optitrust/src/optinlp/resultSchemas.ts @@ -32,17 +32,17 @@ export interface CandidateTransformation { readonly risk: string; } -export interface CandidateScriptResult { - readonly kind: "code_to_candidate_script"; +export interface FullScriptResult { + readonly kind: "code_to_full_script"; readonly codeSummary: string; readonly candidateTransformations: readonly CandidateTransformation[]; readonly recommendedFirstCandidate: string; - readonly candidateScript: string; + readonly fullScript: string; readonly validation: string; readonly missingInformation?: string; } -export type OptiNlpStructuredResult = TargetResult | ScriptResult | CandidateScriptResult; +export type OptiNlpStructuredResult = TargetResult | ScriptResult | FullScriptResult; export const targetResultSchema = { type: "object", @@ -74,11 +74,11 @@ export const scriptResultSchema = { } } as const; -export const candidateScriptResultSchema = { +export const fullScriptResultSchema = { type: "object", - required: ["kind", "codeSummary", "candidateTransformations", "recommendedFirstCandidate", "candidateScript", "validation"], + required: ["kind", "codeSummary", "candidateTransformations", "recommendedFirstCandidate", "fullScript", "validation"], properties: { - kind: { const: "code_to_candidate_script" }, + kind: { const: "code_to_full_script" }, codeSummary: { type: "string" }, candidateTransformations: { type: "array", @@ -95,7 +95,7 @@ export const candidateScriptResultSchema = { } }, recommendedFirstCandidate: { type: "string" }, - candidateScript: { type: "string" }, + fullScript: { type: "string" }, validation: { type: "string" }, missingInformation: { type: "string" } } @@ -117,8 +117,19 @@ export function parseOptiNlpMarkdownResult(mode: OptiNlpMode, markdown: string): return parseTargetResult(markdown); case "command_to_script": return parseScriptResult(markdown); - case "code_to_candidate_script": - return parseCandidateScriptResult(markdown); + case "code_to_full_script": + return parseFullScriptResult(markdown); + } +} + +export function parseOptiNlpMarkdownResultSafely(mode: OptiNlpMode, markdown: string): OptiNlpStructuredResult | undefined { + try { + return parseOptiNlpMarkdownResult(mode, markdown); + } catch (error) { + if (error instanceof OptiNlpSchemaError) { + return undefined; + } + throw error; } } @@ -167,14 +178,14 @@ function parseScriptResult(markdown: string): ScriptResult { }; } -function parseCandidateScriptResult(markdown: string): CandidateScriptResult { +function parseFullScriptResult(markdown: string): FullScriptResult { return { - kind: "code_to_candidate_script", - codeSummary: requiredSection(markdown, "Code Summary", "code_to_candidate_script"), - candidateTransformations: parseCandidateTable(requiredSection(markdown, "Candidate Transformations", "code_to_candidate_script")), - recommendedFirstCandidate: requiredSection(markdown, "Recommended First Candidate", "code_to_candidate_script"), - candidateScript: requiredCodeBlock(markdown, "Candidate Script", "code_to_candidate_script"), - validation: requiredCodeBlock(markdown, "Validation", "code_to_candidate_script"), + kind: "code_to_full_script", + codeSummary: requiredSection(markdown, "Code Summary", "code_to_full_script"), + candidateTransformations: parseCandidateTable(requiredSection(markdown, "Candidate Transformations", "code_to_full_script")), + recommendedFirstCandidate: requiredSection(markdown, "Recommended First Candidate", "code_to_full_script"), + fullScript: requiredCodeBlock(markdown, "Full Transformation Script", "code_to_full_script"), + validation: requiredCodeBlock(markdown, "Validation", "code_to_full_script"), missingInformation: section(markdown, "Missing Information") }; } diff --git a/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts b/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts index 684148f5d..5c54e2821 100644 --- a/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts @@ -45,13 +45,13 @@ function scriptResult(): OptiNlpProviderResult { }; } -function candidateResult(): OptiNlpProviderResult { +function fullScriptResult(): OptiNlpProviderResult { return { provider: "mock", model: "mock-model", markdownOutput: "markdown", structured: { - kind: "code_to_candidate_script", + kind: "code_to_full_script", codeSummary: "A loop.", candidateTransformations: [ { @@ -63,7 +63,7 @@ function candidateResult(): OptiNlpProviderResult { } ], recommendedFirstCandidate: "Try unrolling.", - candidateScript: "open Optitrust\nopen Target\nlet _ = Run.script_cpp (fun _ ->\n !! Loop.unroll [cFor \"i\"];\n)", + fullScript: "open Optitrust\nopen Target\nlet _ = Run.script_cpp (fun _ ->\n !! Loop.unroll [cFor \"i\"];\n)", validation: "dune exec -- ./mock.exe", missingInformation: "None." } @@ -113,8 +113,8 @@ function testKeepsOnlyMaxTurns(): void { function testCandidateSummaryAndClear(): void { const memory = new OptiNlpSessionMemory(); - memory.recordGeneration({ ...baseRequest, mode: "code_to_candidate_script", userRequest: "suggest candidates" }, candidateResult()); - assert.match(memory.summary() ?? "", /Previous candidate script:/u); + memory.recordGeneration({ ...baseRequest, mode: "code_to_full_script", userRequest: "generate full transformation" }, fullScriptResult()); + assert.match(memory.summary() ?? "", /Previous full script:/u); memory.clear(); assert.strictEqual(memory.summary(), undefined); diff --git a/tools/vscode-optitrust/src/optinlp/sessionMemory.ts b/tools/vscode-optitrust/src/optinlp/sessionMemory.ts index 28d38bfb9..6f5cb0870 100644 --- a/tools/vscode-optitrust/src/optinlp/sessionMemory.ts +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.ts @@ -18,7 +18,7 @@ export interface OptiNlpSessionTurn { readonly model: string; readonly target?: string; readonly script?: string; - readonly candidateScript?: string; + readonly fullScript?: string; readonly assumptions: readonly string[]; readonly validation?: string; } @@ -89,8 +89,8 @@ export class OptiNlpSessionMemory { if (lastTurn.script) { lines.push(`Previous script: ${truncateOneLine(lastTurn.script, 500)}`); } - if (lastTurn.candidateScript) { - lines.push(`Previous candidate script: ${truncateOneLine(lastTurn.candidateScript, 500)}`); + if (lastTurn.fullScript) { + lines.push(`Previous full script: ${truncateOneLine(lastTurn.fullScript, 500)}`); } if (lastTurn.assumptions.length > 0) { lines.push(`Previous assumptions: ${lastTurn.assumptions.join("; ")}`); @@ -128,7 +128,7 @@ function turnFromResult(request: OptiNlpProviderRequest, result: OptiNlpProvider model: result.model, target: targetFrom(structured), script: scriptFrom(structured), - candidateScript: candidateScriptFrom(structured), + fullScript: fullScriptFrom(structured), assumptions: assumptionsFrom(structured), validation: validationFrom(structured) }; @@ -143,7 +143,7 @@ function targetFrom(result: OptiNlpStructuredResult | undefined): string | undef return result.recommendedTarget; case "command_to_script": return result.target; - case "code_to_candidate_script": + case "code_to_full_script": return result.candidateTransformations[0]?.target; } } @@ -152,8 +152,8 @@ function scriptFrom(result: OptiNlpStructuredResult | undefined): string | undef return result?.kind === "command_to_script" ? result.generatedScript : undefined; } -function candidateScriptFrom(result: OptiNlpStructuredResult | undefined): string | undefined { - return result?.kind === "code_to_candidate_script" ? result.candidateScript : undefined; +function fullScriptFrom(result: OptiNlpStructuredResult | undefined): string | undefined { + return result?.kind === "code_to_full_script" ? result.fullScript : undefined; } function assumptionsFrom(result: OptiNlpStructuredResult | undefined): readonly string[] { From ec9eb6cd616532a0baf8120a44b43d4c407c1e04 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 1 Jul 2026 09:01:46 -0400 Subject: [PATCH 14/47] Add robust OptiNLP target prompt --- tools/optiNLP/eval/target_cases.md | 71 +++++ tools/optiNLP/eval/target_prompt_smoke.md | 9 + tools/optiNLP/knowledge/target_description.md | 287 ++++++++++++++++++ tools/optiNLP/knowledge/targets.md | 20 +- tools/optiNLP/prompts/01_target_generator.md | 100 +++++- 5 files changed, 477 insertions(+), 10 deletions(-) create mode 100644 tools/optiNLP/knowledge/target_description.md diff --git a/tools/optiNLP/eval/target_cases.md b/tools/optiNLP/eval/target_cases.md index ceef62788..072140cfb 100644 --- a/tools/optiNLP/eval/target_cases.md +++ b/tools/optiNLP/eval/target_cases.md @@ -358,3 +358,74 @@ Expected note: - The `.opti` text is used only for target reasoning. - Do not generate `Run.script_opti`. + +## Case 15: Prefer Call Argument Constraint Over Instruction Text + +Source: + +```c +void f() { + swap(a, b); + swap(c, d); +} +``` + +Request: + +```text +target the call to swap with arguments a and b +``` + +Expected target: + +```ocaml +[cCall "swap" ~args:[[cVar "a"]; [cVar "b"]]] +``` + +Rejected fragile target: + +```ocaml +[sInstr "swap(a, b);"] +``` + +Expected note: + +- Do not use exact instruction text because the call name and arguments provide + a stable semantic target. + +## Case 16: Prefer Array Write Body Constraint Over Expression Text + +Source: + +```c +void f(int n, int* out, int* tmp) { + for (int y = 0; y < n; y++) { + tmp[y] = y; + } + for (int y = 0; y < n; y++) { + out[y] = tmp[y]; + } +} +``` + +Request: + +```text +target the loop y whose body contains out[y] +``` + +Expected target: + +```ocaml +[cFor "y" ~body:[cArrayWrite "out"]] +``` + +Rejected fragile target: + +```ocaml +[cFor "y" ~body:[sExpr "out[y]"]] +``` + +Expected note: + +- Do not use `sExpr` because `cArrayWrite "out"` captures the semantic write. diff --git a/tools/optiNLP/eval/target_prompt_smoke.md b/tools/optiNLP/eval/target_prompt_smoke.md index b133bd139..db13d07ef 100644 --- a/tools/optiNLP/eval/target_prompt_smoke.md +++ b/tools/optiNLP/eval/target_prompt_smoke.md @@ -21,6 +21,10 @@ The evaluation set now covers: - after-loop positions with `tAfter`; - array writes with `cArrayWrite`; - loop body constraints such as `cFor "y" ~body:[cArrayWrite "out"]`; +- robust call argument constraints such as + `cCall "swap" ~args:[[cVar "a"]; [cVar "b"]]`; +- rejection examples for fragile `sExpr` and `sInstr` targets when semantic + selectors are available; - multiple named alternatives using `multi`; - ambiguous repeated loops that should trigger clarification; - printed OptiLambda used only as readable structure. @@ -33,6 +37,9 @@ Prompt 1 now explicitly says: - use stable names instead of line numbers in final target syntax; - add enclosing context when the same target name appears in several scopes; - use occurrence selectors for ordinal requests; +- avoid `sExpr`, `sExprRegexp`, `sInstr`, and `sInstrRegexp` unless semantic + selectors cannot express the requested location; +- prefer body and argument constraints over exact source text; - ask for source code when only a line number is given. ## Manual Pass Criteria @@ -42,6 +49,8 @@ A target-generation response passes when it: - uses existing `Target` constructors only; - returns the expected target or an equally specific accepted variant; - asks for clarification for ambiguous cases; +- rejects fragile text or expression targets when a stable semantic target is + visible in the source; - does not generate a transformation script; - does not claim `.opti` text is runnable input. diff --git a/tools/optiNLP/knowledge/target_description.md b/tools/optiNLP/knowledge/target_description.md new file mode 100644 index 000000000..52e73af0a --- /dev/null +++ b/tools/optiNLP/knowledge/target_description.md @@ -0,0 +1,287 @@ +# OptiTrust Target Description + +OptiTrust transformations operate on program locations. A target describes one +or more of those locations in the current AST. In generated scripts, a target is +written as an OCaml `constr list` using constructors from `Target`. + +Examples: + +```ocaml +[cFor "i"] +[cTopFunDef "main"; cCall "foo"] +[tBefore; cVarDef "tmp"] +[occIndex 1; cFor "i"] +``` + +## Purpose + +Targets let transformations find the right AST node without depending on fragile +editor details such as screen position or a source line number. A good target is +specific enough to select the intended node, but semantic enough to survive +small code edits. + +Use a target to name: + +- a whole function, function body, loop, call, variable definition, read, write, + array access, return, mark, label, or sequence; +- an insertion position before or after a node; +- a span or sequence boundary; +- one occurrence among several similar nodes; +- several intended nodes when a transformation is meant to apply to all of them. + +## Syntax + +A target is an OCaml list: + +```ocaml +[constraint1; constraint2; constraint3] +``` + +Constraints are resolved from left to right. Earlier constraints narrow the +search context for later constraints. + +Examples: + +```ocaml +[cTopFunDef "main"; cCall "foo"] +``` + +This means: find the top-level function definition `main`, then find a call to +`foo` inside it. + +```ocaml +[cFor "i"; cArrayWrite "out"] +``` + +This means: find the loop named `i`, then find a write to array `out` inside the +loop. + +## Common Constructors And Meanings + +Use these constructors as the default vocabulary for generated targets. + +Functions: + +```ocaml +[cFunDef "f"] (* any function definition named f *) +[cTopFunDef "f"] (* top-level function definition named f *) +[cFunBody "f"] (* body block of function f *) +[cTopFunBody "f"] (* body block of top-level function f *) +[cFunDefAndDecl "f"] (* function declaration and definition named f *) +``` + +Use a function definition target when the transformation affects the whole +function. Use a function body target when the transformation affects statements +inside the function. + +Loops: + +```ocaml +[cFor "i"] (* for loop whose loop index is i *) +[cFor_c "i"] (* C-style for loop whose loop index is i *) +[cForBody "i"] (* body of the for loop whose loop index is i *) +[cFors ["i"; "j"]] (* several named for loops *) +[cWhile ()] (* while loop *) +[cDoWhile ()] (* do-while loop *) +``` + +`cFor "i"` targets the loop instruction itself. `cForBody "i"` targets the +sequence of statements inside the loop. Use the body form for transformations +that operate on the loop contents rather than on the loop node. + +Calls: + +```ocaml +[cCall "foo"] (* call site of function foo *) +[cCalls ["foo"; "bar"]] (* call sites of foo and bar *) +[cCall "foo" ~args:[[cVar "x"]]] +``` + +Use argument constraints when the same function is called multiple times with +different arguments. + +Variables, reads, and writes: + +```ocaml +[cVarDef "x"] (* variable declaration or definition of x *) +[cVarDefs ["x"; "y"]] (* definitions of x and y *) +[cVarsDef "x"] (* variable definition group containing x *) +[cVarInit "x"] (* initializer of variable x *) +[cVar "x"] (* any occurrence of variable x *) +[cReadVar "x"] (* read occurrence of x *) +[cWriteVar "x"] (* write occurrence of x *) +[cWrite ()] (* any write instruction or write expression *) +[cRead ()] (* any read expression *) +[cReadOrWrite ()] (* any read or write *) +``` + +Prefer `cReadVar` or `cWriteVar` when the user says read or write. Use `cVar` +only when either kind of occurrence is acceptable. + +Arrays, cells, and fields: + +```ocaml +[cArrayRead "a"] (* read from array a *) +[cArrayWrite "a"] (* write to array a *) +[cCellRead ~base:[cVar "a"] ()] (* read from a cell based on a *) +[cCellWrite ~base:[cVar "a"] ()] (* write to a cell based on a *) +[cFieldRead ~field:"x" ()] (* read of field x *) +[cFieldWrite ~field:"x" ()] (* write of field x *) +``` + +Use array or field selectors when the user identifies a location by memory +access, for example "the loop that writes to out". + +Control flow, sequences, and markers: + +```ocaml +[cIf ()] (* if statement *) +[cThen] (* then branch *) +[cSeq ()] (* sequence/block *) +[cReturn ()] (* return statement *) +[cBreak] (* break statement *) +[cContinue] (* continue statement *) +[cLabel "done"] (* label named done *) +[cGoto ~label:"done" ()] +[cMark "name"] (* OptiTrust mark named name *) +[cMarkAny] (* any OptiTrust mark *) +[cOmp ()] (* OpenMP directive *) +``` + +Use marks when the script or source already contains a stable OptiTrust mark. + +## Composition Patterns + +Add enclosing context before the node selector: + +```ocaml +[cTopFunDef "main"; cCall "foo"] +[cFunBody "f"; cFor "i"] +[cFor "i"; cArrayWrite "out"] +``` + +Add body constraints when the target should be identified by what appears inside +it: + +```ocaml +[cFor "y" ~body:[cArrayWrite "out"]] +[cFor "i" ~body:[cCall "work"]] +``` + +Add argument constraints when a call is best identified by its arguments: + +```ocaml +[cCall "swap" ~args:[[cVar "a"]; [cVar "b"]]] +``` + +Use list constructors for several named nodes: + +```ocaml +[cFors ["i"; "j"]] +[cVarDefs ["x"; "y"]] +[cCalls ["init"; "cleanup"]] +``` + +## Robust Target Style + +Prefer semantic selectors because they describe program structure: + +```ocaml +[cFor "i"] +[cTopFunDef "f"; cCall "work"] +[cFor "y" ~body:[cArrayWrite "out"]] +[tBefore; cVarDef "c"] +``` + +Use context when a simple selector may match too much: + +```ocaml +[cTopFunDef "main"; cCall "foo"] +[cFunBody "f"; cFor "i"] +``` + +Use occurrence selectors when the request names an ordinal occurrence: + +```ocaml +[occIndex 1; cFor "i"] (* second loop named i *) +[occLast; cCall "cleanup"] +[occFirst; cVarDef "tmp"] +[cTopFunDef "f"; occIndex 2; cCall "foo"] +``` + +Use multiple-match constraints only when the user asks for all matches: + +```ocaml +[nbMulti; cCall "update"] +[nbExact 2; cFor "i"] +[nbAny; cMark "optional"] +``` + +Occurrence constraints: + +```ocaml +occIndex 0 (* first matching node, zero-based *) +occIndex 1 (* second matching node *) +occIndex (-1) (* last matching node *) +occFirst (* first matching node *) +occLast (* last matching node *) +nbMulti (* one or more matches, for intentional all-match targets *) +nbAny (* zero or more matches *) +nbExact 2 (* exactly two matches *) +``` + +## Position Targets + +Use `tBefore`, `tAfter`, `tFirst`, `tLast`, `tBetweenAll`, or span targets only +when the requested operation needs a position, such as insertion, movement, or a +boundary. + +Examples: + +```ocaml +[tBefore; cVarDef "x"] +[tAfter; cFor "i"] +[cFunBody "main"; tFirst] +[cForBody "i"; tBetweenAll] +``` + +Do not return only `[cVarDef "x"]` when the user asks for the position before +the declaration of `x`. + +## Fragile Fallbacks + +String and expression selectors are fallback tools: + +```ocaml +sInstr "x++;" +sExpr "i + 1" +sInstrRegexp "A\\[.*\\]" +sExprRegexp "MINDEX.*" +``` + +They can break when whitespace, formatting, parentheses, temporary variables, or +minor expression rewrites change. Prefer `cFor`, `cCall`, `cVarDef`, +`cArrayRead`, `cArrayWrite`, `cReadVar`, `cWriteVar`, body constraints, argument +constraints, and occurrence selectors whenever those can identify the target. + +Use `sExpr` only when no available semantic selector can express the requested +location, such as targeting a specific anonymous condition or expression that +has no stable name, call, variable, array, field, mark, or structural context. + +## Validation + +Target-only answers should include a small validation script using `Show.target` +when possible: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Show.target [cFor "i"]; +) +``` + +If a target may match several nodes, the answer must say so and either use an +occurrence selector for one node or `nbMulti` for an intentional multi-node +target. diff --git a/tools/optiNLP/knowledge/targets.md b/tools/optiNLP/knowledge/targets.md index f055f247e..263b8b066 100644 --- a/tools/optiNLP/knowledge/targets.md +++ b/tools/optiNLP/knowledge/targets.md @@ -28,6 +28,10 @@ unstated files or examples. interstitial position such as before or after an instruction. - Transformations often expect either one target or explicitly multiple targets. Use occurrence constraints when multiplicity matters. +- Robust targets should describe semantic AST structure rather than exact source + text. Prefer named functions, loops, calls, variables, array accesses, fields, + marks, enclosing context, body constraints, argument constraints, and + occurrence selectors before string or expression matching. ## Occurrence Constraints @@ -156,7 +160,6 @@ Nested constraints narrow the match by context: [cTopFunDef "main"; cCall "foo"] [cFunBody "main"; cFor "i"] [cFor "i"; cArrayWrite "A"] -[cIf ~cond:[sExpr "x < n"] (); dThen] [cFor "i" ~body:[cArrayWrite "out"]] [cCall "foo" ~args:[[cVar "x"]]] ``` @@ -188,6 +191,10 @@ Use empty names only when the user clearly wants a broad match, for example and `cVar "x"` only when either use is acceptable. - Exact instruction fallback: use `sInstr "..."` only when semantic selectors are not enough or the user explicitly references source text. +- Expression fallback: use `sExpr "..."` only when the requested expression has + no stable semantic selector, such as a specific anonymous condition that + cannot be identified by function, loop, call, variable, array, field, mark, + argument, body, or occurrence context. ## Ambiguity And Safety Rules @@ -212,10 +219,15 @@ sInstr "x++;" sExpr "i + 1" sInstrRegexp "A\\[.*\\]" sExprRegexp "MINDEX.*" +[cIf ~cond:[sExpr "x < n"] (); dThen] ``` Prefer semantic constructors such as `cFor`, `cCall`, `cVarDef`, `cArrayRead`, -and `cArrayWrite` before falling back to string matching. +`cArrayWrite`, `cReadVar`, `cWriteVar`, body constraints, argument constraints, +and occurrence selectors before falling back to string or expression matching. +String and expression selectors are more fragile because they can break after +formatting changes, equivalent expression rewrites, added temporaries, or small +source edits. ## Prompt Policy @@ -223,8 +235,10 @@ The target generator should: - quote exact identifiers as OCaml strings; - use current OptiTrust target constructors only; -- prefer semantic targets over line-number-only targets; +- prefer semantic targets over line-number-only, text-only, or expression-only + targets; - turn line references into structural targets when source code is available; +- avoid `sExpr` unless no stable semantic selector is available; - ask for clarification when two plausible targets remain; - mention why a target may match multiple nodes; - avoid inventing selectors not present in `Target`. diff --git a/tools/optiNLP/prompts/01_target_generator.md b/tools/optiNLP/prompts/01_target_generator.md index 4651bcba5..a751ff2a1 100644 --- a/tools/optiNLP/prompts/01_target_generator.md +++ b/tools/optiNLP/prompts/01_target_generator.md @@ -8,6 +8,14 @@ Your output must use the current OCaml target syntax used by OptiTrust scripts. Do not invent target constructors. If the request is ambiguous, ask a focused clarification instead of guessing. +Your main goal is to generate concise, robust targets. Prefer targets that +describe stable program structure over targets that depend on exact text, +formatting, or source line numbers. + +Concise means: use the shortest target that is still unambiguous in the provided +code. Robust means: use identifiers, AST structure, and explicit occurrence +constraints before exact source text. + ## Inputs You may receive: @@ -26,12 +34,19 @@ examples. ## Hard Rules - Use only known `Target` constructors. -- Prefer semantic targets over line-number-only reasoning. +- Prefer semantic targets over line-number-only or text-only reasoning. - Convert line references into structural targets when source code is available. - When a line contains a node with a stable name, use that name instead of the line number in the final target. - Use occurrence selectors when a target may match multiple nodes. - Do not pretend a target is unique if the code contains several matches. +- Avoid `sExpr`, `sExprRegexp`, `sInstr`, and `sInstrRegexp` unless semantic + selectors cannot express the requested location. +- If you must use a string or expression selector, explain why a more semantic + target is not available and mention that it may be more fragile. +- Do not use `sExpr` just because a condition, bound, or expression appears in + the request. First try to target the enclosing loop, branch, call, variable, + array access, field access, mark, occurrence, or body/argument structure. - Do not generate a full transformation script unless asked; this prompt only generates targets. - Do not assume `.opti` is runnable input. Use `.opti` only as readable program @@ -43,11 +58,34 @@ examples. 1. Identify the requested program entity: function, loop, call, variable definition, assignment, read/write, statement, mark, sequence, or position. 2. Locate all matching candidates in the provided code. -3. Choose the narrowest stable semantic target. -4. Add context constraints when needed, such as enclosing function or loop. +3. Choose the narrowest stable semantic target, favoring named functions, loops, + calls, variables, array accesses, fields, marks, and structural context. +4. Add context constraints when needed, such as enclosing function, loop body, + call arguments, or loop body contents. 5. Add occurrence constraints when the same selector still matches more than one node. -6. If ambiguity remains, ask a clarification and show the competing candidates. +6. Use text or expression selectors only as a final fallback. +7. If ambiguity remains, ask a clarification and show the competing candidates. + +## Robustness Priority + +Choose the first priority level that can identify the requested location: + +1. Named semantic selector: + `[cFor "i"]`, `[cCall "foo"]`, `[cVarDef "x"]`, `[cArrayWrite "out"]`. +2. Semantic selector with enclosing context: + `[cTopFunDef "main"; cCall "foo"]`. +3. Semantic selector with body or argument constraint: + `[cFor "y" ~body:[cArrayWrite "out"]]`, + `[cCall "swap" ~args:[[cVar "a"]; [cVar "b"]]]`. +4. Occurrence selector for repeated equivalent matches: + `[occIndex 1; cFor "i"]`, `[occLast; cCall "cleanup"]`. +5. Positional target when the operation needs a boundary: + `[tBefore; cVarDef "x"]`, `[tAfter; cFor "i"]`. +6. Exact instruction or expression fallback: + `[sInstr "..."]`, `[sExpr "..."]`. + +Do not skip directly to priority 6 when priorities 1-5 can work. ## Disambiguation Policy @@ -100,8 +138,6 @@ Variables and statements: [cVar "x"] [cReadVar "x"] [cWriteVar "x"] -[sInstr "x++;"] -[sExpr "i + 1"] ``` Array, field, and assignment-like targets: @@ -139,6 +175,37 @@ Multiple named alternatives: [any cArrayWrite ["A"; "B"]] ``` +Fallback string selectors, only when semantic selectors are insufficient: + +```ocaml +[sInstr "x++;"] +[sExpr "i + 1"] +``` + +Prefer this: + +```ocaml +[cFor "y" ~body:[cArrayWrite "out"]] +``` + +over this fragile form: + +```ocaml +[cFor "y" ~body:[sExpr "out[y]"]] +``` + +Prefer this: + +```ocaml +[cCall "swap" ~args:[[cVar "a"]; [cVar "b"]]] +``` + +over this fragile form: + +```ocaml +[sInstr "swap(a, b);"] +``` + ## Output Format Use this exact structure: @@ -157,7 +224,8 @@ One sentence describing the requested location. ``` ## Why This Target -Short explanation of why the target is stable and what it matches. +Short explanation of why the target is stable, what it matches, and whether it +depends on fragile text or expression matching. ## Ambiguities State "None." or ask one focused clarification question. @@ -220,6 +288,24 @@ Recommended target: User request: +```text +target the y loop that writes to out +``` + +Recommended target: + +```ocaml +[cFor "y" ~body:[cArrayWrite "out"]] +``` + +Why not `sExpr`: + +```text +The array write selector is more robust than matching the exact expression text. +``` + +User request: + ```text insert before variable c ``` From 9c39e11c7ba0a5c012a05abfcdc3b99f58219483 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 1 Jul 2026 09:01:57 -0400 Subject: [PATCH 15/47] Refine OptiNLP modes and result handling --- .../src/commands/optinlpCommands.ts | 67 ++++++++++++++++--- .../vscode-optitrust/src/optinlp/cli.test.ts | 12 ++++ tools/vscode-optitrust/src/optinlp/modes.ts | 9 ++- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/tools/vscode-optitrust/src/commands/optinlpCommands.ts b/tools/vscode-optitrust/src/commands/optinlpCommands.ts index bebc7f3d9..75810c79d 100644 --- a/tools/vscode-optitrust/src/commands/optinlpCommands.ts +++ b/tools/vscode-optitrust/src/commands/optinlpCommands.ts @@ -6,7 +6,7 @@ import { loadOptiNlpAssets, inferLanguage } from "../optinlp/assets"; import { generateOptiNlp } from "../optinlp/generation"; import { modeDefinition, resolveRequestedMode } from "../optinlp/modes"; import { OptiNlpProviderError } from "../optinlp/providerErrors"; -import { createOptiNlpProvider, DEFAULT_OPTINLP_PROVIDER, OptiNlpProviderId, parseOptiNlpProviderId } from "../optinlp/providerFactory"; +import { createOptiNlpProvider, DEFAULT_OPTINLP_PROVIDER, parseOptiNlpProviderId } from "../optinlp/providerFactory"; import { OptiNlpMode, OptiNlpProviderRequest, OptiNlpProviderResult } from "../optinlp/providerTypes"; import { editorActionForResult } from "../optinlp/resultActions"; import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; @@ -17,9 +17,9 @@ import { OptitrustWorkspace } from "../optitrust/workspace"; const GEMINI_API_KEY_SECRET = "optinlp.geminiApiKey"; const OPENAI_API_KEY_SECRET = "optinlp.openaiApiKey"; -interface SourceContext { +export interface SourceContext { readonly text: string; - readonly label: "selection" | "full file"; + readonly label: "selection" | "full file" | "associated source" | "target-at-cursor context"; } export interface OptiNlpGenerationOutcome { @@ -29,10 +29,13 @@ export interface OptiNlpGenerationOutcome { readonly result: OptiNlpProviderResult; } -interface OptiNlpGenerationOptions { +export interface OptiNlpGenerationOptions { readonly renderToOutput?: boolean; readonly editor?: vscode.TextEditor; readonly throwProviderErrors?: boolean; + readonly sourceContext?: SourceContext; + readonly filePath?: string; + readonly language?: string; } export async function setOptiNlpGeminiApiKey(context: vscode.ExtensionContext): Promise { @@ -130,7 +133,7 @@ export async function runOptiNlpGeneration( ): Promise { const resolvedMode = resolveRequestedMode(mode, userRequest); const editorContext = getActiveEditorContext(workspace.root, options.editor); - const sourceContext = await getSourceContext(editorContext.editor); + const sourceContext = options.sourceContext ?? (await getSourceContext(editorContext.editor)); if (!sourceContext) { return undefined; } @@ -140,8 +143,8 @@ export async function runOptiNlpGeneration( mode: resolvedMode, userRequest, sourceText: sourceContext.text, - filePath: editorContext.relativePath, - language: inferLanguage(editorContext.filePath), + filePath: options.filePath ?? editorContext.relativePath, + language: options.language ?? inferLanguage(editorContext.filePath), promptText: assets.promptText, knowledgeText: assets.knowledgeText, sessionSummary: memory.summary() @@ -191,9 +194,9 @@ export async function runOptiNlpGeneration( } async function getSourceContext(editor: vscode.TextEditor): Promise { - const selectedText = editor.document.getText(editor.selection); - if (selectedText.trim().length > 0) { - return { text: selectedText, label: "selection" }; + const selectedContext = selectedSourceContextFromEditor(editor); + if (selectedContext) { + return selectedContext; } const sendFullFile = await vscode.window.showWarningMessage( @@ -204,9 +207,22 @@ async function getSourceContext(editor: vscode.TextEditor): Promise 0 ? { text: selectedText, label: "selection" } : undefined; +} + +function fullFileSourceContextFromEditor(editor: vscode.TextEditor): SourceContext { return { text: editor.document.getText(), label: "full file" }; } +export function sourceContextFromEditor(editor: vscode.TextEditor): SourceContext { + return selectedSourceContextFromEditor(editor) ?? fullFileSourceContextFromEditor(editor); +} + async function promptForRequest(mode: OptiNlpMode, title: string): Promise { const definition = modeDefinition(mode); const value = await vscode.window.showInputBox({ @@ -276,6 +292,37 @@ export async function insertTextAtCursor(text: string, sourceEditor?: vscode.Tex }); } +export async function insertTargetAtCursor(text: string, sourceEditor?: vscode.TextEditor): Promise { + const editor = sourceEditor ?? vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showWarningMessage("OptiNLP: No active editor for insertion."); + return; + } + + const selection = editor.selection; + await editor.edit(edit => { + if (!selection.isEmpty) { + edit.replace(selection, text); + return; + } + + const line = editor.document.lineAt(selection.active.line); + const emptyTargetMatch = /\[\s*\]/u.exec(line.text); + if (emptyTargetMatch?.index !== undefined) { + edit.replace( + new vscode.Range( + new vscode.Position(line.lineNumber, emptyTargetMatch.index), + new vscode.Position(line.lineNumber, emptyTargetMatch.index + emptyTargetMatch[0].length) + ), + text + ); + return; + } + + edit.insert(selection.active, text); + }); +} + export async function openOcamlDocument(text: string): Promise { const document = await vscode.workspace.openTextDocument({ content: text.endsWith("\n") ? text : `${text}\n`, diff --git a/tools/vscode-optitrust/src/optinlp/cli.test.ts b/tools/vscode-optitrust/src/optinlp/cli.test.ts index 191ba3073..3378a5a08 100644 --- a/tools/vscode-optitrust/src/optinlp/cli.test.ts +++ b/tools/vscode-optitrust/src/optinlp/cli.test.ts @@ -37,12 +37,24 @@ async function testLanguageInference(): Promise { async function testAssetLoading(): Promise { const assets = await loadOptiNlpAssets(path.resolve(__dirname, "../../../.."), "target"); assert.match(assets.promptText, /OptiTrust Target Generator/u); + assert.match(assets.promptText, /Robustness Priority/u); + assert.match(assets.promptText, /Do not skip directly to priority 6/u); + assert.match(assets.knowledgeText, /OptiTrust Target Description/u); + assert.match(assets.knowledgeText, /cFor "i"` targets the loop instruction itself/u); assert.match(assets.knowledgeText, /OptiTrust Target Knowledge/u); + assert.match(assets.knowledgeText, /Use `sExpr` only when no available semantic selector/u); } async function testWholeFileScriptRouting(): Promise { assert.strictEqual(resolveRequestedMode("command_to_script", "generate a complete transformation script for the whole file"), "code_to_full_script"); assert.strictEqual(resolveRequestedMode("auto", "write matmul.ml for this source"), "code_to_full_script"); + assert.strictEqual( + resolveRequestedMode( + "target", + "Generate robust OptiTrust target suggestions for tests/loop/unroll/loop_unroll.ml:11.\nCurrent transformation line:\n!! Loop.unroll" + ), + "target" + ); } async function testCliTargetMarkdown(): Promise { diff --git a/tools/vscode-optitrust/src/optinlp/modes.ts b/tools/vscode-optitrust/src/optinlp/modes.ts index d4690319b..110b71bbd 100644 --- a/tools/vscode-optitrust/src/optinlp/modes.ts +++ b/tools/vscode-optitrust/src/optinlp/modes.ts @@ -23,7 +23,7 @@ export const OPTINLP_MODE_DEFINITIONS: readonly OptiNlpModeDefinition[] = [ shortLabel: "Target", placeholder: "target the second loop named i", promptFile: "01_target_generator.md", - knowledgeFiles: ["targets.md"] + knowledgeFiles: ["target_description.md", "targets.md"] }, { id: "command_to_script", @@ -32,7 +32,7 @@ export const OPTINLP_MODE_DEFINITIONS: readonly OptiNlpModeDefinition[] = [ shortLabel: "Script", placeholder: "unroll the loop i", promptFile: "02_command_to_script.md", - knowledgeFiles: ["targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] + knowledgeFiles: ["target_description.md", "targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] }, { id: "code_to_full_script", @@ -41,7 +41,7 @@ export const OPTINLP_MODE_DEFINITIONS: readonly OptiNlpModeDefinition[] = [ shortLabel: "Full Transformation", placeholder: "generate a complete transformation script for the whole file", promptFile: "03_code_to_full_script.md", - knowledgeFiles: ["targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] + knowledgeFiles: ["target_description.md", "targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] } ] as const; @@ -69,6 +69,9 @@ export function resolveAutoMode(mode: OptiNlpUiMode, request: string): OptiNlpMo } export function resolveRequestedMode(mode: OptiNlpUiMode, request: string): OptiNlpMode { + if (mode === "target") { + return "target"; + } if (isFullFileScriptRequest(request)) { return "code_to_full_script"; } From 84fb2de1e658249793d7a07912d237632276c2fc Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 1 Jul 2026 09:02:08 -0400 Subject: [PATCH 16/47] Improve OptiNLP panel target suggestions --- .../src/commands/optinlpPanel.ts | 172 +++++++++++++++--- 1 file changed, 142 insertions(+), 30 deletions(-) diff --git a/tools/vscode-optitrust/src/commands/optinlpPanel.ts b/tools/vscode-optitrust/src/commands/optinlpPanel.ts index 82860295e..a2a278920 100644 --- a/tools/vscode-optitrust/src/commands/optinlpPanel.ts +++ b/tools/vscode-optitrust/src/commands/optinlpPanel.ts @@ -2,7 +2,14 @@ // generation, provider selection, and editor actions live in shared modules. import * as path from "path"; import * as vscode from "vscode"; -import { clearOptiNlpSession, insertTextAtCursor, openOcamlDocument, runOptiNlpGeneration, setOptiNlpConfiguredProviderApiKey } from "./optinlpCommands"; +import { + clearOptiNlpSession, + insertTargetAtCursor, + openOcamlDocument, + OptiNlpGenerationOutcome, + runOptiNlpGeneration, + setOptiNlpConfiguredProviderApiKey +} from "./optinlpCommands"; import { inferLanguage } from "../optinlp/assets"; import { modeDefinition, OPTINLP_MODE_DEFINITIONS, OptiNlpUiMode, resolveAutoMode } from "../optinlp/modes"; import { OptiNlpMode } from "../optinlp/providerTypes"; @@ -18,6 +25,12 @@ interface WebviewMessage { readonly request?: string; readonly text?: string; readonly resultId?: string; + readonly target?: string; +} + +interface TargetSuggestion { + readonly label: string; + readonly target: string; } export class OptiNlpPanel { @@ -26,6 +39,7 @@ export class OptiNlpPanel { private resultSeq = 0; private readonly results = new Map(); private sourceEditor: vscode.TextEditor | undefined; + private sourceEditorLocked = false; private disposables: vscode.Disposable[] = []; private constructor( @@ -41,18 +55,18 @@ export class OptiNlpPanel { this.panel.webview.onDidReceiveMessage(message => this.handleMessage(message as WebviewMessage), undefined, this.disposables); vscode.window.onDidChangeActiveTextEditor(editor => { const fileEditor = asFileTextEditor(editor); - if (fileEditor) { + if (fileEditor && !this.sourceEditorLocked) { this.sourceEditor = fileEditor; } void this.refreshContext(); }, undefined, this.disposables); } - static show(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): void { + static show(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): OptiNlpPanel { if (OptiNlpPanel.current) { OptiNlpPanel.current.panel.reveal(vscode.ViewColumn.Beside); void OptiNlpPanel.current.refreshContext(); - return; + return OptiNlpPanel.current; } const panel = vscode.window.createWebviewPanel("optitrustOptiNlp", "OptiNLP", vscode.ViewColumn.Beside, { @@ -61,6 +75,26 @@ export class OptiNlpPanel { }); OptiNlpPanel.current = new OptiNlpPanel(context, workspace, memory, panel); void OptiNlpPanel.current.refreshContext(); + return OptiNlpPanel.current; + } + + setSourceEditor(editor: vscode.TextEditor, locked = false): void { + this.sourceEditor = editor; + this.sourceEditorLocked = locked; + void this.refreshContext(); + } + + postGenerationOutcome(outcome: OptiNlpGenerationOutcome): void { + this.postOutcome(outcome); + } + + postUserRequest(mode: OptiNlpUiMode, request: string, meta?: string): void { + this.post({ + type: "externalRequest", + mode, + request, + meta + }); } private async handleMessage(message: WebviewMessage): Promise { @@ -85,7 +119,7 @@ export class OptiNlpPanel { } return; case "insertTarget": - await this.insertTarget(message.resultId); + await this.insertTarget(message.resultId, message.target); return; case "openScript": await this.openScript(message.resultId); @@ -100,6 +134,7 @@ export class OptiNlpPanel { return; } + this.sourceEditorLocked = false; const resolvedMode = resolveAutoMode(mode, trimmed); this.post({ type: "busy", busy: true }); try { @@ -112,18 +147,7 @@ export class OptiNlpPanel { this.post({ type: "busy", busy: false }); return; } - const resultId = this.storeStructuredResult(outcome.result.structured); - this.post({ - type: "result", - resultId, - mode: outcome.mode, - request: outcome.userRequest, - sourceLabel: outcome.sourceLabel, - provider: outcome.result.provider, - model: outcome.result.model, - markdown: outcome.result.markdownOutput, - structured: outcome.result.structured - }); + this.postOutcome(outcome); await this.refreshContext(); } catch (error) { const message = @@ -138,13 +162,19 @@ export class OptiNlpPanel { } } - private async insertTarget(resultId: string | undefined): Promise { + private async insertTarget(resultId: string | undefined, target: string | undefined): Promise { + if (target && target.trim().length > 0) { + await insertTargetAtCursor(target, this.getSourceEditor()); + this.post({ type: "inserted" }); + return; + } + const result = resultId ? this.results.get(resultId) : undefined; if (!result || result.kind !== "target" || !result.recommendedTarget) { this.post({ type: "error", message: "No target expression is available for insertion." }); return; } - await insertTextAtCursor(result.recommendedTarget, this.getSourceEditor()); + await insertTargetAtCursor(result.recommendedTarget, this.getSourceEditor()); this.post({ type: "inserted" }); } @@ -171,6 +201,22 @@ export class OptiNlpPanel { return id; } + private postOutcome(outcome: OptiNlpGenerationOutcome): void { + const resultId = this.storeStructuredResult(outcome.result.structured); + this.post({ + type: "result", + resultId, + mode: outcome.mode, + request: outcome.userRequest, + sourceLabel: outcome.sourceLabel, + provider: outcome.result.provider, + model: outcome.result.model, + markdown: outcome.result.markdownOutput, + structured: outcome.result.structured, + targetSuggestions: targetSuggestions(outcome.result.structured) + }); + } + private async refreshContext(): Promise { const editor = this.getSourceEditor(); if (!editor) { @@ -190,6 +236,9 @@ export class OptiNlpPanel { } private getSourceEditor(): vscode.TextEditor | undefined { + if (this.sourceEditorLocked && this.sourceEditor) { + return this.sourceEditor; + } const activeEditor = asFileTextEditor(vscode.window.activeTextEditor); if (activeEditor) { this.sourceEditor = activeEditor; @@ -211,6 +260,27 @@ export class OptiNlpPanel { } } +function targetSuggestions(result: OptiNlpStructuredResult | undefined): TargetSuggestion[] { + if (!result || result.kind !== "target") { + return []; + } + + const seen = new Set(); + const suggestions: TargetSuggestion[] = []; + const add = (label: string, target: string | undefined): void => { + const trimmed = target?.trim(); + if (!trimmed || seen.has(trimmed)) { + return; + } + seen.add(trimmed); + suggestions.push({ label, target: trimmed }); + }; + + add("Recommended", result.recommendedTarget); + result.alternatives.forEach((target, index) => add(`Alternative ${index + 1}`, target)); + return suggestions; +} + function asFileTextEditor(editor: vscode.TextEditor | undefined): vscode.TextEditor | undefined { return editor?.document.uri.scheme === "file" ? editor : undefined; } @@ -417,6 +487,19 @@ function renderPanelHtml(): string { gap: 6px; align-items: center; } + .suggestions { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 0 12px 12px; + } + .suggestion { + max-width: 100%; + text-align: left; + font-family: var(--vscode-editor-font-family); + font-size: var(--vscode-editor-font-size); + overflow-wrap: anywhere; + } .status { min-height: 18px; color: var(--vscode-descriptionForeground); @@ -471,7 +554,10 @@ function renderPanelHtml(): string { form.addEventListener('submit', event => { event.preventDefault(); - const text = request.value.trim(); + sendRequest(request.value.trim()); + }); + + function sendRequest(text) { if (!text) return; const userMessage = { id: createMessageId(), @@ -485,7 +571,7 @@ function renderPanelHtml(): string { request.value = ''; saveAndRender(); vscode.postMessage({ type: 'generate', mode: mode.value, request: text }); - }); + } setKey.addEventListener('click', () => vscode.postMessage({ type: 'setApiKey' })); clear.addEventListener('click', () => { @@ -504,6 +590,8 @@ function renderPanelHtml(): string { status.textContent = message.busy ? 'Running...' : ''; } else if (message.type === 'result') { appendAssistantResult(message); + } else if (message.type === 'externalRequest') { + appendExternalRequest(message); } else if (message.type === 'error') { appendError(message.message || 'Unknown error'); } else if (message.type === 'copied') { @@ -526,12 +614,26 @@ function renderPanelHtml(): string { meta: modeTitle(message.mode) + ' · ' + message.sourceLabel + ' · ' + message.provider + ' · ' + message.model, resultId: message.resultId, kind: message.structured && message.structured.kind, + targetSuggestions: Array.isArray(message.targetSuggestions) ? message.targetSuggestions : [], replyTo: pendingUserId }); pendingUserId = undefined; saveAndRender(); } + function appendExternalRequest(message) { + const userMessage = { + id: createMessageId(), + role: 'user', + mode: message.mode || 'target', + text: message.request || '', + meta: message.meta || modeTitle(message.mode || 'target') + }; + pendingUserId = userMessage.id; + history.push(userMessage); + saveAndRender(); + } + function appendError(text) { history.push({ id: createMessageId(), @@ -574,15 +676,6 @@ function renderPanelHtml(): string { copy.addEventListener('click', () => vscode.postMessage({ type: 'copy', text: item.text })); actions.appendChild(copy); } - if (item.resultId && item.kind === 'target') { - const insert = document.createElement('button'); - insert.className = 'secondary icon'; - insert.type = 'button'; - insert.title = 'Insert target'; - insert.textContent = 'Insert'; - insert.addEventListener('click', () => vscode.postMessage({ type: 'insertTarget', resultId: item.resultId })); - actions.appendChild(insert); - } if (item.resultId && (item.kind === 'command_to_script' || item.kind === 'code_to_full_script')) { const open = document.createElement('button'); open.className = 'secondary icon'; @@ -599,6 +692,25 @@ function renderPanelHtml(): string { body.textContent = item.text; bubble.appendChild(header); bubble.appendChild(body); + if (item.targetSuggestions && item.targetSuggestions.length > 0) { + const suggestions = document.createElement('div'); + suggestions.className = 'suggestions'; + for (const suggestion of item.targetSuggestions) { + if (!suggestion || !suggestion.target) continue; + const button = document.createElement('button'); + button.className = 'secondary suggestion'; + button.type = 'button'; + button.title = suggestion.label ? suggestion.label + ': insert target' : 'Insert target'; + button.textContent = suggestion.target; + button.addEventListener('click', () => vscode.postMessage({ + type: 'insertTarget', + resultId: item.resultId, + target: suggestion.target + })); + suggestions.appendChild(button); + } + bubble.appendChild(suggestions); + } turn.appendChild(bubble); messages.appendChild(turn); } From 6ac0572f26e671c3954c5eaf4ff6fb3acc784d50 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 1 Jul 2026 09:04:55 -0400 Subject: [PATCH 17/47] Add OptiNLP target-at-cursor workflow --- tools/vscode-optitrust/README.md | 1 + tools/vscode-optitrust/package.json | 49 ++- .../src/commands/optinlpTargetAtCursor.ts | 287 ++++++++++++++++++ tools/vscode-optitrust/src/extension.ts | 11 + 4 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts diff --git a/tools/vscode-optitrust/README.md b/tools/vscode-optitrust/README.md index fd234898e..d8093316e 100644 --- a/tools/vscode-optitrust/README.md +++ b/tools/vscode-optitrust/README.md @@ -233,6 +233,7 @@ The QuickPick menu can: | `Ctrl+F5` | View trace with `-save-steps script` | | `Shift+F6` | View step trace | | `F5` | Redo last view command | +| `F7` | OptiNLP suggest target at cursor | | `F10` | Rerun last-tried tests | | `Ctrl+F10` | Run current test | | `Ctrl+Shift+F10` | Run current test and open diff | diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index c6461d4ad..b2fd02dd9 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -39,13 +39,16 @@ "onCommand:optitrust.openUnitTestMlCppFiles", "onCommand:optitrust.selectViewSyntax", "onCommand:optitrust.healthCheck", + "onCommand:optitrust.showShortcuts", "onCommand:optitrust.optinlpChat", "onCommand:optitrust.optinlpGenerateTarget", "onCommand:optitrust.optinlpGenerateScript", "onCommand:optitrust.optinlpGenerateFullTransformation", + "onCommand:optitrust.optinlpSuggestTargetAtCursor", "onCommand:optitrust.optinlpSetGeminiApiKey", "onCommand:optitrust.optinlpSetOpenAiApiKey", - "onCommand:optitrust.optinlpClearSession" + "onCommand:optitrust.optinlpClearSession", + "onChatParticipant:optitrust.optinlp" ], "main": "./out/extension.js", "bin": { @@ -126,9 +129,15 @@ "command": "optitrust.healthCheck", "title": "OptiTrust: Health Check" }, + { + "command": "optitrust.showShortcuts", + "title": "OptiTrust: Show Shortcuts", + "icon": "$(keyboard)" + }, { "command": "optitrust.optinlpChat", - "title": "OptiTrust: OptiNLP Chat" + "title": "OptiTrust: OptiNLP Chat", + "icon": "$(comment-discussion)" }, { "command": "optitrust.optinlpGenerateTarget", @@ -142,6 +151,11 @@ "command": "optitrust.optinlpGenerateFullTransformation", "title": "OptiTrust: OptiNLP Generate Full Transformation" }, + { + "command": "optitrust.optinlpSuggestTargetAtCursor", + "title": "OptiTrust: OptiNLP Suggest Target At Cursor", + "icon": "$(target)" + }, { "command": "optitrust.optinlpSetGeminiApiKey", "title": "OptiTrust: OptiNLP Set Gemini API Key" @@ -262,6 +276,11 @@ "key": "f5", "when": "editorTextFocus && config.optitrust.enableKeybindings" }, + { + "command": "optitrust.optinlpSuggestTargetAtCursor", + "key": "f7", + "when": "editorTextFocus && resourceExtname == .ml && config.optitrust.enableKeybindings" + }, { "command": "optitrust.runCurrentTest", "key": "ctrl+f10", @@ -291,9 +310,29 @@ "command": "optitrust.openAssociatedFiles", "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace", "group": "navigation@50" + }, + { + "command": "optitrust.optinlpChat", + "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", + "group": "navigation@55" + }, + { + "command": "optitrust.optinlpSuggestTargetAtCursor", + "when": "resourceExtname == .ml", + "group": "navigation@60" + }, + { + "command": "optitrust.showShortcuts", + "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace", + "group": "navigation@70" } ], "editor/context": [ + { + "command": "optitrust.optinlpSuggestTargetAtCursor", + "when": "resourceExtname == .ml", + "group": "optinlp@0" + }, { "command": "optitrust.optinlpGenerateTarget", "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", @@ -365,6 +404,9 @@ { "command": "optitrust.healthCheck" }, + { + "command": "optitrust.showShortcuts" + }, { "command": "optitrust.optinlpChat" }, @@ -377,6 +419,9 @@ { "command": "optitrust.optinlpGenerateFullTransformation" }, + { + "command": "optitrust.optinlpSuggestTargetAtCursor" + }, { "command": "optitrust.optinlpSetGeminiApiKey" }, diff --git a/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts new file mode 100644 index 000000000..698a49869 --- /dev/null +++ b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts @@ -0,0 +1,287 @@ +// F7 OptiNLP workflow: execute an OptiTrust script up to the line before the +// cursor, open the generated OptiLambda state, and ask the configured provider +// for robust target suggestions for the current transformation line. +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; +import { OptiNlpPanel } from "./optinlpPanel"; +import { runOptiNlpGeneration } from "./optinlpCommands"; +import { getActiveEditorContext } from "../optitrust/editor"; +import { markExecutedLine } from "../optitrust/decorations"; +import { runCommand } from "../optitrust/runner"; +import { validateTransformationScript } from "../optitrust/scripts"; +import { findAssociatedCSourceFile } from "../optitrust/files"; +import { openFileOrHtml } from "../optitrust/views"; +import { OptitrustWorkspace, relativeToRoot } from "../optitrust/workspace"; +import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; + +interface TargetAtCursorContext { + readonly scriptPath: string; + readonly scriptRelativePath: string; + readonly sourcePath: string; + readonly sourceRelativePath: string; + readonly afterOptiPath: string; + readonly transformationLine: number; + readonly executionLine: number; + readonly transformationText: string; + readonly scriptPrefix: string; + readonly sourceText: string; + readonly afterOptiText: string; +} + +interface PrefixScript { + readonly filePath: string; + readonly relativePath: string; + readonly noOpLine: number; +} + +async function exists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function readText(filePath: string): Promise { + return fs.readFile(filePath, "utf8"); +} + +async function writeText(filePath: string, text: string): Promise { + await fs.writeFile(filePath, text, "utf8"); +} + +async function findAfterOptiFile(scriptPath: string): Promise { + const parsed = path.parse(scriptPath); + const candidates = [ + path.join(parsed.dir, `${parsed.name}_after_surface.opti`), + path.join(parsed.dir, `${parsed.name}_after.opti`), + path.join(parsed.dir, `${parsed.name}_after_internal.opti`), + path.join(parsed.dir, `${parsed.name}_after_typed.opti`) + ]; + for (const candidate of candidates) { + if (await exists(candidate)) { + return candidate; + } + } + return undefined; +} + +function scriptPrefix(document: vscode.TextDocument, stopBeforeLine: number): string { + const end = new vscode.Position(Math.max(0, stopBeforeLine - 1), 0); + return document.getText(new vscode.Range(new vscode.Position(0, 0), end)); +} + +function lineText(document: vscode.TextDocument, line: number): string { + const index = Math.max(0, Math.min(document.lineCount - 1, line - 1)); + return document.lineAt(index).text; +} + +function currentTransformationStartLine(document: vscode.TextDocument, cursorLine: number): number { + for (let line = Math.max(1, cursorLine); line >= 1; line -= 1) { + const text = lineText(document, line); + if (/^\s*!!!?/u.test(text)) { + return line; + } + } + return cursorLine; +} + +function currentTransformationText(document: vscode.TextDocument, startLine: number, cursorLine: number): string { + const stopLine = Math.min(document.lineCount, Math.max(cursorLine, startLine) + 4); + const lines: string[] = []; + for (let line = startLine; line <= stopLine; line += 1) { + const text = lineText(document, line); + lines.push(text); + if (line > cursorLine && /^\s*\);?\s*$/u.test(text)) { + break; + } + } + return lines.join("\n").trim(); +} + +function patchScriptCppCall(scriptText: string, sourceFileName: string, outputPrefix: string): string { + const withExistingFilename = scriptText.replace( + /\bRun\.script_cpp\s+~filename\s*:\s*"[^"]*"/u, + `Run.script_cpp ~filename:"${sourceFileName}" ~prefix:"${outputPrefix}"` + ); + if (withExistingFilename !== scriptText) { + return withExistingFilename; + } + return scriptText.replace(/\bRun\.script_cpp\b/u, `Run.script_cpp ~filename:"${sourceFileName}" ~prefix:"${outputPrefix}"`); +} + +async function createPrefixScript( + workspace: OptitrustWorkspace, + editorContext: ReturnType, + sourcePath: string, + stopBeforeLine: number +): Promise { + const parsed = path.parse(editorContext.filePath); + const tempBase = `${editorContext.fileBase}_optinlp_prefix`; + const tempPath = path.join(parsed.dir, `${tempBase}.ml`); + const prefix = scriptPrefix(editorContext.document, stopBeforeLine); + const patchedPrefix = patchScriptCppCall(prefix, path.basename(sourcePath), editorContext.fileBase); + const trimmedPrefix = patchedPrefix.trimEnd(); + const noOpLine = trimmedPrefix.length === 0 ? 1 : trimmedPrefix.split(/\r?\n/u).length + 1; + const tempText = [ + trimmedPrefix, + " !!();", + ")", + "" + ].join("\n"); + + await writeText(tempPath, tempText); + return { + filePath: tempPath, + relativePath: relativeToRoot(workspace.root, tempPath), + noOpLine + }; +} + +async function cleanupPrefixScript(prefixScript: PrefixScript): Promise { + const parsed = path.parse(prefixScript.filePath); + const candidates = [ + prefixScript.filePath, + path.join(parsed.dir, `${parsed.name}.cmxs`) + ]; + await Promise.all(candidates.map(async candidate => { + try { + await fs.unlink(candidate); + } catch { + // Best-effort cleanup only. The generated after-state is kept. + } + })); +} + +function targetRequest(context: TargetAtCursorContext): string { + return [ + `Generate robust OptiTrust target suggestions for ${context.scriptRelativePath}:${context.transformationLine}.`, + `The OptiTrust script has been executed through line ${context.executionLine}, immediately before the current line.`, + "Use the matching C/C++ source and the generated OptiLambda after-state to infer the target for the current transformation line.", + "Prefer concise semantic targets. Avoid sExpr, sInstr, and other exact-text selectors unless no semantic target can work.", + "", + "Current transformation line:", + "```ocaml", + context.transformationText, + "```" + ].join("\n"); +} + +function targetSourceContext(context: TargetAtCursorContext): string { + return [ + `# Matching C/C++ Source: ${context.sourceRelativePath}`, + "```cpp", + context.sourceText.trimEnd(), + "```", + "", + `# OptiTrust Script Prefix Through Line ${context.executionLine}: ${context.scriptRelativePath}`, + "```ocaml", + context.scriptPrefix.trimEnd(), + "```", + "", + `# Current Transformation Line ${context.transformationLine}`, + "```ocaml", + context.transformationText, + "```", + "", + `# Generated OptiLambda State Before Current Line: ${relativeToRoot(path.dirname(context.scriptPath), context.afterOptiPath)}`, + "```optilambda", + context.afterOptiText.trimEnd(), + "```" + ].join("\n"); +} + +async function collectTargetAtCursorContext(workspace: OptitrustWorkspace, editorContext: ReturnType): Promise { + const source = await findAssociatedCSourceFile(editorContext.filePath); + if (!source) { + vscode.window.showWarningMessage(`OptiNLP: no matching C/C++ source file found for ${editorContext.fileBase}.ml.`); + return undefined; + } + const sourcePath = source.path; + + const transformationStartLine = currentTransformationStartLine(editorContext.document, editorContext.line); + const executionLine = Math.max(1, transformationStartLine - 1); + const prefixScript = await createPrefixScript(workspace, editorContext, sourcePath, transformationStartLine); + markExecutedLine(editorContext.editor, executionLine); + + try { + await runCommand({ + cwd: workspace.root, + command: path.join(workspace.root, "tools", "view_result.sh"), + args: ["step_diff", prefixScript.relativePath, String(prefixScript.noOpLine)], + title: "OptiTrust: Prepare OptiNLP Target Context", + env: { + NODIFFDISPLAY: "1", + OPTITRUST_NO_BROWSER: "1" + } + }); + } catch { + return undefined; + } finally { + await cleanupPrefixScript(prefixScript); + } + + const afterOptiPath = await findAfterOptiFile(editorContext.filePath); + if (!afterOptiPath) { + vscode.window.showWarningMessage(`OptiNLP: script ran, but no ${editorContext.fileBase}_after.opti file was found.`); + return undefined; + } + + return { + scriptPath: editorContext.filePath, + scriptRelativePath: editorContext.relativePath, + sourcePath, + sourceRelativePath: relativeToRoot(workspace.root, sourcePath), + afterOptiPath, + transformationLine: editorContext.line, + executionLine, + transformationText: currentTransformationText(editorContext.document, transformationStartLine, editorContext.line), + scriptPrefix: scriptPrefix(editorContext.document, transformationStartLine), + sourceText: await readText(sourcePath), + afterOptiText: await readText(afterOptiPath) + }; +} + +export async function suggestOptiNlpTargetAtCursor( + extensionContext: vscode.ExtensionContext, + workspace: OptitrustWorkspace, + memory: OptiNlpSessionMemory +): Promise { + const editorContext = getActiveEditorContext(workspace.root); + const validation = validateTransformationScript(editorContext); + if (!validation.ok) { + vscode.window.showWarningMessage(validation.reason ?? "Unsupported OptiTrust script."); + return; + } + + const collected = await collectTargetAtCursorContext(workspace, editorContext); + if (!collected) { + return; + } + + await vscode.window.showTextDocument(editorContext.document, { viewColumn: vscode.ViewColumn.One, preserveFocus: false }); + await openFileOrHtml(workspace.root, collected.afterOptiPath, path.basename(collected.afterOptiPath)); + + const panel = OptiNlpPanel.show(extensionContext, workspace, memory); + panel.setSourceEditor(editorContext.editor, true); + + const request = targetRequest(collected); + panel.postUserRequest("target", request, `Target · ${collected.scriptRelativePath}:${collected.transformationLine}`); + + const outcome = await runOptiNlpGeneration(extensionContext, workspace, memory, "target", request, { + renderToOutput: false, + editor: editorContext.editor, + sourceContext: { + text: targetSourceContext(collected), + label: "target-at-cursor context" + }, + filePath: collected.sourceRelativePath, + language: "cpp+optilambda+ocaml" + }); + + if (outcome) { + panel.postGenerationOutcome(outcome); + } +} diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index fdf76b469..5beb6a6a5 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -16,7 +16,9 @@ import { generateOptiNlpFullTransformation } from "./commands/optinlpCommands"; import { OptiNlpPanel } from "./commands/optinlpPanel"; +import { suggestOptiNlpTargetAtCursor } from "./commands/optinlpTargetAtCursor"; import { rerunLastTests, runCurrentTest, runCurrentTestAndOpenDiff } from "./commands/runTests"; +import { showShortcuts } from "./commands/shortcuts"; import { redoLastViewCommand, runViewCommand, @@ -214,6 +216,8 @@ export async function activate(context: vscode.ExtensionContext): Promise } }); + registerCommand(context, "optitrust.showShortcuts", showShortcuts); + registerCommand(context, "optitrust.optinlpChat", async () => { const workspace = await requireWorkspace(); if (workspace && optiNlpSession) { @@ -242,6 +246,13 @@ export async function activate(context: vscode.ExtensionContext): Promise } }); + registerCommand(context, "optitrust.optinlpSuggestTargetAtCursor", async () => { + const workspace = await requireWorkspace(); + if (workspace && optiNlpSession) { + await suggestOptiNlpTargetAtCursor(context, workspace, optiNlpSession); + } + }); + registerCommand(context, "optitrust.optinlpSetGeminiApiKey", async () => { await setOptiNlpGeminiApiKey(context); }); From 06c07c3bd87c2c52e8b9340e291e4ee254c7c8a9 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 1 Jul 2026 09:05:23 -0400 Subject: [PATCH 18/47] Add OptiTrust shortcuts command --- .../src/commands/shortcuts.ts | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tools/vscode-optitrust/src/commands/shortcuts.ts diff --git a/tools/vscode-optitrust/src/commands/shortcuts.ts b/tools/vscode-optitrust/src/commands/shortcuts.ts new file mode 100644 index 000000000..be48d31f9 --- /dev/null +++ b/tools/vscode-optitrust/src/commands/shortcuts.ts @@ -0,0 +1,96 @@ +// Shortcut help command. The list mirrors the default keybinding table in the +// VS Code extension README so users can discover the built-in workflow keys +// without opening documentation. +import * as vscode from "vscode"; + +interface ShortcutItem extends vscode.QuickPickItem { + readonly command: string; +} + +const SHORTCUTS: readonly ShortcutItem[] = [ + { + label: "F6", + description: "View step diff", + detail: "OptiTrust: View Step Diff", + command: "optitrust.viewDiff" + }, + { + label: "Ctrl+F6", + description: "View diff only code", + detail: "OptiTrust: View Diff Only Code", + command: "optitrust.viewDiffOnlyCode" + }, + { + label: "Ctrl+Shift+F6", + description: "View diff using internal syntax", + detail: "OptiTrust: View Diff Using Internal Syntax", + command: "optitrust.viewDiffInternalSyntax" + }, + { + label: "Shift+F5", + description: "View full trace", + detail: "OptiTrust: View Full Trace", + command: "optitrust.viewFullTrace" + }, + { + label: "Ctrl+F5", + description: "View trace with -save-steps script", + detail: "OptiTrust: View Trace Save Steps Script", + command: "optitrust.viewTraceSaveStepsScript" + }, + { + label: "Shift+F6", + description: "View step trace", + detail: "OptiTrust: View Step Trace", + command: "optitrust.viewStepTrace" + }, + { + label: "F5", + description: "Redo last view command", + detail: "OptiTrust: Redo Last View Command", + command: "optitrust.redoLastViewCommand" + }, + { + label: "F7", + description: "OptiNLP suggest target at cursor", + detail: "OptiTrust: OptiNLP Suggest Target At Cursor", + command: "optitrust.optinlpSuggestTargetAtCursor" + }, + { + label: "F10", + description: "Rerun last-tried tests", + detail: "OptiTrust: Rerun Last-Tried Tests", + command: "optitrust.rerunLastTests" + }, + { + label: "Ctrl+F10", + description: "Run current test", + detail: "OptiTrust: Run Current Test", + command: "optitrust.runCurrentTest" + }, + { + label: "Ctrl+Shift+F10", + description: "Run current test and open diff", + detail: "OptiTrust: Run Current Test And Open Diff", + command: "optitrust.runCurrentTestAndOpenDiff" + }, + { + label: "Alt+Shift+F10", + description: "Open unit test ML and CPP files", + detail: "OptiTrust: Open Unit Test ML And CPP Files", + command: "optitrust.openUnitTestMlCppFiles" + } +]; + +export async function showShortcuts(): Promise { + const picked = await vscode.window.showQuickPick(SHORTCUTS, { + title: "OptiTrust Shortcuts", + placeHolder: "Select a shortcut to run its command", + matchOnDescription: true, + matchOnDetail: true + }); + + if (picked) { + await vscode.commands.executeCommand(picked.command); + } +} From 39463408ce9c70cabaf313e94245c047deb93cf5 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Thu, 2 Jul 2026 04:36:45 -0400 Subject: [PATCH 19/47] refine prompt knowledge --- tools/optiNLP/knowledge/targets.md | 396 ++++++++++++++++++ tools/optiNLP/prompts/01_target_generator.md | 11 + tools/optiNLP/prompts/02_command_to_script.md | 10 + .../optiNLP/prompts/03_code_to_full_script.md | 10 + 4 files changed, 427 insertions(+) diff --git a/tools/optiNLP/knowledge/targets.md b/tools/optiNLP/knowledge/targets.md index 263b8b066..4bf38a480 100644 --- a/tools/optiNLP/knowledge/targets.md +++ b/tools/optiNLP/knowledge/targets.md @@ -20,6 +20,20 @@ The AI can only use the source code, script fragments, traces, errors, prompt text, and knowledge text included in the current request. Do not rely on unstated files or examples. +## Marked Selection Convention + +Some extension requests send the whole active file while wrapping the user's +selected focus in markers: + +```text +selected source text +``` + +Treat the marked span as the current focus, and use the rest of the file as +context for robust disambiguation. The markers are artificial annotations, not +program syntax. Never copy `` or `` into a generated target or +script. + ## Core Model - A target is a `constr list`. @@ -229,6 +243,388 @@ String and expression selectors are more fragile because they can break after formatting changes, equivalent expression rewrites, added temporaries, or small source edits. +## Full `Target` Constructor Coverage + +This section summarizes the important smart constructors exposed by +`lib/framework/target/target.ml`. Generate user-facing targets with these +constructors when they fit. Do not copy implementation helpers or invent +constructors outside this vocabulary. + +### Logic, Depth, And Grouping + +- `cTrue` matches anything; `cFalse` matches nothing. +- `cStrictNew` matches at depth zero. Use it inside low-level composite + targets when the same node must satisfy the next constraint exactly. +- `cStrict` matches at depth one. It is useful for direct children. +- `cInDepth` searches at any depth. +- `cInContracts` also searches inside contracts. +- `cTarget [ ... ]` wraps a list of constraints as one constraint. +- `cOr [[...]; [...]]` is a union of alternative targets. +- `cAnd [[...]; [...]]` is an intersection. +- `cDiff [[...]] [[...]]` matches the first target minus the second. +- `any cFor ["i"; "j"]` means any of several named alternatives. +- `multi cFor ["i"; "j"]` means multiple named alternatives and includes + `nbMulti`. +- `cPath p`, `target_of_path p`, and `target_of_paths ps` are path-based + helpers. Prefer semantic constructors over paths in generated scripts unless + the user is working directly with resolved paths. + +### Direction Constraints + +Direction constraints navigate inside a matched AST node. They are lower-level +than semantic constructors, but useful when the requested location is a part of +a construct: + +```ocaml +dRoot +dBefore 0 +dAfter 0 +dSeqNth 2 +dCond +dThen +dElse +dBody +dLetBody +dVarBody +dVarInit +dInit +dForStart +dForStop +dForStep +dForCInit +dForCStep +dName +dType +dArg 0 +dLHS +dRHS +``` + +Use examples: + +```ocaml +[cIf (); dCond] (* condition of an if *) +[cIf (); dThen] (* then branch *) +[cIf (); dElse] (* else branch *) +[cCall "f"; dArg 0] (* first argument of call f *) +[cWrite (); dLHS] (* left-hand side of a write *) +[cWrite (); dRHS] (* right-hand side of a write *) +[cFor "i"; dForStop] (* bound of loop i *) +``` + +Switch and enum directions exist for specialized cases: + +```ocaml +dDirCase 0 (dCaseName 0) +dDirCase 0 dCaseBody +dEnumConst 0 dEnumConstName +dEnumConst 0 dEnumConstVal +``` + +Prefer higher-level selectors when they express the same idea. + +### Type Constraints + +Many constructors accept `~typ:"..."` or `~typ_pred:...` to restrict by type. +Use `~typ` only when the type is visible and important to disambiguation. + +```ocaml +[cVarDef ~typ:"int" "n"] +[cVar ~typ:"double" "x"] +[cWriteVar ~typ:"int" "i"] +``` + +Related helpers: + +- `cHasType "int"` matches nodes with a printed type. +- `cHasTypeAst ty` and `cHasTypePred pred` are OCaml-level helpers. +- `with_type ~typ target` adds a type constraint to an existing target. +- `cArg "x"` and `cArg ~typ:"int" "x"` match function arguments by name and + optional type. + +### Variable And Definition Targets + +Variable definitions can be matched by name, optional regexp/substr matching, +initializer/body, and type: + +```ocaml +[cVarDef "x"] +[cVarDef ~body:[cInt 0] "x"] +[cVarDef ~typ:"int" "x"] +[cVarDefReg "tmp.*"] +[cVarDefs ["x"; "y"]] +[cVarsDef "x"] +[cVarInit "x"] +[cDef "x"] +``` + +Use `cVarsDef` for grouped declarations when the transformation targets a +multi-variable definition group. Use `cVarInit "x"` for the initializer, not the +whole definition. + +Variable occurrences: + +```ocaml +[cVar "x"] +[cVar ~substr:true "tmp"] +[cVarReg "tmp.*"] +[cReadVar "x"] +[cWriteVar "x"] +``` + +Use `cReadVar` for reads and `cWriteVar` for writes. Use `cVar` only when read +versus write does not matter. + +### Function Targets + +Function targets can match names, arguments, return type, body contents, top +level only, and declaration-vs-definition scope: + +```ocaml +[cFunDef "f"] +[cFunDefs ["f"; "g"]] +[cFunBody "f"] +[cFunDefAndDecl "f"] +[cTopFunDef "f"] +[cTopFunDefs ["f"; "g"]] +[cTopFunBody "f"] +[cTopFunDefAndDecl "f"] +[cTopFunDefReg "kernel_.*"] +[cTopFunDefAndDeclReg "kernel_.*"] +[cTop "f"] +``` + +Optional refinements: + +```ocaml +[cFunDef ~args:[[cVarDef "n"]] "f"] +[cTopFunDef ~ret_typ:"int" "main"] +[cTopFunDef ~body:[cFor "i"] "f"] +``` + +Use `cTopFunDef` when the user names a top-level C/C++ function. Use `cFunBody` +or `cTopFunBody` when the transformation targets the statement sequence inside +the function. + +### Loop And Branch Targets + +Simple OptiTrust loops: + +```ocaml +[cFor "i"] +[cFor ~start:[cInt 0] "i"] +[cFor ~stop:[cVar "n"] "i"] +[cFor ~step:[cInt 1] "i"] +[cFor ~body:[cArrayWrite "out"] "i"] +[cFors ["i"; "j"]] +[cForBody "i"] +[cForNestedAtDepth 2] +``` + +C-style loops: + +```ocaml +[cFor_c "i"] +[cFor_c ~cond:[sExpr "i < n"] "i"] +``` + +Prefer `cFor "i"` for normal OptiTrust simple loops and `cFor_c "i"` when the +source still has a C-style `for (init; cond; step)` shape. + +Other control flow: + +```ocaml +[cWhile ()] +[cWhile ~cond:[cVar "keep"] ()] +[cDoWhile ()] +[cIf ()] +[cIf ~cond:[cVar "ok"] ()] +[cThen] +[cSwitch ()] +[cSwitch ~cond:[cVar "tag"] ()] +[cReturn ()] +[cReturn ~res:[cVar "x"] ()] +[cBreak] +[cContinue] +[cAbort ()] +``` + +Use `cIf` with semantic `~cond`, `~then_`, or `~else_` targets when possible. +Use `dThen` or `dElse` after `cIf` when the user asks for a branch position or +branch body. + +Switch cases use case descriptors inside `cSwitch`, not as standalone +constraints: + +```ocaml +[cSwitch ~cases:[(cCase ~value:[cInt 0] (), [cBreak])] ()] +[cSwitch ~cases:[(cDefault, [cReturn ()])] ()] +``` + +### Calls, Arguments, And Primitive Operations + +Function calls: + +```ocaml +[cCall "foo"] +[cCalls ["foo"; "bar"]] +[cCall "foo" ~args:[[cVar "x"]; [cVar "y"]]] +[cCall ~regexp:true "foo_.*"] +[cCall ~fun_:[cVar "fp"] ""] +``` + +Do not provide both `name` and `~fun_` except with an empty name. Use +`~accept_encoded:true` only for primitive or encoded calls when examples show it. + +Argument-list helpers used by call/function constructors: + +- `target_list_simpl [[...]; [...]]` means exact argument targets. +- `target_list_one_st target` means at least one item satisfies the target. +- `target_list_all_st target` means all items satisfy the target. +- `target_list_pred_default` means no argument restriction. + +Primitive and operator targets: + +```ocaml +[cPrim p] +[cPrimCall p] +[cPrimPredCall pred] +[cPrimCallArith ()] +[cBinop Binop_mul] +[cPlusEq ()] +[cDiv ()] +[cMul ()] +[cRef ()] +[cNew ()] +[cDelete ()] +``` + +These are lower-level. Prefer domain-specific selectors such as `cWrite`, +`cRead`, `cArrayWrite`, `cCall`, or `cVar` unless the user explicitly asks for +an operator or primitive. + +### Reads, Writes, Arrays, Cells, And Fields + +General reads/writes: + +```ocaml +[cWrite ()] +[cWrite ~lhs:[cVar "x"] ()] +[cWrite ~lhs:[cVar "x"] ~rhs:[cInt 0] ()] +[cRead ()] +[cRead ~addr:[cVar "x"] ()] +[cReadOrWrite ()] +``` + +Array and cell access: + +```ocaml +[cAccesses ()] +[cCellAccess ~base:[cVar "a"] ()] +[cCellAccess ~base:[cVar "a"] ~index:[cVar "i"] ()] +[cCellRead ~base:[cVar "a"] ()] +[cCellWrite ~base:[cVar "a"] ()] +[cCellReadOrWrite ~base:[cVar "a"] ()] +[cArrayRead "a"] +[cArrayRead ~index:[cVar "i"] "a"] +[cArrayWrite "a"] +[cArrayWriteAccess "a"] +[cArrayInit] +[cCell ()] +``` + +`cArrayRead "a"` excludes writes to `a`; `cArrayWrite "a"` matches writes to +cells of `a`. Use `cCellAccess` when the base/index structure matters. Use +`cCell` mainly for array-initialization cells. + +Field and struct access: + +```ocaml +[cFieldAccess ~field:"next" ()] +[cFieldRead ~field:"next" ()] +[cFieldWrite ~field:"next" ()] +[cFieldReadOrWrite ~field:"next" ()] +``` + +Access constructors support `~base`, `~field`, `~substr`, and `~regexp`. + +### Literals, Types, Enums, Namespaces, And Includes + +Use literal selectors only when the literal itself is the requested target or a +needed disambiguator: + +```ocaml +[cLit] +[cInt 0] +[cDouble 1.0] +[cBool true] +[cString "hello"] +``` + +Other declarations: + +```ocaml +[cInclude "stdio.h"] +[cTypDef "T"] +[cEnum ~name:"Color" ()] +[cEnum ~constants:[("RED", [cInt 0])] ()] +[cNamespace "ns"] +``` + +Labels and special OptiTrust helper calls: + +```ocaml +[cLabel "done"] +[cGoto ~label:"done" ()] +[cAny] +[cChoose] +[cMindex ()] +[cOmp ()] +``` + +### Marks And Spans + +Marks are stable when the script intentionally placed them: + +```ocaml +[cMark "m"] +[cMarks ["m1"; "m2"]] +[cMarkAny] +[cMarkSpan "m"] +[cMarkSpanStart "m"] +[cMarkSpanStop "m"] +``` + +Use `cMarkSpan "m"` for a span marked by OptiTrust span marks. Use +`cMarkSpanStart` or `cMarkSpanStop` only when the boundary mark itself is the +target. + +### Resolver And Transformation Utilities + +These functions explain how targets are used by transformations. They are not +usually emitted by the target generator unless the user asks for target-debug or +transformation implementation code. + +- `check target` resolves a target for debugging. +- `enable_multi_targets target` adds `nbMulti` if no occurrence constraint is + already present. +- `filter_constr_occurrence target` removes occurrence constraints. +- `fix_target_multi target` automatically permits multiple matches for logical + `cOr`/`cAnd` targets when no occurrence constraint is present. +- `resolve_target`, `resolve_target_exactly_one`, `resolve_target_between`, + `resolve_target_span`, and exact-one variants resolve targets to paths. +- `get_trm_at target` and `get_trm_at_exn target` retrieve the AST node at a + unique target. +- `iter`, `iteri`, and `foreach` apply code to each resolved path. +- `apply_at_target_paths`, `applyi_at_target_paths`, + `apply_at_target_paths_before`, and `apply_at_target_paths_in_seq` are used + by transformations that edit target nodes or positions. +- `reparse_after` wraps transformations that need the modified C/C++ to be + reparsed after editing. +- String representation helpers compute printed code for `sInstr`, `sExpr`, + and regexp selectors. This is why string selectors are slower and more + fragile than semantic constructors. + ## Prompt Policy The target generator should: diff --git a/tools/optiNLP/prompts/01_target_generator.md b/tools/optiNLP/prompts/01_target_generator.md index a751ff2a1..3fa0c2118 100644 --- a/tools/optiNLP/prompts/01_target_generator.md +++ b/tools/optiNLP/prompts/01_target_generator.md @@ -31,6 +31,17 @@ Use only the source code, script fragments, trace/error text, prompts, and knowledge included in the current request. Do not rely on unstated files or examples. +## Marked Selection Input + +When the source contains `` and `` markers, the full file is still +the available context. The text between the markers is the user's selected +focus. Use the surrounding file to disambiguate names, occurrences, enclosing +functions, and nearby statements, but prioritize the marked span when deciding +which target the user is asking about. + +The markers are not part of the program and must never appear inside generated +OptiTrust targets or scripts. + ## Hard Rules - Use only known `Target` constructors. diff --git a/tools/optiNLP/prompts/02_command_to_script.md b/tools/optiNLP/prompts/02_command_to_script.md index f73c55f1b..714b69e2d 100644 --- a/tools/optiNLP/prompts/02_command_to_script.md +++ b/tools/optiNLP/prompts/02_command_to_script.md @@ -22,6 +22,16 @@ Use only the source code, script examples, trace/error text, prompts, and knowledge included in the current request. Do not rely on unstated files or examples. +## Marked Selection Input + +When the source contains `` and `` markers, the full file is still +the available context. The text between the markers is the user's selected +focus. Use the surrounding file to resolve the target robustly, then generate +the script for the requested command. + +The markers are not part of the program and must never appear inside generated +OptiTrust targets or scripts. + ## Hard Rules - Use only transformations described in the prompt, knowledge, or current diff --git a/tools/optiNLP/prompts/03_code_to_full_script.md b/tools/optiNLP/prompts/03_code_to_full_script.md index 17fc01002..46076bd55 100644 --- a/tools/optiNLP/prompts/03_code_to_full_script.md +++ b/tools/optiNLP/prompts/03_code_to_full_script.md @@ -27,6 +27,16 @@ Use only the source code, script examples, trace/error text, prompts, and knowledge included in the current request. Do not rely on unstated files or examples. +## Marked Selection Input + +When the source contains `` and `` markers, the full file is still +the available context. The text between the markers is the user's selected +focus. Use it as a strong hint for the transformation region or current point +of interest, but keep the generated script coherent for the full input file. + +The markers are not part of the program and must never appear inside generated +OptiTrust targets or scripts. + ## Hard Rules - Generate a complete OCaml OptiTrust script, not just a target and not just a From 4b1c6aa0c37974c9ff9f59b0962ff0d6512a7871 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Thu, 2 Jul 2026 04:37:06 -0400 Subject: [PATCH 20/47] add provider session reuse for optinlp --- .../vscode-optitrust/src/optinlp/cli.test.ts | 12 ++++++++++ .../src/optinlp/geminiProvider.ts | 3 ++- .../src/optinlp/openaiProvider.ts | 18 +++++++++++--- .../src/optinlp/provider.test.ts | 24 +++++++++++++++++-- .../src/optinlp/providerTypes.ts | 13 +++++++++- .../src/optinlp/sessionMemory.test.ts | 23 ++++++++++++++++++ .../src/optinlp/sessionMemory.ts | 24 ++++++++++++++++++- 7 files changed, 109 insertions(+), 8 deletions(-) diff --git a/tools/vscode-optitrust/src/optinlp/cli.test.ts b/tools/vscode-optitrust/src/optinlp/cli.test.ts index 3378a5a08..e3b9bcbba 100644 --- a/tools/vscode-optitrust/src/optinlp/cli.test.ts +++ b/tools/vscode-optitrust/src/optinlp/cli.test.ts @@ -6,6 +6,7 @@ import { Writable } from "stream"; import { inferLanguage, loadOptiNlpAssets } from "./assets"; import { runOptiNlpCli } from "./cli"; import { resolveRequestedMode } from "./modes"; +import { markSelectedRangeInText } from "./sourceContext"; class MemoryWritable extends Writable { chunks: string[] = []; @@ -57,6 +58,16 @@ async function testWholeFileScriptRouting(): Promise { ); } +async function testMarkedSelectionContext(): Promise { + const source = "void f() {\n a();\n b();\n}\n"; + const selected = " b();"; + const start = source.indexOf(selected); + assert.strictEqual( + markSelectedRangeInText(source, start, start + selected.length), + "void f() {\n a();\n b();\n}\n" + ); +} + async function testCliTargetMarkdown(): Promise { await withTempSource(async filePath => { const stdout = new MemoryWritable(); @@ -123,6 +134,7 @@ async function main(): Promise { await testLanguageInference(); await testAssetLoading(); await testWholeFileScriptRouting(); + await testMarkedSelectionContext(); await testCliTargetMarkdown(); await testCliScriptJson(); await testCliFullRequest(); diff --git a/tools/vscode-optitrust/src/optinlp/geminiProvider.ts b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts index 4901cf3a0..b4a586fea 100644 --- a/tools/vscode-optitrust/src/optinlp/geminiProvider.ts +++ b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts @@ -85,7 +85,8 @@ export class GeminiProvider implements OptiNlpProvider { response = await this.fetchImpl(url, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) + body: JSON.stringify(body), + signal: request.abortSignal }); } catch (error) { throw new OptiNlpProviderError( diff --git a/tools/vscode-optitrust/src/optinlp/openaiProvider.ts b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts index 8f45c44b3..06e54338f 100644 --- a/tools/vscode-optitrust/src/optinlp/openaiProvider.ts +++ b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts @@ -25,6 +25,7 @@ interface OpenAiOutputItem { } interface OpenAiResponse { + readonly id?: string; readonly output?: readonly OpenAiOutputItem[]; readonly output_text?: string; readonly error?: { @@ -34,6 +35,7 @@ interface OpenAiResponse { export class OpenAiProvider implements OptiNlpProvider { readonly name = "openai"; + readonly supportsProviderSession = true; readonly model: string; private readonly apiKey?: string; private readonly apiKeyProvider?: OpenAiProviderOptions["apiKeyProvider"]; @@ -70,7 +72,14 @@ export class OpenAiProvider implements OptiNlpProvider { throw new OptiNlpProviderError(this.name, "Set OpenAI API key before using OptiNLP.", "Missing OpenAI API key."); } - const body = { + const providerSessionEnabled = request.providerSessionEnabled === true; + const body: { + readonly model: string; + readonly instructions: string; + readonly input: string; + readonly store: boolean; + readonly previous_response_id?: string; + } = { model: this.model, instructions: [ "You are an OptiNLP provider.", @@ -79,7 +88,8 @@ export class OpenAiProvider implements OptiNlpProvider { "Do not add provider notes, apologies, or extra sections." ].join("\n"), input: buildOpenAiPrompt(request), - store: false + store: providerSessionEnabled, + previous_response_id: providerSessionEnabled ? request.previousProviderResponseId : undefined }; let response: Response; @@ -90,7 +100,8 @@ export class OpenAiProvider implements OptiNlpProvider { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` }, - body: JSON.stringify(body) + body: JSON.stringify(body), + signal: request.abortSignal }); } catch (error) { throw new OptiNlpProviderError(this.name, "OpenAI request failed. Check your network connection and API key.", technicalDetailFrom(error), error); @@ -119,6 +130,7 @@ export class OpenAiProvider implements OptiNlpProvider { provider: this.name, model: this.model, markdownOutput, + providerResponseId: rawResponse.id, structured, rawResponse }; diff --git a/tools/vscode-optitrust/src/optinlp/provider.test.ts b/tools/vscode-optitrust/src/optinlp/provider.test.ts index 38d8307ef..7c73b7b97 100644 --- a/tools/vscode-optitrust/src/optinlp/provider.test.ts +++ b/tools/vscode-optitrust/src/optinlp/provider.test.ts @@ -200,10 +200,10 @@ async function testOpenAiProviderException(): Promise { } async function testOpenAiSuccessfulResponse(): Promise { - let requestBody: { model?: string; store?: boolean; input?: string } | undefined; + let requestBody: { model?: string; store?: boolean; input?: string; previous_response_id?: string } | undefined; const fetchImpl: typeof fetch = async (_url, init) => { requestBody = JSON.parse(String(init?.body)) as typeof requestBody; - return new Response(JSON.stringify({ output: [{ content: [{ type: "output_text", text: validScriptMarkdown }] }] }), { + return new Response(JSON.stringify({ id: "resp_test", output: [{ content: [{ type: "output_text", text: validScriptMarkdown }] }] }), { status: 200, headers: { "Content-Type": "application/json" } }); @@ -215,12 +215,31 @@ async function testOpenAiSuccessfulResponse(): Promise { assert.strictEqual(result.provider, "openai"); assert.strictEqual(result.model, "openai-test"); assert.strictEqual(result.markdownOutput, validScriptMarkdown); + assert.strictEqual(result.providerResponseId, "resp_test"); assert.strictEqual(result.structured?.kind, "command_to_script"); assert.strictEqual(requestBody?.model, "openai-test"); assert.strictEqual(requestBody?.store, false); assert.match(requestBody?.input ?? "", /# User Request/); } +async function testOpenAiPreviousResponseId(): Promise { + let requestBody: { store?: boolean; previous_response_id?: string } | undefined; + const fetchImpl: typeof fetch = async (_url, init) => { + requestBody = JSON.parse(String(init?.body)) as typeof requestBody; + return new Response(JSON.stringify({ id: "resp_next", output_text: validTargetMarkdown }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + }; + + const provider = new OpenAiProvider({ apiKey: "test-key", fetchImpl }); + const result = await provider.generateTarget({ ...sampleRequest, providerSessionEnabled: true, previousProviderResponseId: "resp_previous" }); + + assert.strictEqual(requestBody?.store, true); + assert.strictEqual(requestBody?.previous_response_id, "resp_previous"); + assert.strictEqual(result.providerResponseId, "resp_next"); +} + async function testOpenAiUnstructuredResponseStillDisplays(): Promise { const fetchImpl: typeof fetch = async () => new Response(JSON.stringify({ output_text: "## Intent\nGenerated." }), { @@ -355,6 +374,7 @@ async function main(): Promise { await testOpenAiEmptyResponse(); await testOpenAiProviderException(); await testOpenAiSuccessfulResponse(); + await testOpenAiPreviousResponseId(); await testOpenAiUnstructuredResponseStillDisplays(); await testMarkdownSchemaParsing(); console.log("OptiNLP provider tests passed."); diff --git a/tools/vscode-optitrust/src/optinlp/providerTypes.ts b/tools/vscode-optitrust/src/optinlp/providerTypes.ts index 0dcb51054..4e4b7e79c 100644 --- a/tools/vscode-optitrust/src/optinlp/providerTypes.ts +++ b/tools/vscode-optitrust/src/optinlp/providerTypes.ts @@ -1,5 +1,5 @@ // Provider-neutral OptiNLP request/result contracts. CLI, VS Code commands, -// panels, and provider implementations should communicate through these types. +// UI commands, native chat, and provider implementations communicate through these types. import type { OptiNlpStructuredResult } from "./resultSchemas"; export type OptiNlpMode = "target" | "command_to_script" | "code_to_full_script"; @@ -12,13 +12,23 @@ export interface OptiNlpProviderRequest { readonly language: string; readonly promptText: string; readonly knowledgeText: string; + readonly stableContextKey?: string; + readonly stableContextLabel?: string; + readonly stableContextOmitted?: boolean; + readonly stableSourceContextKey?: string; + readonly stableSourceContextLabel?: string; + readonly stableSourceContextOmitted?: boolean; + readonly providerSessionEnabled?: boolean; + readonly previousProviderResponseId?: string; readonly sessionSummary?: string; + readonly abortSignal?: AbortSignal; } export interface OptiNlpProviderResult { readonly provider: string; readonly model: string; readonly markdownOutput: string; + readonly providerResponseId?: string; // Best-effort parsed fields for editor actions; raw markdown is still valid // output when a provider does not follow the exact section schema. readonly structured?: OptiNlpStructuredResult; @@ -28,6 +38,7 @@ export interface OptiNlpProviderResult { export interface OptiNlpProvider { readonly name: string; readonly model: string; + readonly supportsProviderSession?: boolean; generateTarget(request: OptiNlpProviderRequest): Promise; generateScript(request: OptiNlpProviderRequest): Promise; diff --git a/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts b/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts index 5c54e2821..69b15646c 100644 --- a/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts @@ -121,11 +121,34 @@ function testCandidateSummaryAndClear(): void { assert.strictEqual(memory.snapshot().turns.length, 0); } +function testStableContextSessionState(): void { + const memory = new OptiNlpSessionMemory(); + memory.recordGeneration( + { + ...baseRequest, + stableContextKey: "stable-target-context", + stableContextLabel: "01_target_generator.md, targets.md", + stableSourceContextKey: "stable-eval-file", + stableSourceContextLabel: "tools/optiNLP/eval/target_cases.md" + }, + { ...targetResult("[cFor \"i\"]"), provider: "openai", model: "gpt-test", providerResponseId: "resp_1" } + ); + + assert.strictEqual(memory.snapshot().stableContextCount, 2); + assert.strictEqual(memory.stableContextState("openai", "gpt-test", "stable-target-context")?.providerResponseId, "resp_1"); + assert.strictEqual(memory.stableContextState("openai", "gpt-test", "stable-eval-file")?.providerResponseId, "resp_1"); + assert.strictEqual(memory.stableContextState("openai", "other-model", "stable-target-context"), undefined); + + memory.clear(); + assert.strictEqual(memory.snapshot().stableContextCount, 0); +} + function main(): void { testRecordsCompactGenerationState(); testSummaryIncludesLatestScriptAssumptionsAndValidation(); testKeepsOnlyMaxTurns(); testCandidateSummaryAndClear(); + testStableContextSessionState(); console.log("OptiNLP session memory tests passed."); } diff --git a/tools/vscode-optitrust/src/optinlp/sessionMemory.ts b/tools/vscode-optitrust/src/optinlp/sessionMemory.ts index 6f5cb0870..6e002318f 100644 --- a/tools/vscode-optitrust/src/optinlp/sessionMemory.ts +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.ts @@ -27,6 +27,7 @@ export interface OptiNlpSessionSnapshot { readonly turns: readonly OptiNlpSessionTurn[]; readonly acceptedAssumptions: readonly string[]; readonly lastValidation?: OptiNlpValidationRecord; + readonly stableContextCount: number; } export interface OptiNlpSessionMemoryOptions { @@ -38,6 +39,7 @@ export class OptiNlpSessionMemory { private turns: OptiNlpSessionTurn[] = []; private acceptedAssumptions: string[] = []; private lastValidation: OptiNlpValidationRecord | undefined; + private stableContexts = new Map(); constructor(options: OptiNlpSessionMemoryOptions = {}) { this.maxTurns = Math.max(1, options.maxTurns ?? 8); @@ -46,6 +48,12 @@ export class OptiNlpSessionMemory { recordGeneration(request: OptiNlpProviderRequest, result: OptiNlpProviderResult): void { const turn = turnFromResult(request, result); this.turns = [...this.turns, turn].slice(-this.maxTurns); + if (request.stableContextKey && !request.stableContextOmitted) { + this.recordStableContext(result.provider, result.model, request.stableContextKey, result.providerResponseId); + } + if (request.stableSourceContextKey && !request.stableSourceContextOmitted) { + this.recordStableContext(result.provider, result.model, request.stableSourceContextKey, result.providerResponseId); + } } recordValidation(record: OptiNlpValidationRecord): void { @@ -66,7 +74,8 @@ export class OptiNlpSessionMemory { return { turns: [...this.turns], acceptedAssumptions: [...this.acceptedAssumptions], - lastValidation: this.lastValidation + lastValidation: this.lastValidation, + stableContextCount: this.stableContexts.size }; } @@ -74,6 +83,15 @@ export class OptiNlpSessionMemory { this.turns = []; this.acceptedAssumptions = []; this.lastValidation = undefined; + this.stableContexts.clear(); + } + + stableContextState(provider: string, model: string, stableContextKey: string): { readonly providerResponseId?: string } | undefined { + return this.stableContexts.get(stableContextSessionKey(provider, model, stableContextKey)); + } + + private recordStableContext(provider: string, model: string, stableContextKey: string, providerResponseId?: string): void { + this.stableContexts.set(stableContextSessionKey(provider, model, stableContextKey), { providerResponseId }); } summary(maxChars = 2000): string | undefined { @@ -117,6 +135,10 @@ export class OptiNlpSessionMemory { } } +function stableContextSessionKey(provider: string, model: string, stableContextKey: string): string { + return `${provider}\0${model}\0${stableContextKey}`; +} + function turnFromResult(request: OptiNlpProviderRequest, result: OptiNlpProviderResult): OptiNlpSessionTurn { const structured = result.structured; return { From 97d5f8e5131637aea941534e6bf8c44bdd1ed2f8 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Thu, 2 Jul 2026 04:37:32 -0400 Subject: [PATCH 21/47] improve source context handling --- .../src/commands/associatedFiles.ts | 4 +- .../src/commands/optinlpCommands.ts | 207 ++++++++++++++++-- tools/vscode-optitrust/src/optinlp/assets.ts | 27 ++- .../src/optinlp/resultActions.ts | 31 ++- .../src/optinlp/sourceContext.ts | 12 + tools/vscode-optitrust/src/optitrust/files.ts | 48 +++- 6 files changed, 292 insertions(+), 37 deletions(-) create mode 100644 tools/vscode-optitrust/src/optinlp/sourceContext.ts diff --git a/tools/vscode-optitrust/src/commands/associatedFiles.ts b/tools/vscode-optitrust/src/commands/associatedFiles.ts index f306dd190..000f8442d 100644 --- a/tools/vscode-optitrust/src/commands/associatedFiles.ts +++ b/tools/vscode-optitrust/src/commands/associatedFiles.ts @@ -2,7 +2,7 @@ import * as path from "path"; import * as fs from "fs/promises"; import * as vscode from "vscode"; import { getActiveEditorContext } from "../optitrust/editor"; -import { AssociatedFile, findAssociatedFiles, outputPairs, pickAssociatedFile } from "../optitrust/files"; +import { AssociatedFile, findAssociatedFiles, OPTITRUST_C_SOURCE_EXTENSIONS, outputPairs, pickAssociatedFile } from "../optitrust/files"; import { openFileOrHtml } from "../optitrust/views"; import { OptitrustWorkspace } from "../optitrust/workspace"; @@ -47,7 +47,7 @@ function isFrequentAssociatedFile(file: AssociatedFile): boolean { const name = stripOptilambdaRepresentationSuffix(parsed.name); return ( parsed.ext === ".ml" || - [".cpp", ".c"].includes(parsed.ext) || + (OPTITRUST_C_SOURCE_EXTENSIONS as readonly string[]).includes(parsed.ext) || /_(out|exp|after)$/u.test(name) ); } diff --git a/tools/vscode-optitrust/src/commands/optinlpCommands.ts b/tools/vscode-optitrust/src/commands/optinlpCommands.ts index 75810c79d..a7a86fb60 100644 --- a/tools/vscode-optitrust/src/commands/optinlpCommands.ts +++ b/tools/vscode-optitrust/src/commands/optinlpCommands.ts @@ -1,25 +1,38 @@ // VS Code command handlers for OptiNLP. This file owns editor interaction // (quick input, selection/full-file consent, insertion/opening documents), while // core prompt/provider behavior stays in src/optinlp. +import { createHash } from "crypto"; +import * as path from "path"; import * as vscode from "vscode"; import { loadOptiNlpAssets, inferLanguage } from "../optinlp/assets"; import { generateOptiNlp } from "../optinlp/generation"; import { modeDefinition, resolveRequestedMode } from "../optinlp/modes"; import { OptiNlpProviderError } from "../optinlp/providerErrors"; -import { createOptiNlpProvider, DEFAULT_OPTINLP_PROVIDER, parseOptiNlpProviderId } from "../optinlp/providerFactory"; +import { + createOptiNlpProvider, + DEFAULT_OPTINLP_PROVIDER, + IMPLEMENTED_OPTINLP_PROVIDER_IDS, + OptiNlpProviderId, + parseOptiNlpProviderId +} from "../optinlp/providerFactory"; import { OptiNlpMode, OptiNlpProviderRequest, OptiNlpProviderResult } from "../optinlp/providerTypes"; import { editorActionForResult } from "../optinlp/resultActions"; import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; +import { markSelectedRangeInText } from "../optinlp/sourceContext"; import { getActiveEditorContext } from "../optitrust/editor"; import { appendHeader, appendLine, showOutput } from "../optitrust/output"; import { OptitrustWorkspace } from "../optitrust/workspace"; const GEMINI_API_KEY_SECRET = "optinlp.geminiApiKey"; const OPENAI_API_KEY_SECRET = "optinlp.openaiApiKey"; +const OPTINLP_PROVIDER_SETTING = "optinlpProvider"; +const OPTINLP_MODEL_SETTING = "optinlpModel"; +const OPTINLP_PROVIDER_SESSION_SETTING = "optinlpUseProviderSession"; +const STABLE_OPTINLP_FILE_PATTERN = /(^|\/)tools\/optiNLP\/(knowledge|eval|prompts)\//u; export interface SourceContext { readonly text: string; - readonly label: "selection" | "full file" | "associated source" | "target-at-cursor context"; + readonly label: "marked selection" | "full file" | "associated source" | "target-at-cursor context"; } export interface OptiNlpGenerationOutcome { @@ -36,6 +49,7 @@ export interface OptiNlpGenerationOptions { readonly sourceContext?: SourceContext; readonly filePath?: string; readonly language?: string; + readonly cancellationToken?: vscode.CancellationToken; } export async function setOptiNlpGeminiApiKey(context: vscode.ExtensionContext): Promise { @@ -47,9 +61,7 @@ export async function setOptiNlpOpenAiApiKey(context: vscode.ExtensionContext): } export async function setOptiNlpConfiguredProviderApiKey(context: vscode.ExtensionContext): Promise { - const config = vscode.workspace.getConfiguration("optitrust"); - const configuredProvider = config.get("optinlpProvider", DEFAULT_OPTINLP_PROVIDER); - const provider = parseOptiNlpProviderId(configuredProvider) ?? DEFAULT_OPTINLP_PROVIDER; + const provider = configuredOptiNlpProvider(); switch (provider) { case "openai": await setOptiNlpOpenAiApiKey(context); @@ -66,6 +78,69 @@ export async function setOptiNlpConfiguredProviderApiKey(context: vscode.Extensi } } +export async function selectOptiNlpProvider(): Promise { + const configured = configuredOptiNlpProvider(); + const picked = await vscode.window.showQuickPick( + IMPLEMENTED_OPTINLP_PROVIDER_IDS.map(provider => ({ + label: provider, + description: provider === configured ? "current" : undefined, + provider + })), + { placeHolder: "Select OptiNLP provider" } + ); + if (!picked) { + return; + } + await updateOptiNlpSetting(OPTINLP_PROVIDER_SETTING, picked.provider); + vscode.window.showInformationMessage(`OptiNLP provider set to ${picked.provider}.`); +} + +export async function setOptiNlpModel(): Promise { + const current = configuredOptiNlpModel(); + const value = await vscode.window.showInputBox({ + title: "OptiNLP: Set Model", + prompt: "Enter a model override, or leave empty to use the provider default.", + value: current, + ignoreFocusOut: true + }); + if (value === undefined) { + return; + } + const model = value.trim(); + await updateOptiNlpSetting(OPTINLP_MODEL_SETTING, model); + vscode.window.showInformationMessage(model.length > 0 ? `OptiNLP model set to ${model}.` : "OptiNLP model reset to provider default."); +} + +export function optiNlpConfigurationSummary(): { readonly provider: OptiNlpProviderId; readonly model: string; readonly useProviderSession: boolean } { + return { + provider: configuredOptiNlpProvider(), + model: configuredOptiNlpModel(), + useProviderSession: configuredOptiNlpUseProviderSession() + }; +} + +function configuredOptiNlpProvider(): OptiNlpProviderId { + const configuredProvider = vscode.workspace.getConfiguration("optitrust").get(OPTINLP_PROVIDER_SETTING, DEFAULT_OPTINLP_PROVIDER); + return parseOptiNlpProviderId(configuredProvider) ?? DEFAULT_OPTINLP_PROVIDER; +} + +function configuredOptiNlpModel(): string { + return vscode.workspace.getConfiguration("optitrust").get(OPTINLP_MODEL_SETTING, "").trim(); +} + +function configuredOptiNlpUseProviderSession(): boolean { + return vscode.workspace.getConfiguration("optitrust").get(OPTINLP_PROVIDER_SESSION_SETTING, true); +} + +async function updateOptiNlpSetting(key: string, value: string): Promise { + const config = vscode.workspace.getConfiguration("optitrust"); + const inspected = config.inspect(key); + const target = inspected?.workspaceFolderValue !== undefined || inspected?.workspaceValue !== undefined + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global; + await config.update(key, value, target); +} + async function setProviderApiKey(context: vscode.ExtensionContext, providerLabel: string, secretKey: string): Promise { const apiKey = await vscode.window.showInputBox({ title: `OptiNLP: Set ${providerLabel} API Key`, @@ -138,20 +213,37 @@ export async function runOptiNlpGeneration( return undefined; } + const provider = createConfiguredProvider(context); const assets = await loadOptiNlpAssets(workspace.root, resolvedMode); + const stableContextState = memory.stableContextState(provider.name, provider.model, assets.stableContextKey); + const useProviderSession = configuredOptiNlpUseProviderSession(); + const canOmitStableContext = useProviderSession && provider.supportsProviderSession === true && stableContextState?.providerResponseId !== undefined; + const filePath = options.filePath ?? editorContext.relativePath; + const stableSource = stableSourceContext(filePath, sourceContext); + const stableSourceState = stableSource ? memory.stableContextState(provider.name, provider.model, stableSource.key) : undefined; + const canOmitStableSource = useProviderSession && provider.supportsProviderSession === true && stableSourceState?.providerResponseId !== undefined; + const abortController = new AbortController(); + const cancellationSubscription = options.cancellationToken?.onCancellationRequested(() => abortController.abort()); const request: OptiNlpProviderRequest = { mode: resolvedMode, userRequest, - sourceText: sourceContext.text, - filePath: options.filePath ?? editorContext.relativePath, + sourceText: canOmitStableSource && stableSource ? stableSourceOmittedText(stableSource.label) : sourceContext.text, + filePath, language: options.language ?? inferLanguage(editorContext.filePath), - promptText: assets.promptText, - knowledgeText: assets.knowledgeText, - sessionSummary: memory.summary() + promptText: canOmitStableContext ? stableContextOmittedText("prompt", assets.stableContextLabel) : assets.promptText, + knowledgeText: canOmitStableContext ? stableContextOmittedText("knowledge", assets.stableContextLabel) : assets.knowledgeText, + stableContextKey: assets.stableContextKey, + stableContextLabel: assets.stableContextLabel, + stableContextOmitted: canOmitStableContext, + stableSourceContextKey: stableSource?.key, + stableSourceContextLabel: stableSource?.label, + stableSourceContextOmitted: canOmitStableSource, + providerSessionEnabled: useProviderSession, + previousProviderResponseId: stableSourceState?.providerResponseId ?? stableContextState?.providerResponseId, + sessionSummary: memory.summary(), + abortSignal: abortController.signal }; - const provider = createConfiguredProvider(context); - let result: OptiNlpProviderResult; const definition = modeDefinition(resolvedMode); try { @@ -159,11 +251,17 @@ export async function runOptiNlpGeneration( { location: vscode.ProgressLocation.Notification, title: `OptiNLP: ${definition.label}`, - cancellable: false + cancellable: true }, - () => generateOptiNlp(provider, request) + (_progress, token) => { + const progressCancellationSubscription = token.onCancellationRequested(() => abortController.abort()); + return generateOptiNlp(provider, request).finally(() => progressCancellationSubscription.dispose()); + } ); } catch (error) { + if (abortController.signal.aborted || options.cancellationToken?.isCancellationRequested) { + return undefined; + } if (error instanceof OptiNlpProviderError) { if (options.throwProviderErrors) { throw error; @@ -178,6 +276,8 @@ export async function runOptiNlpGeneration( return undefined; } throw error; + } finally { + cancellationSubscription?.dispose(); } memory.recordGeneration(request, result); @@ -193,6 +293,43 @@ export async function runOptiNlpGeneration( }; } +function stableSourceContext(filePath: string, sourceContext: SourceContext): { readonly key: string; readonly label: string } | undefined { + const isStablePayload = sourceContext.label === "full file" || sourceContext.label === "marked selection"; + if (!isStablePayload || !STABLE_OPTINLP_FILE_PATTERN.test(filePath)) { + return undefined; + } + return { + key: stableSourceContextKey(filePath, sourceContext.text), + label: filePath + }; +} + +function stableSourceContextKey(filePath: string, sourceText: string): string { + return createHash("sha256") + .update("stable-source") + .update("\0") + .update(filePath) + .update("\0") + .update(sourceText) + .digest("hex"); +} + +function stableSourceOmittedText(label: string): string { + return [ + "Stable OptiNLP source file was already provided earlier in this provider session.", + `File: ${label}.`, + "Continue using that prior source file content." + ].join("\n"); +} + +function stableContextOmittedText(kind: "prompt" | "knowledge", label: string): string { + return [ + `Stable OptiNLP ${kind} files were already provided earlier in this provider session.`, + `Files: ${label}.`, + "Continue using that prior stable context. The current request still includes fresh source/context/user input." + ].join("\n"); +} + async function getSourceContext(editor: vscode.TextEditor): Promise { const selectedContext = selectedSourceContextFromEditor(editor); if (selectedContext) { @@ -211,8 +348,15 @@ async function getSourceContext(editor: vscode.TextEditor): Promise 0 ? { text: selectedText, label: "selection" } : undefined; + const selection = editor.selection; + const selectedText = editor.document.getText(selection); + if (selectedText.trim().length === 0) { + return undefined; + } + return { + text: markSelectedRangeInText(editor.document.getText(), editor.document.offsetAt(selection.start), editor.document.offsetAt(selection.end)), + label: "marked selection" + }; } function fullFileSourceContextFromEditor(editor: vscode.TextEditor): SourceContext { @@ -263,10 +407,8 @@ export async function applyDefaultEditorAction(outcome: OptiNlpGenerationOutcome } function createConfiguredProvider(context: vscode.ExtensionContext): ReturnType { - const config = vscode.workspace.getConfiguration("optitrust"); - const configuredProvider = config.get("optinlpProvider", DEFAULT_OPTINLP_PROVIDER); - const provider = parseOptiNlpProviderId(configuredProvider) ?? DEFAULT_OPTINLP_PROVIDER; - const model = config.get("optinlpModel", "").trim() || undefined; + const provider = configuredOptiNlpProvider(); + const model = configuredOptiNlpModel() || undefined; return createOptiNlpProvider({ provider, gemini: { @@ -323,6 +465,31 @@ export async function insertTargetAtCursor(text: string, sourceEditor?: vscode.T }); } +export async function insertTargetAtCursorInFile(text: string, filePath?: string): Promise { + if (!filePath) { + await insertTargetAtCursor(text); + return; + } + + const normalizedPath = path.resolve(filePath); + const visibleEditor = vscode.window.visibleTextEditors.find(editor => + editor.document.uri.scheme === "file" && path.resolve(editor.document.uri.fsPath) === normalizedPath + ); + if (visibleEditor) { + await vscode.window.showTextDocument(visibleEditor.document, visibleEditor.viewColumn, false); + await insertTargetAtCursor(text, visibleEditor); + return; + } + + const document = await vscode.workspace.openTextDocument(normalizedPath); + const editor = await vscode.window.showTextDocument(document, { + preview: false, + preserveFocus: false, + viewColumn: vscode.ViewColumn.One + }); + await insertTargetAtCursor(text, editor); +} + export async function openOcamlDocument(text: string): Promise { const document = await vscode.workspace.openTextDocument({ content: text.endsWith("\n") ? text : `${text}\n`, diff --git a/tools/vscode-optitrust/src/optinlp/assets.ts b/tools/vscode-optitrust/src/optinlp/assets.ts index 6eb8972d5..5beb51255 100644 --- a/tools/vscode-optitrust/src/optinlp/assets.ts +++ b/tools/vscode-optitrust/src/optinlp/assets.ts @@ -1,5 +1,6 @@ // Loads OptiNLP prompt-kit markdown from the repository and builds provider // requests. Prompt/knowledge filenames come from the central mode registry. +import { createHash } from "crypto"; import * as fs from "fs/promises"; import * as path from "path"; import { modeDefinition } from "./modes"; @@ -8,6 +9,8 @@ import { OptiNlpMode, OptiNlpProviderRequest } from "./providerTypes"; export interface OptiNlpAssets { readonly promptText: string; readonly knowledgeText: string; + readonly stableContextKey: string; + readonly stableContextLabel: string; } export interface BuildOptiNlpRequestOptions { @@ -31,6 +34,8 @@ export async function buildOptiNlpProviderRequest(options: BuildOptiNlpRequestOp language: inferLanguage(absoluteFilePath), promptText: assets.promptText, knowledgeText: assets.knowledgeText, + stableContextKey: assets.stableContextKey, + stableContextLabel: assets.stableContextLabel, sessionSummary: options.sessionSummary }; } @@ -46,14 +51,30 @@ export async function loadOptiNlpAssets(root: string, mode: OptiNlpMode): Promis ...knowledgePaths.map(filePath => fs.readFile(filePath, "utf8")) ]); + const knowledgeText = knowledgeParts + .map((text, index) => `# Knowledge: ${definition.knowledgeFiles[index]}\n\n${text.trim()}`) + .join("\n\n"); + const stableContextLabel = [definition.promptFile, ...definition.knowledgeFiles].join(", "); return { promptText, - knowledgeText: knowledgeParts - .map((text, index) => `# Knowledge: ${definition.knowledgeFiles[index]}\n\n${text.trim()}`) - .join("\n\n") + knowledgeText, + stableContextKey: stableContextKey(definition.promptFile, promptText, definition.knowledgeFiles, knowledgeText), + stableContextLabel }; } +function stableContextKey(promptFile: string, promptText: string, knowledgeFiles: readonly string[], knowledgeText: string): string { + return createHash("sha256") + .update(promptFile) + .update("\0") + .update(promptText) + .update("\0") + .update(knowledgeFiles.join("\0")) + .update("\0") + .update(knowledgeText) + .digest("hex"); +} + export async function findOptiTrustRoot(startPath: string): Promise { let current = path.resolve(startPath); const stat = await safeStat(current); diff --git a/tools/vscode-optitrust/src/optinlp/resultActions.ts b/tools/vscode-optitrust/src/optinlp/resultActions.ts index ce620b9fe..7179ac2bc 100644 --- a/tools/vscode-optitrust/src/optinlp/resultActions.ts +++ b/tools/vscode-optitrust/src/optinlp/resultActions.ts @@ -1,5 +1,5 @@ // Utilities for turning structured OptiNLP results into editor actions. -// The VS Code commands and panel both use these to avoid divergent behavior. +// VS Code commands and native chat use these to avoid divergent behavior. import { OptiNlpStructuredResult } from "./resultSchemas"; export type OptiNlpEditorAction = @@ -19,3 +19,32 @@ export function editorActionForResult(result: OptiNlpStructuredResult | undefine return { kind: "open_script", text: result.fullScript }; } } + +const TARGET_SELECTOR_HINT = /\b(?:nbMulti|nbAny|nbExact|occIndex|occFirst|occLast|tBefore|tAfter|tFirst|tLast|tBetweenAll|tSpan|cFor|cFor_c|cWhile|cIf|cFunDef|cTopFunDef|cFunBody|cTopFunBody|cCall|cVarDef|cVarsDef|cVar|cReadVar|cWriteVar|cArrayRead|cArrayWrite|cFieldRead|cFieldWrite|cSeq|cReturn|cLabel|cMark)\b/u; +const OCAML_CODE_BLOCK_PATTERN = /```(?:ocaml)?\s*([\s\S]*?)```/giu; +const TARGET_LIST_PATTERN = /\[[^\]\n]*(?:\][^\[\n]*)?\]/gu; + +export function targetSuggestionsFromMarkdown(markdown: string): string[] { + const suggestions: string[] = []; + const seen = new Set(); + const add = (candidate: string): void => { + const trimmed = candidate.trim().replace(/;?\s*$/u, ""); + if (!trimmed || seen.has(trimmed) || !TARGET_SELECTOR_HINT.test(trimmed)) { + return; + } + seen.add(trimmed); + suggestions.push(trimmed); + }; + + for (const block of markdown.matchAll(OCAML_CODE_BLOCK_PATTERN)) { + collectTargetsFromText(block[1], add); + } + collectTargetsFromText(markdown.replace(OCAML_CODE_BLOCK_PATTERN, ""), add); + return suggestions; +} + +function collectTargetsFromText(text: string, add: (candidate: string) => void): void { + for (const match of text.matchAll(TARGET_LIST_PATTERN)) { + add(match[0]); + } +} diff --git a/tools/vscode-optitrust/src/optinlp/sourceContext.ts b/tools/vscode-optitrust/src/optinlp/sourceContext.ts new file mode 100644 index 000000000..7a1ca2960 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/sourceContext.ts @@ -0,0 +1,12 @@ +// Helpers for preparing source text before it is sent to an OptiNLP provider. +// VS Code-specific code computes ranges; this file only owns provider-neutral +// source annotations. + +export const SELECTED_SOURCE_START_MARKER = ""; +export const SELECTED_SOURCE_END_MARKER = ""; + +export function markSelectedRangeInText(text: string, startOffset: number, endOffset: number): string { + const start = Math.max(0, Math.min(startOffset, text.length)); + const end = Math.max(start, Math.min(endOffset, text.length)); + return `${text.slice(0, start)}${SELECTED_SOURCE_START_MARKER}${text.slice(start, end)}${SELECTED_SOURCE_END_MARKER}${text.slice(end)}`; +} diff --git a/tools/vscode-optitrust/src/optitrust/files.ts b/tools/vscode-optitrust/src/optitrust/files.ts index aa686a4fe..9f0b980f7 100644 --- a/tools/vscode-optitrust/src/optitrust/files.ts +++ b/tools/vscode-optitrust/src/optitrust/files.ts @@ -14,6 +14,16 @@ export interface OutputPair { readonly exp: string; } +export const OPTITRUST_C_SOURCE_EXTENSIONS = [".cpp", ".cc", ".cxx", ".c"] as const; +const OPTITRUST_PRIMARY_INPUT_EXTENSIONS = [...OPTITRUST_C_SOURCE_EXTENSIONS, ".opti"] as const; +const C_SOURCE_EXTENSION_PRIORITY: ReadonlyMap = new Map(OPTITRUST_C_SOURCE_EXTENSIONS.map((ext, index) => [ext, index])); +const OUTPUT_EXTENSION_LABELS = new Map([ + [".cpp", "C++ output"], + [".cc", "C++ output"], + [".cxx", "C++ output"], + [".c", "C output"], + [".opti", "OptiLambda output"] +]); const KIND_ORDER: AssociatedFile["kind"][] = ["script", "input", "generated", "expected", "diff", "trace", "other"]; const OPTILAMBDA_REPRESENTATIONS = ["surface", "internal", "typed"] as const; @@ -87,7 +97,7 @@ function classifyAssociatedFile(base: string, fileName: string): AssociatedFile[ if (parsed.ext === ".ml" && parsed.name === base) { return "script"; } - if ([".cpp", ".c", ".opti"].includes(parsed.ext) && parsed.name === base) { + if (isPrimaryInputExtension(parsed.ext) && parsed.name === base) { return "input"; } if (parsed.ext === ".opti" && representation && semanticName === base) { @@ -103,15 +113,23 @@ function classifyAssociatedFile(base: string, fileName: string): AssociatedFile[ ) { return "trace"; } - if (/_(out|before|after)$/u.test(semanticName) && [".cpp", ".c", ".opti"].includes(parsed.ext)) { + if (/_(out|before|after)$/u.test(semanticName) && isPrimaryInputExtension(parsed.ext)) { return "generated"; } - if (/_exp$/u.test(semanticName) && [".cpp", ".c", ".opti"].includes(parsed.ext)) { + if (/_exp$/u.test(semanticName) && isPrimaryInputExtension(parsed.ext)) { return "expected"; } return "other"; } +function isPrimaryInputExtension(ext: string): boolean { + return (OPTITRUST_PRIMARY_INPUT_EXTENSIONS as readonly string[]).includes(ext); +} + +function isCSourceExtension(ext: string): boolean { + return (OPTITRUST_C_SOURCE_EXTENSIONS as readonly string[]).includes(ext); +} + function compareAssociatedFiles(a: AssociatedFile, b: AssociatedFile): number { const kindOrder = KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind); if (kindOrder !== 0) { @@ -142,6 +160,20 @@ export async function findAssociatedFiles(filePath: string): Promise { + const files = await findAssociatedFiles(filePath); + const sources = files.filter(file => file.kind === "input" && isCSourceExtension(path.extname(file.path))); + return sources.sort(compareCSourcePriority)[0]; +} + +function compareCSourcePriority(a: AssociatedFile, b: AssociatedFile): number { + return cSourcePriority(a) - cSourcePriority(b) || a.label.localeCompare(b.label); +} + +function cSourcePriority(file: AssociatedFile): number { + return C_SOURCE_EXTENSION_PRIORITY.get(path.extname(file.path)) ?? Number.MAX_SAFE_INTEGER; +} + /** * Detect output/expected pairs generically. VS Code's native diff command can * then compare any supported output language without command-specific code. @@ -149,15 +181,9 @@ export async function findAssociatedFiles(filePath: string): Promise { const { dir, base } = baseNameForAssociatedFiles(filePath); const pairs: OutputPair[] = []; - const labels = new Map([ - [".cpp", "C/C++ output"], - [".c", "C output"], - [".opti", "OptiLambda output"] - ]); - - for (const ext of [".cpp", ".c", ".opti"]) { + for (const [ext, label] of OUTPUT_EXTENSION_LABELS) { const pair = { - label: labels.get(ext) ?? `${ext.slice(1).toUpperCase()} output`, + label, out: path.join(dir, `${base}_out${ext}`), exp: path.join(dir, `${base}_exp${ext}`) }; From ffeb549f5d9985495c2a8612eefc255b05cc4700 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Thu, 2 Jul 2026 04:37:54 -0400 Subject: [PATCH 22/47] move assistant flow to native vs code chat --- tools/vscode-optitrust/README.md | 49 ++ tools/vscode-optitrust/package.json | 83 +- .../src/commands/optinlpChatContext.ts | 54 ++ .../src/commands/optinlpChatParticipant.ts | 295 +++++++ .../src/commands/optinlpPanel.ts | 761 ------------------ .../src/commands/optinlpTargetAtCursor.ts | 35 +- tools/vscode-optitrust/src/extension.ts | 101 ++- 7 files changed, 586 insertions(+), 792 deletions(-) create mode 100644 tools/vscode-optitrust/src/commands/optinlpChatContext.ts create mode 100644 tools/vscode-optitrust/src/commands/optinlpChatParticipant.ts delete mode 100644 tools/vscode-optitrust/src/commands/optinlpPanel.ts diff --git a/tools/vscode-optitrust/README.md b/tools/vscode-optitrust/README.md index d8093316e..76b00a1f6 100644 --- a/tools/vscode-optitrust/README.md +++ b/tools/vscode-optitrust/README.md @@ -221,6 +221,55 @@ The QuickPick menu can: | `OptiTrust: Open Unit Test ML And CPP Files` | Opens the `.ml` and `.cpp` files for a unit test. | | `OptiTrust: Select Diff/Trace Syntax` | Selects the default server-backed view syntax. | | `OptiTrust: Health Check` | Runs installation and backend checks. | +| `OptiTrust: Show Shortcuts` | Shows the extension shortcuts from inside VS Code. | +| `OptiTrust: Open OptiNLP Chat` | Opens native VS Code Chat for `@optinlp`. | +| `OptiTrust: OptiNLP Generate Target` | Generates a target for the active selection or file. | +| `OptiTrust: OptiNLP Generate Script` | Generates a transformation script from a command. | +| `OptiTrust: OptiNLP Generate Full Transformation` | Generates a complete transformation script for the active file. | +| `OptiTrust: OptiNLP Suggest Target At Cursor` | Runs the F7 target-at-cursor workflow for `.ml` scripts. | + +## OptiNLP Native Chat + +The extension contributes a native VS Code Chat participant named `@optinlp`. +This is the only OptiNLP chat UI; voice input belongs to VS Code Chat through +VS Code Speech. + +Examples: + +```text +@optinlp target the second loop named i +@optinlp /target target the y loop that writes to out +@optinlp /script unroll the loop i +@optinlp /full generate a full transformation script for this file +@optinlp /config +@optinlp /clear +@optinlp /help +``` + +When `/target` or auto mode resolves to target generation from an active `.ml` +script, OptiNLP sends the matching same-basename `.cpp` or `.c` source file as +the model context. + +`F7` prepares richer target-at-cursor context by executing the current `.ml` +script through the line before the cursor, opening the generated `_after.opti` +state, and focusing native VS Code Chat. To avoid creating a new chat session, +the prepared `@optinlp /target ...` prompt is copied to the clipboard; paste it +into the existing Chat input and send it. The pending context is short-lived and +is consumed by that request. + +OptiNLP source context such as `.ml`, `.cpp`, and `.opti` files is refreshed on +each request because those files may change while you work. Stable OptiNLP +prompt, knowledge, and eval files under `tools/optiNLP/` are tracked by session +hash. With a stateful provider such as OpenAI, stable prompt-kit context is sent +once per session and later requests continue from the previous provider +response. Stateless providers such as Gemini keep receiving stable context on +each request so the model has the necessary context. Disable +`optitrust.optinlpUseProviderSession` to force every request to send full +context. + +For voice input, install Microsoft's `VS Code Speech` extension, open VS Code +Chat, focus the chat input, choose `@optinlp`, and use the microphone button +provided by VS Code Chat. ## Default Keybindings diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index b2fd02dd9..9d6ac0f7a 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -11,7 +11,7 @@ "directory": "tools/vscode-optitrust" }, "engines": { - "vscode": "^1.85.0" + "vscode": "^1.90.0" }, "categories": [ "Other", @@ -45,8 +45,13 @@ "onCommand:optitrust.optinlpGenerateScript", "onCommand:optitrust.optinlpGenerateFullTransformation", "onCommand:optitrust.optinlpSuggestTargetAtCursor", + "onCommand:optitrust.optinlpSelectProvider", + "onCommand:optitrust.optinlpSetModel", + "onCommand:optitrust.optinlpSetConfiguredApiKey", "onCommand:optitrust.optinlpSetGeminiApiKey", "onCommand:optitrust.optinlpSetOpenAiApiKey", + "onCommand:optitrust.optinlpInsertTarget", + "onCommand:optitrust.optinlpOpenScript", "onCommand:optitrust.optinlpClearSession", "onChatParticipant:optitrust.optinlp" ], @@ -136,7 +141,7 @@ }, { "command": "optitrust.optinlpChat", - "title": "OptiTrust: OptiNLP Chat", + "title": "OptiTrust: Open OptiNLP Chat", "icon": "$(comment-discussion)" }, { @@ -164,6 +169,18 @@ "command": "optitrust.optinlpSetOpenAiApiKey", "title": "OptiTrust: OptiNLP Set OpenAI API Key" }, + { + "command": "optitrust.optinlpSetConfiguredApiKey", + "title": "OptiTrust: OptiNLP Set API Key" + }, + { + "command": "optitrust.optinlpSelectProvider", + "title": "OptiTrust: OptiNLP Select Provider" + }, + { + "command": "optitrust.optinlpSetModel", + "title": "OptiTrust: OptiNLP Set Model" + }, { "command": "optitrust.optinlpClearSession", "title": "OptiTrust: OptiNLP Clear Session" @@ -231,15 +248,66 @@ "Use deterministic local mock responses for UI testing.", "Use OpenAI through the configured API key." ], - "description": "AI provider used by OptiNLP commands and the OptiNLP panel." + "description": "AI provider used by OptiNLP commands and native VS Code Chat." }, "optitrust.optinlpModel": { "type": "string", "default": "", "description": "Optional model override for the configured OptiNLP provider. Leave empty to use the provider default." + }, + "optitrust.optinlpUseProviderSession": { + "type": "boolean", + "default": true, + "description": "Let stateful OptiNLP providers reuse prior prompt/knowledge/eval context in the current in-memory session. Disable to send full context every request." } } }, + "chatParticipants": [ + { + "id": "optitrust.optinlp", + "name": "optinlp", + "fullName": "OptiNLP", + "description": "Generate OptiTrust targets and transformation scripts for the active file.", + "isSticky": true, + "commands": [ + { + "name": "target", + "description": "Generate robust OptiTrust target suggestions for the active file." + }, + { + "name": "script", + "description": "Generate an OptiTrust transformation script from a command." + }, + { + "name": "full", + "description": "Generate a complete OptiTrust transformation script for the active file." + }, + { + "name": "config", + "description": "Show and change OptiNLP provider, model, and API key configuration." + }, + { + "name": "clear", + "description": "Clear OptiNLP session memory." + }, + { + "name": "help", + "description": "Show OptiNLP chat usage examples." + } + ], + "disambiguation": [ + { + "category": "optitrust_targets", + "description": "The user wants to generate an OptiTrust target, transformation script, or full transformation script for C, C++, OptiLambda, or OCaml OptiTrust code.", + "examples": [ + "target the second loop named i", + "generate an OptiTrust script that unrolls loop i", + "create a full transformation script for this file" + ] + } + ] + } + ], "keybindings": [ { "command": "optitrust.viewDiff", @@ -428,6 +496,15 @@ { "command": "optitrust.optinlpSetOpenAiApiKey" }, + { + "command": "optitrust.optinlpSetConfiguredApiKey" + }, + { + "command": "optitrust.optinlpSelectProvider" + }, + { + "command": "optitrust.optinlpSetModel" + }, { "command": "optitrust.optinlpClearSession" } diff --git a/tools/vscode-optitrust/src/commands/optinlpChatContext.ts b/tools/vscode-optitrust/src/commands/optinlpChatContext.ts new file mode 100644 index 000000000..28479b0cf --- /dev/null +++ b/tools/vscode-optitrust/src/commands/optinlpChatContext.ts @@ -0,0 +1,54 @@ +// Short-lived context handoff from editor commands to the native OptiNLP chat +// participant. F7 prepares rich source/after-state context, then chat consumes +// it when the user submits the prefilled @optinlp request. +import { randomUUID } from "crypto"; +import { OptiNlpMode } from "../optinlp/providerTypes"; +import { SourceContext } from "./optinlpCommands"; + +export interface PendingOptiNlpChatRequest { + readonly id: string; + readonly createdAt: number; + readonly mode: OptiNlpMode; + readonly chatPrompt: string; + readonly userRequest: string; + readonly sourceContext: SourceContext; + readonly filePath: string; + readonly language: string; + readonly targetInsertionFilePath?: string; +} + +export type NewPendingOptiNlpChatRequest = Omit; + +const PENDING_REQUEST_TTL_MS = 10 * 60 * 1000; +let pendingRequest: PendingOptiNlpChatRequest | undefined; + +export function setPendingOptiNlpChatRequest(request: NewPendingOptiNlpChatRequest): PendingOptiNlpChatRequest { + pendingRequest = { + ...request, + id: randomUUID(), + createdAt: Date.now() + }; + return pendingRequest; +} + +export function takePendingOptiNlpChatRequest(mode: OptiNlpMode, chatPrompt: string): PendingOptiNlpChatRequest | undefined { + if (!pendingRequest) { + return undefined; + } + if (Date.now() - pendingRequest.createdAt > PENDING_REQUEST_TTL_MS) { + pendingRequest = undefined; + return undefined; + } + const trimmedPrompt = chatPrompt.trim(); + const matchesPrompt = + trimmedPrompt === pendingRequest.chatPrompt || + trimmedPrompt.startsWith(`${pendingRequest.chatPrompt} `) || + trimmedPrompt.includes(pendingRequest.id); + if (pendingRequest.mode !== mode || !matchesPrompt) { + return undefined; + } + + const request = pendingRequest; + pendingRequest = undefined; + return request; +} diff --git a/tools/vscode-optitrust/src/commands/optinlpChatParticipant.ts b/tools/vscode-optitrust/src/commands/optinlpChatParticipant.ts new file mode 100644 index 000000000..03fc957e5 --- /dev/null +++ b/tools/vscode-optitrust/src/commands/optinlpChatParticipant.ts @@ -0,0 +1,295 @@ +// Native VS Code Chat integration for OptiNLP. Voice input is intentionally +// delegated to VS Code's chat surface, where the VS Code Speech extension can +// provide microphone transcription without custom webview recording code. +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; +import { inferLanguage } from "../optinlp/assets"; +import { modeDefinition, modeFromCliCommand, resolveAutoMode } from "../optinlp/modes"; +import { OptiNlpMode } from "../optinlp/providerTypes"; +import { OptiNlpProviderError } from "../optinlp/providerErrors"; +import { editorActionForResult, targetSuggestionsFromMarkdown } from "../optinlp/resultActions"; +import { OptiNlpStructuredResult } from "../optinlp/resultSchemas"; +import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; +import { findAssociatedCSourceFile, findAssociatedFiles } from "../optitrust/files"; +import { OptitrustWorkspace, relativeToRoot } from "../optitrust/workspace"; +import { + clearOptiNlpSession, + optiNlpConfigurationSummary, + runOptiNlpGeneration, + SourceContext, + sourceContextFromEditor +} from "./optinlpCommands"; +import { takePendingOptiNlpChatRequest } from "./optinlpChatContext"; + +type WorkspaceProvider = () => Promise; + +const CHAT_PARTICIPANT_ID = "optitrust.optinlp"; +const COMMAND_INSERT_TARGET = "optitrust.optinlpInsertTarget"; +const COMMAND_OPEN_SCRIPT = "optitrust.optinlpOpenScript"; +const COMMAND_SELECT_PROVIDER = "optitrust.optinlpSelectProvider"; +const COMMAND_SET_MODEL = "optitrust.optinlpSetModel"; +const COMMAND_SET_API_KEY = "optitrust.optinlpSetConfiguredApiKey"; + +interface ChatSourceContext { + readonly sourceContext: SourceContext; + readonly filePath: string; + readonly language: string; + readonly targetInsertionFilePath?: string; +} + +export function registerOptiNlpChatParticipant( + context: vscode.ExtensionContext, + getWorkspace: WorkspaceProvider, + memory: OptiNlpSessionMemory +): void { + const handler: vscode.ChatRequestHandler = async (request, _chatContext, stream, token) => { + const prompt = request.prompt.trim(); + if (await handleUtilityCommand(request.command, memory, stream)) { + return {}; + } + + if (!prompt) { + stream.markdown("Describe the OptiTrust target or transformation you want, or use `/help`."); + return {}; + } + + const workspace = await getWorkspace(); + if (!workspace) { + stream.markdown("OptiTrust workspace not detected."); + return {}; + } + + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.uri.scheme !== "file") { + stream.markdown("Open an OptiTrust source or script file before asking OptiNLP."); + return {}; + } + + const mode = modeForChatRequest(request.command, prompt); + stream.progress(`OptiNLP: ${modeDefinition(mode).label}`); + const pending = takePendingOptiNlpChatRequest(mode, prompt); + const source = pending ?? (await sourceContextForChatRequest(workspace, editor, mode)); + if (!source) { + stream.markdown(`No matching C/C++ source file found for \`${path.basename(editor.document.uri.fsPath)}\`.`); + return {}; + } + + try { + const outcome = await runOptiNlpGeneration(context, workspace, memory, mode, pending?.userRequest ?? prompt, { + renderToOutput: false, + editor, + throwProviderErrors: true, + sourceContext: source.sourceContext, + filePath: source.filePath, + language: source.language, + cancellationToken: token + }); + if (!outcome) { + stream.markdown("OptiNLP did not produce a result."); + return {}; + } + + stream.markdown(outcome.result.markdownOutput); + renderActionButtons(stream, outcome.mode, outcome.result.structured, outcome.result.markdownOutput, source.targetInsertionFilePath); + return {}; + } catch (error) { + if (error instanceof OptiNlpProviderError) { + stream.markdown(`**OptiNLP Error**\n\n${error.userMessage}`); + if (error.technicalDetail) { + stream.markdown(`\n\n\`\`\`text\n${error.technicalDetail}\n\`\`\``); + } + renderConfigurationButtons(stream); + return {}; + } + throw error; + } + }; + + const participant = vscode.chat.createChatParticipant(CHAT_PARTICIPANT_ID, handler); + participant.iconPath = new vscode.ThemeIcon("sparkle"); + participant.followupProvider = { + provideFollowups: () => [ + { label: "Generate target", prompt: "target the loop i", command: "target" }, + { label: "Generate script", prompt: "unroll the loop i", command: "script" }, + { label: "Show config", prompt: "show configuration", command: "config" } + ] + }; + context.subscriptions.push(participant); +} + +async function handleUtilityCommand( + command: string | undefined, + memory: OptiNlpSessionMemory, + stream: vscode.ChatResponseStream +): Promise { + switch (command) { + case "config": + renderConfig(stream); + return true; + case "clear": + await clearOptiNlpSession(memory); + stream.markdown("OptiNLP session memory cleared."); + return true; + case "help": + renderHelp(stream); + return true; + default: + return false; + } +} + +function renderHelp(stream: vscode.ChatResponseStream): void { + stream.markdown([ + "## OptiNLP Help", + "", + "Use `@optinlp` with one of these commands:", + "", + "- `/target`: generate robust OptiTrust targets.", + "- `/script`: generate an OptiTrust transformation script from a command.", + "- `/full`: generate a complete transformation script for the active file.", + "- `/config`: show provider/model configuration.", + "- `/clear`: clear OptiNLP session memory.", + "", + "Examples:", + "", + "```text", + "@optinlp /target target the second loop named i", + "@optinlp /script unroll the loop i", + "@optinlp /full generate a full transformation script for this file", + "```", + "", + "When `/target` runs from an active `.ml` script, OptiNLP uses the matching same-basename C/C++ source file as context.", + "", + "Source files are sent fresh each turn. Stable OptiNLP prompt/knowledge/eval files are sent once per stateful provider session when supported, and resent for stateless providers.", + "", + "Voice input is provided by native VS Code Chat through VS Code Speech." + ].join("\n")); + renderConfigurationButtons(stream); +} + +function renderConfig(stream: vscode.ChatResponseStream): void { + const config = optiNlpConfigurationSummary(); + stream.markdown([ + "## OptiNLP Configuration", + "", + `- Provider: \`${config.provider}\``, + `- Model: ${config.model.length > 0 ? `\`${config.model}\`` : "provider default"}`, + `- Provider session memory: ${config.useProviderSession ? "enabled" : "disabled"}` + ].join("\n")); + renderConfigurationButtons(stream); +} + +function renderActionButtons( + stream: vscode.ChatResponseStream, + mode: OptiNlpMode, + result: OptiNlpStructuredResult | undefined, + markdownOutput: string, + targetInsertionFilePath?: string +): void { + if (!result) { + if (mode === "target") { + renderFallbackTargetButtons(stream, markdownOutput, targetInsertionFilePath); + } + return; + } + + if (result.kind === "target") { + const suggestions = targetSuggestions(result); + const targets = suggestions.length > 0 ? suggestions.map(suggestion => suggestion.target) : targetSuggestionsFromMarkdown(markdownOutput); + for (const target of targets) { + stream.button({ + title: target, + command: COMMAND_INSERT_TARGET, + arguments: [target, targetInsertionFilePath] + }); + } + return; + } + + const action = editorActionForResult(result); + if (action?.kind === "open_script") { + stream.button({ + title: "Open Script", + command: COMMAND_OPEN_SCRIPT, + arguments: [action.text] + }); + } +} + +function renderFallbackTargetButtons(stream: vscode.ChatResponseStream, markdownOutput: string, targetInsertionFilePath?: string): void { + for (const target of targetSuggestionsFromMarkdown(markdownOutput)) { + stream.button({ + title: target, + command: COMMAND_INSERT_TARGET, + arguments: [target, targetInsertionFilePath] + }); + } +} + +function renderConfigurationButtons(stream: vscode.ChatResponseStream): void { + stream.button({ title: "Select Provider", command: COMMAND_SELECT_PROVIDER }); + stream.button({ title: "Set Model", command: COMMAND_SET_MODEL }); + stream.button({ title: "Set API Key", command: COMMAND_SET_API_KEY }); +} + +function targetSuggestions(result: Extract): { readonly target: string }[] { + const seen = new Set(); + const suggestions: { readonly target: string }[] = []; + const add = (target: string | undefined): void => { + const trimmed = target?.trim(); + if (!trimmed || seen.has(trimmed)) { + return; + } + seen.add(trimmed); + suggestions.push({ target: trimmed }); + }; + add(result.recommendedTarget); + result.alternatives.forEach(add); + return suggestions; +} + +function modeForChatRequest(command: string | undefined, prompt: string): OptiNlpMode { + return command ? modeFromCliCommand(command) ?? resolveAutoMode("auto", prompt) : resolveAutoMode("auto", prompt); +} + +async function sourceContextForChatRequest( + workspace: OptitrustWorkspace, + editor: vscode.TextEditor, + mode: OptiNlpMode +): Promise { + const activePath = editor.document.uri.fsPath; + if (mode === "target" && path.extname(activePath) === ".ml") { + const source = await findAssociatedCSourceFile(activePath); + if (!source) { + return undefined; + } + return { + sourceContext: { + text: await fs.readFile(source.path, "utf8"), + label: "associated source" + }, + filePath: relativeToRoot(workspace.root, source.path), + language: inferLanguage(source.path), + targetInsertionFilePath: activePath + }; + } + + return { + sourceContext: sourceContextFromEditor(editor), + filePath: relativeToRoot(workspace.root, activePath), + language: inferLanguage(activePath), + targetInsertionFilePath: mode === "target" ? await targetInsertionPathForActiveFile(activePath) : undefined + }; +} + +async function targetInsertionPathForActiveFile(activePath: string): Promise { + if (path.extname(activePath) === ".ml") { + return activePath; + } + if (path.extname(activePath) !== ".opti") { + return undefined; + } + const files = await findAssociatedFiles(activePath); + return files.find(file => file.kind === "script")?.path; +} diff --git a/tools/vscode-optitrust/src/commands/optinlpPanel.ts b/tools/vscode-optitrust/src/commands/optinlpPanel.ts deleted file mode 100644 index a2a278920..000000000 --- a/tools/vscode-optitrust/src/commands/optinlpPanel.ts +++ /dev/null @@ -1,761 +0,0 @@ -// Webview side panel for OptiNLP. The panel is intentionally presentation-only: -// generation, provider selection, and editor actions live in shared modules. -import * as path from "path"; -import * as vscode from "vscode"; -import { - clearOptiNlpSession, - insertTargetAtCursor, - openOcamlDocument, - OptiNlpGenerationOutcome, - runOptiNlpGeneration, - setOptiNlpConfiguredProviderApiKey -} from "./optinlpCommands"; -import { inferLanguage } from "../optinlp/assets"; -import { modeDefinition, OPTINLP_MODE_DEFINITIONS, OptiNlpUiMode, resolveAutoMode } from "../optinlp/modes"; -import { OptiNlpMode } from "../optinlp/providerTypes"; -import { OptiNlpProviderError } from "../optinlp/providerErrors"; -import { editorActionForResult } from "../optinlp/resultActions"; -import { OptiNlpStructuredResult } from "../optinlp/resultSchemas"; -import { OptiNlpSessionMemory } from "../optinlp/sessionMemory"; -import { OptitrustWorkspace, relativeToRoot } from "../optitrust/workspace"; - -interface WebviewMessage { - readonly type: string; - readonly mode?: OptiNlpUiMode; - readonly request?: string; - readonly text?: string; - readonly resultId?: string; - readonly target?: string; -} - -interface TargetSuggestion { - readonly label: string; - readonly target: string; -} - -export class OptiNlpPanel { - private static current: OptiNlpPanel | undefined; - private readonly panel: vscode.WebviewPanel; - private resultSeq = 0; - private readonly results = new Map(); - private sourceEditor: vscode.TextEditor | undefined; - private sourceEditorLocked = false; - private disposables: vscode.Disposable[] = []; - - private constructor( - private readonly context: vscode.ExtensionContext, - private readonly workspace: OptitrustWorkspace, - private readonly memory: OptiNlpSessionMemory, - panel: vscode.WebviewPanel - ) { - this.panel = panel; - this.sourceEditor = asFileTextEditor(vscode.window.activeTextEditor); - this.panel.webview.html = renderPanelHtml(); - this.panel.onDidDispose(() => this.dispose(), undefined, this.disposables); - this.panel.webview.onDidReceiveMessage(message => this.handleMessage(message as WebviewMessage), undefined, this.disposables); - vscode.window.onDidChangeActiveTextEditor(editor => { - const fileEditor = asFileTextEditor(editor); - if (fileEditor && !this.sourceEditorLocked) { - this.sourceEditor = fileEditor; - } - void this.refreshContext(); - }, undefined, this.disposables); - } - - static show(context: vscode.ExtensionContext, workspace: OptitrustWorkspace, memory: OptiNlpSessionMemory): OptiNlpPanel { - if (OptiNlpPanel.current) { - OptiNlpPanel.current.panel.reveal(vscode.ViewColumn.Beside); - void OptiNlpPanel.current.refreshContext(); - return OptiNlpPanel.current; - } - - const panel = vscode.window.createWebviewPanel("optitrustOptiNlp", "OptiNLP", vscode.ViewColumn.Beside, { - enableScripts: true, - retainContextWhenHidden: true - }); - OptiNlpPanel.current = new OptiNlpPanel(context, workspace, memory, panel); - void OptiNlpPanel.current.refreshContext(); - return OptiNlpPanel.current; - } - - setSourceEditor(editor: vscode.TextEditor, locked = false): void { - this.sourceEditor = editor; - this.sourceEditorLocked = locked; - void this.refreshContext(); - } - - postGenerationOutcome(outcome: OptiNlpGenerationOutcome): void { - this.postOutcome(outcome); - } - - postUserRequest(mode: OptiNlpUiMode, request: string, meta?: string): void { - this.post({ - type: "externalRequest", - mode, - request, - meta - }); - } - - private async handleMessage(message: WebviewMessage): Promise { - switch (message.type) { - case "ready": - await this.refreshContext(); - return; - case "generate": - await this.generate(message.mode ?? "auto", message.request ?? ""); - return; - case "setApiKey": - await setOptiNlpConfiguredProviderApiKey(this.context); - return; - case "clearSession": - await clearOptiNlpSession(this.memory); - this.post({ type: "sessionCleared" }); - return; - case "copy": - if (message.text) { - await vscode.env.clipboard.writeText(message.text); - this.post({ type: "copied" }); - } - return; - case "insertTarget": - await this.insertTarget(message.resultId, message.target); - return; - case "openScript": - await this.openScript(message.resultId); - return; - } - } - - private async generate(mode: OptiNlpUiMode, request: string): Promise { - const trimmed = request.trim(); - if (trimmed.length === 0) { - this.post({ type: "error", message: "Request cannot be empty." }); - return; - } - - this.sourceEditorLocked = false; - const resolvedMode = resolveAutoMode(mode, trimmed); - this.post({ type: "busy", busy: true }); - try { - const outcome = await runOptiNlpGeneration(this.context, this.workspace, this.memory, resolvedMode, trimmed, { - renderToOutput: false, - editor: this.getSourceEditor(), - throwProviderErrors: true - }); - if (!outcome) { - this.post({ type: "busy", busy: false }); - return; - } - this.postOutcome(outcome); - await this.refreshContext(); - } catch (error) { - const message = - error instanceof OptiNlpProviderError && error.technicalDetail - ? `${error.userMessage}\n\n${error.technicalDetail}` - : error instanceof Error - ? error.message - : String(error); - this.post({ type: "error", message }); - } finally { - this.post({ type: "busy", busy: false }); - } - } - - private async insertTarget(resultId: string | undefined, target: string | undefined): Promise { - if (target && target.trim().length > 0) { - await insertTargetAtCursor(target, this.getSourceEditor()); - this.post({ type: "inserted" }); - return; - } - - const result = resultId ? this.results.get(resultId) : undefined; - if (!result || result.kind !== "target" || !result.recommendedTarget) { - this.post({ type: "error", message: "No target expression is available for insertion." }); - return; - } - await insertTargetAtCursor(result.recommendedTarget, this.getSourceEditor()); - this.post({ type: "inserted" }); - } - - private async openScript(resultId: string | undefined): Promise { - const result = resultId ? this.results.get(resultId) : undefined; - const action = editorActionForResult(result); - if (!action) { - this.post({ type: "error", message: "No generated script is available." }); - return; - } - if (action.kind === "open_script") { - await openOcamlDocument(action.text); - return; - } - this.post({ type: "error", message: "This result does not contain a generated script." }); - } - - private storeStructuredResult(result: OptiNlpStructuredResult | undefined): string | undefined { - if (!result) { - return undefined; - } - const id = String(++this.resultSeq); - this.results.set(id, result); - return id; - } - - private postOutcome(outcome: OptiNlpGenerationOutcome): void { - const resultId = this.storeStructuredResult(outcome.result.structured); - this.post({ - type: "result", - resultId, - mode: outcome.mode, - request: outcome.userRequest, - sourceLabel: outcome.sourceLabel, - provider: outcome.result.provider, - model: outcome.result.model, - markdown: outcome.result.markdownOutput, - structured: outcome.result.structured, - targetSuggestions: targetSuggestions(outcome.result.structured) - }); - } - - private async refreshContext(): Promise { - const editor = this.getSourceEditor(); - if (!editor) { - this.post({ type: "context", label: "No active file" }); - return; - } - const filePath = editor.document.uri.fsPath; - const selectedText = editor.document.getText(editor.selection); - const relativePath = relativeToRoot(this.workspace.root, filePath); - const selectionLabel = selectedText.trim().length > 0 ? "selection" : "full file"; - this.post({ - type: "context", - label: `${relativePath} · ${selectionLabel} · ${inferLanguage(filePath)}`, - fileName: path.basename(filePath), - hasSelection: selectedText.trim().length > 0 - }); - } - - private getSourceEditor(): vscode.TextEditor | undefined { - if (this.sourceEditorLocked && this.sourceEditor) { - return this.sourceEditor; - } - const activeEditor = asFileTextEditor(vscode.window.activeTextEditor); - if (activeEditor) { - this.sourceEditor = activeEditor; - return activeEditor; - } - return this.sourceEditor; - } - - private post(message: unknown): void { - void this.panel.webview.postMessage(message); - } - - private dispose(): void { - OptiNlpPanel.current = undefined; - for (const disposable of this.disposables) { - disposable.dispose(); - } - this.disposables = []; - } -} - -function targetSuggestions(result: OptiNlpStructuredResult | undefined): TargetSuggestion[] { - if (!result || result.kind !== "target") { - return []; - } - - const seen = new Set(); - const suggestions: TargetSuggestion[] = []; - const add = (label: string, target: string | undefined): void => { - const trimmed = target?.trim(); - if (!trimmed || seen.has(trimmed)) { - return; - } - seen.add(trimmed); - suggestions.push({ label, target: trimmed }); - }; - - add("Recommended", result.recommendedTarget); - result.alternatives.forEach((target, index) => add(`Alternative ${index + 1}`, target)); - return suggestions; -} - -function asFileTextEditor(editor: vscode.TextEditor | undefined): vscode.TextEditor | undefined { - return editor?.document.uri.scheme === "file" ? editor : undefined; -} - -function renderPanelHtml(): string { - const nonce = createNonce(); - const modeOptions = [ - '', - ...OPTINLP_MODE_DEFINITIONS.map(definition => ``) - ].join(""); - const defaultPlaceholder = modeDefinition("target").placeholder; - return ` - - - - - - OptiNLP - - - -
-
-
OptiNLP
- - -
-
-
- - - -
-
-
- - -`; -} - -function createNonce(): string { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let nonce = ""; - for (let index = 0; index < 32; index += 1) { - nonce += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return nonce; -} - -function escapeHtml(value: string): string { - return value.replace(/&/gu, "&").replace(//gu, ">").replace(/"/gu, """); -} diff --git a/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts index 698a49869..09b420656 100644 --- a/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts +++ b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts @@ -4,8 +4,7 @@ import * as fs from "fs/promises"; import * as path from "path"; import * as vscode from "vscode"; -import { OptiNlpPanel } from "./optinlpPanel"; -import { runOptiNlpGeneration } from "./optinlpCommands"; +import { setPendingOptiNlpChatRequest } from "./optinlpChatContext"; import { getActiveEditorContext } from "../optitrust/editor"; import { markExecutedLine } from "../optitrust/decorations"; import { runCommand } from "../optitrust/runner"; @@ -193,6 +192,10 @@ function targetSourceContext(context: TargetAtCursorContext): string { ].join("\n"); } +function targetChatPrompt(context: TargetAtCursorContext): string { + return `Use the prepared F7 target-at-cursor context for ${context.scriptRelativePath}:${context.transformationLine}.`; +} + async function collectTargetAtCursorContext(workspace: OptitrustWorkspace, editorContext: ReturnType): Promise { const source = await findAssociatedCSourceFile(editorContext.filePath); if (!source) { @@ -245,9 +248,9 @@ async function collectTargetAtCursorContext(workspace: OptitrustWorkspace, edito } export async function suggestOptiNlpTargetAtCursor( - extensionContext: vscode.ExtensionContext, + _extensionContext: vscode.ExtensionContext, workspace: OptitrustWorkspace, - memory: OptiNlpSessionMemory + _memory: OptiNlpSessionMemory ): Promise { const editorContext = getActiveEditorContext(workspace.root); const validation = validateTransformationScript(editorContext); @@ -263,25 +266,23 @@ export async function suggestOptiNlpTargetAtCursor( await vscode.window.showTextDocument(editorContext.document, { viewColumn: vscode.ViewColumn.One, preserveFocus: false }); await openFileOrHtml(workspace.root, collected.afterOptiPath, path.basename(collected.afterOptiPath)); - - const panel = OptiNlpPanel.show(extensionContext, workspace, memory); - panel.setSourceEditor(editorContext.editor, true); - + const chatPrompt = targetChatPrompt(collected); const request = targetRequest(collected); - panel.postUserRequest("target", request, `Target · ${collected.scriptRelativePath}:${collected.transformationLine}`); - - const outcome = await runOptiNlpGeneration(extensionContext, workspace, memory, "target", request, { - renderToOutput: false, - editor: editorContext.editor, + const pending = setPendingOptiNlpChatRequest({ + mode: "target", + chatPrompt, + userRequest: request, sourceContext: { text: targetSourceContext(collected), label: "target-at-cursor context" }, filePath: collected.sourceRelativePath, - language: "cpp+optilambda+ocaml" + language: "cpp+optilambda+ocaml", + targetInsertionFilePath: collected.scriptPath }); - if (outcome) { - panel.postGenerationOutcome(outcome); - } + await vscode.commands.executeCommand("optitrust.optinlpChat", { + query: `@optinlp /target ${chatPrompt} [context:${pending.id}]`, + preserveExisting: true + }); } diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index 5beb6a6a5..ff3c7b846 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -7,15 +7,20 @@ import { openUnitTestMlCppFiles } from "./commands/associatedFiles"; import { runHealthCheck } from "./commands/healthCheck"; +import { registerOptiNlpChatParticipant } from "./commands/optinlpChatParticipant"; import { clearOptiNlpSession, generateOptiNlpScript, generateOptiNlpTarget, - setOptiNlpOpenAiApiKey, + generateOptiNlpFullTransformation, + insertTargetAtCursorInFile, + openOcamlDocument, + selectOptiNlpProvider, + setOptiNlpConfiguredProviderApiKey, setOptiNlpGeminiApiKey, - generateOptiNlpFullTransformation + setOptiNlpModel, + setOptiNlpOpenAiApiKey } from "./commands/optinlpCommands"; -import { OptiNlpPanel } from "./commands/optinlpPanel"; import { suggestOptiNlpTargetAtCursor } from "./commands/optinlpTargetAtCursor"; import { rerunLastTests, runCurrentTest, runCurrentTestAndOpenDiff } from "./commands/runTests"; import { showShortcuts } from "./commands/shortcuts"; @@ -63,11 +68,11 @@ async function requireWorkspace(): Promise { return workspace; } -function registerCommand(context: vscode.ExtensionContext, command: string, callback: () => Promise | void): void { +function registerCommand(context: vscode.ExtensionContext, command: string, callback: (...args: unknown[]) => Promise | void): void { context.subscriptions.push( - vscode.commands.registerCommand(command, async () => { + vscode.commands.registerCommand(command, async (...args: unknown[]) => { try { - await callback(); + await callback(...args); } catch (error) { const message = error instanceof Error ? error.message : String(error); vscode.window.showErrorMessage(`OptiTrust: ${message}`); @@ -79,6 +84,7 @@ function registerCommand(context: vscode.ExtensionContext, command: string, call export async function activate(context: vscode.ExtensionContext): Promise { await refreshWorkspace(vscode.window.activeTextEditor?.document.uri.fsPath); optiNlpSession = new OptiNlpSessionMemory(); + registerOptiNlpChatParticipant(context, requireWorkspace, optiNlpSession); registerCommand(context, "optitrust.hello", async () => { const workspace = await requireWorkspace(); @@ -218,11 +224,8 @@ export async function activate(context: vscode.ExtensionContext): Promise registerCommand(context, "optitrust.showShortcuts", showShortcuts); - registerCommand(context, "optitrust.optinlpChat", async () => { - const workspace = await requireWorkspace(); - if (workspace && optiNlpSession) { - OptiNlpPanel.show(context, workspace, optiNlpSession); - } + registerCommand(context, "optitrust.optinlpChat", async (options: unknown) => { + await openOptiNlpChat(openChatOptions(options)); }); registerCommand(context, "optitrust.optinlpGenerateTarget", async () => { @@ -261,6 +264,30 @@ export async function activate(context: vscode.ExtensionContext): Promise await setOptiNlpOpenAiApiKey(context); }); + registerCommand(context, "optitrust.optinlpSetConfiguredApiKey", async () => { + await setOptiNlpConfiguredProviderApiKey(context); + }); + + registerCommand(context, "optitrust.optinlpSelectProvider", selectOptiNlpProvider); + + registerCommand(context, "optitrust.optinlpSetModel", setOptiNlpModel); + + registerCommand(context, "optitrust.optinlpInsertTarget", async (target: unknown, filePath: unknown) => { + if (typeof target !== "string" || target.trim().length === 0) { + vscode.window.showWarningMessage("OptiNLP: no target was provided for insertion."); + return; + } + await insertTargetAtCursorInFile(target, typeof filePath === "string" && filePath.length > 0 ? filePath : undefined); + }); + + registerCommand(context, "optitrust.optinlpOpenScript", async (script: unknown) => { + if (typeof script !== "string" || script.trim().length === 0) { + vscode.window.showWarningMessage("OptiNLP: no generated script was provided."); + return; + } + await openOcamlDocument(script); + }); + registerCommand(context, "optitrust.optinlpClearSession", async () => { if (optiNlpSession) { await clearOptiNlpSession(optiNlpSession); @@ -290,6 +317,58 @@ export async function activate(context: vscode.ExtensionContext): Promise updateDecorations(); } +interface OpenOptiNlpChatOptions { + readonly query: string; + readonly preserveExisting?: boolean; +} + +function openChatOptions(value: unknown): OpenOptiNlpChatOptions { + if (typeof value === "string") { + return { query: value }; + } + if (value && typeof value === "object") { + const maybeOptions = value as { readonly query?: unknown; readonly preserveExisting?: unknown }; + return { + query: typeof maybeOptions.query === "string" ? maybeOptions.query : "@optinlp ", + preserveExisting: maybeOptions.preserveExisting === true + }; + } + return { query: "@optinlp " }; +} + +async function openOptiNlpChat(options: OpenOptiNlpChatOptions): Promise { + if (options.preserveExisting) { + await focusExistingChatAndCopyPrompt(options.query); + return; + } + + try { + await vscode.commands.executeCommand("workbench.action.chat.open", { query: options.query }); + return; + } catch { + // Older VS Code builds may not support opening chat with a prefilled query. + } + + try { + await vscode.commands.executeCommand("workbench.action.chat.open"); + } catch { + vscode.window.showInformationMessage("Open VS Code Chat and type @optinlp to use OptiNLP."); + return; + } + vscode.window.showInformationMessage(`Type ${options.query} in VS Code Chat to use OptiNLP.`); +} + +async function focusExistingChatAndCopyPrompt(query: string): Promise { + try { + await vscode.commands.executeCommand("workbench.action.chat.open"); + } catch { + vscode.window.showInformationMessage("Open VS Code Chat and type @optinlp to use OptiNLP."); + return; + } + await vscode.env.clipboard.writeText(query); + vscode.window.showInformationMessage("OptiNLP target context is ready. The chat prompt was copied; paste it into the existing VS Code Chat and send it."); +} + export function deactivate(): void { disposeDecorations(); disposeOutput(); From 819d20094223fd190e0588e3b712ed1d4073a965 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Thu, 2 Jul 2026 08:32:26 -0400 Subject: [PATCH 23/47] fix: preserve includes in OptiLambda output --- lib/framework/runtime/run.ml | 58 ++++++++++++++++++++-------- lib/framework/runtime/trace.ml | 7 ++-- lib/optilambda/optilambda.ml | 2 + lib/optilambda/optilambda_printer.ml | 55 ++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/lib/framework/runtime/run.ml b/lib/framework/runtime/run.ml index e0f558f74..2d13cf495 100644 --- a/lib/framework/runtime/run.ml +++ b/lib/framework/runtime/run.ml @@ -42,18 +42,44 @@ let debug_inline_cpp = false source file and not those referring to the include path. *) let generate_source_with_inlined_header_cpp (basepath : string) (input_file : string) (inline : string list) (output_file : string) : unit = - (* FIXME: Inefficient because it performs one full pass for each inlined file. Moreover, order inside inline matters... *) - let s = ref (File.get_contents (Filename.concat basepath input_file)) in - let perform_inline finline = - let include_instr = "#include \"" ^ finline ^ "\"" in - if debug_inline_cpp then Tools.debug "Inlined %s" include_instr; - let contents = File.get_contents (Filename.concat basepath finline) in - s := Tools.string_subst include_instr contents !s; - in - List.iter perform_inline inline; - File.put_contents (Filename.concat basepath output_file) !s; + let absolute_path path = + let path = if Filename.is_relative path then Filename.concat (Unix.getcwd ()) path else path in + Filename.concat (Unix.realpath (Filename.dirname path)) (Filename.basename path) + in + let output_path = Filename.concat basepath output_file in + let output_source_path = absolute_path output_path in + let quote_line_file filename = String.escaped filename in + let include_target line = + List.find_opt + (fun finline -> String.trim line = "#include \"" ^ finline ^ "\"") + inline + in + let rec expand_file ~(source_path : string) (read_path : string) : string = + File.get_lines read_path + |> List.mapi (fun i line -> + match include_target line with + | None -> line + | Some finline -> + if debug_inline_cpp then Tools.debug "Inlined #include \"%s\"" finline; + let included_path = absolute_path (Filename.concat basepath finline) in + Printf.sprintf "#line 1 \"%s\"\n%s\n#line %d \"%s\"" + (quote_line_file included_path) + (expand_file ~source_path:included_path included_path) + (i + 2) + (quote_line_file source_path)) + |> String.concat "\n" + in + let input_path = Filename.concat basepath input_file in + let contents = expand_file ~source_path:output_source_path input_path in + File.put_contents output_path contents; if debug_inline_cpp then Tools.debug "Generated %s" output_file +(** [get_c_includes filename] returns the include directives visibly present in [filename]. *) +let get_c_includes (filename : string) : string = + File.get_lines filename + |> List.filter (fun line -> String.starts_with ~prefix:"#include" (String.trim line)) + |> String.concat "\n\n" + (** [get_program_basename ()]: returns the basename of the current binary program being used. It takes care to remove the leading './' and takes care to remove the "with_lines" suffix. *) let get_program_basename () : string = @@ -112,7 +138,7 @@ let may_report_time (msg : string) (f : unit -> 'a) : 'a = This flag only has an effect if a [-exit_line] option was passed on the command line. - [~prefix:string] allows providing the basename for the output files produced *) -let script ?(filename : string option) ~(extension : string) ?(check_exit_at_end : bool = true) ?(prefix : string option) ?(capture_show_in_batch = false) (f : unit -> unit) : unit = +let script ?(filename : string option) ?(header : string option) ~(extension : string) ?(check_exit_at_end : bool = true) ?(prefix : string option) ?(capture_show_in_batch = false) (f : unit -> unit) : unit = Flags.process_cmdline_args (); Target.show_next_id_reset (); @@ -149,7 +175,7 @@ let script ?(filename : string option) ~(extension : string) ?(check_exit_at_end try let trace_filename = prefix ^ "_trace.js" in if Sys.file_exists trace_filename then Sys.remove trace_filename; - Trace.init ~program:program_basename ~prefix filename; + Trace.init ?header ~program:program_basename ~prefix filename; if !Flags.check_validity || !Flags.recompute_resources_between_steps then Trace.step ~kind:Step_small ~tags:["pre-post-processing"] ~name:"Preprocessing contracts" (fun () -> Resources.fix_types_in_contracts (); @@ -245,9 +271,9 @@ let script_cpp ?(filename : string option) ?(prepro : string list = []) ?(inline *) (* Handles on-the-fly inlining *) - let filename = + let filename, header = match inline with - | [] -> filename + | [] -> filename, None | _ -> let program_basename = get_program_basename () in let basepath = Filename.dirname program_basename in @@ -259,10 +285,10 @@ let script_cpp ?(filename : string option) ?(prepro : string list = []) ?(inline let basename = Filename.chop_extension filename in let inlinefilename = basename ^ "_inlined.cpp" in generate_source_with_inlined_header_cpp basepath filename inline inlinefilename; - Some inlinefilename + Some inlinefilename, Some (get_c_includes (Filename.concat basepath filename)) in - script ?filename ~capture_show_in_batch ~extension:".cpp" ~check_exit_at_end ?prefix f) + script ?filename ?header ~capture_show_in_batch ~extension:".cpp" ~check_exit_at_end ?prefix f) let stg_name (stg: int): string = diff --git a/lib/framework/runtime/trace.ml b/lib/framework/runtime/trace.ml index 8f65ee0ca..f7591ce01 100644 --- a/lib/framework/runtime/trace.ml +++ b/lib/framework/runtime/trace.ml @@ -557,7 +557,7 @@ let output_prog (style:output_style) ?(beautify:bool=true) (ctx : context) (pref begin try begin match style.print with | Lang_OptiLambda optilambda_style -> - output_string out_prog (Optitrust_optilambda.Optilambda.trm_to_string ~style:optilambda_style ast) + output_string out_prog (Optitrust_optilambda.Optilambda.program_to_string ~style:optilambda_style ~header:ctx.header ast) | Lang_AST _ -> raise (TraceFailure "output_prog requires a Lang_C or Lang_OptiLambda printing mode, not a Lang_AST") | Lang_C cstyle -> (* Print the header, in particular the include directives *) (* LATER: include header directives into the AST representation *) @@ -1342,7 +1342,7 @@ let get_initial_ast (filename : string) : (string * trm) = [~prefix:"foo"] allows to use a custom prefix for all output files, instead of the basename of [f]. style is computed based on the global flags. *) -let init ~(prefix : string) ~(program : string) (filename : string) : unit = +let init ?(header : string option) ~(prefix : string) ~(program : string) (filename : string) : unit = ast_just_before_first_call_to_restore_original := None; (* TEMPORARY HACK *) invalidate (); let basename = Filename.basename filename in @@ -1374,7 +1374,8 @@ let init ~(prefix : string) ~(program : string) (filename : string) : unit = init_logs prefix; - let (header, cur_ast), stats_parse = Stats.measure_stats (fun () -> get_initial_ast filename) in + let ((parsed_header, cur_ast), stats_parse) = Stats.measure_stats (fun () -> get_initial_ast filename) in + let header = Option.value ~default:parsed_header header in let context = { extension; prefix; header } in the_trace.next_step_id <- 0; diff --git a/lib/optilambda/optilambda.ml b/lib/optilambda/optilambda.ml index d3e90d4d5..19af721df 100644 --- a/lib/optilambda/optilambda.ml +++ b/lib/optilambda/optilambda.ml @@ -10,6 +10,8 @@ let default_style = Style.default let trm_to_doc = Printer.trm_to_doc let trm_to_string = Printer.trm_to_string +let program_to_doc = Printer.program_to_doc +let program_to_string = Printer.program_to_string let trm_to_html = Html.trm_to_html let typ_to_doc = Printer.typ_to_doc let typ_to_string = Printer.typ_to_string diff --git a/lib/optilambda/optilambda_printer.ml b/lib/optilambda/optilambda_printer.ml index 190d306bc..c32720a02 100644 --- a/lib/optilambda/optilambda_printer.ml +++ b/lib/optilambda/optilambda_printer.ml @@ -1,5 +1,6 @@ open PPrint open Ast +open Trm open Typ open Optilambda_style @@ -37,6 +38,38 @@ type contract_clause = ContractClause of string * resource_item | ContractRaw of type read_only_formula = { read_frac : trm; read_body : trm } +(** [header_include_to_doc line] converts a C/C++ header include into an OptiLambda include directive. *) +let header_include_to_doc (line : string) : document option = + let line = String.trim line in + if String.starts_with ~prefix:"#include" line then + let include_target = String.trim (String.sub line 8 (String.length line - 8)) in + if include_target = "" then None else Some (string "include" ^^ blank 1 ^^ string include_target) + else + None + +(** [header_to_docs header] extracts OptiLambda include directives from the parser header. *) +let header_to_docs (header : string) : document list = + header + |> String.split_on_char '\n' + |> List.filter_map header_include_to_doc + +(** [main_source_file t] returns the source file attached to the root program, when known. *) +let main_source_file (t : trm) : string option = + match t.loc with + | Some { loc_file; _ } -> Some loc_file + | None -> None + +(** [is_from_included_file main_file t] detects top-level declarations whose source location comes from an included file. + + Include annotations are the primary signal, but some C encoding passes may flatten included sequences while preserving source locations + on their declarations. Location-based filtering keeps generated OptiLambda output from expanding included file contents. *) +let is_from_included_file (main_file : string option) (t : trm) : bool = + trm_is_include t + || + match main_file, t.loc with + | Some main_file, Some { loc_file; _ } -> loc_file <> main_file + | _ -> false + (** [code_block_doc items] prints executable block items. Regular items always end with a semicolon. The optional final expression is printed without a trailing semicolon, matching @@ -854,8 +887,30 @@ and trm_to_doc_at (style : Optilambda_style.style) (ctx_prec : int) (t : trm) : (** [trm_to_doc style t] is the main entry point for printing terms. *) and trm_to_doc (style : Optilambda_style.style) (t : trm) : document = trm_to_doc_at style 0 t +(** [program_to_doc style ~header t] prints a complete program. + + C/C++ parsing stores textual header includes separately from the AST, while declarations from included files remain in the AST as + [Included_file] sequences. Program printing keeps the visible include directives and drops those included-file sequences, so OptiLambda + output describes the source program without expanding every included declaration. *) +let program_to_doc (style : Optilambda_style.style) ~(header : string) (t : trm) : document = + let include_docs = header_to_docs header in + let program = + match t.desc with + | Trm_seq (instrs, result) -> + let main_file = main_source_file t in + trm_to_doc style { t with desc = Trm_seq (Mlist.filter (fun instr -> not (is_from_included_file main_file instr)) instrs, result) } + | _ -> trm_to_doc style t + in + match include_docs with + | [] -> program + | _ -> separate (semi ^^ hardline) include_docs ^^ semi ^^ twice hardline ^^ program + (** [typ_to_string ?style ty] prints a type directly to a string. *) let typ_to_string ?(style = Optilambda_style.default) (ty : typ) : string = Tools.document_to_string (typ_to_doc style ty) (** [trm_to_string ?style t] prints a term directly to a string. *) let trm_to_string ?(style = Optilambda_style.default) (t : trm) : string = Tools.document_to_string (trm_to_doc style t) + +(** [program_to_string ?style ~header t] prints a complete program directly to a string. *) +let program_to_string ?(style = Optilambda_style.default) ~(header : string) (t : trm) : string = + Tools.document_to_string (program_to_doc style ~header t) From 3b63ce55dcdf559636adc4128fd4e70b399f2c80 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Fri, 3 Jul 2026 08:36:54 -0400 Subject: [PATCH 24/47] open OptiTrust diffs in native VS Code diff --- tools/vscode-optitrust/package.json | 17 ++ .../src/commands/viewCommands.ts | 22 +- tools/vscode-optitrust/src/extension.ts | 6 + .../src/optitrust/nativeDiff.ts | 285 ++++++++++++++++++ 4 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 tools/vscode-optitrust/src/optitrust/nativeDiff.ts diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index 9d6ac0f7a..eea3ee95f 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -25,6 +25,7 @@ "onCommand:optitrust.viewDiff", "onCommand:optitrust.viewDiffOnlyCode", "onCommand:optitrust.viewDiffInternalSyntax", + "onCommand:optitrust.switchDiffSyntax", "onCommand:optitrust.viewFullTrace", "onCommand:optitrust.viewTraceSaveStepsScript", "onCommand:optitrust.viewStepTrace", @@ -77,6 +78,11 @@ "command": "optitrust.viewDiffInternalSyntax", "title": "OptiTrust: View Diff Using Internal Syntax" }, + { + "command": "optitrust.switchDiffSyntax", + "title": "OptiTrust: Switch Diff Syntax", + "icon": "$(replace)" + }, { "command": "optitrust.viewFullTrace", "title": "OptiTrust: View Full Trace" @@ -262,6 +268,9 @@ } } }, + "configurationDefaults": { + "diffEditor.renderSideBySide": true + }, "chatParticipants": [ { "id": "optitrust.optinlp", @@ -379,6 +388,11 @@ "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace", "group": "navigation@50" }, + { + "command": "optitrust.switchDiffSyntax", + "when": "resourceScheme == optitrust-diff", + "group": "navigation@52" + }, { "command": "optitrust.optinlpChat", "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", @@ -430,6 +444,9 @@ { "command": "optitrust.viewDiffInternalSyntax" }, + { + "command": "optitrust.switchDiffSyntax" + }, { "command": "optitrust.viewFullTrace" }, diff --git a/tools/vscode-optitrust/src/commands/viewCommands.ts b/tools/vscode-optitrust/src/commands/viewCommands.ts index 2ba2432ef..f20ad4d36 100644 --- a/tools/vscode-optitrust/src/commands/viewCommands.ts +++ b/tools/vscode-optitrust/src/commands/viewCommands.ts @@ -6,7 +6,8 @@ import { markExecutedLine } from "../optitrust/decorations"; import { appendLine } from "../optitrust/output"; import { runCommand } from "../optitrust/runner"; import { validateTransformationScript } from "../optitrust/scripts"; -import { backendFlagsForViewMode, getSelectedViewMode, ViewModeDefinition } from "../optitrust/viewMode"; +import { backendFlagsForViewMode, getSelectedViewMode, VIEW_MODES, ViewModeDefinition } from "../optitrust/viewMode"; +import { openNativeStepDiff } from "../optitrust/nativeDiff"; import { openHtmlView } from "../optitrust/views"; import { OptitrustWorkspace } from "../optitrust/workspace"; @@ -66,7 +67,9 @@ export async function runViewCommand(workspace: OptitrustWorkspace, mode: ViewMo markExecutedLine(context.editor, context.line); const selectedViewMode = getSelectedViewMode(); - const extraArgs = viewArgs(spec.mode, selectedViewMode, option); + const initialDiffViewMode = option === undefined ? DEFAULT_STEP_DIFF_VIEW_MODE : selectedViewMode; + const commandViewMode = mode === "step_diff" ? initialDiffViewMode : selectedViewMode; + const extraArgs = viewArgs(spec.mode, commandViewMode, option); const args = [spec.scriptMode, context.relativePath, String(context.line), ...extraArgs]; try { @@ -83,6 +86,21 @@ export async function runViewCommand(workspace: OptitrustWorkspace, mode: ViewMo return; } + if (mode === "step_diff") { + await openNativeStepDiff( + { + root: workspace.root, + scriptRelativePath: context.relativePath, + line: context.line, + fileDir: context.fileDir, + fileBase: context.fileBase + }, + commandViewMode, + { markGenerated: true } + ); + return; + } + const htmlFile = path.join(context.fileDir, `${context.fileBase}${spec.htmlSuffix}`); if (await exists(htmlFile)) { await openHtmlView(workspace.root, htmlFile, spec.viewKind, `${selectedViewMode.id}:${option ?? "default"}:${context.relativePath}`, `${context.fileBase} ${spec.viewKind}`); diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index ff3c7b846..d10f429ca 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -32,6 +32,7 @@ import { runViewTraceSaveStepsScript } from "./commands/viewCommands"; import { disposeDecorations, updateDecorations } from "./optitrust/decorations"; +import { registerNativeDiffProvider, switchNativeDiffSyntax } from "./optitrust/nativeDiff"; import { appendLine, disposeOutput } from "./optitrust/output"; import { getSelectedViewMode, updateSelectedViewMode, VIEW_MODES } from "./optitrust/viewMode"; import { findOptitrustRoot, OptitrustWorkspace } from "./optitrust/workspace"; @@ -84,6 +85,7 @@ function registerCommand(context: vscode.ExtensionContext, command: string, call export async function activate(context: vscode.ExtensionContext): Promise { await refreshWorkspace(vscode.window.activeTextEditor?.document.uri.fsPath); optiNlpSession = new OptiNlpSessionMemory(); + registerNativeDiffProvider(context); registerOptiNlpChatParticipant(context, requireWorkspace, optiNlpSession); registerCommand(context, "optitrust.hello", async () => { @@ -139,6 +141,10 @@ export async function activate(context: vscode.ExtensionContext): Promise await runViewDiffInternalSyntax(workspace); }); + registerCommand(context, "optitrust.switchDiffSyntax", async () => { + await switchNativeDiffSyntax(); + }); + registerCommand(context, "optitrust.redoLastViewCommand", async () => { const workspace = await requireWorkspace(); if (workspace) { diff --git a/tools/vscode-optitrust/src/optitrust/nativeDiff.ts b/tools/vscode-optitrust/src/optitrust/nativeDiff.ts new file mode 100644 index 000000000..f3d78d32b --- /dev/null +++ b/tools/vscode-optitrust/src/optitrust/nativeDiff.ts @@ -0,0 +1,285 @@ +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; +import { appendLine } from "./output"; +import { runCommand } from "./runner"; +import { backendFlagsForViewMode, ViewModeDefinition, VIEW_MODES } from "./viewMode"; + +const OPTITRUST_DIFF_SCHEME = "optitrust-diff"; + +interface DiffFilePair { + readonly before: string; + readonly after: string; + readonly label: string; +} + +interface NativeDiffSession { + readonly id: string; + readonly root: string; + readonly scriptRelativePath: string; + readonly line: number; + readonly fileDir: string; + readonly fileBase: string; + readonly generatedModes: Set; +} + +export interface NativeStepDiffContext { + readonly root: string; + readonly scriptRelativePath: string; + readonly line: number; + readonly fileDir: string; + readonly fileBase: string; +} + +interface OpenNativeStepDiffOptions { + readonly viewColumn?: vscode.ViewColumn; + readonly markGenerated?: boolean; + readonly generateIfMissing?: boolean; +} + +const sessions = new Map(); + +async function exists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +function stepDiffCandidates(fileDir: string, fileBase: string, selectedViewMode: ViewModeDefinition): DiffFilePair[] { + if (selectedViewMode.id === "optilambda.surface") { + return [ + { + before: path.join(fileDir, `${fileBase}_before.opti`), + after: path.join(fileDir, `${fileBase}_after.opti`), + label: selectedViewMode.label + }, + { + before: path.join(fileDir, `${fileBase}_before_surface.opti`), + after: path.join(fileDir, `${fileBase}_after_surface.opti`), + label: selectedViewMode.label + } + ]; + } + + if (selectedViewMode.id === "optilambda.internal" || selectedViewMode.id === "optilambda.typed") { + const representation = selectedViewMode.optilambdaRepresentation ?? "surface"; + return [ + { + before: path.join(fileDir, `${fileBase}_before_${representation}.opti`), + after: path.join(fileDir, `${fileBase}_after_${representation}.opti`), + label: selectedViewMode.label + } + ]; + } + + return [".cpp", ".c", ".cu"].map(extension => ({ + before: path.join(fileDir, `${fileBase}_before${extension}`), + after: path.join(fileDir, `${fileBase}_after${extension}`), + label: selectedViewMode.label + })); +} + +async function findExistingPair(candidates: DiffFilePair[]): Promise { + for (const candidate of candidates) { + if ((await exists(candidate.before)) && (await exists(candidate.after))) { + return candidate; + } + } + return undefined; +} + +function sessionId(fileDir: string, fileBase: string): string { + return path.resolve(fileDir, fileBase); +} + +function diffUri(filePath: string, session: NativeDiffSession): vscode.Uri { + const query = new URLSearchParams({ + file: filePath, + session: session.id, + root: session.root, + scriptRelativePath: session.scriptRelativePath, + line: String(session.line), + fileDir: session.fileDir, + fileBase: session.fileBase + }); + return vscode.Uri.from({ + scheme: OPTITRUST_DIFF_SCHEME, + path: `/${path.basename(filePath)}`, + query: query.toString() + }); +} + +function filePathFromUri(uri: vscode.Uri): string { + const filePath = new URLSearchParams(uri.query).get("file"); + if (!filePath) { + throw new Error(`Missing backing file in ${uri.toString()}`); + } + return filePath; +} + +function sessionFromUri(uri: vscode.Uri): NativeDiffSession | undefined { + const query = new URLSearchParams(uri.query); + const id = query.get("session"); + const existing = id ? sessions.get(id) : undefined; + if (existing) { + return existing; + } + + const fileDir = query.get("fileDir"); + const fileBase = query.get("fileBase"); + const root = query.get("root"); + const scriptRelativePath = query.get("scriptRelativePath"); + const line = Number(query.get("line")); + if (!id || !fileDir || !fileBase || !root || !scriptRelativePath || !Number.isInteger(line)) { + return undefined; + } + const restored = { id, root, scriptRelativePath, line, fileDir, fileBase, generatedModes: new Set() }; + sessions.set(id, restored); + return restored; +} + +function activeNativeDiffUri(): vscode.Uri | undefined { + const activeEditorUri = vscode.window.activeTextEditor?.document.uri; + if (activeEditorUri?.scheme === OPTITRUST_DIFF_SCHEME) { + return activeEditorUri; + } + + const input = vscode.window.tabGroups.activeTabGroup.activeTab?.input; + if (input instanceof vscode.TabInputTextDiff && input.modified.scheme === OPTITRUST_DIFF_SCHEME) { + return input.modified; + } + return undefined; +} + +class NativeDiffContentProvider implements vscode.TextDocumentContentProvider { + async provideTextDocumentContent(uri: vscode.Uri): Promise { + return fs.readFile(filePathFromUri(uri), "utf8"); + } +} + +export function registerNativeDiffProvider(context: vscode.ExtensionContext): void { + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(OPTITRUST_DIFF_SCHEME, new NativeDiffContentProvider())); +} + +async function generateStepDiff(session: NativeDiffSession, selectedViewMode: ViewModeDefinition): Promise { + const args = [ + "step_diff", + session.scriptRelativePath, + String(session.line), + ...backendFlagsForViewMode(selectedViewMode) + ]; + + try { + await runCommand({ + cwd: session.root, + command: path.join(session.root, "tools", "view_result.sh"), + args, + title: `OptiTrust: Generate ${selectedViewMode.label} Diff`, + env: { + OPTITRUST_NO_BROWSER: "1" + } + }); + session.generatedModes.add(selectedViewMode.id); + return true; + } catch { + return false; + } +} + +export async function openNativeStepDiff( + context: NativeStepDiffContext, + selectedViewMode: ViewModeDefinition, + options: OpenNativeStepDiffOptions = {} +): Promise { + const id = sessionId(context.fileDir, context.fileBase); + const existingSession = sessions.get(id); + const session: NativeDiffSession = + existingSession?.scriptRelativePath === context.scriptRelativePath && existingSession.line === context.line + ? existingSession + : { + id, + root: context.root, + scriptRelativePath: context.scriptRelativePath, + line: context.line, + fileDir: context.fileDir, + fileBase: context.fileBase, + generatedModes: new Set() + }; + sessions.set(session.id, session); + + if (options.markGenerated) { + session.generatedModes.add(selectedViewMode.id); + } + + if (options.generateIfMissing && !session.generatedModes.has(selectedViewMode.id)) { + const generated = await generateStepDiff(session, selectedViewMode); + if (!generated) { + return; + } + } + + await openExistingNativeStepDiff(session, selectedViewMode, options.viewColumn ?? vscode.ViewColumn.Beside); +} + +async function openExistingNativeStepDiff( + session: NativeDiffSession, + selectedViewMode: ViewModeDefinition, + viewColumn: vscode.ViewColumn +): Promise { + const candidates = stepDiffCandidates(session.fileDir, session.fileBase, selectedViewMode); + const pair = await findExistingPair(candidates); + if (!pair) { + appendLine(`Generated native diff files were not found for ${session.fileBase} (${selectedViewMode.label}).`); + for (const candidate of candidates) { + appendLine(`Missing candidate: ${candidate.before} <-> ${candidate.after}`); + } + vscode.window.showWarningMessage(`OptiTrust command finished, but generated ${selectedViewMode.label} diff files were not found.`); + return; + } + + await vscode.commands.executeCommand( + "vscode.diff", + diffUri(pair.before, session), + diffUri(pair.after, session), + `OptiTrust Diff: ${session.fileBase} (${pair.label})`, + { preview: false, viewColumn } + ); +} + +export async function switchNativeDiffSyntax(): Promise { + const activeUri = activeNativeDiffUri(); + if (!activeUri) { + vscode.window.showWarningMessage("Open an OptiTrust native diff before switching syntax."); + return; + } + + const session = sessionFromUri(activeUri); + if (!session) { + vscode.window.showWarningMessage("This OptiTrust diff can no longer be switched. Re-run View Step Diff."); + return; + } + + const picked = await vscode.window.showQuickPick( + VIEW_MODES.map(mode => ({ + label: mode.label, + description: mode.description, + mode + })), + { + title: "OptiTrust Diff Syntax", + placeHolder: "Select syntax for this diff" + } + ); + + if (!picked) { + return; + } + + await openNativeStepDiff(session, picked.mode, { + viewColumn: vscode.ViewColumn.Active, + generateIfMissing: true + }); +} From d577fbc9cc5022151d297812ddea5c8a25951308 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Fri, 3 Jul 2026 08:37:53 -0400 Subject: [PATCH 25/47] feat: lazily generate OptiTrust diff syntaxes --- lib/framework/runtime/trace.ml | 27 +++++++++++-------- .../src/commands/viewCommands.ts | 2 ++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/framework/runtime/trace.ml b/lib/framework/runtime/trace.ml index f7591ce01..294b7388d 100644 --- a/lib/framework/runtime/trace.ml +++ b/lib/framework/runtime/trace.ml @@ -1816,19 +1816,24 @@ let produce_diff_output_internal (step:step_tree) : unit = output_prog style ctx filename_prefix ast; Flags.verbose_info "Generated: %s" (output_filename style ctx filename_prefix); in - let output_optilambda_pair suffix representation = - let style = optilambda_style representation in - output_ast style (prefix ^ "_before" ^ suffix) ast_before; - output_ast style (prefix ^ "_after" ^ suffix) ast_after; + let diff_filename_prefix style side = + let suffix = + match style.Style.print with + | Lang_OptiLambda optilambda_style -> + begin match optilambda_style.representation with + | Optitrust_optilambda.Optilambda.Style.Surface -> "" + | Optitrust_optilambda.Optilambda.Style.Internal -> "_internal" + | Optitrust_optilambda.Optilambda.Style.FullyTypedInternal -> "_typed" + end + | Lang_AST _ + | Lang_C _ -> "" + in + prefix ^ "_" ^ side ^ suffix in (* Generate files. *) - output_ast style_before (prefix ^ "_before") ast_before; - output_ast style_after (prefix ^ "_after") ast_after; - output_optilambda_pair "" Optitrust_optilambda.Optilambda.Style.Surface; - List.iter - (fun (suffix, representation) -> output_optilambda_pair ("_" ^ suffix) representation) - optilambda_representations; - Flags.verbose_info "Writing ast and code into %s.js" prefix + output_ast style_before (diff_filename_prefix style_before "before") ast_before; + output_ast style_after (diff_filename_prefix style_after "after") ast_after; + Flags.verbose_info "Generated diff files for %s" prefix (** [produce_trace_output step] is an auxiliary function for [produce_output_and_exit] *) let produce_trace_output (step:step_tree) : unit = diff --git a/tools/vscode-optitrust/src/commands/viewCommands.ts b/tools/vscode-optitrust/src/commands/viewCommands.ts index f20ad4d36..6892a168b 100644 --- a/tools/vscode-optitrust/src/commands/viewCommands.ts +++ b/tools/vscode-optitrust/src/commands/viewCommands.ts @@ -46,6 +46,8 @@ const VIEW_COMMANDS: Record = { } }; +const DEFAULT_STEP_DIFF_VIEW_MODE = VIEW_MODES.find(mode => mode.id === "optilambda.surface") ?? VIEW_MODES[1]; + async function exists(filePath: string): Promise { try { await fs.access(filePath); From e50f7bd753f8cb23ddd3caa138e4339ebc4d37e4 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Fri, 3 Jul 2026 08:38:39 -0400 Subject: [PATCH 26/47] lazily generate OptiTrust diff syntaxes --- .../src/commands/viewCommands.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tools/vscode-optitrust/src/commands/viewCommands.ts b/tools/vscode-optitrust/src/commands/viewCommands.ts index 6892a168b..cca087575 100644 --- a/tools/vscode-optitrust/src/commands/viewCommands.ts +++ b/tools/vscode-optitrust/src/commands/viewCommands.ts @@ -16,10 +16,10 @@ type ViewOption = "diff-only-code" | "diff-internal-syntax" | "trace-save-steps- interface ViewCommandSpec { readonly mode: ViewMode; - readonly scriptMode: "step_diff" | "full_trace" | "step_trace" | "standalone_full_trace"; + readonly scriptMode: "step_diff" | "full_trace" | "step_trace"; readonly title: string; readonly viewKind: "diff" | "trace" | "step-trace"; - readonly htmlSuffix: "_diff.html" | "_trace.html" | "_standalone_trace.html"; + readonly htmlSuffix: "_diff.html" | "_trace.html"; } const VIEW_COMMANDS: Record = { @@ -32,10 +32,10 @@ const VIEW_COMMANDS: Record = { }, full_trace: { mode: "full_trace", - scriptMode: "standalone_full_trace", + scriptMode: "full_trace", title: "OptiTrust: View Full Trace", viewKind: "trace", - htmlSuffix: "_standalone_trace.html" + htmlSuffix: "_trace.html" }, step_trace: { mode: "step_trace", @@ -123,11 +123,9 @@ function viewArgs(mode: ViewMode, selectedViewMode: ViewModeDefinition, option?: return ["-save-steps", "script"]; } - // Full standalone traces and step diffs generate both C/C++ and OptiLambda - // payloads when supported, then switch syntax inside the webview. Passing the - // global syntax flag here would collapse that dual-view behavior into a single - // backend output. - if (mode === "full_trace" || mode === "step_diff") { + // Full traces use serialized, server-backed data for in-window switching. + // Step diffs are generated lazily by the native VS Code diff integration. + if (mode === "full_trace") { return []; } return backendFlagsForViewMode(selectedViewMode); From eacc9b24a8bf333f880ccac7b9240486f011246b Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Fri, 3 Jul 2026 08:39:48 -0400 Subject: [PATCH 27/47] feat: lazily load trace syntax views in VS Code --- tools/trace_server/trace_server.ml | 7 +++++ tools/vscode-optitrust/src/optitrust/views.ts | 26 +++++++++++++++---- tools/web_view/optitrust_trace.js | 12 +++++++-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/tools/trace_server/trace_server.ml b/tools/trace_server/trace_server.ml index 447f0dfe4..7d707cf52 100644 --- a/tools/trace_server/trace_server.ml +++ b/tools/trace_server/trace_server.ml @@ -16,6 +16,12 @@ let handle_exn_response sub_handler request = | Trace.MissingAst -> Dream.respond ~status:`Not_Found ("This AST is missing, maybe retry generating the trace with Flags.save_ast_for_steps := Some Steps_all") | exn -> Dream.respond ~status:`Internal_Server_Error (Printexc.to_string exn ^ "\n" ^ Printexc.get_backtrace ()) +let with_cors sub_handler request = + let open Lwt.Syntax in + let* response = sub_handler request in + Dream.set_header response "Access-Control-Allow-Origin" "*"; + Lwt.return response + let get_query request query_name = match Dream.query request query_name with | Some query -> query @@ -139,6 +145,7 @@ let () = Dream.run ~port:6775 ~adjust_terminal:false @@ Dream.logger + @@ with_cors @@ handle_exn_response @@ Dream.router [ Dream.get "**" handle_get_request; diff --git a/tools/vscode-optitrust/src/optitrust/views.ts b/tools/vscode-optitrust/src/optitrust/views.ts index 6516e66dd..a31396e42 100644 --- a/tools/vscode-optitrust/src/optitrust/views.ts +++ b/tools/vscode-optitrust/src/optitrust/views.ts @@ -14,15 +14,31 @@ function webviewKey(filePath: string, viewKind: string, metadata: string): strin * webviews run with a stricter resource model, so local assets must be inlined * or rewritten before the HTML can be displayed reliably inside the editor. */ -async function htmlWithBase(webview: vscode.Webview, htmlFile: string): Promise { +async function htmlWithBase(webview: vscode.Webview, root: string, htmlFile: string): Promise { const html = await fs.readFile(htmlFile, "utf8"); const htmlDir = path.dirname(htmlFile); const inlined = await inlineLocalScriptsAndStyles(htmlDir, html); const rewritten = rewriteLocalResourceUris(webview, htmlDir, inlined); - const withHighlightingConfig = await injectSyntaxHighlightingConfig(rewritten); + const withTraceServerBase = injectTraceServerBase(root, htmlFile, rewritten); + const withHighlightingConfig = await injectSyntaxHighlightingConfig(withTraceServerBase); return injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)); } +function injectTraceServerBase(root: string, htmlFile: string, html: string): string { + if (!html.includes("serialized_trace") || html.includes('id="optitrustTraceServerBaseUrl"')) { + return html; + } + + const relativeDir = path.dirname(path.relative(root, htmlFile)); + const urlPath = relativeDir === "." ? "" : `${relativeDir.split(path.sep).map(encodeURIComponent).join("/")}/`; + const baseUrl = `http://localhost:6775/${urlPath}`; + const script = ``; + if (html.includes("")) { + return html.replace("", `${script}\n`); + } + return `${script}\n${html}`; +} + /** * Inline local JS/CSS dependencies generated by OptiTrust. This keeps the * existing HTML templates usable both in a browser and in VS Code webviews, @@ -349,8 +365,8 @@ export async function openHtmlView(root: string, htmlFile: string, viewKind: str const key = webviewKey(htmlFile, viewKind, metadata); const existing = panels.get(key); if (existing) { - existing.reveal(vscode.ViewColumn.Beside); - existing.webview.html = await htmlWithBase(existing.webview, htmlFile); + existing.reveal(existing.viewColumn, true); + existing.webview.html = await htmlWithBase(existing.webview, root, htmlFile); return; } @@ -360,7 +376,7 @@ export async function openHtmlView(root: string, htmlFile: string, viewKind: str }); panel.onDidDispose(() => panels.delete(key)); - panel.webview.html = await htmlWithBase(panel.webview, htmlFile); + panel.webview.html = await htmlWithBase(panel.webview, root, htmlFile); panels.set(key, panel); } diff --git a/tools/web_view/optitrust_trace.js b/tools/web_view/optitrust_trace.js index 87915b7a8..cec4dafdc 100644 --- a/tools/web_view/optitrust_trace.js +++ b/tools/web_view/optitrust_trace.js @@ -506,6 +506,13 @@ function syntaxQueryString() { return `syntax=optilambda&repr=${optilambdaRepresentationSuffix(representation)}`; } +function traceServerRequestUrl(query) { + if (window.optitrustTraceServerBaseUrl && !/^(?:https?:)?\/\//.test(serialized_trace)) { + return new URL(serialized_trace + query, window.optitrustTraceServerBaseUrl).toString(); + } + return serialized_trace + query; +} + function resetView() { /*$("#sourceDiv").hide(); $("#diffDiv").hide(); @@ -661,7 +668,8 @@ function queryStepDetails(step, view, hadEmptyDiff = false) { var stepCode; if (serialized_trace) { // We have a serialized trace server, get the step details from there - stepCode = fetch(serialized_trace + `?view=${view}&step=${step.id + root_serialized_step_id}&${syntaxQueryString()}×tamp=${serialized_trace_timestamp}`) + const requestUrl = traceServerRequestUrl(`?view=${view}&step=${step.id + root_serialized_step_id}&${syntaxQueryString()}×tamp=${serialized_trace_timestamp}`); + stepCode = fetch(requestUrl) .then((response) => { if (response.status == 419) { window.location.reload(); @@ -669,7 +677,7 @@ function queryStepDetails(step, view, hadEmptyDiff = false) { } else if(!response.ok) { return response.text().then((error) => { - throw new Error(`Failed to retreive data:
${error}`); + throw new Error(`Failed to retrieve data from ${requestUrl} (${response.status} ${response.statusText}):
${error}`); }); } return response.text(); From b920f436ebcff82c5d44f00bba8c37d3f810a3e2 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Fri, 3 Jul 2026 08:40:17 -0400 Subject: [PATCH 28/47] style: align OptiTrust trace UI with VS Code --- tools/web_view/optitrust_trace.css | 255 +++++++++++++++++++++++------ tools/web_view/trace_template.html | 38 +++-- 2 files changed, 228 insertions(+), 65 deletions(-) diff --git a/tools/web_view/optitrust_trace.css b/tools/web_view/optitrust_trace.css index a5c2d91f9..7c51d0f97 100644 --- a/tools/web_view/optitrust_trace.css +++ b/tools/web_view/optitrust_trace.css @@ -13,6 +13,12 @@ pre { margin: 0; } +button, +input, +label { + font: inherit; +} + /*codemirror infinite height*/ #source_code { @@ -20,7 +26,7 @@ pre { } .CodeMirror { - border: 1px solid var(--vscode-panel-border, #eeeeee); + border: 0; height: auto; background: var(--vscode-editor-background, #ffffff); color: var(--vscode-editor-foreground, #222222); @@ -50,12 +56,12 @@ pre { align-items: flex-start; width: 100%; margin: 0 0 0 -100%; - padding: 0 0 0 100%; + padding: 1px 0 1px 100%; } .step-big { - background: var(--vscode-editor-selectionHighlightBackground, #ccffbb); - border-top: 2px solid var(--vscode-panel-border, #000000); + background: var(--vscode-list-inactiveSelectionBackground, var(--vscode-editor-selectionHighlightBackground, #e8f2ff)); + border-top: 1px solid var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border, #dddddd)); font-weight: bold; } @@ -64,41 +70,40 @@ pre { } .step-title { - padding-left: 5px; - padding-right: 5px; - border-radius: 5px; + min-width: 0; + padding: 1px 6px; + border-radius: 3px; cursor:pointer; overflow: hidden; text-overflow: ellipsis; } .step-title:hover { - box-shadow: inset 0 0 1em var(--vscode-list-hoverBackground, #ffffff); + background: var(--vscode-list-hoverBackground, rgba(127, 127, 127, 0.14)); + color: var(--vscode-list-hoverForeground, var(--vscode-sideBar-foreground, inherit)); + box-shadow: none; } .step-selected .step-title { - outline: 1px solid var(--vscode-focusBorder, #666666); - box-shadow: inset 0 0 0.5em var(--vscode-list-activeSelectionBackground, #666666); + outline: 1px solid var(--vscode-focusBorder, #007fd4); + background: var(--vscode-list-activeSelectionBackground, #0060c0); + color: var(--vscode-list-activeSelectionForeground, #ffffff); + box-shadow: none; } .step-small.step-valid .step-title { - background: var(--vscode-gitDecoration-addedResourceForeground, #009944); - color: var(--vscode-editor-background, #ffffff); + color: var(--vscode-testing-iconPassed, var(--vscode-gitDecoration-addedResourceForeground, #2ea043)); } .step-invalid .step-title { - background: var(--vscode-inputValidation-warningBackground, #fff4ce); - color: var(--vscode-inputValidation-warningForeground, #222222); + color: var(--vscode-inputValidation-warningForeground, var(--vscode-editorWarning-foreground, #cca700)); } .step-error .step-title { - background: var(--vscode-inputValidation-errorBackground, #ff3333); - color: var(--vscode-inputValidation-errorForeground, #ffffff); + color: var(--vscode-inputValidation-errorForeground, var(--vscode-errorForeground, #f85149)); font-weight: bold; } .step-show .step-title { - background: var(--vscode-inputOption-activeBackground, #55ccff); - color: var(--vscode-inputOption-activeForeground, var(--vscode-editor-foreground, #222222)); + color: var(--vscode-textLink-foreground, var(--vscode-editor-foreground, #222222)); } .step-io-target .step-title { - background: var(--vscode-editorBracketHighlight-foreground3, #d0bffd); - color: var(--vscode-editor-background, #ffffff); + color: var(--vscode-symbolIcon-eventForeground, var(--vscode-editorBracketHighlight-foreground3, #c586c0)); } .step-nodiff { @@ -158,9 +163,11 @@ pre { .step-justif-text { margin-left: 3em; - background: var(--vscode-textBlockQuote-background, #ddffee); + border-left: 2px solid var(--vscode-textBlockQuote-border, var(--vscode-panel-border, #dddddd)); + background: var(--vscode-textBlockQuote-background, transparent); color: var(--vscode-editor-foreground, #222222); font-style: italic; + padding: 2px 6px; } .step-tag { @@ -169,7 +176,7 @@ pre { background: var(--vscode-badge-background, #eeeebb); color: var(--vscode-badge-foreground, #222222); padding: 0 4px; - border-radius: 5px; + border-radius: 999px; font-size: 0.8em; } @@ -186,115 +193,261 @@ pre { } .exectime-heavy { - background: var(--vscode-inputValidation-errorBackground, #ff6666); + color: var(--vscode-errorForeground, #f85149); } .exectime-mid { - background: var(--vscode-inputValidation-warningBackground, #ffdd88); + color: var(--vscode-editorWarning-foreground, #cca700); } .exectime-small { - background: var(--vscode-editorWidget-background, #ffffff); + color: var(--vscode-descriptionForeground, #777777); } .exectime-heavy, .exectime-mid, .exectime-small { margin-right: 5px; padding-right: 2px; padding-left: 2px; padding-bottom: 1px; - border-radius: 5px; + border-radius: 3px; font-size: 0.8em; } .has-debug-msg { padding-left: 5px; padding-right: 5px; - background: var(--vscode-editorWarning-background, #ff9d60); - color: var(--vscode-editor-foreground, #222222); - border-radius: 5px; + color: var(--vscode-editorWarning-foreground, var(--vscode-editor-foreground, #222222)); + border: 1px solid var(--vscode-editorWarning-border, var(--vscode-panel-border, #dddddd)); + border-radius: 3px; font-size: 0.8em; } #stepMsgDiv { - padding-left: 10px; - padding-right: 10px; + grid-row: 3; + padding: 6px 10px; font-family: var(--vscode-editor-font-family, monospace); - background: var(--vscode-editorWarning-background, #ff9d60); + background: var(--vscode-editorWidget-background, var(--vscode-editor-background, #ffffff)); color: var(--vscode-editor-foreground, #222222); + border-bottom: 1px solid var(--vscode-panel-border, #dddddd); + position: relative; + z-index: 2; } #debugMsgDiv { - padding: 0 5px; + grid-row: 2; + padding: 6px 10px; font-family: var(--vscode-editor-font-family, monospace); - background: var(--vscode-editorWidget-background, var(--vscode-editor-background, #ffffff)); - color: var(--vscode-editor-foreground, #222222); + background: var(--vscode-editor-background, #ffffff); + color: var(--vscode-descriptionForeground, var(--vscode-editor-foreground, #222222)); + border-bottom: 1px solid var(--vscode-panel-border, #dddddd); overflow: auto; + position: relative; + z-index: 2; } /* ---- vertical split view */ +.trace-shell { + display: flex; + flex-direction: column; + height: 100vh; + background: var(--vscode-editor-background, #ffffff); + color: var(--vscode-editor-foreground, #222222); +} + +.trace-titlebar { + display: flex; + align-items: center; + min-height: 34px; + padding: 0 12px; + border-bottom: 1px solid var(--vscode-panel-border, #dddddd); + background: var(--vscode-titleBar-activeBackground, var(--vscode-editorGroupHeader-tabsBackground, #f3f3f3)); + color: var(--vscode-titleBar-activeForeground, var(--vscode-editor-foreground, #222222)); +} + +.trace-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 600; +} + .split-pane { display: flex; flex-direction: row; overflow: hidden; - height: 100%; + flex: 1 1 auto; + min-height: 0; width: 100%; } .left-pane { display: flex; flex-direction: column; + min-width: 220px; + max-width: 70%; + background: var(--vscode-sideBar-background, #f3f3f3); + color: var(--vscode-sideBar-foreground, var(--vscode-editor-foreground, #222222)); } .right-pane { - display: flex; - flex-direction: column; - justify-content: space-between; + display: grid; + grid-template-rows: auto auto auto minmax(0, 1fr); flex: 1 1 0; + min-width: 0; overflow: hidden; + background: var(--vscode-editor-background, #ffffff); } #diffDiv { - flex-grow: 1; - overflow-y: auto; + grid-row: 4; + min-height: 0; + overflow: auto; + background: var(--vscode-editor-background, #ffffff); + position: relative; + z-index: 1; } #sourceDiv { - flex-grow: 1; - overflow-y: auto; + grid-row: 4; + min-height: 0; + overflow: auto; + background: var(--vscode-editor-background, #ffffff); + position: relative; + z-index: 1; +} +#statsDiv { + grid-row: 4; + min-height: 0; + overflow: auto; + position: relative; + z-index: 1; } .resizing-handle { background: var(--vscode-panel-border, #888888); - width: 4px; + width: 1px; cursor: col-resize; z-index: 999; } .resizing-handle:hover { - width: 8px; - margin: 0 -2px; + background: var(--vscode-sash-hoverBorder, var(--vscode-focusBorder, #007fd4)); + width: 4px; + margin: 0 -1.5px; } .resizing-handle:active { background: var(--vscode-focusBorder, #999999); } +.pane-header { + display: flex; + align-items: center; + min-height: 32px; + padding: 0 10px; + border-bottom: 1px solid var(--vscode-panel-border, #dddddd); + background: var(--vscode-sideBarSectionHeader-background, var(--vscode-editorGroupHeader-tabsBackground, #f3f3f3)); + color: var(--vscode-sideBarSectionHeader-foreground, var(--vscode-sideBar-foreground, #222222)); + font-size: 11px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.details-header { + grid-row: 1; + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: start; + gap: 8px; + padding: 8px 10px 10px; + background: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-editor-background, #ffffff)); + color: var(--vscode-foreground, var(--vscode-editor-foreground, #222222)); + position: relative; + z-index: 3; +} + +.details-header > span { + line-height: 18px; +} + +.details-header .controls { + font-size: var(--vscode-font-size, 13px); + font-weight: 400; + text-transform: none; +} + #treeDiv { background: var(--vscode-sideBar-background, #eeeeee); color: var(--vscode-sideBar-foreground, var(--vscode-editor-foreground, #222222)); overflow-y: auto; - flex-grow: 1; + flex: 1 1 auto; + min-height: 0; + padding: 4px 0; } .controls { display: flex; flex-wrap: wrap; - align-items: baseline; + align-items: center; + color: var(--vscode-sideBar-foreground, var(--vscode-editor-foreground, #222222)); + gap: 4px; +} +.trace-controls { border-top: 1px solid var(--vscode-panel-border, #888888); background: var(--vscode-sideBar-background, #eeeeee); - color: var(--vscode-sideBar-foreground, var(--vscode-editor-foreground, #222222)); padding: 8px; - gap: 0 4px; + max-height: 30%; + overflow: auto; +} +.ast-controls { + width: 100%; + justify-content: flex-start; + min-width: 0; + row-gap: 4px; } .details-button { - min-width: 5em; + min-width: 4.5em; text-align: center; } +.controls button, +.checkbox-label { + display: inline-flex; + align-items: center; + min-height: 26px; + box-sizing: border-box; + border: 1px solid transparent; + border-radius: 3px; + background: transparent; + color: var(--vscode-button-secondaryForeground, var(--vscode-foreground, #222222)); + padding: 2px 6px; + white-space: nowrap; +} + +.controls button:hover, +.checkbox-label:hover { + background: var(--vscode-toolbar-hoverBackground, var(--vscode-list-hoverBackground, rgba(127, 127, 127, 0.14))); +} + +.controls button:focus-visible, +.checkbox-label:focus-within { + outline: 1px solid var(--vscode-focusBorder, #007fd4); + outline-offset: 1px; +} + +.checkbox-label input { + margin: 0 4px 0 0; +} + +.checkbox-label input[type="radio"] { + appearance: none; + width: 0; + height: 0; + margin: 0; +} + +.checkbox-label:has(input[type="radio"]:checked), +.checkbox-label:has(input[type="checkbox"]:checked) { + background: var(--vscode-inputOption-activeBackground, var(--vscode-button-secondaryBackground, rgba(127, 127, 127, 0.18))); + color: var(--vscode-inputOption-activeForeground, var(--vscode-foreground, #222222)); + border-color: var(--vscode-inputOption-activeBorder, transparent); +} + ol { list-style-type: none; margin: 0; diff --git a/tools/web_view/trace_template.html b/tools/web_view/trace_template.html index 62290ca34..3ead5bb29 100644 --- a/tools/web_view/trace_template.html +++ b/tools/web_view/trace_template.html @@ -32,21 +32,31 @@ -
-
-
-
-
-
-
-
Loading the trace {TRACEJSFILE}...
-
-
-
- +
+
+
{INSERT_TITLE}
+
+ +
+
+
Trace
+
+
+
+
+
+
+ Step Details +
+
+
Loading the trace {TRACEJSFILE}...
+
+
+
+ +
+
-
-
From d7f047173915a9a7a83aab2c261fea9ca9579784 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Tue, 7 Jul 2026 08:59:05 -0400 Subject: [PATCH 29/47] feat: refine OptiLambda printer syntax --- lib/optilambda/optilambda_printer.ml | 150 +++++++-- lib/optilambda/optilambda_syntax.md | 36 ++- tests_infra/optilambda/printcpp.cpp | 22 +- tests_infra/optilambda/printcpp_exp.opti | 378 ++++++++++------------- tests_infra/optilambda/printer_basic.ml | 72 ++++- 5 files changed, 408 insertions(+), 250 deletions(-) diff --git a/lib/optilambda/optilambda_printer.ml b/lib/optilambda/optilambda_printer.ml index c32720a02..ff38a20bc 100644 --- a/lib/optilambda/optilambda_printer.ml +++ b/lib/optilambda/optilambda_printer.ml @@ -307,12 +307,18 @@ and prim_to_doc (style : Optilambda_style.style) (ty : typ) (prim : prim) : docu | Prim_array -> string "array" | Prim_record -> string "record" +(** [compound_assign_op_to_doc op] returns the surface compound-assignment token for supported operators. *) +and compound_assign_op_to_doc (op : binary_op) : document option = + match binop_to_doc op with + | Some op_doc when op <> Binop_set -> Some (op_doc ^^ equals) + | _ -> None + (** [ghost_args_to_doc style ghost_args] prints call contract arguments, e.g. [[h := g]]. *) and ghost_args_to_doc (style : Optilambda_style.style) (ghost_args : resource_item list) : document = brackets_doc (comma_sep (List.map - (fun (hyp, formula) -> var_to_doc style hyp ^^ blank 1 ^^ string ":=" ^^ blank 1 ^^ trm_to_doc_at style 0 formula) + (fun (hyp, formula) -> var_to_doc style hyp ^^ blank 1 ^^ string ":=" ^^ blank 1 ^^ formula_to_doc style formula) ghost_args)) (** [ghost_bind_to_doc style ghost_bind] prints returned contract bindings, e.g. [[z : h]]. *) @@ -368,6 +374,13 @@ and fraction_var_of_formula (formula : trm) : var option = | Trm_var v -> Some v | _ -> None +(** [same_formula t1 t2] compares formulas structurally, accepting alpha-equivalent binders when variables are scoped. *) +and same_formula (t1 : trm) (t2 : trm) : bool = + t1 = t2 + || + try Trm_unify.are_same_trm t1 t2 with + | _ -> false + (** [is_fraction_type_formula formula] recognizes the pure type formula [_Fraction]. *) and is_fraction_type_formula (formula : trm) : bool = var_has_name "_Fraction" formula @@ -396,9 +409,95 @@ and uninit_formula_body (formula : trm) : trm option = | Trm_apps (f, [ body ], [], []) when var_has_name "Uninit" f || var_has_name "_Uninit" f -> Some body | _ -> None +(** [formula_to_doc_at style ctx_prec formula] prints logical/resource formulas in the surface style used by contracts. + + This intentionally mirrors the important cases of the C resource-formula printer without depending on [optitrust.framework]. *) +and formula_to_doc_at (style : Optilambda_style.style) (ctx_prec : int) (formula : trm) : document = + let formula_app_to_doc (f : trm) (args : trm list) : document = + trm_to_doc_at style 10 f ^^ parens_doc (comma_sep (List.map (formula_to_doc_at style 0) args)) + in + let doc = + match formula.desc with + | Trm_var v -> var_to_doc style v + | Trm_lit lit -> lit_to_doc style lit + | Trm_prim (ty, prim) -> prim_to_doc style ty prim + | Trm_apps ({ desc = Trm_var v; _ }, [ start; stop; step ], [], []) + when v.name = "range" && v.namespaces = [] -> + if is_int_one step then + formula_to_doc_at style 10 start ^^ string ".." ^^ formula_to_doc_at style 10 stop + else + string "range" + ^^ parens_doc (comma_sep [ formula_to_doc_at style 0 start; formula_to_doc_at style 0 stop; formula_to_doc_at style 0 step ]) + | Trm_apps ({ desc = Trm_var v; _ }, [ start; count ], [], []) + when v.name = "range_count" && v.namespaces = [] -> + formula_to_doc_at style 10 start ^^ string "..+" ^^ formula_to_doc_at style 10 count + | Trm_apps ({ desc = Trm_var v; _ }, [ addr; resource ], [], []) + when (v.name = "~>" || v.name = "_Repr") && v.namespaces = [] -> + formula_to_doc_at style 10 addr ^^ blank 1 ^^ string "~>" ^^ blank 1 ^^ formula_to_doc_at style 1 resource + | Trm_apps ({ desc = Trm_var v; _ }, [ addr; resource; mem_typ ], [], []) + when v.name = "__PointsTo" && v.namespaces = [] -> + begin match mem_typ.desc with + | Trm_var mem_var when mem_var.name = "Any" && mem_var.namespaces = [] -> + formula_to_doc_at style 10 addr ^^ blank 1 ^^ string "~~>" ^^ blank 1 ^^ formula_to_doc_at style 1 resource + | _ -> + formula_to_doc_at style 10 addr ^^ blank 1 ^^ string "~~>" ^^ brackets_doc (formula_to_doc_at style 0 mem_typ) + ^^ blank 1 ^^ formula_to_doc_at style 1 resource + end + | Trm_apps ({ desc = Trm_var v; _ }, [ range; { desc = Trm_fun ([ (index, _) ], _, body, _); _ } ], [], []) + when v.name = "Group" && v.namespaces = [] -> + string "for" ^^ blank 1 ^^ var_to_doc style index ^^ blank 1 ^^ string "in" ^^ blank 1 ^^ formula_to_doc_at style 0 range + ^^ blank 1 ^^ string "->" ^^ blank 1 ^^ formula_to_doc_at style 0 body + | Trm_apps ({ desc = Trm_var v; _ }, [ bound; { desc = Trm_fun ([ (index, _) ], _, body, _); _ } ], [], []) + when v.name = "DesyncGroup" && v.namespaces = [] -> + string "desync_for" ^^ blank 1 ^^ var_to_doc style index ^^ blank 1 ^^ string "in" ^^ blank 1 ^^ string ".." + ^^ formula_to_doc_at style 0 bound ^^ blank 1 ^^ string "->" ^^ blank 1 ^^ formula_to_doc_at style 0 body + | Trm_apps ({ desc = Trm_var v; _ }, [ base; divisor ], [], []) + when v.name = "__frac_div" && v.namespaces = [] -> + formula_to_doc_at style 7 base ^^ blank 1 ^^ string "/" ^^ blank 1 ^^ formula_to_doc_at style 8 divisor + | Trm_apps ({ desc = Trm_var v; _ }, [ base; carved ], [], []) + when v.name = "__frac_sub" && v.namespaces = [] -> + formula_to_doc_at style 6 base ^^ blank 1 ^^ string "-" ^^ blank 1 ^^ formula_to_doc_at style 7 carved + | Trm_apps ({ desc = Trm_prim (_, Prim_binop (Binop_array_access | Binop_array_get)); _ }, [ base; index ], [], []) -> + formula_to_doc_at style 10 base ^^ brackets_doc (formula_to_doc_at style 0 index) + | Trm_apps ({ desc = Trm_prim (_, Prim_binop op); _ }, [ lhs; rhs ], [], []) -> + begin match binop_to_doc op with + | Some op_doc -> + let prec = binop_precedence op in + formula_to_doc_at style prec lhs ^^ blank 1 ^^ op_doc ^^ blank 1 ^^ formula_to_doc_at style (prec + 1) rhs + | None -> formula_app_to_doc { formula with desc = Trm_prim (typ_auto, Prim_binop op) } [ lhs; rhs ] + end + | Trm_apps ({ desc = Trm_prim (_, Prim_unop (Unop_struct_get field | Unop_struct_access field)); _ }, [ base ], [], []) -> + formula_to_doc_at style 10 base ^^ string "." ^^ string field + | Trm_apps ({ desc = Trm_prim (_, Prim_unop Unop_get); _ }, [ arg ], [], []) -> string "*" ^^ formula_to_doc_at style 8 arg + | Trm_apps ({ desc = Trm_prim (_, Prim_unop Unop_address); _ }, [ arg ], [], []) -> string "&" ^^ formula_to_doc_at style 8 arg + | Trm_apps ({ desc = Trm_prim (_, Prim_unop Unop_minus); _ }, [ arg ], [], []) -> string "-" ^^ formula_to_doc_at style 8 arg + | Trm_apps ({ desc = Trm_prim (_, Prim_unop Unop_neg); _ }, [ arg ], [], []) -> string "!" ^^ formula_to_doc_at style 8 arg + | Trm_apps (f, args, [], []) -> formula_app_to_doc f args + | Trm_fun (args, ret_ty, body, _) -> + let args_doc = string "fun" ^^ parens_doc (comma_sep (List.map (typed_var_to_doc style) args)) in + if style.print_types && is_type_type ret_ty then + match type_result_body_to_doc style body with + | Some body_type_doc -> args_doc ^^ colon ^^ blank 1 ^^ body_type_doc + | None -> args_doc ^^ colon ^^ blank 1 ^^ typ_to_doc style ret_ty ^^ blank 1 ^^ string "->" ^^ blank 1 ^^ formula_to_doc_at style 0 body + else + args_doc + ^^ (if style.print_types && not (is_auto_type ret_ty) then colon ^^ blank 1 ^^ typ_to_doc style ret_ty else empty) + ^^ blank 1 ^^ string "->" ^^ blank 1 ^^ formula_to_doc_at style 0 body + | _ -> trm_to_doc_at style 0 formula + in + parenthesize_if (trm_precedence formula < ctx_prec) doc + +(** [formula_to_doc style formula] prints a formula using the local surface formula printer when appropriate. *) +and formula_to_doc (style : Optilambda_style.style) (formula : trm) : document = + match style.representation with + | Surface -> formula_to_doc_at style 0 formula + | Internal + | FullyTypedInternal -> + trm_to_doc_at style 0 formula + (** [resource_item_to_doc style item] prints a named logical/resource formula. *) and resource_item_to_doc (style : Optilambda_style.style) ((hyp, formula) : resource_item) : document = - var_to_doc style hyp ^^ colon ^^ blank 1 ^^ trm_to_doc_at style 0 formula + var_to_doc style hyp ^^ colon ^^ blank 1 ^^ formula_to_doc style formula (** [contract_clauses keyword items] builds a group of contract clauses. *) and contract_clauses (keyword : string) (items : resource_item list) : contract_clause list = @@ -424,7 +523,7 @@ and simplify_linear_contract (pre : resource_item list) (post : resource_item li let pred (post_hyp, post_formula) = match read_only_formula_inv post_formula with | Some post_ro - when pre_hyp = post_hyp && pre_ro.read_frac = post_ro.read_frac && pre_ro.read_body = post_ro.read_body -> + when pre_hyp = post_hyp && same_formula pre_ro.read_frac post_ro.read_frac && same_formula pre_ro.read_body post_ro.read_body -> Some post_ro | _ -> None in @@ -442,7 +541,7 @@ and simplify_linear_contract (pre : resource_item list) (post : resource_item li let pred (post_hyp, post_formula) = if pre_hyp = post_hyp then match uninit_formula_body pre_formula with - | Some body when body = post_formula -> Some post_formula + | Some body when same_formula body post_formula -> Some post_formula | _ when is_uninit_formula pre_formula && not (is_uninit_formula post_formula) -> Some post_formula | _ -> None else None @@ -470,26 +569,33 @@ and contract_group_to_doc (style : Optilambda_style.style) (keyword : string) (i let rest_docs = List.map (fun item -> comma ^^ hardline ^^ align_doc ^^ resource_item_to_doc style item) rest in concat (first_doc :: rest_docs) -(** [contract_clauses_to_docs style clauses] merges consecutive clauses with the same keyword. *) +(** [contract_clauses_to_docs style clauses] merges clauses with the same keyword within each raw-clause-delimited group. *) and contract_clauses_to_docs (style : Optilambda_style.style) (clauses : contract_clause list) : document list = - let flush_group keyword items acc = - match (keyword, items) with - | None, _ - | _, [] -> - acc - | Some keyword, items -> contract_group_to_doc style keyword (List.rev items) :: acc + let add_to_groups keyword item groups order = + if List.mem keyword order then + (List.map (fun (group_keyword, items) -> if group_keyword = keyword then (group_keyword, item :: items) else (group_keyword, items)) groups, order) + else ((keyword, [ item ]) :: groups, order @ [ keyword ]) + in + let flush_groups groups order acc = + let docs = + List.filter_map + (fun keyword -> + match List.assoc_opt keyword groups with + | None -> None + | Some items -> Some (contract_group_to_doc style keyword (List.rev items))) + order + in + List.rev_append docs acc in - let rec aux cur_keyword cur_items acc clauses = + let rec aux groups order acc clauses = match clauses with - | [] -> List.rev (flush_group cur_keyword cur_items acc) - | ContractRaw doc :: rest -> aux None [] (doc :: flush_group cur_keyword cur_items acc) rest + | [] -> List.rev (flush_groups groups order acc) + | ContractRaw doc :: rest -> aux [] [] (doc :: flush_groups groups order acc) rest | ContractClause (keyword, item) :: rest -> - begin match cur_keyword with - | Some cur when cur = keyword -> aux cur_keyword (item :: cur_items) acc rest - | _ -> aux (Some keyword) [ item ] (flush_group cur_keyword cur_items acc) rest - end + let groups, order = add_to_groups keyword item groups order in + aux groups order acc rest in - aux None [] [] clauses + aux [] [] [] clauses (** [fun_contract_clause_docs style contract] prints the direct internal function contract. *) and fun_contract_clauses (style : Optilambda_style.style) (contract : fun_contract) : contract_clause list = @@ -691,6 +797,11 @@ and app_to_doc (style : Optilambda_style.style) ~(result_typ : typ) (f : trm) (a | Trm_prim (_, Prim_unop (Unop_cast cast_ty)), [ arg ] -> string "cast" ^^ angles_doc (typ_to_doc style cast_ty) ^^ parens_doc (trm_to_doc_at style 0 arg) | Trm_prim (_, Prim_record), _ -> lbrace ^^ comma_sep (List.map (trm_to_doc_at style 0) args) ^^ rbrace + | Trm_prim (_, Prim_compound_assign_op op), [ lhs; rhs ] when style.representation = Surface -> + begin match compound_assign_op_to_doc op with + | Some op_doc -> trm_to_doc_at style 2 lhs ^^ blank 1 ^^ op_doc ^^ blank 1 ^^ trm_to_doc_at style 1 rhs + | None -> prim_to_doc style typ_auto (Prim_compound_assign_op op) ^^ parens_doc (comma_sep (List.map (trm_to_doc_at style 0) args)) + end | Trm_prim (ty, prim), _ -> prim_to_doc style ty prim ^^ parens_doc (comma_sep (List.map (trm_to_doc_at style 0) args)) | Trm_var v, first_arg :: _ when is_fully_typed_internal style && is_typed_resource_constructor_name v.name -> string v.name ^^ angles_doc (typ_to_doc style (typ_of_trm first_arg)) @@ -775,7 +886,6 @@ and for_to_doc (style : Optilambda_style.style) (range : loop_range) (mode : loo string "for" ^^ angles_doc (loop_mode_to_doc style mode) ^^ blank 1 ^^ range_doc - ^^ contract_summary_to_doc style (loop_contract_items contract) ^^ blank 1 ^^ trm_to_block_doc_with_prefix style (contract_clauses_to_docs style (loop_contract_clauses style contract)) body diff --git a/lib/optilambda/optilambda_syntax.md b/lib/optilambda/optilambda_syntax.md index b21bb8741..7a0b73957 100644 --- a/lib/optilambda/optilambda_syntax.md +++ b/lib/optilambda/optilambda_syntax.md @@ -67,8 +67,14 @@ Mutable assignments: ```optilambda x = 3 +x += y +s += a[MINDEX1(n, bi * 32 + i)] * b[MINDEX1(n, bi * 32 + i)] ``` +Supported compound assignments such as `+=`, `-=`, `*=`, and `/=` are printed +as infix updates in Surface syntax. Unsupported primitive-call shapes keep the +fallback call syntax. + Mutable declarations: ```optilambda @@ -142,7 +148,8 @@ without the `return` keyword and without a trailing semicolon. Loops: ```optilambda -for i in 0..n [h1] { +for i in 0..n { + requires h1: i < n; BODY } @@ -168,20 +175,16 @@ for i in range(n, 0, -1) ``` Resource groups are displayed with the same surface range notation when they -wrap a `range(...)` iterator: +wrap a `range(...)` iterator inside logical formulas: ```optilambda -for i in 0..n { - items(i) -} - -for i in range(0, n, step) { - items(i) -} +for i in 0..n -> items(i) +for i in range(0, n, step) -> items(i) ``` Desugared read-only and write contracts are recovered in Surface syntax when -the consumed and produced resources clearly form the expected pair: +the consumed and produced resources clearly form the expected pair, even if +matching clauses are not adjacent: ```optilambda reads h: H @@ -356,8 +359,17 @@ uninitialized resource is consumed and the initialized resource is produced. Read-only transformations that change the resource shape, split or join fractions, or produce a `Wand(...)` stay explicit as `consumes` / `produces`. -Logical terms follow the existing resource formula syntax used by -`resource_cparser.mly`. +Clauses with the same keyword are grouped across a raw-clause-free contract +region. Raw clauses such as `strict` and `reverts` remain barriers, so clauses +are not moved across them. + +Surface logical terms are printed with the local resource formula style: + +```optilambda +p ~> H +_RO(f / 2, H) +for i in 0..n -> items(i) +``` ## Diff And Trace Integration diff --git a/tests_infra/optilambda/printcpp.cpp b/tests_infra/optilambda/printcpp.cpp index 3f03d614f..80ecc9f6b 100644 --- a/tests_infra/optilambda/printcpp.cpp +++ b/tests_infra/optilambda/printcpp.cpp @@ -292,12 +292,27 @@ void arrow() { #include - - - void one_fork () { __pure(); int x = 0; + int n = 64; + int* a; + int* b; + int s = 0; + int bi = 0; + int ii = 0; + s += a[MINDEX1(n, bi * 32 + ii)] * b[MINDEX1(n, bi * 32 + ii)]; + for (int k = 0; k < n; k++) { + __xconsumes("read: _RO(1, a[MINDEX1(n, k)] ~> Cell)"); + __xconsumes("kept: b[MINDEX1(n, k)] ~> Cell"); + __xconsumes("write: _Uninit(s ~> Cell)"); + __xproduces("write: s ~> Cell"); + __xproduces("read: _RO(1, a[MINDEX1(n, k)] ~> Cell)"); + __xproduces("out: b[MINDEX1(n, k)] ~> Cell"); + __xrequires("k_nonneg: k >= 0"); + __xproduces("done: Done(k)"); + s += a[MINDEX1(n, k)]; + } const __ghost_fn fork_out = __ghost_begin(ro_fork_group, "H := &x ~~> 0, r := 0..5"); for (int i = 0; i < 5; i++) { __strict(); @@ -360,4 +375,3 @@ void arrow() { } __ghost_end(fork_out); } - diff --git a/tests_infra/optilambda/printcpp_exp.opti b/tests_infra/optilambda/printcpp_exp.opti index c9fed2d73..8e3191f20 100644 --- a/tests_infra/optilambda/printcpp_exp.opti +++ b/tests_infra/optilambda/printcpp_exp.opti @@ -45,7 +45,7 @@ fun stack_var(): unit { letmut r = 3; r = r + 1 + 2; - (+=)(r, 2); + r += 2; __ignore(post_incr(r)); letmut s = f(r); }; @@ -286,7 +286,7 @@ ensures #85: __is_true(_Res = n); __admitted(); return m; - ghost(rewrite_linear()[inside := fun(v) { __is_true(_Res = v) }, by := z_cancel_minus_plus]); + ghost(rewrite_linear()[inside := fun(v) -> __is_true(_Res = v), by := z_cancel_minus_plus]); }; ghost(assert_prop()[proof := admit(pure_fun(fun(n: f32, d: f32): __is_true(n - d + d = n)))][r_cancel_minus_plus : proof]); ghost(assert_prop()[proof := admit(pure_fun(fun(n: f32, d: f32): __is_true(n + d - d = n)))][r_cancel_plus_minus : proof]); @@ -483,7 +483,7 @@ b: int, lower_bound: __is_true(x >= a), upper_bound: __is_true(x < b); - ensures range_check: in_range(x, range(a, b, 1)); + ensures range_check: in_range(x, a..b); __admitted(); }; ghost fun expand_subrange() { @@ -500,48 +500,48 @@ requires f: _Fraction, H: HProp; consumes #113: _RO(f, H); - produces #112: _RO(__frac_div(f, 2), H), - #111: _RO(__frac_div(f, 2), H); + produces #112: _RO(f / 2, H), + #111: _RO(f / 2, H); __admitted(); }; ghost fun ro_split3() { requires f: _Fraction, H: HProp; consumes #117: _RO(f, H); - produces #116: _RO(__frac_div(f, 3), H), - #115: _RO(__frac_div(f, 3), H), - #114: _RO(__frac_div(f, 3), H); + produces #116: _RO(f / 3, H), + #115: _RO(f / 3, H), + #114: _RO(f / 3, H); __admitted(); }; ghost fun ro_split4() { requires f: _Fraction, H: HProp; consumes #122: _RO(f, H); - produces #121: _RO(__frac_div(f, 4), H), - #120: _RO(__frac_div(f, 4), H), - #119: _RO(__frac_div(f, 4), H), - #118: _RO(__frac_div(f, 4), H); + produces #121: _RO(f / 4, H), + #120: _RO(f / 4, H), + #119: _RO(f / 4, H), + #118: _RO(f / 4, H); __admitted(); }; ghost fun ro_allow_join2() { requires f: _Fraction, H: HProp; - consumes #124: _RO(__frac_div(f, 2), H); - produces #123: _RO(__frac_sub(f, __frac_div(f, 2)), H); + consumes #124: _RO(f / 2, H); + produces #123: _RO(f - f / 2, H); __admitted(); }; ghost fun ro_allow_join3() { requires f: _Fraction, H: HProp; - consumes #126: _RO(__frac_div(f, 3), H); - produces #125: _RO(__frac_sub(__frac_sub(f, __frac_div(f, 3)), __frac_div(f, 3)), H); + consumes #126: _RO(f / 3, H); + produces #125: _RO(f - f / 3 - f / 3, H); __admitted(); }; ghost fun ro_allow_join4() { requires f: _Fraction, H: HProp; - consumes #128: _RO(__frac_div(f, 4), H); - produces #127: _RO(__frac_sub(__frac_sub(__frac_sub(f, __frac_div(f, 4)), __frac_div(f, 4)), __frac_div(f, 4)), H); + consumes #128: _RO(f / 4, H); + produces #127: _RO(f - f / 4 - f / 4 - f / 4, H); __admitted(); }; ghost fun ro_fork_group() { @@ -549,9 +549,7 @@ H: HProp, r: Range; consumes #131: _RO(f, H); - produces #130: _RO(__frac_div(f, range_count(r)), Group(r, fun(#129: int) { - H - })); + produces #130: _RO(f / range_count(r), for #129 in r -> H); __admitted(); }; ghost fun ro_join_group() { @@ -562,12 +560,8 @@ requires items: pure_fun(fun(#134: int, #135: int): HProp), inner_range: Range, outer_range: Range; - consumes #133: Group(outer_range, fun(i: int) { - Group(inner_range, fun(j: int) { items(i, j) }) - }); - produces #132: Group(inner_range, fun(j: int) { - Group(outer_range, fun(i: int) { items(i, j) }) - }); + consumes #133: for i in outer_range -> for j in inner_range -> items(i, j); + produces #132: for j in inner_range -> for i in outer_range -> items(i, j); __admitted(); }; ghost fun swap_groups_rev() { @@ -579,12 +573,8 @@ inner_range: Range, outer_range: Range, f: _Fraction; - consumes #137: _RO(f, Group(outer_range, fun(i: int) { - Group(inner_range, fun(j: int) { items(i, j) }) - })); - produces #136: _RO(f, Group(inner_range, fun(j: int) { - Group(outer_range, fun(i: int) { items(i, j) }) - })); + consumes #137: _RO(f, for i in outer_range -> for j in inner_range -> items(i, j)); + produces #136: _RO(f, for j in inner_range -> for i in outer_range -> items(i, j)); __admitted(); }; ghost fun ro_swap_groups_rev() { @@ -598,9 +588,9 @@ tile_size: int, size: int, div_check: __is_true(size = tile_count * tile_size), - #142: in_range(tile_index, range(0, tile_count, 1)), - #141: in_range(index, range(0, tile_size, 1)); - ensures #140: in_range(tile_index * tile_size + index, range(0, size, 1)); + #142: in_range(tile_index, 0..tile_count), + #141: in_range(index, 0..tile_size); + ensures #140: in_range(tile_index * tile_size + index, 0..size); __admitted(); }; ghost(assert_prop()[proof := admit(pure_fun(fun(n: int): __is_true(n = n)))][eq_refl : proof]); @@ -616,10 +606,8 @@ items: pure_fun(fun(#145: int): HProp), div_check: __is_true(size = tile_count * tile_size), positive_tile_size: __is_true(tile_size >= 0); - consumes #144: Group(range(0, size, 1), items); - produces #143: for bi in 0..tile_count { - for i in 0..tile_size { items(bi * tile_size + i) } - }; + consumes #144: Group(0..size, items); + produces #143: for bi in 0..tile_count -> for i in 0..tile_size -> items(bi * tile_size + i); __admitted(); }; ghost fun untile_divides() { @@ -634,10 +622,8 @@ div_check: __is_true(size = tile_count * tile_size), positive_tile_size: __is_true(tile_size >= 0), f: _Fraction; - consumes #147: _RO(f, Group(range(0, size, 1), items)); - produces #146: _RO(f, for bi in 0..tile_count { - for i in 0..tile_size { items(bi * tile_size + i) } - }); + consumes #147: _RO(f, Group(0..size, items)); + produces #146: _RO(f, for bi in 0..tile_count -> for i in 0..tile_size -> items(bi * tile_size + i)); __admitted(); }; ghost fun ro_untile_divides() { @@ -648,8 +634,8 @@ requires n: int, m: int, items: pure_fun(fun(#151: int, #152: int): HProp); - consumes #150: for i in 0..n { for j in 0..m { items(i, j) } }; - produces #149: for ij in 0..n * m { items(ij / m, ij % m) }; + consumes #150: for i in 0..n -> for j in 0..m -> items(i, j); + produces #149: for ij in 0..(n * m) -> items(ij / m, ij % m); __admitted(); }; ghost fun group_uncollapse() { @@ -661,8 +647,8 @@ m: int, items: pure_fun(fun(#155: int, #156: int): HProp), f: _Fraction; - consumes #154: _RO(f, for i in 0..n { for j in 0..m { items(i, j) } }); - produces #153: _RO(f, for ij in 0..n * m { items(ij / m, ij % m) }); + consumes #154: _RO(f, for i in 0..n -> for j in 0..m -> items(i, j)); + produces #153: _RO(f, for ij in 0..(n * m) -> items(ij / m, ij % m)); __admitted(); }; ghost fun ro_group_uncollapse() { @@ -707,13 +693,9 @@ items: pure_fun(fun(#168: int, #169: int): HProp), f: _Fraction, bound_check: in_range(i, r); - consumes #167: _RO(f, Group(r2, fun(i2: int) { - Group(r, fun(i: int) { items(i2, i) }) - })); - produces #166: Wand(_RO(f, Group(r2, fun(i2: int) { items(i2, i) })), _RO(f, Group(r2, fun(i2: int) { - Group(r, fun(i: int) { items(i2, i) }) - }))), - #165: _RO(f, Group(r2, fun(i2: int) { items(i2, i) })); + consumes #167: _RO(f, for i2 in r2 -> for i in r -> items(i2, i)); + produces #166: Wand(_RO(f, for i2 in r2 -> items(i2, i)), _RO(f, for i2 in r2 -> for i in r -> items(i2, i))), + #165: _RO(f, for i2 in r2 -> items(i2, i)); __admitted(); }; ghost fun ro_group2_unfocus() { @@ -755,15 +737,9 @@ big_range: Range, items: pure_fun(fun(#182: int): pure_fun(fun(#181: int): HProp)), bound_check: is_subrange(sub_range, big_range); - consumes #180: Group(outer_range, fun(i: int) { - Group(big_range, items(i)) - }); - produces #179: Wand(Group(outer_range, fun(i: int) { - Group(sub_range, items(i)) - }), Group(outer_range, fun(i: int) { Group(big_range, items(i)) })), - #178: Group(outer_range, fun(i: int) { - Group(sub_range, items(i)) - }); + consumes #180: for i in outer_range -> Group(big_range, items(i)); + produces #179: Wand(for i in outer_range -> Group(sub_range, items(i)), for i in outer_range -> Group(big_range, items(i))), + #178: for i in outer_range -> Group(sub_range, items(i)); __admitted(); }; ghost fun group2_unfocus_subrange() { @@ -780,10 +756,8 @@ new_stop: int, check_start: __is_true(new_start = start + shift), check_stop: __is_true(new_stop = stop + shift); - consumes #184: for i in range(start, stop, step) { items(i) }; - produces #183: for i in range(new_start, new_stop, step) { - items(i - shift) - }; + consumes #184: for i in range(start, stop, step) -> items(i); + produces #183: for i in range(new_start, new_stop, step) -> items(i - shift); __admitted(); }; ghost fun group_unshift() { @@ -801,10 +775,8 @@ check_start: __is_true(new_start = start + shift), check_stop: __is_true(new_stop = stop + shift), f: _Fraction; - consumes #187: _RO(f, for i in range(start, stop, step) { items(i) }); - produces #186: _RO(f, for i in range(new_start, new_stop, step) { - items(i - shift) - }); + consumes #187: _RO(f, for i in range(start, stop, step) -> items(i)); + produces #186: _RO(f, for i in range(new_start, new_stop, step) -> items(i - shift)); __admitted(); }; ghost fun ro_group_unshift() { @@ -821,10 +793,8 @@ check_stop: __is_true(new_stop = factor * stop), check_step: __is_true(new_step = factor * step), check_factor: __is_true(factor <> 0); - consumes #190: for i in range(0, stop, step) { items(i) }; - produces #189: for i in range(0, new_stop, new_step) { - items(i / factor) - }; + consumes #190: for i in range(0, stop, step) -> items(i); + produces #189: for i in range(0, new_stop, new_step) -> items(i / factor); __admitted(); }; ghost fun group_unscale() { @@ -841,10 +811,8 @@ check_stop: __is_true(new_stop = factor * stop), check_step: __is_true(new_step = factor * step), f: _Fraction; - consumes #193: _RO(f, for i in range(0, stop, step) { items(i) }); - produces #192: _RO(f, for i in range(0, new_stop, new_step) { - items(i / factor) - }); + consumes #193: _RO(f, for i in range(0, stop, step) -> items(i)); + produces #192: _RO(f, for i in range(0, new_stop, new_step) -> items(i / factor)); __admitted(); }; ghost fun ro_group_unscale() { @@ -858,9 +826,9 @@ split: int, items: pure_fun(fun(#198: int): HProp), range_check: is_subrange(range(start, split, step), range(start, stop, step)); - consumes #197: for i in range(start, stop, step) { items(i) }; - produces #196: for i in range(start, split, step) { items(i) }, - #195: for i in range(split, stop, step) { items(i) }; + consumes #197: for i in range(start, stop, step) -> items(i); + produces #196: for i in range(start, split, step) -> items(i), + #195: for i in range(split, stop, step) -> items(i); __admitted(); }; ghost fun group_join() { @@ -875,9 +843,9 @@ items: pure_fun(fun(#202: int): HProp), bound_check: in_range(split, range(start, stop, step)), f: _Fraction; - consumes #201: _RO(f, for i in range(start, stop, step) { items(i) }); - produces #200: _RO(f, for i in range(start, split, step) { items(i) }), - #199: _RO(f, for i in range(split, stop, step) { items(i) }); + consumes #201: _RO(f, for i in range(start, stop, step) -> items(i)); + produces #200: _RO(f, for i in range(start, split, step) -> items(i)), + #199: _RO(f, for i in range(split, stop, step) -> items(i)); __admitted(); }; ghost fun ro_group_join() { @@ -902,13 +870,13 @@ }; ghost fun group_intro_zero() { requires items: pure_fun(fun(#211: int): HProp); - produces #210: for i in 0..0 { items(i) }; + produces #210: for i in 0..0 -> items(i); __admitted(); }; ghost fun group_intro_empty() { requires N: int, items: pure_fun(fun(#213: int): HProp); - produces #212: for i in N..N { items(i) }; + produces #212: for i in N..N -> items(i); __admitted(); }; ghost fun group_elim_zero() { @@ -922,7 +890,7 @@ ghost fun group_intro_one() { requires item: HProp; consumes #215: item; - produces #214: for i in 0..1 { item }; + produces #214: for i in 0..1 -> item; __admitted(); }; ghost fun group_elim_one() { @@ -933,8 +901,8 @@ requires H: pure_fun(fun(#220: pure_fun(fun(#218: int, #219: int): int)): HProp), n1: int, n2: int; - consumes #217: H(fun(i1, i2) { DMINDEX1(n1 * n2, i1 * n2 + i2) }); - produces #216: H(fun(i1, i2) { DMINDEX2(n1, n2, i1, i2) }); + consumes #217: H(fun(i1, i2) -> DMINDEX1(n1 * n2, i1 * n2 + i2)); + produces #216: H(fun(i1, i2) -> DMINDEX2(n1, n2, i1, i2)); __admitted(); }; ghost fun dmindex2_tile() { @@ -946,10 +914,8 @@ n1: int, n2: int, n3: int; - consumes #222: H(fun(i1, i2, i3) { - DMINDEX2(n1 * n2, n3, i1 * n2 + i2, i3) - }); - produces #221: H(fun(i1, i2, i3) { DMINDEX3(n1, n2, n3, i1, i2, i3) }); + consumes #222: H(fun(i1, i2, i3) -> DMINDEX2(n1 * n2, n3, i1 * n2 + i2, i3)); + produces #221: H(fun(i1, i2, i3) -> DMINDEX3(n1, n2, n3, i1, i2, i3)); __admitted(); }; ghost fun dmindex3_tile() { @@ -962,8 +928,8 @@ matrix: ptr(T), n1: int, n2: int; - consumes #228: H(fun(i1, i2) { matrix[MINDEX2(n1, n2, i1, i2)] }); - produces #227: H(fun(i1, i2) { matrix[i1 * n2][MINDEX1(n2, i2)] }); + consumes #228: H(fun(i1, i2) -> matrix[MINDEX2(n1, n2, i1, i2)]); + produces #227: H(fun(i1, i2) -> matrix[i1 * n2][MINDEX1(n2, i2)]); __admitted(); }; ghost fun mindex2_fold() { @@ -977,12 +943,8 @@ n1: int, n2: int, n3: int; - consumes #233: H(fun(i1, i2, i3) { - matrix[MINDEX3(n1, n2, n3, i1, i2, i3)] - }); - produces #232: H(fun(i1, i2, i3) { - matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)] - }); + consumes #233: H(fun(i1, i2, i3) -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)]); + produces #232: H(fun(i1, i2, i3) -> matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)]); __admitted(); }; ghost fun mindex3_fold() { @@ -996,8 +958,8 @@ n1: int, n2: int, f: _Fraction; - consumes #239: _RO(f, H(fun(i1, i2) { matrix[MINDEX2(n1, n2, i1, i2)] })); - produces #238: _RO(f, H(fun(i1, i2) { matrix[i1 * n2][MINDEX1(n2, i2)] })); + consumes #239: _RO(f, H(fun(i1, i2) -> matrix[MINDEX2(n1, n2, i1, i2)])); + produces #238: _RO(f, H(fun(i1, i2) -> matrix[i1 * n2][MINDEX1(n2, i2)])); __admitted(); }; ghost fun ro_mindex2_fold() { @@ -1012,12 +974,8 @@ n2: int, n3: int, f: _Fraction; - consumes #244: _RO(f, H(fun(i1, i2, i3) { - matrix[MINDEX3(n1, n2, n3, i1, i2, i3)] - })); - produces #243: _RO(f, H(fun(i1, i2, i3) { - matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)] - })); + consumes #244: _RO(f, H(fun(i1, i2, i3) -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)])); + produces #243: _RO(f, H(fun(i1, i2, i3) -> matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)])); __admitted(); }; ghost fun ro_mindex3_fold() { @@ -1062,8 +1020,8 @@ n2: int, items: pure_fun(fun(#258: int): HProp), expand_check: __is_true(n1 <= n2); - consumes #257: for i in 0..n1 { items(i) }; - produces #256: for i in 0..n2 { If(__is_true(i < n1), items(i)) }; + consumes #257: for i in 0..n1 -> items(i); + produces #256: for i in 0..n2 -> If(__is_true(i < n1), items(i)); __admitted(); }; ghost fun group_shrink_r_if_elim() { @@ -1074,13 +1032,13 @@ requires n: int, H: HProp; consumes #260: H; - produces #259: for i in 0..n { If(__is_true(i = 0), H) }; + produces #259: for i in 0..n -> If(__is_true(i = 0), H); __admitted(); }; ghost fun group_singleton_if_elim() { requires n: int, H: HProp; - consumes #262: for i in 0..n { If(__is_true(i = 0), H) }; + consumes #262: for i in 0..n -> If(__is_true(i = 0), H); produces #261: H; __admitted(); }; @@ -1091,9 +1049,9 @@ items: pure_fun(fun(#266: int): HProp), r: Range; consumes #265: ThreadsCtx(r), - #264: for i in 0..N { items(i) }; + #264: for i in 0..N -> items(i); produces #265: ThreadsCtx(r), - #263: DesyncGroup(N, fun(i: int) { items(i) }); + #263: desync_for i in ..N -> items(i); __admitted(); }; ghost fun unwrap_singleton_desyncgroup() { @@ -1113,9 +1071,7 @@ div_check: __is_true(size = tile_count * tile_size), positive_tile_size: __is_true(tile_size >= 0); consumes #272: DesyncGroup(size, items); - produces #271: DesyncGroup(tile_count, fun(bi: int) { - DesyncGroup(tile_size, fun(i: int) { items(bi * tile_size + i) }) - }); + produces #271: desync_for bi in ..tile_count -> desync_for i in ..tile_size -> items(bi * tile_size + i); __admitted(); }; ghost fun desync_untile_divides() { @@ -1141,10 +1097,10 @@ MT: MemType, M: pure_fun(fun(#280: int): T), f: _Fraction, - bound_check: in_range(i, range(0, n, 1)); - consumes #279: _RO(f, (matrix ~> Matrix1Of(n, MT, M))); - produces #278: Wand(_RO(f, (matrix[MINDEX1(n, i)] ~> CellOf(MT))), _RO(f, (matrix ~> Matrix1Of(n, MT, M)))), - #277: _RO(f, (matrix[MINDEX1(n, i)] ~> CellOf(MT))); + bound_check: in_range(i, 0..n); + consumes #279: _RO(f, matrix ~> Matrix1Of(n, MT, M)); + produces #278: Wand(_RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)), _RO(f, matrix ~> Matrix1Of(n, MT, M))), + #277: _RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)); __admitted(); ghost(ro_group_focus()[f := f, i := i, bound_check := bound_check]); }; @@ -1163,11 +1119,11 @@ MT: MemType, M: pure_fun(fun(#284: int, #285: int): T), f: _Fraction, - bound_check_i: in_range(i, range(0, m, 1)), - bound_check_j: in_range(j, range(0, n, 1)); - consumes #283: _RO(f, (matrix ~> Matrix2Of(m, n, MT, M))); - produces #282: Wand(_RO(f, (matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT))), _RO(f, (matrix ~> Matrix2Of(m, n, MT, M)))), - #281: _RO(f, (matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT))); + bound_check_i: in_range(i, 0..m), + bound_check_j: in_range(j, 0..n); + consumes #283: _RO(f, matrix ~> Matrix2Of(m, n, MT, M)); + produces #282: Wand(_RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)), _RO(f, matrix ~> Matrix2Of(m, n, MT, M))), + #281: _RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)); __admitted(); ghost(ro_group_focus()[f := f, i := i, bound_check := bound_check_i]); ghost(ro_group_focus()[f := f, i := j, bound_check := bound_check_j]); @@ -1180,64 +1136,64 @@ }; fun MATRIX1_COPY_int(dest: ptr(int), src: ptr(int), length: int): unit [model, #288, #287, #286, #287, #286] { requires model: pure_fun(fun(#289: int): int); - reads #287: (src ~> Matrix1(length, model)); - writes #286: (dest ~> Matrix1(length, model)); + reads #287: src ~> Matrix1(length, model); + writes #286: dest ~> Matrix1(length, model); __admitted(); __ignore(memcpy(dest, src, length * sizeof(int))); }; fun MATRIX2_COPY_int(dest: ptr(int), src: ptr(int), n1: int, n2: int): unit [model, #292, #291, #290, #291, #290] { requires model: pure_fun(fun(#293: int, #294: int): int); - reads #291: (src ~> Matrix2(n1, n2, model)); - writes #290: (dest ~> Matrix2(n1, n2, model)); + reads #291: src ~> Matrix2(n1, n2, model); + writes #290: dest ~> Matrix2(n1, n2, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * sizeof(int))); }; fun MATRIX3_COPY_int(dest: ptr(int), src: ptr(int), n1: int, n2: int, n3: int): unit [model, #297, #296, #295, #296, #295] { requires model: pure_fun(fun(#298: int, #299: int, #300: int): int); - reads #296: (src ~> Matrix3(n1, n2, n3, model)); - writes #295: (dest ~> Matrix3(n1, n2, n3, model)); + reads #296: src ~> Matrix3(n1, n2, n3, model); + writes #295: dest ~> Matrix3(n1, n2, n3, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * n3 * sizeof(int))); }; fun MATRIX1_COPY_float(dest: ptr(f32), src: ptr(f32), length: int): unit [model, #303, #302, #301, #302, #301] { requires model: pure_fun(fun(#304: int): f32); - reads #302: (src ~> Matrix1(length, model)); - writes #301: (dest ~> Matrix1(length, model)); + reads #302: src ~> Matrix1(length, model); + writes #301: dest ~> Matrix1(length, model); __admitted(); __ignore(memcpy(dest, src, length * sizeof(f32))); }; fun MATRIX2_COPY_float(dest: ptr(f32), src: ptr(f32), n1: int, n2: int): unit [model, #307, #306, #305, #306, #305] { requires model: pure_fun(fun(#308: int, #309: int): f32); - reads #306: (src ~> Matrix2(n1, n2, model)); - writes #305: (dest ~> Matrix2(n1, n2, model)); + reads #306: src ~> Matrix2(n1, n2, model); + writes #305: dest ~> Matrix2(n1, n2, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * sizeof(f32))); }; fun MATRIX3_COPY_float(dest: ptr(f32), src: ptr(f32), n1: int, n2: int, n3: int): unit [model, #312, #311, #310, #311, #310] { requires model: pure_fun(fun(#313: int, #314: int, #315: int): f32); - reads #311: (src ~> Matrix3(n1, n2, n3, model)); - writes #310: (dest ~> Matrix3(n1, n2, n3, model)); + reads #311: src ~> Matrix3(n1, n2, n3, model); + writes #310: dest ~> Matrix3(n1, n2, n3, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * n3 * sizeof(f32))); }; fun MATRIX1_COPY_double(dest: ptr(f64), src: ptr(f64), length: int): unit [model, #318, #317, #316, #317, #316] { requires model: pure_fun(fun(#319: int): f64); - reads #317: (src ~> Matrix1(length, model)); - writes #316: (dest ~> Matrix1(length, model)); + reads #317: src ~> Matrix1(length, model); + writes #316: dest ~> Matrix1(length, model); __admitted(); __ignore(memcpy(dest, src, length * sizeof(f64))); }; fun MATRIX2_COPY_double(dest: ptr(f64), src: ptr(f64), n1: int, n2: int): unit [model, #322, #321, #320, #321, #320] { requires model: pure_fun(fun(#323: int, #324: int): f64); - reads #321: (src ~> Matrix2(n1, n2, model)); - writes #320: (dest ~> Matrix2(n1, n2, model)); + reads #321: src ~> Matrix2(n1, n2, model); + writes #320: dest ~> Matrix2(n1, n2, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * sizeof(f64))); }; fun MATRIX3_COPY_double(dest: ptr(f64), src: ptr(f64), n1: int, n2: int, n3: int): unit [model, #327, #326, #325, #326, #325] { requires model: pure_fun(fun(#328: int, #329: int, #330: int): f64); - reads #326: (src ~> Matrix3(n1, n2, n3, model)); - writes #325: (dest ~> Matrix3(n1, n2, n3, model)); + reads #326: src ~> Matrix3(n1, n2, n3, model); + writes #325: dest ~> Matrix3(n1, n2, n3, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * n3 * sizeof(f64))); }; @@ -1249,10 +1205,8 @@ b: int, MT: MemType, M: pure_fun(fun(#333: int): T); - consumes #332: for i in a..b { (matrix[MINDEX1(n1, i)] ~> CellOf(MT)) }; - produces #331: for i in 0..b - a { - (matrix[a][MINDEX1(b - a, i)] ~> CellOf(MT)) - }; + consumes #332: for i in a..b -> matrix[MINDEX1(n1, i)] ~> CellOf(MT); + produces #331: for i in 0..(b - a) -> matrix[a][MINDEX1(b - a, i)] ~> CellOf(MT); __admitted(); }; ghost fun matrix1_span_unshift() { @@ -1268,14 +1222,8 @@ b: int, MT: MemType, M: pure_fun(fun(#336: int, #337: int): T); - consumes #335: for i in a..b { - for j in 0..n2 { (matrix[MINDEX2(n1, n2, i, j)] ~> CellOf(MT)) } - }; - produces #334: for i in 0..b - a { - for j in 0..n2 { - (matrix[a * n2][MINDEX2(b - a, n2, i, j)] ~> CellOf(MT)) - } - }; + consumes #335: for i in a..b -> for j in 0..n2 -> matrix[MINDEX2(n1, n2, i, j)] ~> CellOf(MT); + produces #334: for i in 0..(b - a) -> for j in 0..n2 -> matrix[a * n2][MINDEX2(b - a, n2, i, j)] ~> CellOf(MT); __admitted(); }; ghost fun matrix2_span_unshift() { @@ -1292,20 +1240,8 @@ b: int, MT: MemType, M: pure_fun(fun(#340: int, #341: int, #342: int): T); - consumes #339: for i1 in a..b { - for i2 in 0..n2 { - for i3 in 0..n3 { - (matrix[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(MT)) - } - } - }; - produces #338: for i1 in 0..b - a { - for i2 in 0..n2 { - for i3 in 0..n3 { - (matrix[a * n2 * n3][MINDEX3(b - a, n2, n3, i1, i2, i3)] ~> CellOf(MT)) - } - } - }; + consumes #339: for i1 in a..b -> for i2 in 0..n2 -> for i3 in 0..n3 -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(MT); + produces #338: for i1 in 0..(b - a) -> for i2 in 0..n2 -> for i3 in 0..n3 -> matrix[a * n2 * n3][MINDEX3(b - a, n2, n3, i1, i2, i3)] ~> CellOf(MT); __admitted(); }; ghost fun matrix3_span_unshift() { @@ -1320,73 +1256,91 @@ }; fun one_fork(): unit { letmut x = 0; - let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)])); - for i in 0..5 [#344, #343, #343] { + letmut n = 64; + letmut a; + letmut b; + letmut s = 0; + letmut bi = 0; + letmut ii = 0; + s += (a)[MINDEX1(n, bi * 32 + ii)] * (b)[MINDEX1(n, bi * 32 + ii)]; + for k in 0..n { + xrequires k_nonneg: __is_true(k >= 0); + xconsumes read: _RO(1, (*(*a)[MINDEX1(*n, k)]) ~> CellOf(Any)), + kept: (*(*b)[MINDEX1(*n, k)]) ~> CellOf(Any), + write: _Uninit((*s) ~> CellOf(Any)); + xproduces write: (*s) ~> CellOf(Any), + read: _RO(1, (*(*a)[MINDEX1(*n, k)]) ~> CellOf(Any)), + out: (*(*b)[MINDEX1(*n, k)]) ~> CellOf(Any), + done: Done(k); + s += (a)[MINDEX1(n, k)]; + }; + let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := x ~> CellOf(Any), r := 0..5])); + for i in 0..5 { strict; requires #344: _Fraction; - xconsumes #343: _RO(#344, (x ~> CellOf(Any))); - xproduces #343: _RO(#344, (x ~> CellOf(Any))); - ghost(ro_split2()[f := #_1, H := (x ~> CellOf(Any))]); - ghost(ro_fork_group()[f := #_1 / 2, H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); - ghost(ro_allow_join2()[f := #_1, H := (x ~> CellOf(Any))]); - for j in 0..5 [#346, #345, #345] { + xconsumes #343: _RO(#344, x ~> CellOf(Any)); + xproduces #343: _RO(#344, x ~> CellOf(Any)); + ghost(ro_split2()[f := #_1, H := x ~> CellOf(Any)]); + ghost(ro_fork_group()[f := #_1 / 2, H := x ~> CellOf(Any), r := 0..5]); + ghost(ro_allow_join2()[f := #_1, H := x ~> CellOf(Any)]); + for j in 0..5 { strict; requires #346: _Fraction; - xconsumes #345: _RO(#346, (x ~> CellOf(Any))); - xproduces #345: _RO(#346, (x ~> CellOf(Any))); + xconsumes #345: _RO(#346, x ~> CellOf(Any)); + xproduces #345: _RO(#346, x ~> CellOf(Any)); __ignore(x + 1); }; - ghost(ro_join_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); + ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); }; __ghost_end(fork_out); }; fun two_forks(): unit { letmut x = 0; - let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)])); - for i in 0..5 [#348, #347, #347] { + let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := x ~> CellOf(Any), r := 0..5])); + for i in 0..5 { strict; requires #348: _Fraction; - xconsumes #347: _RO(#348, (x ~> CellOf(Any))); - xproduces #347: _RO(#348, (x ~> CellOf(Any))); - ghost(ro_split3()[f := #_1, H := (x ~> CellOf(Any))]); - ghost(ro_fork_group()[f := #_1 / 3, H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); - ghost(ro_fork_group()[f := #_1 / 3, H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); - ghost(ro_allow_join3()[f := #_1, H := (x ~> CellOf(Any))]); - for j in 0..5 [#350, #349, #349] { + xconsumes #347: _RO(#348, x ~> CellOf(Any)); + xproduces #347: _RO(#348, x ~> CellOf(Any)); + ghost(ro_split3()[f := #_1, H := x ~> CellOf(Any)]); + ghost(ro_fork_group()[f := #_1 / 3, H := x ~> CellOf(Any), r := 0..5]); + ghost(ro_fork_group()[f := #_1 / 3, H := x ~> CellOf(Any), r := 0..5]); + ghost(ro_allow_join3()[f := #_1, H := x ~> CellOf(Any)]); + for j in 0..5 { strict; requires #350: _Fraction; - xconsumes #349: _RO(#350, (x ~> CellOf(Any))); - xproduces #349: _RO(#350, (x ~> CellOf(Any))); + xconsumes #349: _RO(#350, x ~> CellOf(Any)); + xproduces #349: _RO(#350, x ~> CellOf(Any)); __ignore(x + 1); }; - ghost(ro_join_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); - ghost(ro_join_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); + ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); + ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); }; __ghost_end(fork_out); }; fun two_forks_spe_twice(): unit { letmut x = 0; - let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)])); - for i in 0..5 [#352, #351, #351] { + let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := x ~> CellOf(Any), r := 0..5])); + for i in 0..5 { strict; requires #352: _Fraction; - xconsumes #351: _RO(#352, (x ~> CellOf(Any))); - xproduces #351: _RO(#352, (x ~> CellOf(Any))); - ghost(ro_split2()[f := #_1, H := (x ~> CellOf(Any))]); - ghost(ro_split2()[f := #_1 / 2, H := (x ~> CellOf(Any))]); - ghost(ro_fork_group()[f := #_1 / 2, H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); - ghost(ro_fork_group()[f := #_1 / 2 / 2, H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); - ghost(ro_allow_join2()[f := #_1 / 2, H := (x ~> CellOf(Any))]); - for j in 0..5 [#354, #353, #353] { + xconsumes #351: _RO(#352, x ~> CellOf(Any)); + xproduces #351: _RO(#352, x ~> CellOf(Any)); + ghost(ro_split2()[f := #_1, H := x ~> CellOf(Any)]); + ghost(ro_split2()[f := #_1 / 2, H := x ~> CellOf(Any)]); + ghost(ro_fork_group()[f := #_1 / 2, H := x ~> CellOf(Any), r := 0..5]); + ghost(ro_fork_group()[f := #_1 / 2 / 2, H := x ~> CellOf(Any), r := 0..5]); + ghost(ro_allow_join2()[f := #_1 / 2, H := x ~> CellOf(Any)]); + for j in 0..5 { strict; requires #354: _Fraction; - xconsumes #353: _RO(#354, (x ~> CellOf(Any))); - xproduces #353: _RO(#354, (x ~> CellOf(Any))); + xconsumes #353: _RO(#354, x ~> CellOf(Any)); + xproduces #353: _RO(#354, x ~> CellOf(Any)); __ignore(x + 1); }; - ghost(ro_join_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); - ghost(ro_allow_join2()[f := #_1, H := (x ~> CellOf(Any))]); - ghost(ro_join_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); + ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); + ghost(ro_allow_join2()[f := #_1, H := x ~> CellOf(Any)]); + ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); }; __ghost_end(fork_out); }; diff --git a/tests_infra/optilambda/printer_basic.ml b/tests_infra/optilambda/printer_basic.ml index 09cafc4a0..824f61528 100644 --- a/tests_infra/optilambda/printer_basic.ml +++ b/tests_infra/optilambda/printer_basic.ml @@ -57,6 +57,45 @@ let surface_writes_contract = post = resource_set ~linear:[ (v "x", body) ] (); } +let surface_formula_contract = + { empty_fun_contract with pre = resource_set ~linear:[ (v "h", points_to_formula (term "src") (term "H")) ] () } + +let mixed_recovery_contract = + let frac = term "f" in + let read_body = term "ReadH" in + let write_body = term "WriteH" in + { + pre = + resource_set + ~pure:[ (v "f", term "_Fraction") ] + ~linear:[ (v "read", read_only_formula frac read_body); (v "kept", term "Kept"); (v "write", uninit_formula write_body) ] + (); + post = + resource_set + ~linear:[ (v "write", write_body); (v "read", read_only_formula frac read_body); (v "new_out", term "Produced") ] + (); + } + +let alpha_group_reads_contract = + let frac = term "f" in + let range_var = Ast.new_var "range" in + let group_var = Ast.new_var "Group" in + let h_var = Ast.new_var "H" in + let n_var = Ast.new_var "n" in + let pre_i = Ast.new_var "i" in + let post_i = Ast.new_var "i" in + let app_var fn args = Trm.trm_apps (Trm.trm_var fn) args in + let range = app_var range_var [ Trm.trm_int 0; Trm.trm_var n_var; Trm.trm_int 1 ] in + let group_formula index body = + app_var group_var [ range; Trm.trm_fun [ (index, Typ.typ_int) ] Typ.typ_auto body ] + in + let pre_body = group_formula pre_i (app_var h_var [ Trm.trm_var pre_i ]) in + let post_body = group_formula post_i (app_var h_var [ Trm.trm_var post_i ]) in + { + pre = resource_set ~pure:[ (v "f", term "_Fraction") ] ~linear:[ (v "read", read_only_formula frac pre_body) ] (); + post = resource_set ~linear:[ (v "read", read_only_formula frac post_body) ] (); + } + let read_only_focus_contract = let frac = term "f" in let whole = term "Whole" in @@ -316,6 +355,28 @@ let () = (Trm.trm_seq_nomarks [])) "fun write_example(): unit [x, x] { writes x: H; }"; + check "surface local formula printer in contract" + (Trm.trm_let_fun ~contract:(FunSpecContract surface_formula_contract) (v "formula_example") Typ.typ_unit [] + (Trm.trm_seq_nomarks [])) + "fun formula_example(): unit [h] { consumes h: src ~> H; }"; + + check "non-adjacent reads and writes recovery" + (Trm.trm_let_fun ~contract:(FunSpecContract mixed_recovery_contract) (v "mixed_example") Typ.typ_unit [] + (Trm.trm_seq_nomarks [])) + "fun mixed_example(): unit [f, read, kept, write, write, read, new_out] {\n\ + \ reads read: ReadH;\n\ + \ writes write: WriteH;\n\ + \ consumes kept: Kept;\n\ + \ produces new_out: Produced;\n\ + }"; + + check "alpha-equivalent group reads recovery" + (Trm.trm_let_fun ~contract:(FunSpecContract alpha_group_reads_contract) (v "alpha_group_read_example") Typ.typ_unit [] + (Trm.trm_seq_nomarks [])) + "fun alpha_group_read_example(): unit [f, read, read] {\n\ + \ reads read: for i in 0..n -> H(i);\n\ + }"; + check_with_style "internal reads contract" internal_style (Trm.trm_let_fun ~contract:(FunSpecContract surface_reads_contract) (v "read_example") Typ.typ_unit [] @@ -369,7 +430,7 @@ let () = (Trm.trm_for ~contract:simple_loop_contract { index = v "i"; start = Trm.trm_int 0; direction = DirUp; stop = term "n"; step = Trm.trm_int 1 } (Trm.trm_seq_nomarks [ Trm.trm_set (term "x") (Trm.trm_add ~typ:Typ.typ_int (term "x") (Trm.trm_int 1)) ])) - "for i in 0..n [h_loop, h_inv, h_xreq, h_xprod] {\n\ + "for i in 0..n {\n\ \ requires h_loop: i < n,\n\ \ h_inv: 0 <= i;\n\ \ xrequires h_xreq: i < n;\n\ @@ -377,7 +438,14 @@ let () = \ x = x + 1;\n\ }"; - check "compound operator call" (Trm.trm_compound_assign ~typ:Typ.typ_int Binop_add (term "r") (Trm.trm_int 2)) "(+=)(r, 2)"; + check "compound operator assignment" (Trm.trm_compound_assign ~typ:Typ.typ_int Binop_add (term "r") (Trm.trm_int 2)) "r += 2"; + + let mindex = + app "MINDEX1" [ term "n"; Trm.trm_add ~typ:Typ.typ_int (Trm.trm_mul ~typ:Typ.typ_int (term "bi") (Trm.trm_int 32)) (term "i") ] + in + let indexed_product = Trm.trm_mul ~typ:Typ.typ_int (Trm.trm_array_get (term "a") mindex) (Trm.trm_array_get (term "b") mindex) in + check "compound assignment with indexed product" (Trm.trm_compound_assign ~typ:Typ.typ_int Binop_add (term "s") indexed_product) + "s += a[MINDEX1(n, bi * 32 + i)] * b[MINDEX1(n, bi * 32 + i)]"; check "struct access" (Trm.trm_struct_access ~struct_typ:Typ.typ_auto (term "v") "x") "v.x"; From bd4493e62a62ca11f80bf62f55c2540689fcae30 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Tue, 7 Jul 2026 08:59:34 -0400 Subject: [PATCH 30/47] improve OptiTrust VS Code diff and trace views --- tools/vscode-optitrust/package.json | 22 +- .../scripts/run_extension_dev_host.sh | 37 ++- .../src/commands/viewCommands.ts | 227 ++++++++++++++---- tools/vscode-optitrust/src/extension.ts | 16 +- .../src/optitrust/fileSystem.ts | 10 + .../src/optitrust/liveView.ts | 117 +++++++++ .../src/optitrust/nativeDiff.ts | 103 ++++++-- tools/vscode-optitrust/src/optitrust/views.ts | 138 ++++++++++- 8 files changed, 573 insertions(+), 97 deletions(-) create mode 100644 tools/vscode-optitrust/src/optitrust/fileSystem.ts create mode 100644 tools/vscode-optitrust/src/optitrust/liveView.ts diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index eea3ee95f..89f3e027f 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -26,6 +26,7 @@ "onCommand:optitrust.viewDiffOnlyCode", "onCommand:optitrust.viewDiffInternalSyntax", "onCommand:optitrust.switchDiffSyntax", + "onCommand:optitrust.detachView", "onCommand:optitrust.viewFullTrace", "onCommand:optitrust.viewTraceSaveStepsScript", "onCommand:optitrust.viewStepTrace", @@ -83,6 +84,11 @@ "title": "OptiTrust: Switch Diff Syntax", "icon": "$(replace)" }, + { + "command": "optitrust.detachView", + "title": "OptiTrust: Detach View", + "icon": "$(debug-disconnect)" + }, { "command": "optitrust.viewFullTrace", "title": "OptiTrust: View Full Trace" @@ -385,27 +391,32 @@ "editor/title": [ { "command": "optitrust.openAssociatedFiles", - "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace", + "when": "resourceScheme == file && (resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace)", "group": "navigation@50" }, { "command": "optitrust.switchDiffSyntax", "when": "resourceScheme == optitrust-diff", + "group": "navigation@51" + }, + { + "command": "optitrust.detachView", + "when": "resourceScheme == optitrust-diff", "group": "navigation@52" }, { "command": "optitrust.optinlpChat", - "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", + "when": "resourceScheme == file && (resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti)", "group": "navigation@55" }, { "command": "optitrust.optinlpSuggestTargetAtCursor", - "when": "resourceExtname == .ml", + "when": "resourceScheme == file && resourceExtname == .ml", "group": "navigation@60" }, { "command": "optitrust.showShortcuts", - "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace", + "when": "resourceScheme == file && (resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace)", "group": "navigation@70" } ], @@ -447,6 +458,9 @@ { "command": "optitrust.switchDiffSyntax" }, + { + "command": "optitrust.detachView" + }, { "command": "optitrust.viewFullTrace" }, diff --git a/tools/vscode-optitrust/scripts/run_extension_dev_host.sh b/tools/vscode-optitrust/scripts/run_extension_dev_host.sh index 4258a0e3a..2ef8ebbd2 100755 --- a/tools/vscode-optitrust/scripts/run_extension_dev_host.sh +++ b/tools/vscode-optitrust/scripts/run_extension_dev_host.sh @@ -15,17 +15,42 @@ repo_root="$(cd "$extension_dir/../.." && pwd)" code_cmd="${CODE_CMD:-code}" vsix_path="$extension_dir/.optitrust-dev.vsix" +run_code() { + local candidate + while IFS= read -r candidate; do + if [[ "$candidate" == *"/.vscode-server/"*"/remote-cli/code" ]]; then + if [[ -n "${VSCODE_IPC_HOOK_CLI:-}" && -S "${VSCODE_IPC_HOOK_CLI}" ]]; then + if "$candidate" "$@"; then + return 0 + fi + fi + continue + fi + + if env -u VSCODE_IPC_HOOK_CLI "$candidate" "$@"; then + return 0 + fi + done < <(type -P -a "$code_cmd" 2>/dev/null || printf '%s\n' "$code_cmd") + + return 1 +} + +install_dev_vsix() { + echo "Packaging and installing the OptiTrust extension..." + ./node_modules/.bin/vsce package --out "$vsix_path" --no-dependencies + run_code --install-extension "$vsix_path" --force +} + cd "$extension_dir" npm run compile -if "$code_cmd" --help 2>&1 | grep -q -- "--extensionDevelopmentPath"; then +if run_code --help 2>&1 | grep -q -- "--extensionDevelopmentPath"; then echo "Opening VS Code Extension Development Host..." - "$code_cmd" --new-window --extensionDevelopmentPath="$extension_dir" "$repo_root" + run_code --new-window --extensionDevelopmentPath="$extension_dir" "$repo_root" + install_dev_vsix else echo "The '$code_cmd' CLI does not support --extensionDevelopmentPath." - echo "Packaging and installing the OptiTrust extension instead..." - ./node_modules/.bin/vsce package --out "$vsix_path" --no-dependencies - "$code_cmd" --install-extension "$vsix_path" --force + install_dev_vsix echo "Opening OptiTrust with the installed extension..." - "$code_cmd" --new-window "$repo_root" + run_code --new-window "$repo_root" fi diff --git a/tools/vscode-optitrust/src/commands/viewCommands.ts b/tools/vscode-optitrust/src/commands/viewCommands.ts index cca087575..7f893fd82 100644 --- a/tools/vscode-optitrust/src/commands/viewCommands.ts +++ b/tools/vscode-optitrust/src/commands/viewCommands.ts @@ -3,6 +3,7 @@ import * as path from "path"; import * as vscode from "vscode"; import { getActiveEditorContext } from "../optitrust/editor"; import { markExecutedLine } from "../optitrust/decorations"; +import { fileExists } from "../optitrust/fileSystem"; import { appendLine } from "../optitrust/output"; import { runCommand } from "../optitrust/runner"; import { validateTransformationScript } from "../optitrust/scripts"; @@ -15,7 +16,6 @@ type ViewMode = "step_diff" | "full_trace" | "step_trace"; type ViewOption = "diff-only-code" | "diff-internal-syntax" | "trace-save-steps-script"; interface ViewCommandSpec { - readonly mode: ViewMode; readonly scriptMode: "step_diff" | "full_trace" | "step_trace"; readonly title: string; readonly viewKind: "diff" | "trace" | "step-trace"; @@ -24,21 +24,18 @@ interface ViewCommandSpec { const VIEW_COMMANDS: Record = { step_diff: { - mode: "step_diff", scriptMode: "step_diff", title: "OptiTrust: View Step Diff", viewKind: "diff", htmlSuffix: "_diff.html" }, full_trace: { - mode: "full_trace", scriptMode: "full_trace", title: "OptiTrust: View Full Trace", viewKind: "trace", htmlSuffix: "_trace.html" }, step_trace: { - mode: "step_trace", scriptMode: "step_trace", title: "OptiTrust: View Step Trace", viewKind: "step-trace", @@ -46,19 +43,27 @@ const VIEW_COMMANDS: Record = { } }; +const CPP_VIEW_MODE = VIEW_MODES.find(mode => mode.id === "cpp") ?? VIEW_MODES[0]; const DEFAULT_STEP_DIFF_VIEW_MODE = VIEW_MODES.find(mode => mode.id === "optilambda.surface") ?? VIEW_MODES[1]; -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } +interface StoredViewContext { + readonly root: string; + readonly relativePath: string; + readonly line: number; + readonly fileDir: string; + readonly fileBase: string; +} + +interface StoredViewRequest { + readonly mode: ViewMode; + readonly option?: ViewOption; + readonly context: StoredViewContext; + readonly viewMode: ViewModeDefinition; } +let lastViewRequest: StoredViewRequest | undefined; + export async function runViewCommand(workspace: OptitrustWorkspace, mode: ViewMode, option?: ViewOption): Promise { - const spec = VIEW_COMMANDS[mode]; const context = getActiveEditorContext(workspace.root); const validation = validateTransformationScript(context); if (!validation.ok) { @@ -71,45 +76,19 @@ export async function runViewCommand(workspace: OptitrustWorkspace, mode: ViewMo const selectedViewMode = getSelectedViewMode(); const initialDiffViewMode = option === undefined ? DEFAULT_STEP_DIFF_VIEW_MODE : selectedViewMode; const commandViewMode = mode === "step_diff" ? initialDiffViewMode : selectedViewMode; - const extraArgs = viewArgs(spec.mode, commandViewMode, option); - const args = [spec.scriptMode, context.relativePath, String(context.line), ...extraArgs]; - - try { - await runCommand({ - cwd: workspace.root, - command: path.join(workspace.root, "tools", "view_result.sh"), - args, - title: spec.title, - env: { - OPTITRUST_NO_BROWSER: "1" - } - }); - } catch { - return; - } - - if (mode === "step_diff") { - await openNativeStepDiff( - { - root: workspace.root, - scriptRelativePath: context.relativePath, - line: context.line, - fileDir: context.fileDir, - fileBase: context.fileBase - }, - commandViewMode, - { markGenerated: true } - ); - return; - } - const htmlFile = path.join(context.fileDir, `${context.fileBase}${spec.htmlSuffix}`); - if (await exists(htmlFile)) { - await openHtmlView(workspace.root, htmlFile, spec.viewKind, `${selectedViewMode.id}:${option ?? "default"}:${context.relativePath}`, `${context.fileBase} ${spec.viewKind}`); - } else { - appendLine(`Generated view was not found: ${htmlFile}`); - vscode.window.showWarningMessage(`OptiTrust command finished, but generated view was not found: ${path.basename(htmlFile)}`); - } + await executeViewRequest(workspace, { + mode, + option, + context: { + root: workspace.root, + relativePath: context.relativePath, + line: context.line, + fileDir: context.fileDir, + fileBase: context.fileBase + }, + viewMode: commandViewMode + }); } function viewArgs(mode: ViewMode, selectedViewMode: ViewModeDefinition, option?: ViewOption): string[] { @@ -144,14 +123,160 @@ export function runViewTraceSaveStepsScript(workspace: OptitrustWorkspace): Prom } export async function redoLastViewCommand(workspace: OptitrustWorkspace): Promise { + const request = lastViewRequest ?? (await readLastViewRequest(workspace)); + if (request) { + await executeViewRequest(workspace, request, "OptiTrust: Redo Last View Command"); + return; + } + const redoScript = path.join(workspace.root, "tools", "_last_view_result.sh"); try { await runCommand({ cwd: workspace.root, command: redoScript, - title: "OptiTrust: Redo Last View Command" + title: "OptiTrust: Redo Last View Command", + env: { + OPTITRUST_NO_BROWSER: "1" + } }); } catch { return; } + vscode.window.showWarningMessage("Redo finished, but no extension view context was available. Run View Step Diff or View Full Trace once from the extension."); +} + +async function executeViewRequest(workspace: OptitrustWorkspace, request: StoredViewRequest, titleOverride?: string): Promise { + const spec = VIEW_COMMANDS[request.mode]; + const args = [ + spec.scriptMode, + request.context.relativePath, + String(request.context.line), + ...viewArgs(request.mode, request.viewMode, request.option) + ]; + + try { + await runCommand({ + cwd: workspace.root, + command: path.join(workspace.root, "tools", "view_result.sh"), + args, + title: titleOverride ?? spec.title, + env: { + OPTITRUST_NO_BROWSER: "1" + } + }); + } catch { + return; + } + + lastViewRequest = request; + await openViewResult(request); +} + +async function openViewResult(request: StoredViewRequest): Promise { + const spec = VIEW_COMMANDS[request.mode]; + if (request.mode === "step_diff") { + await openNativeStepDiff( + { + root: request.context.root, + scriptRelativePath: request.context.relativePath, + line: request.context.line, + fileDir: request.context.fileDir, + fileBase: request.context.fileBase + }, + request.viewMode, + { markGenerated: true, useLiveView: true } + ); + return; + } + + const htmlFile = path.join(request.context.fileDir, `${request.context.fileBase}${spec.htmlSuffix}`); + if (await fileExists(htmlFile)) { + await openHtmlView( + request.context.root, + htmlFile, + spec.viewKind, + `${request.viewMode.id}:${request.option ?? "default"}:${request.context.relativePath}`, + `${request.context.fileBase} ${spec.viewKind}`, + { useLiveView: true } + ); + } else { + appendLine(`Generated view was not found: ${htmlFile}`); + vscode.window.showWarningMessage(`OptiTrust command finished, but generated view was not found: ${path.basename(htmlFile)}`); + } +} + +async function readLastViewRequest(workspace: OptitrustWorkspace): Promise { + const redoScript = path.join(workspace.root, "tools", "_last_view_result.sh"); + let content: string; + try { + content = await fs.readFile(redoScript, "utf8"); + } catch { + return undefined; + } + + const args = parseLastViewResultArgs(content); + if (args.length < 3) { + return undefined; + } + + const mode = modeFromScriptMode(args[0]); + const line = Number(args[2]); + if (!mode || !Number.isInteger(line)) { + return undefined; + } + + const filePath = path.resolve(workspace.root, args[1]); + return { + mode, + context: { + root: workspace.root, + relativePath: path.relative(workspace.root, filePath), + line, + fileDir: path.dirname(filePath), + fileBase: path.basename(filePath, path.extname(filePath)) + }, + viewMode: viewModeFromArgs(args.slice(3), mode) + }; +} + +function parseLastViewResultArgs(content: string): string[] { + const tokens = splitShellWords(content.trim()); + const scriptIndex = tokens.findIndex(token => token.endsWith("view_result.sh")); + return scriptIndex >= 0 ? tokens.slice(scriptIndex + 1) : []; +} + +function splitShellWords(text: string): string[] { + const words: string[] = []; + const pattern = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^']*)'|(\S+)/gu; + for (const match of text.matchAll(pattern)) { + words.push((match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["\\])/gu, "$1")); + } + return words; +} + +function modeFromScriptMode(scriptMode: string): ViewMode | undefined { + if (scriptMode === "step_diff" || scriptMode === "step_diff_from_inter") { + return "step_diff"; + } + if (scriptMode === "full_trace" || scriptMode === "standalone_full_trace" || scriptMode === "full_trace_from_inter") { + return "full_trace"; + } + if (scriptMode === "step_trace") { + return "step_trace"; + } + return undefined; +} + +function viewModeFromArgs(args: string[], mode: ViewMode): ViewModeDefinition { + if (args.includes("-print-optilambda-syntax")) { + const representation = optionValue(args, "-optilambda-repr") ?? "surface"; + return VIEW_MODES.find(viewMode => viewMode.optilambdaRepresentation === representation) ?? DEFAULT_STEP_DIFF_VIEW_MODE; + } + + return mode === "step_diff" ? CPP_VIEW_MODE : getSelectedViewMode(); +} + +function optionValue(args: string[], option: string): string | undefined { + const index = args.indexOf(option); + return index >= 0 ? args[index + 1] : undefined; } diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index d10f429ca..41634a0db 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -32,6 +32,7 @@ import { runViewTraceSaveStepsScript } from "./commands/viewCommands"; import { disposeDecorations, updateDecorations } from "./optitrust/decorations"; +import { detachLiveView, initializeLiveViewContext, refreshLiveViewContexts } from "./optitrust/liveView"; import { registerNativeDiffProvider, switchNativeDiffSyntax } from "./optitrust/nativeDiff"; import { appendLine, disposeOutput } from "./optitrust/output"; import { getSelectedViewMode, updateSelectedViewMode, VIEW_MODES } from "./optitrust/viewMode"; @@ -85,6 +86,7 @@ function registerCommand(context: vscode.ExtensionContext, command: string, call export async function activate(context: vscode.ExtensionContext): Promise { await refreshWorkspace(vscode.window.activeTextEditor?.document.uri.fsPath); optiNlpSession = new OptiNlpSessionMemory(); + initializeLiveViewContext(); registerNativeDiffProvider(context); registerOptiNlpChatParticipant(context, requireWorkspace, optiNlpSession); @@ -145,6 +147,14 @@ export async function activate(context: vscode.ExtensionContext): Promise await switchNativeDiffSyntax(); }); + registerCommand(context, "optitrust.detachView", () => { + if (detachLiveView()) { + vscode.window.showInformationMessage("OptiTrust view detached. The next view command will open a new live view."); + } else { + vscode.window.showInformationMessage("No live OptiTrust view is currently attached."); + } + }); + registerCommand(context, "optitrust.redoLastViewCommand", async () => { const workspace = await requireWorkspace(); if (workspace) { @@ -301,7 +311,11 @@ export async function activate(context: vscode.ExtensionContext): Promise }); context.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor(editor => updateDecorations(editor)), + vscode.window.onDidChangeActiveTextEditor(editor => { + updateDecorations(editor); + refreshLiveViewContexts(); + }), + vscode.window.tabGroups.onDidChangeTabs(() => refreshLiveViewContexts()), vscode.workspace.onDidChangeTextDocument(event => { if (event.document === vscode.window.activeTextEditor?.document) { updateDecorations(vscode.window.activeTextEditor); diff --git a/tools/vscode-optitrust/src/optitrust/fileSystem.ts b/tools/vscode-optitrust/src/optitrust/fileSystem.ts new file mode 100644 index 000000000..12de31a94 --- /dev/null +++ b/tools/vscode-optitrust/src/optitrust/fileSystem.ts @@ -0,0 +1,10 @@ +import * as fs from "fs/promises"; + +export async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} diff --git a/tools/vscode-optitrust/src/optitrust/liveView.ts b/tools/vscode-optitrust/src/optitrust/liveView.ts new file mode 100644 index 000000000..fb05a5cf6 --- /dev/null +++ b/tools/vscode-optitrust/src/optitrust/liveView.ts @@ -0,0 +1,117 @@ +import * as vscode from "vscode"; + +type LiveViewKind = "native-diff" | "html"; + +interface AttachedLiveView { + readonly kind: LiveViewKind; + readonly viewColumn: vscode.ViewColumn; + readonly getViewColumn?: () => vscode.ViewColumn | undefined; + readonly detach?: () => void; + readonly dispose?: () => Thenable | Promise | void; + readonly ownsUri?: (uri: vscode.Uri) => boolean; +} + +interface PrepareAttachedLiveViewOptions { + readonly replaceSameKind?: boolean; +} + +let attachedLiveView: AttachedLiveView | undefined; +let liveViewSlotId = 1; + +export function initializeLiveViewContext(): void { + setLiveViewContexts(false, false); +} + +export async function prepareAttachedLiveView(kind: LiveViewKind, options: PrepareAttachedLiveViewOptions = {}): Promise { + const viewColumn = currentAttachedViewColumn(); + if (attachedLiveView?.kind === kind && !options.replaceSameKind) { + return viewColumn; + } + + await closeAttachedLiveView(); + return viewColumn; +} + +export function currentAttachedViewColumn(fallback: vscode.ViewColumn = vscode.ViewColumn.Beside): vscode.ViewColumn { + return attachedLiveView?.getViewColumn?.() ?? attachedLiveView?.viewColumn ?? fallback; +} + +export function attachLiveView(view: AttachedLiveView): void { + attachedLiveView = view; + refreshLiveViewContexts(); +} + +export function detachLiveView(): boolean { + if (!attachedLiveView) { + return false; + } + attachedLiveView.detach?.(); + attachedLiveView = undefined; + liveViewSlotId += 1; + setLiveViewContexts(false, false); + return true; +} + +export function currentLiveViewSlotId(): number { + return liveViewSlotId; +} + +export function clearLiveView(view: AttachedLiveView): void { + if (attachedLiveView === view) { + attachedLiveView = undefined; + setLiveViewContexts(false, false); + } +} + +export function isAttachedLiveViewUri(uri: vscode.Uri): boolean { + return attachedLiveView?.ownsUri?.(uri) ?? false; +} + +export function isAttachedLiveView(view: AttachedLiveView): boolean { + return attachedLiveView === view; +} + +export function refreshLiveViewContexts(): void { + setLiveViewContexts(attachedLiveView !== undefined, activeEditorIsAttachedLiveView()); +} + +export function activeViewIsAttachedLiveView(): boolean { + return activeEditorIsAttachedLiveView(); +} + +export function setActiveLiveViewContext(active: boolean): void { + setLiveViewContexts(attachedLiveView !== undefined, active); +} + +async function closeAttachedLiveView(): Promise { + const view = attachedLiveView; + attachedLiveView = undefined; + setLiveViewContexts(false, false); + await view?.dispose?.(); +} + +function activeEditorIsAttachedLiveView(): boolean { + if (!attachedLiveView) { + return false; + } + + return activeEditorUris().some(uri => attachedLiveView?.ownsUri?.(uri)); +} + +function activeEditorUris(): vscode.Uri[] { + const input = vscode.window.tabGroups.activeTabGroup.activeTab?.input; + if (input instanceof vscode.TabInputTextDiff) { + return [input.original, input.modified]; + } + if (input instanceof vscode.TabInputText) { + return [input.uri]; + } + + const activeEditorUri = vscode.window.activeTextEditor?.document.uri; + return activeEditorUri ? [activeEditorUri] : []; +} + +function setLiveViewContexts(attached: boolean, active: boolean): void { + void vscode.commands.executeCommand("setContext", "optitrust.liveViewAttached", attached); + void vscode.commands.executeCommand("setContext", "optitrust.activeViewIsLiveView", active); +} diff --git a/tools/vscode-optitrust/src/optitrust/nativeDiff.ts b/tools/vscode-optitrust/src/optitrust/nativeDiff.ts index f3d78d32b..f17fc0a47 100644 --- a/tools/vscode-optitrust/src/optitrust/nativeDiff.ts +++ b/tools/vscode-optitrust/src/optitrust/nativeDiff.ts @@ -3,9 +3,12 @@ import * as path from "path"; import * as vscode from "vscode"; import { appendLine } from "./output"; import { runCommand } from "./runner"; +import { fileExists } from "./fileSystem"; +import { attachLiveView, currentLiveViewSlotId, isAttachedLiveViewUri, prepareAttachedLiveView } from "./liveView"; import { backendFlagsForViewMode, ViewModeDefinition, VIEW_MODES } from "./viewMode"; const OPTITRUST_DIFF_SCHEME = "optitrust-diff"; +const nativeDiffChanges = new vscode.EventEmitter(); interface DiffFilePair { readonly before: string; @@ -35,19 +38,11 @@ interface OpenNativeStepDiffOptions { readonly viewColumn?: vscode.ViewColumn; readonly markGenerated?: boolean; readonly generateIfMissing?: boolean; + readonly useLiveView?: boolean; } const sessions = new Map(); -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } -} - function stepDiffCandidates(fileDir: string, fileBase: string, selectedViewMode: ViewModeDefinition): DiffFilePair[] { if (selectedViewMode.id === "optilambda.surface") { return [ @@ -84,26 +79,22 @@ function stepDiffCandidates(fileDir: string, fileBase: string, selectedViewMode: async function findExistingPair(candidates: DiffFilePair[]): Promise { for (const candidate of candidates) { - if ((await exists(candidate.before)) && (await exists(candidate.after))) { + if ((await fileExists(candidate.before)) && (await fileExists(candidate.after))) { return candidate; } } return undefined; } -function sessionId(fileDir: string, fileBase: string): string { - return path.resolve(fileDir, fileBase); +function sessionId(fileDir: string, fileBase: string, liveSlotId?: number): string { + const baseId = path.resolve(fileDir, fileBase); + return liveSlotId === undefined ? baseId : `${baseId}::live-${liveSlotId}`; } function diffUri(filePath: string, session: NativeDiffSession): vscode.Uri { const query = new URLSearchParams({ file: filePath, - session: session.id, - root: session.root, - scriptRelativePath: session.scriptRelativePath, - line: String(session.line), - fileDir: session.fileDir, - fileBase: session.fileBase + session: session.id }); return vscode.Uri.from({ scheme: OPTITRUST_DIFF_SCHEME, @@ -155,6 +146,8 @@ function activeNativeDiffUri(): vscode.Uri | undefined { } class NativeDiffContentProvider implements vscode.TextDocumentContentProvider { + readonly onDidChange = nativeDiffChanges.event; + async provideTextDocumentContent(uri: vscode.Uri): Promise { return fs.readFile(filePathFromUri(uri), "utf8"); } @@ -194,7 +187,7 @@ export async function openNativeStepDiff( selectedViewMode: ViewModeDefinition, options: OpenNativeStepDiffOptions = {} ): Promise { - const id = sessionId(context.fileDir, context.fileBase); + const id = sessionId(context.fileDir, context.fileBase, options.useLiveView ? currentLiveViewSlotId() : undefined); const existingSession = sessions.get(id); const session: NativeDiffSession = existingSession?.scriptRelativePath === context.scriptRelativePath && existingSession.line === context.line @@ -221,13 +214,14 @@ export async function openNativeStepDiff( } } - await openExistingNativeStepDiff(session, selectedViewMode, options.viewColumn ?? vscode.ViewColumn.Beside); + await openExistingNativeStepDiff(session, selectedViewMode, options.viewColumn ?? vscode.ViewColumn.Beside, options.useLiveView ?? false); } async function openExistingNativeStepDiff( session: NativeDiffSession, selectedViewMode: ViewModeDefinition, - viewColumn: vscode.ViewColumn + viewColumn: vscode.ViewColumn, + useLiveView: boolean ): Promise { const candidates = stepDiffCandidates(session.fileDir, session.fileBase, selectedViewMode); const pair = await findExistingPair(candidates); @@ -240,12 +234,43 @@ async function openExistingNativeStepDiff( return; } + const beforeUri = diffUri(pair.before, session); + const afterUri = diffUri(pair.after, session); + const title = `OptiTrust Diff: ${session.fileBase} (${pair.label})`; + if (useLiveView && isAttachedLiveViewUri(beforeUri) && isAttachedLiveViewUri(afterUri)) { + const existingColumn = nativeDiffViewColumn(beforeUri, afterUri); + if (existingColumn !== undefined) { + nativeDiffChanges.fire(beforeUri); + nativeDiffChanges.fire(afterUri); + await vscode.commands.executeCommand( + "vscode.diff", + beforeUri, + afterUri, + title, + { preview: false, viewColumn: existingColumn } + ); + return; + } + } + + const targetColumn = useLiveView ? await prepareAttachedLiveView("native-diff", { replaceSameKind: true }) : viewColumn; + + if (useLiveView) { + attachLiveView({ + kind: "native-diff", + viewColumn: targetColumn, + getViewColumn: () => nativeDiffViewColumn(beforeUri, afterUri), + ownsUri: uri => uri.toString() === beforeUri.toString() || uri.toString() === afterUri.toString(), + dispose: () => closeNativeDiffTabs(beforeUri, afterUri) + }); + } + await vscode.commands.executeCommand( "vscode.diff", - diffUri(pair.before, session), - diffUri(pair.after, session), - `OptiTrust Diff: ${session.fileBase} (${pair.label})`, - { preview: false, viewColumn } + beforeUri, + afterUri, + title, + { preview: false, viewColumn: targetColumn } ); } @@ -280,6 +305,32 @@ export async function switchNativeDiffSyntax(): Promise { await openNativeStepDiff(session, picked.mode, { viewColumn: vscode.ViewColumn.Active, - generateIfMissing: true + generateIfMissing: true, + useLiveView: isAttachedLiveViewUri(activeUri) }); } + +function nativeDiffViewColumn(beforeUri: vscode.Uri, afterUri: vscode.Uri): vscode.ViewColumn | undefined { + for (const group of vscode.window.tabGroups.all) { + if (group.tabs.some(tab => isNativeDiffTab(tab, beforeUri, afterUri))) { + return group.viewColumn; + } + } + return undefined; +} + +async function closeNativeDiffTabs(beforeUri: vscode.Uri, afterUri: vscode.Uri): Promise { + const tabs = vscode.window.tabGroups.all.flatMap(group => group.tabs.filter(tab => isNativeDiffTab(tab, beforeUri, afterUri))); + if (tabs.length > 0) { + await vscode.window.tabGroups.close(tabs, true); + } +} + +function isNativeDiffTab(tab: vscode.Tab, beforeUri: vscode.Uri, afterUri: vscode.Uri): boolean { + const input = tab.input; + return ( + input instanceof vscode.TabInputTextDiff && + input.original.toString() === beforeUri.toString() && + input.modified.toString() === afterUri.toString() + ); +} diff --git a/tools/vscode-optitrust/src/optitrust/views.ts b/tools/vscode-optitrust/src/optitrust/views.ts index a31396e42..e303f52b0 100644 --- a/tools/vscode-optitrust/src/optitrust/views.ts +++ b/tools/vscode-optitrust/src/optitrust/views.ts @@ -1,9 +1,27 @@ import * as fs from "fs/promises"; import * as path from "path"; import * as vscode from "vscode"; +import { + attachLiveView, + clearLiveView, + detachLiveView, + isAttachedLiveView, + prepareAttachedLiveView, + setActiveLiveViewContext +} from "./liveView"; const panels = new Map(); const MAX_INLINE_ASSET_BYTES = 2 * 1024 * 1024; +const LIVE_VIEW_KEY = "optitrust-live-view"; +export const OPTITRUST_WEBVIEW_TYPE = "optitrustView"; + +interface OpenHtmlViewOptions { + readonly useLiveView?: boolean; +} + +interface HtmlTransformOptions { + readonly includeDetachButton?: boolean; +} function webviewKey(filePath: string, viewKind: string, metadata: string): string { return `${path.resolve(filePath)}::${viewKind}::${metadata}`; @@ -14,14 +32,15 @@ function webviewKey(filePath: string, viewKind: string, metadata: string): strin * webviews run with a stricter resource model, so local assets must be inlined * or rewritten before the HTML can be displayed reliably inside the editor. */ -async function htmlWithBase(webview: vscode.Webview, root: string, htmlFile: string): Promise { +async function htmlWithBase(webview: vscode.Webview, root: string, htmlFile: string, options: HtmlTransformOptions = {}): Promise { const html = await fs.readFile(htmlFile, "utf8"); const htmlDir = path.dirname(htmlFile); const inlined = await inlineLocalScriptsAndStyles(htmlDir, html); const rewritten = rewriteLocalResourceUris(webview, htmlDir, inlined); const withTraceServerBase = injectTraceServerBase(root, htmlFile, rewritten); const withHighlightingConfig = await injectSyntaxHighlightingConfig(withTraceServerBase); - return injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)); + const withDiffSupport = injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)); + return options.includeDetachButton ? injectDetachButton(withDiffSupport) : withDiffSupport; } function injectTraceServerBase(root: string, htmlFile: string, html: string): string { @@ -361,23 +380,124 @@ document.addEventListener('DOMContentLoaded', function () { return `${html}\n${fallbackScript}`; } -export async function openHtmlView(root: string, htmlFile: string, viewKind: string, metadata: string, title: string): Promise { - const key = webviewKey(htmlFile, viewKind, metadata); +function injectDetachButton(html: string): string { + if (html.includes('id="optitrustDetachViewButton"')) { + return html; + } + + const detachHtml = ` + + +`; + + if (html.includes("")) { + return html.replace("", `${detachHtml}\n`); + } + return `${html}\n${detachHtml}`; +} + +export async function openHtmlView( + root: string, + htmlFile: string, + viewKind: string, + metadata: string, + title: string, + options: OpenHtmlViewOptions = {} +): Promise { + const key = options.useLiveView ? LIVE_VIEW_KEY : webviewKey(htmlFile, viewKind, metadata); const existing = panels.get(key); if (existing) { - existing.reveal(existing.viewColumn, true); - existing.webview.html = await htmlWithBase(existing.webview, root, htmlFile); + existing.title = title; + if (!options.useLiveView) { + existing.reveal(existing.viewColumn, true); + } + existing.webview.html = await htmlWithBase(existing.webview, root, htmlFile, { includeDetachButton: options.useLiveView }); return; } - const panel = vscode.window.createWebviewPanel("optitrustView", title, vscode.ViewColumn.Beside, { + const viewColumn = options.useLiveView ? await prepareAttachedLiveView("html") : vscode.ViewColumn.Beside; + const panel = vscode.window.createWebviewPanel(OPTITRUST_WEBVIEW_TYPE, title, viewColumn, { enableScripts: true, localResourceRoots: [vscode.Uri.file(root), vscode.Uri.file(path.dirname(htmlFile))] }); - panel.onDidDispose(() => panels.delete(key)); - panel.webview.html = await htmlWithBase(panel.webview, root, htmlFile); + const liveView = options.useLiveView + ? { + kind: "html" as const, + viewColumn, + getViewColumn: () => panel.viewColumn, + detach: () => panels.delete(key), + dispose: () => panel.dispose() + } + : undefined; + + const messageSubscription = panel.webview.onDidReceiveMessage((message: unknown) => { + if (!isRecord(message) || message.type !== "optitrust.detachView") { + return; + } + if (liveView && isAttachedLiveView(liveView) && detachLiveView()) { + vscode.window.showInformationMessage("OptiTrust view detached. The next view command will open a new live view."); + } else { + vscode.window.showInformationMessage("This OptiTrust view is already detached."); + } + }); + + panel.onDidDispose(() => { + messageSubscription.dispose(); + panels.delete(key); + if (liveView) { + clearLiveView(liveView); + } + }); + panel.onDidChangeViewState(event => { + if (event.webviewPanel.active) { + setActiveLiveViewContext(liveView ? isAttachedLiveView(liveView) : false); + } + }); + panel.webview.html = await htmlWithBase(panel.webview, root, htmlFile, { includeDetachButton: options.useLiveView }); panels.set(key, panel); + if (liveView) { + attachLiveView(liveView); + } } export async function openFileOrHtml(root: string, filePath: string, title?: string): Promise { From dd956bdc27280fd8107a83294250b1ffdd2cc300 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Tue, 7 Jul 2026 08:59:51 -0400 Subject: [PATCH 31/47] improve OptiTrust associated-file discovery --- .../src/commands/associatedFiles.ts | 15 +++------------ tools/vscode-optitrust/src/optitrust/files.ts | 14 +++----------- tools/vscode-optitrust/src/optitrust/workspace.ts | 14 +++----------- 3 files changed, 9 insertions(+), 34 deletions(-) diff --git a/tools/vscode-optitrust/src/commands/associatedFiles.ts b/tools/vscode-optitrust/src/commands/associatedFiles.ts index 000f8442d..c5e0cbaad 100644 --- a/tools/vscode-optitrust/src/commands/associatedFiles.ts +++ b/tools/vscode-optitrust/src/commands/associatedFiles.ts @@ -1,8 +1,8 @@ import * as path from "path"; -import * as fs from "fs/promises"; import * as vscode from "vscode"; import { getActiveEditorContext } from "../optitrust/editor"; import { AssociatedFile, findAssociatedFiles, OPTITRUST_C_SOURCE_EXTENSIONS, outputPairs, pickAssociatedFile } from "../optitrust/files"; +import { fileExists } from "../optitrust/fileSystem"; import { openFileOrHtml } from "../optitrust/views"; import { OptitrustWorkspace } from "../optitrust/workspace"; @@ -20,15 +20,6 @@ function activePathOrThrow(): string { return editor.document.uri.fsPath; } -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } -} - async function openAssociated(workspace: OptitrustWorkspace, candidates: AssociatedFile[], message: string): Promise { const file = await pickAssociatedFile(candidates, message); if (!file) { @@ -183,13 +174,13 @@ export async function openUnitTestMlCppFiles(workspace: OptitrustWorkspace): Pro const mlFile = `${base}.ml`; const cppFile = `${base}.cpp`; - if (!(await exists(mlFile))) { + if (!(await fileExists(mlFile))) { vscode.window.showWarningMessage(`No unit test script found: ${path.basename(mlFile)}`); return; } await openFileOrHtml(workspace.root, mlFile, path.basename(mlFile)); - if (await exists(cppFile)) { + if (await fileExists(cppFile)) { await openFileOrHtml(workspace.root, cppFile, path.basename(cppFile)); } } diff --git a/tools/vscode-optitrust/src/optitrust/files.ts b/tools/vscode-optitrust/src/optitrust/files.ts index 9f0b980f7..a778cba0c 100644 --- a/tools/vscode-optitrust/src/optitrust/files.ts +++ b/tools/vscode-optitrust/src/optitrust/files.ts @@ -1,6 +1,7 @@ import * as fs from "fs/promises"; import * as path from "path"; import * as vscode from "vscode"; +import { fileExists } from "./fileSystem"; export interface AssociatedFile { readonly label: string; @@ -35,15 +36,6 @@ const OPTILAMBDA_REPRESENTATION_LABELS: Record typed: "Fully-Typed" }; -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } -} - export function baseNameForAssociatedFiles(filePath: string): { dir: string; base: string } { const parsed = path.parse(filePath); const base = normalizeAssociatedBase(parsed.name, parsed.ext); @@ -187,7 +179,7 @@ export async function outputPairs(filePath: string): Promise { out: path.join(dir, `${base}_out${ext}`), exp: path.join(dir, `${base}_exp${ext}`) }; - if ((await exists(pair.out)) && (await exists(pair.exp))) { + if ((await fileExists(pair.out)) && (await fileExists(pair.exp))) { pairs.push(pair); } } @@ -198,7 +190,7 @@ export async function outputPairs(filePath: string): Promise { out: path.join(dir, `${base}_out_${representation}.opti`), exp: path.join(dir, `${base}_exp_${representation}.opti`) }; - if ((await exists(pair.out)) && (await exists(pair.exp))) { + if ((await fileExists(pair.out)) && (await fileExists(pair.exp))) { pairs.push(pair); } } diff --git a/tools/vscode-optitrust/src/optitrust/workspace.ts b/tools/vscode-optitrust/src/optitrust/workspace.ts index af55b2a3c..fec2dfb26 100644 --- a/tools/vscode-optitrust/src/optitrust/workspace.ts +++ b/tools/vscode-optitrust/src/optitrust/workspace.ts @@ -1,6 +1,7 @@ import * as fs from "fs/promises"; import * as path from "path"; import * as vscode from "vscode"; +import { fileExists } from "./fileSystem"; export interface OptitrustWorkspace { readonly root: string; @@ -18,15 +19,6 @@ const REQUIRED_MARKERS = [ path.join("lib", "optitrust.ml") ]; -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } -} - async function readText(filePath: string): Promise { try { return await fs.readFile(filePath, "utf8"); @@ -50,7 +42,7 @@ function parentDirectories(start: string): string[] { async function isOptitrustRoot(candidate: string): Promise { const duneProject = path.join(candidate, "dune-project"); - if (!(await exists(duneProject))) { + if (!(await fileExists(duneProject))) { return { reason: `Missing ${duneProject}` }; } @@ -61,7 +53,7 @@ async function isOptitrustRoot(candidate: string): Promise { const missing: string[] = []; for (const marker of REQUIRED_MARKERS) { - if (!(await exists(path.join(candidate, marker)))) { + if (!(await fileExists(path.join(candidate, marker)))) { missing.push(marker); } } From fb9a171960ed20e81fddf40bc9f70410d0cf3912 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Tue, 7 Jul 2026 09:00:09 -0400 Subject: [PATCH 32/47] improve OptiNLP target-at-cursor support --- .../src/commands/optinlpTargetAtCursor.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts index 09b420656..2f6269bed 100644 --- a/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts +++ b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts @@ -7,6 +7,7 @@ import * as vscode from "vscode"; import { setPendingOptiNlpChatRequest } from "./optinlpChatContext"; import { getActiveEditorContext } from "../optitrust/editor"; import { markExecutedLine } from "../optitrust/decorations"; +import { fileExists } from "../optitrust/fileSystem"; import { runCommand } from "../optitrust/runner"; import { validateTransformationScript } from "../optitrust/scripts"; import { findAssociatedCSourceFile } from "../optitrust/files"; @@ -34,15 +35,6 @@ interface PrefixScript { readonly noOpLine: number; } -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } -} - async function readText(filePath: string): Promise { return fs.readFile(filePath, "utf8"); } @@ -60,7 +52,7 @@ async function findAfterOptiFile(scriptPath: string): Promise Date: Tue, 7 Jul 2026 09:00:22 -0400 Subject: [PATCH 33/47] update OptiTrust VS Code extension guide --- tools/vscode-optitrust/README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tools/vscode-optitrust/README.md b/tools/vscode-optitrust/README.md index 76b00a1f6..20a829d67 100644 --- a/tools/vscode-optitrust/README.md +++ b/tools/vscode-optitrust/README.md @@ -140,6 +140,16 @@ The trace opens in the standard OptiTrust trace viewer inside VS Code. The tree, step navigation, and controls are preserved. The representation selector switches the displayed code and diff content without replacing the trace UI. +### Live View Reuse And Detach + +Diff and trace commands share one attached OptiTrust view slot. Re-running +`F6`, `Shift+F5`, `Shift+F6`, or `F5` updates that attached slot instead of +leaving many old views open. + +Use `OptiTrust: Detach View` to freeze the current view. Detached views stay +open and are no longer updated by later diff/trace commands. Trace webviews also +show a `Detach` button in the panel itself. + ### View A Step Trace Run: @@ -207,10 +217,11 @@ The QuickPick menu can: | `OptiTrust: View Step Diff` | Shows the diff for the transformation at the cursor line. | | `OptiTrust: View Diff Only Code` | Shows a reduced code-only diff. | | `OptiTrust: View Diff Using Internal Syntax` | Shows the legacy internal syntax diff mode. | -| `OptiTrust: View Full Trace` | Generates and opens a full standalone trace. | +| `OptiTrust: View Full Trace` | Generates and opens a full trace in the attached OptiTrust view. | | `OptiTrust: View Trace Save Steps Script` | Generates a full trace with `-save-steps script`. | | `OptiTrust: View Step Trace` | Generates and opens a trace for the current step. | -| `OptiTrust: Redo Last View Command` | Runs `tools/_last_view_result.sh`. | +| `OptiTrust: Redo Last View Command` | Re-runs the last extension view command and reopens it in the attached OptiTrust view. | +| `OptiTrust: Detach View` | Keeps the current OptiTrust view open and removes it from future live updates. | | `OptiTrust: Run Current Test` | Runs the current OptiTrust test. | | `OptiTrust: Rerun Last-Tried Tests` | Re-runs the last test selection. | | `OptiTrust: Run Current Test And Open Diff` | Runs the current test, then opens the associated diff. | From fb6100a9213b0a86f064d311f69e3deed062410c Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Tue, 7 Jul 2026 10:27:32 -0400 Subject: [PATCH 34/47] Clean up Surface OptiLambda syntax --- lib/optilambda/optilambda_printer.ml | 178 ++++-- lib/optilambda/optilambda_syntax.md | 23 +- tests_infra/optilambda/printcpp_exp.opti | 658 ++++++++++------------- tests_infra/optilambda/printer_basic.ml | 57 +- tools/optiNLP/knowledge/optilambda.md | 5 +- 5 files changed, 514 insertions(+), 407 deletions(-) diff --git a/lib/optilambda/optilambda_printer.ml b/lib/optilambda/optilambda_printer.ml index ff38a20bc..2bfc0d02d 100644 --- a/lib/optilambda/optilambda_printer.ml +++ b/lib/optilambda/optilambda_printer.ml @@ -34,7 +34,7 @@ let block_doc docs = | _ -> surround 2 1 lbrace (semi_sep docs) rbrace type block_item = Regular of document | FinalExpr of document -type contract_clause = ContractClause of string * resource_item | ContractRaw of document +type contract_clause = ContractClause of string * var list * resource_item | ContractRaw of document type read_only_formula = { read_frac : trm; read_body : trm } @@ -100,6 +100,33 @@ let is_fully_typed_internal (style : Optilambda_style.style) : bool = let is_explicit_internal (style : Optilambda_style.style) : bool = is_internal style || is_fully_typed_internal style +let is_surface (style : Optilambda_style.style) : bool = + match style.representation with + | Surface -> true + | Internal + | FullyTypedInternal -> + false + +let is_generated_name (v : var) : bool = String.starts_with ~prefix:"#" (var_name v) + +let same_var_for_display (v1 : var) (v2 : var) : bool = + if (not (has_unset_id v1)) && not (has_unset_id v2) then v1.id = v2.id + else v1.id = v2.id && v1.name = v2.name && v1.namespaces = v2.namespaces + +let var_list_mem (v : var) (vars : var list) : bool = List.exists (same_var_for_display v) vars + +let formula_vars (formula : trm) : var list = + let vars = ref [] in + let add_var v = if not (var_list_mem v !vars) then vars := v :: !vars in + trm_iter_vars (fun () v -> add_var v) () formula; + !vars + +let resource_items_used_vars (items : resource_item list) : var list = + List.fold_left + (fun vars (_, formula) -> + List.fold_left (fun acc v -> if var_list_mem v acc then acc else v :: acc) vars (formula_vars formula)) + [] items + let is_typed_resource_constructor_name = function | "cell" | "Cell" @@ -135,6 +162,22 @@ let rec typ_to_doc (style : Optilambda_style.style) (ty : typ) : document = | Trm_var v -> var_to_doc style v | Trm_apps ({ desc = Trm_var v; _ }, args, [], []) -> begin match (v.name, args) with + | "pure_fun", [ { desc = Trm_fun (args, ret_ty, body, _); _ } ] when is_surface style -> + let arg_to_doc (_, ty) = + let doc = typ_to_doc style ty in + match ty.desc with + | Trm_apps ({ desc = Trm_var v; _ }, _, [], []) when v.name = "pure_fun" && v.namespaces = [] -> parens_doc doc + | _ -> doc + in + let args_doc = separate (blank 1 ^^ star ^^ blank 1) (List.map arg_to_doc args) in + let ret_doc = + if is_type_type ret_ty then + match type_result_body_to_doc style body with + | Some doc -> doc + | None -> typ_to_doc style ret_ty + else typ_to_doc style ret_ty + in + args_doc ^^ blank 1 ^^ string "->" ^^ blank 1 ^^ ret_doc | "ptr", [ inner ] -> string "ptr" ^^ parens_doc (typ_to_doc style inner) | "const", [ inner ] -> string "const" ^^ parens_doc (typ_to_doc style inner) | "array", [ inner ] -> string "array" ^^ parens_doc (typ_to_doc style inner) @@ -161,6 +204,10 @@ and elem_typ_of_access_result (ty : typ) : typ = and typed_var_to_doc (style : Optilambda_style.style) ((v, ty) : typed_var) : document = if style.print_types && not (is_auto_type ty) then var_to_doc style v ^^ colon ^^ blank 1 ^^ typ_to_doc style ty else var_to_doc style v +(** [surface_typed_var_to_doc style (v, ty)] hides type annotations in Surface snippets that are meant to stay C-like. *) +and surface_typed_var_to_doc (style : Optilambda_style.style) ((v, ty) : typed_var) : document = + if is_surface style then var_to_doc style v else typed_var_to_doc style (v, ty) + (** [lit_to_doc style lit] prints a literal value. *) and lit_to_doc (style : Optilambda_style.style) (lit : lit) : document = match lit with @@ -409,6 +456,42 @@ and uninit_formula_body (formula : trm) : trm option = | Trm_apps (f, [ body ], [], []) when var_has_name "Uninit" f || var_has_name "_Uninit" f -> Some body | _ -> None +(** [normalize_surface_formula_binders formula] renames generated binders locally to stable names such as [#_1]. *) +and normalize_surface_formula_binders (formula : trm) : trm = + let rec erase_hidden_fun_arg_types t = + match t.desc with + | Trm_fun (args, ret_ty, body, contract) -> + let ret_ty = erase_hidden_fun_arg_types ret_ty in + let body = erase_hidden_fun_arg_types body in + let args = List.map (fun (v, _) -> (v, typ_auto)) args in + trm_like ~old:t (trm_fun ~contract args ret_ty body) + | _ -> trm_map erase_hidden_fun_arg_types ~f_formula:erase_hidden_fun_arg_types t + in + let formula = erase_hidden_fun_arg_types formula in + let next_id = ref 0 in + let renamings : (var * var) list ref = ref [] in + let find_renaming v = + List.find_map (fun (old_v, new_v) -> if same_var_for_display old_v v then Some new_v else None) !renamings + in + let rename_generated_binder v = + if not (is_generated_name v) then v + else + match find_renaming v with + | Some v' -> v' + | None -> + incr next_id; + let v' = { v with name = "#_" ^ string_of_int !next_id } in + renamings := (v, v') :: !renamings; + v' + in + let map_var () v = + match find_renaming v with + | Some v' -> v' + | None -> v + in + let map_binder () v _ = ((), rename_generated_binder v) in + trm_rename_vars map_var ~map_binder () formula + (** [formula_to_doc_at style ctx_prec formula] prints logical/resource formulas in the surface style used by contracts. This intentionally mirrors the important cases of the C resource-formula printer without depending on [optitrust.framework]. *) @@ -474,7 +557,7 @@ and formula_to_doc_at (style : Optilambda_style.style) (ctx_prec : int) (formula | Trm_apps ({ desc = Trm_prim (_, Prim_unop Unop_neg); _ }, [ arg ], [], []) -> string "!" ^^ formula_to_doc_at style 8 arg | Trm_apps (f, args, [], []) -> formula_app_to_doc f args | Trm_fun (args, ret_ty, body, _) -> - let args_doc = string "fun" ^^ parens_doc (comma_sep (List.map (typed_var_to_doc style) args)) in + let args_doc = string "fun" ^^ parens_doc (comma_sep (List.map (surface_typed_var_to_doc style) args)) in if style.print_types && is_type_type ret_ty then match type_result_body_to_doc style body with | Some body_type_doc -> args_doc ^^ colon ^^ blank 1 ^^ body_type_doc @@ -490,18 +573,20 @@ and formula_to_doc_at (style : Optilambda_style.style) (ctx_prec : int) (formula (** [formula_to_doc style formula] prints a formula using the local surface formula printer when appropriate. *) and formula_to_doc (style : Optilambda_style.style) (formula : trm) : document = match style.representation with - | Surface -> formula_to_doc_at style 0 formula + | Surface -> formula_to_doc_at style 0 (normalize_surface_formula_binders formula) | Internal | FullyTypedInternal -> trm_to_doc_at style 0 formula (** [resource_item_to_doc style item] prints a named logical/resource formula. *) -and resource_item_to_doc (style : Optilambda_style.style) ((hyp, formula) : resource_item) : document = - var_to_doc style hyp ^^ colon ^^ blank 1 ^^ formula_to_doc style formula +and resource_item_to_doc (style : Optilambda_style.style) (used_vars : var list) ((hyp, formula) : resource_item) : document = + let formula_doc = formula_to_doc style formula in + if is_surface style && is_generated_name hyp && not (var_list_mem hyp used_vars) then formula_doc + else var_to_doc style hyp ^^ colon ^^ blank 1 ^^ formula_doc (** [contract_clauses keyword items] builds a group of contract clauses. *) -and contract_clauses (keyword : string) (items : resource_item list) : contract_clause list = - List.map (fun item -> ContractClause (keyword, item)) items +and contract_clauses ?(used_vars = []) (keyword : string) (items : resource_item list) : contract_clause list = + List.map (fun item -> ContractClause (keyword, used_vars, item)) items (** [simplify_linear_contract pre post] recovers user-facing [reads] and [writes] clauses from desugared linear resources. It only recovers preserved [_RO] resources and [Uninit] writes; transformations that reshape resources remain explicit. *) @@ -559,14 +644,32 @@ and simplify_linear_contract (pre : resource_item list) (post : resource_item li and remove_used_fraction_requirements (used_fracs : var list) (pure : resource_item list) : resource_item list = List.filter (fun (hyp, formula) -> not (List.exists (( = ) hyp) used_fracs && is_fraction_type_formula formula)) pure +(** [is_surface_type_only_formula formula] recognizes pure contract entries that only describe types. *) +and is_surface_type_only_formula (formula : trm) : bool = + match formula.desc with + | Trm_var v -> v.name = "Type" && v.namespaces = [] + | Trm_apps ({ desc = Trm_var v; _ }, _, [], []) -> + (v.name = "fun" || v.name = "pure_fun") && v.namespaces = [] + | _ -> false + +and filter_surface_type_only_requirements (style : Optilambda_style.style) (pure : resource_item list) : resource_item list = + if is_surface style then List.filter (fun (_, formula) -> not (is_surface_type_only_formula formula)) pure else pure + (** [contract_group_to_doc style keyword items] prints consecutive clauses sharing a keyword. *) -and contract_group_to_doc (style : Optilambda_style.style) (keyword : string) (items : resource_item list) : document = +and contract_group_to_doc (style : Optilambda_style.style) (keyword : string) (items : (var list * resource_item) list) : document = let align_doc = string (String.make (String.length keyword + 1) ' ') in match items with | [] -> empty | first :: rest -> - let first_doc = string keyword ^^ blank 1 ^^ resource_item_to_doc style first in - let rest_docs = List.map (fun item -> comma ^^ hardline ^^ align_doc ^^ resource_item_to_doc style item) rest in + let first_doc = + let used_vars, item = first in + string keyword ^^ blank 1 ^^ resource_item_to_doc style used_vars item + in + let rest_docs = + List.map + (fun (used_vars, item) -> comma ^^ hardline ^^ align_doc ^^ resource_item_to_doc style used_vars item) + rest + in concat (first_doc :: rest_docs) (** [contract_clauses_to_docs style clauses] merges clauses with the same keyword within each raw-clause-delimited group. *) @@ -591,8 +694,8 @@ and contract_clauses_to_docs (style : Optilambda_style.style) (clauses : contrac match clauses with | [] -> List.rev (flush_groups groups order acc) | ContractRaw doc :: rest -> aux [] [] (doc :: flush_groups groups order acc) rest - | ContractClause (keyword, item) :: rest -> - let groups, order = add_to_groups keyword item groups order in + | ContractClause (keyword, used_vars, item) :: rest -> + let groups, order = add_to_groups keyword (used_vars, item) groups order in aux groups order acc rest in aux [] [] [] clauses @@ -603,12 +706,14 @@ and fun_contract_clauses (style : Optilambda_style.style) (contract : fun_contra else let consumes, produces, reads, writes, used_fracs = simplify_linear_contract contract.pre.linear contract.post.linear in let pure = remove_used_fraction_requirements used_fracs contract.pre.pure in - contract_clauses "requires" pure - @ contract_clauses "reads" reads - @ contract_clauses "writes" writes - @ contract_clauses "consumes" consumes - @ contract_clauses "ensures" contract.post.pure - @ contract_clauses "produces" produces + let pure = filter_surface_type_only_requirements style pure in + let used_vars = resource_items_used_vars (pure @ reads @ writes @ consumes @ contract.post.pure @ produces) in + contract_clauses ~used_vars "requires" pure + @ contract_clauses ~used_vars "reads" reads + @ contract_clauses ~used_vars "writes" writes + @ contract_clauses ~used_vars "consumes" consumes + @ contract_clauses ~used_vars "ensures" contract.post.pure + @ contract_clauses ~used_vars "produces" produces (** [fun_spec_clause_docs style spec] prints clauses carried by a function spec. *) and fun_spec_clauses (style : Optilambda_style.style) (spec : fun_spec) : contract_clause list = @@ -622,15 +727,24 @@ and loop_contract_clauses (style : Optilambda_style.style) (contract : loop_cont if not style.print_contracts then [] else let strict_doc = if contract.strict then [ ContractRaw (string "strict") ] else [] in + let loop_ghosts = filter_surface_type_only_requirements style contract.loop_ghosts in + let invariant_pure = filter_surface_type_only_requirements style contract.invariant.pure in + let iter_pre_pure = filter_surface_type_only_requirements style contract.iter_contract.pre.pure in + let iter_post_pure = filter_surface_type_only_requirements style contract.iter_contract.post.pure in + let items = + loop_ghosts @ invariant_pure @ contract.invariant.linear @ contract.parallel_reads @ iter_pre_pure + @ contract.iter_contract.pre.linear @ iter_post_pure @ contract.iter_contract.post.linear + in + let used_vars = resource_items_used_vars items in strict_doc - @ contract_clauses "requires" contract.loop_ghosts - @ contract_clauses "requires" contract.invariant.pure - @ contract_clauses "preserves" contract.invariant.linear - @ contract_clauses "reads" contract.parallel_reads - @ contract_clauses "xrequires" contract.iter_contract.pre.pure - @ contract_clauses "xconsumes" contract.iter_contract.pre.linear - @ contract_clauses "xensures" contract.iter_contract.post.pure - @ contract_clauses "xproduces" contract.iter_contract.post.linear + @ contract_clauses ~used_vars "requires" loop_ghosts + @ contract_clauses ~used_vars "requires" invariant_pure + @ contract_clauses ~used_vars "preserves" contract.invariant.linear + @ contract_clauses ~used_vars "reads" contract.parallel_reads + @ contract_clauses ~used_vars "xrequires" iter_pre_pure + @ contract_clauses ~used_vars "xconsumes" contract.iter_contract.pre.linear + @ contract_clauses ~used_vars "xensures" iter_post_pure + @ contract_clauses ~used_vars "xproduces" contract.iter_contract.post.linear (** [fun_spec_items spec] collects resources mentioned by a function spec. *) and fun_spec_items (spec : fun_spec) : resource_item list = @@ -647,8 +761,11 @@ and loop_contract_items (contract : loop_contract) : resource_item list = (** [contract_summary_to_doc style items] prints the header contract hypothesis list. *) and contract_summary_to_doc (style : Optilambda_style.style) (items : resource_item list) : document = - if (not style.print_contracts) || items = [] then empty - else blank 1 ^^ brackets_doc (comma_sep (List.map (fun (hyp, _) -> var_to_doc style hyp) items)) + let visible_items = + if is_surface style then List.filter (fun (hyp, _) -> not (is_generated_name hyp)) items else items + in + if (not style.print_contracts) || visible_items = [] then empty + else blank 1 ^^ brackets_doc (comma_sep (List.map (fun (hyp, _) -> var_to_doc style hyp) visible_items)) (** [trm_to_block_doc_with_prefix style prefix t] prints [t] as a block after prefix lines. *) and trm_to_block_doc_with_prefix (style : Optilambda_style.style) (prefix_docs : document list) (t : trm) : document = @@ -687,10 +804,11 @@ and fun_def_to_doc (style : Optilambda_style.style) ?(type_params = []) (name : | [] -> empty | _ -> brackets_doc (comma_sep (List.map (var_to_doc style) type_params)) in - let args_doc = parens_doc (comma_sep (List.map (typed_var_to_doc style) args)) in + let args_doc = parens_doc (comma_sep (List.map (surface_typed_var_to_doc style) args)) in let is_ghost = is_ghost_ret_type ret_ty in let ret_doc = if is_ghost then empty + else if is_surface style then empty else if style.print_types && not (is_auto_type ret_ty) then colon ^^ blank 1 ^^ typ_to_doc style ret_ty else empty in @@ -698,7 +816,7 @@ and fun_def_to_doc (style : Optilambda_style.style) ?(type_params = []) (name : let contract_summary_doc = if is_ghost then empty else contract_summary_to_doc style (fun_spec_items spec) in let fun_prefix = if is_ghost then string "ghost fun" else string "fun" in let body_doc = - if style.print_types && is_type_type ret_ty then + if (not (is_surface style)) && style.print_types && is_type_type ret_ty then match type_result_body_to_doc style body with | Some body_type_doc -> colon ^^ blank 1 ^^ body_type_doc | None -> ret_doc ^^ contract_summary_doc ^^ blank 1 ^^ trm_to_block_doc_with_prefix style contract_docs body diff --git a/lib/optilambda/optilambda_syntax.md b/lib/optilambda/optilambda_syntax.md index 7a0b73957..b16bfaf57 100644 --- a/lib/optilambda/optilambda_syntax.md +++ b/lib/optilambda/optilambda_syntax.md @@ -108,7 +108,7 @@ v.x Function definitions: ```optilambda -fun f[A](x: A, y: B): A [h1, h2] { +fun f[A](x, y) [h1, h2] { requires h1: x = y; produces h2: y = x; @@ -116,6 +116,9 @@ fun f[A](x: A, y: B): A [h1, h2] { } ``` +Surface function headers omit argument and return types. Those details remain +available in the Internal and Fully-Typed representations. + Ghost functions hide the internal `__ghost_ret` return type in Surface syntax: ```optilambda @@ -132,6 +135,24 @@ Function calls with contract arguments and returned contract bindings: f(x1, y1)[h1 := g1, h2 := g2][z : h2] ``` +Surface contract clauses hide generated resource names when the name is only an +implementation detail: + +```optilambda +consumes for i in outer_range -> Group(big_range, items(i)); +``` + +User-provided hypothesis names stay visible. Generated names remain visible only +when another formula refers to them, for example a fraction name used by `_RO`. + +Type-only pure requirements such as `model: int * int -> f64` are omitted from +Surface contracts. Pure function types that still need to be displayed use +compact arrow notation, for example: + +```optilambda +int * int -> f64 +``` + Blocks: ```optilambda diff --git a/tests_infra/optilambda/printcpp_exp.opti b/tests_infra/optilambda/printcpp_exp.opti index 8e3191f20..061eff6c6 100644 --- a/tests_infra/optilambda/printcpp_exp.opti +++ b/tests_infra/optilambda/printcpp_exp.opti @@ -16,21 +16,21 @@ type vect3 = vect2; type int2 = array(int, 2); type intstar = ptr(int); - fun addr_array_cell(): unit { + fun addr_array_cell() { letmut p; letmut n = p[0]; }; - fun initlist(): unit { + fun initlist() { letmut v1 = {1, 2}; letmut v2 = {1, 2}; letmut p = {1, 2}; letmut n = (p)[0]; }; - fun f(n: int): int { + fun f(n) { let __res: int = n; __res }; - fun test_loop(): unit { + fun test_loop() { letmut a = 0; for i in 0..10 { __ignore(post_incr(a)); }; for i in range(10, 0, -1) { __ignore(post_decr(a)); }; @@ -42,19 +42,19 @@ }; let z: int = x + y; }; - fun stack_var(): unit { + fun stack_var() { letmut r = 3; r = r + 1 + 2; r += 2; __ignore(post_incr(r)); letmut s = f(r); }; - fun stack_array(): unit { + fun stack_array() { letmut t = array(5, 6); letmut a = t[0]; t[1] = a + 2; }; - fun stack_struct(): unit { + fun stack_struct() { letmut v = {5, 6}; letmut a = v.x; v.y = a + 2; @@ -63,37 +63,37 @@ letmut p1 = {v, v}; letmut p2 = {v, {7, 8}}; }; - fun references(): unit { + fun references() { letmut a = 3; let b: ptr(int) = a; b = b + 4; }; - fun constants(): unit { + fun constants() { let a: int = 3; let b: int = a + 3; letmut c = b + 4; let v: vect = {0, 1}; letmut d = v.x; }; - fun const_pointers(): unit { + fun const_pointers() { letmut a = 3; letmut b = a; let c: int = b + 4; }; - fun nonconst_pointers(): unit { + fun nonconst_pointers() { letmut a = 3; letmut b = a; b = b + 4; letmut c = 3; b = c; }; - fun main(): int {}; - fun h(x: int): int { + fun main() {}; + fun h(x) { letmut y = x + 1; let __res: int = y; __res }; - fun immutable_stack_ptr(): int { + fun immutable_stack_ptr() { letmut x = 3; letmut y = f(x); letmut p = x; @@ -104,7 +104,7 @@ let __res: int = p + q + r; __res }; - fun immutable_stack_array(): int { + fun immutable_stack_array() { letmut x = 3; letmut y = 4; letmut t = array(x, y); @@ -115,20 +115,20 @@ let __res: int = t[0]; __res }; - fun immutable_stack_var(): int { + fun immutable_stack_var() { let a: int = 4; let r: int = 3; let s: int = r + 1; r }; - fun mutable_stack_var(): int { + fun mutable_stack_var() { letmut r = 3; r = r + 1; __ignore(post_incr(r)); let __res: int = r; __res }; - fun mutable_stack_array(): int { + fun mutable_stack_array() { letmut x = 3; letmut y = 4; letmut w = array(x, y); @@ -137,18 +137,18 @@ let __res: int = w[0]; __res }; - fun access_encoding(): unit { + fun access_encoding() { let a: vect = {0, 1}; let b: vect = a; letmut c = a; let ax: int = a.x; let cy: int = c.y; }; - fun foo(v: vect): int { + fun foo(v) { let __res: int = v.x; __res }; - fun mutable_var_encoding(): int { + fun mutable_var_encoding() { let a: vect = {0, 1}; letmut ax = foo(a); letmut c = a; @@ -160,7 +160,7 @@ fst: vect; snd: vect }; - fun lvalue_encoding(): unit { + fun lvalue_encoding() { letmut p; (p).x = 2; (p).x = 3; @@ -170,26 +170,23 @@ letmut v; v = 4; }; - fun arrow(): unit { + fun arrow() { letmut v = {0, 1}; letmut p = v; (p).x = (p).y; (p).x = (p).y; }; { - fun __ghost_begin(#2: __ghost_fn, #3: __ghost_args, #4: __ghost_bind): __ghost_fn { - __admitted - }; - fun __ghost_end(#5: __ghost_fn): unit {}; - fun __with_reverse(g: __ghost_fn, g_rev: __ghost_fn): __ghost_fn { + fun __ghost_begin(#2, #3, #4) { __admitted }; + fun __ghost_end(#5) {}; + fun __with_reverse(g, g_rev) { let __res: __ghost_fn = g; __res }; - fun __reverts(#6: __ghost_fn): unit {}; - ghost fun __clear(#7: __ghost_args) {}; + fun __reverts(#6) {}; + ghost fun __clear(#7) {}; ghost fun assert_inhabited() { - requires T: Type, - x: T; + requires x: T; ensures x: T; }; ghost fun define() {}; @@ -207,45 +204,40 @@ ghost fun rewrite_prop() { requires from: int, to: int, - inside: pure_fun(fun(#71: int): Prop), by: __is_true(from = to), - #70: inside(from); + inside(from); ensures out: inside(to); __admitted(); }; ghost fun rewrite_linear() { requires from: int, to: int, - inside: pure_fun(fun(#74: int): HProp), by: __is_true(from = to); - consumes #73: inside(from); - produces #72: inside(to); + consumes inside(from); + produces inside(to); __admitted(); }; ghost fun rewrite_float_prop() { requires from: f32, to: f32, - inside: pure_fun(fun(#76: f32): Prop), by: __is_true(from = to), - #75: inside(from); + inside(from); ensures out: inside(to); __admitted(); }; ghost fun rewrite_float_linear() { requires from: f32, to: f32, - inside: pure_fun(fun(#79: f32): HProp), by: __is_true(from = to); - consumes #78: inside(from); - produces #77: inside(to); + consumes inside(from); + produces inside(to); __admitted(); }; ghost fun rewrite_float_linear_admitted() { requires from: f32, - to: f32, - inside: pure_fun(fun(#82: f32): HProp); - consumes #81: inside(from); - produces #80: inside(to); + to: f32; + consumes inside(from); + produces inside(to); __admitted(); }; ghost fun eq_refl_float() { @@ -273,131 +265,131 @@ ghost fun to_prove_hprop() { requires H1: HProp, H2: HProp; - consumes #84: H1; - produces #83: H2; + consumes H1; + produces H2; __admitted(); }; - ghost(assert_prop()[proof := admit(pure_fun(fun(n: int, d: int): __is_true(n - d + d = n)))][z_cancel_minus_plus : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(n: int, d: int): __is_true(n + d - d = n)))][z_cancel_plus_minus : proof]); - fun wrap_z_cancel_minus_plus(m: int): int [n, d, #86, #85] { + ghost(assert_prop()[proof := admit(pure_fun(fun(n, d): __is_true(n - d + d = n)))][z_cancel_minus_plus : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(n, d): __is_true(n + d - d = n)))][z_cancel_plus_minus : proof]); + fun wrap_z_cancel_minus_plus(m) [n, d] { requires n: int, d: int, - #86: __is_true(m = n - d + d); - ensures #85: __is_true(_Res = n); + __is_true(m = n - d + d); + ensures __is_true(_Res = n); __admitted(); return m; ghost(rewrite_linear()[inside := fun(v) -> __is_true(_Res = v), by := z_cancel_minus_plus]); }; - ghost(assert_prop()[proof := admit(pure_fun(fun(n: f32, d: f32): __is_true(n - d + d = n)))][r_cancel_minus_plus : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(n: f32, d: f32): __is_true(n + d - d = n)))][r_cancel_plus_minus : proof]); - fun MINDEX0(): int { + ghost(assert_prop()[proof := admit(pure_fun(fun(n, d): __is_true(n - d + d = n)))][r_cancel_minus_plus : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(n, d): __is_true(n + d - d = n)))][r_cancel_plus_minus : proof]); + fun MINDEX0() { let __res: int = 0; __res }; - fun MINDEX1(N1: int, i1: int): int { + fun MINDEX1(N1, i1) { let __res: int = i1; __res }; - fun MINDEX2(N1: int, N2: int, i1: int, i2: int): int { + fun MINDEX2(N1, N2, i1, i2) { let __res: int = i1 * N2 + i2; __res }; - fun MINDEX3(N1: int, N2: int, N3: int, i1: int, i2: int, i3: int): int { + fun MINDEX3(N1, N2, N3, i1, i2, i3) { let __res: int = i1 * N2 * N3 + i2 * N3 + i3; __res }; - fun MINDEX4(N1: int, N2: int, N3: int, N4: int, i1: int, i2: int, i3: int, i4: int): int { + fun MINDEX4(N1, N2, N3, N4, i1, i2, i3, i4) { let __res: int = i1 * N2 * N3 * N4 + i2 * N3 * N4 + i3 * N4 + i4; __res }; - fun MINDEX5(N1: int, N2: int, N3: int, N4: int, N5: int, i1: int, i2: int, i3: int, i4: int, i5: int): int { + fun MINDEX5(N1, N2, N3, N4, N5, i1, i2, i3, i4, i5) { let __res: int = i1 * N2 * N3 * N4 * N5 + i2 * N3 * N4 * N5 + i3 * N4 * N5 + i4 * N5 + i5; __res }; - fun DMINDEX0(): int { + fun DMINDEX0() { let __res: int = 0; __res }; - fun DMINDEX1(N1: int, i1: int): int { + fun DMINDEX1(N1, i1) { let __res: int = 0; __res }; - fun DMINDEX2(N1: int, N2: int, i1: int, i2: int): int { + fun DMINDEX2(N1, N2, i1, i2) { let __res: int = 0; __res }; - fun DMINDEX3(N1: int, N2: int, N3: int, i1: int, i2: int, i3: int): int { + fun DMINDEX3(N1, N2, N3, i1, i2, i3) { let __res: int = 0; __res }; - fun DMINDEX4(N1: int, N2: int, N3: int, N4: int, i1: int, i2: int, i3: int, i4: int): int { + fun DMINDEX4(N1, N2, N3, N4, i1, i2, i3, i4) { let __res: int = 0; __res }; - fun DMINDEX5(N1: int, N2: int, N3: int, N4: int, N5: int, i1: int, i2: int, i3: int, i4: int, i5: int): int { + fun DMINDEX5(N1, N2, N3, N4, N5, i1, i2, i3, i4, i5) { let __res: int = 0; __res }; - fun MSIZE0(): usize { + fun MSIZE0() { let __res: int = 1; __res }; - fun MSIZE1(N1: int): usize { + fun MSIZE1(N1) { let __res: usize = cast(N1); __res }; - fun MSIZE2(N1: int, N2: int): usize { + fun MSIZE2(N1, N2) { let __res: usize = cast(N1) * cast(N2); __res }; - fun MSIZE3(N1: int, N2: int, N3: int): usize { + fun MSIZE3(N1, N2, N3) { let __res: usize = cast(N1) * cast(N2) * cast(N3); __res }; - fun MSIZE4(N1: int, N2: int, N3: int, N4: int): usize { + fun MSIZE4(N1, N2, N3, N4) { let __res: usize = cast(N1) * cast(N2) * cast(N3) * cast(N4); __res }; - fun MSIZE5(N1: int, N2: int, N3: int, N4: int, N5: int): usize { + fun MSIZE5(N1, N2, N3, N4, N5) { let __res: usize = cast(N1) * cast(N2) * cast(N3) * cast(N4) * cast(N5); __res }; - fun exact_div(n: int, b: int): int { + fun exact_div(n, b) { __admitted(); let __res: int = n / b; __res }; - fun min(a: int, b: int): int { + fun min(a, b) { __admitted(); if (a < b) { a } else { b } }; - fun max(a: int, b: int): int { + fun max(a, b) { __admitted(); if (a > b) { a } else { b } }; - fun maxf(a: f32, b: f32): f32 { + fun maxf(a, b) { __admitted(); if (a > b) { a } else { b } }; - fun ANY(maxValue: int): int { + fun ANY(maxValue) { let __res: int = 0; __res }; - ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#355: HProp, #356: HProp): HProp))][Wand : x]); + ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#_1, #_2): HProp))][Wand : x]); ghost fun close_wand() { requires H1: HProp, H2: HProp; - consumes #89: Wand(H1, H2), - #88: H1; - produces #87: H2; + consumes Wand(H1, H2), + H1; + produces H2; __admitted(); }; ghost fun hide() { requires H: HProp; - consumes #92: H; + consumes H; ensures H2: HProp; - produces #91: Wand(H2, H), - #90: H2; + produces Wand(H2, H), + H2; __admitted(); }; ghost fun hide_rev() { @@ -408,27 +400,27 @@ requires H1: HProp, H2: HProp, H3: HProp; - consumes #95: Wand(H1, H2), - #94: Wand(H2, H3); - produces #93: Wand(H1, H3); + consumes Wand(H1, H2), + Wand(H2, H3); + produces Wand(H1, H3); __admitted(); }; ghost fun assert_hprop() { requires H: HProp; - consumes #97: H; - produces #96: H; + consumes H; + produces H; }; ghost fun forget_init() {}; - ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#357: int, #358: Range): Prop))][in_range : x]); - ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#359: Range, #360: Range): Prop))][is_subrange : x]); - ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#361: Range): int))][range_count : x]); + ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#_1, #_2): Prop))][in_range : x]); + ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#_1, #_2): Prop))][is_subrange : x]); + ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#_1): int))][range_count : x]); ghost fun in_range_extend() { requires x: int, r1: Range, r2: Range, - #100: in_range(x, r1), - #99: is_subrange(r1, r2); - ensures #98: in_range(x, r2); + in_range(x, r1), + is_subrange(r1, r2); + ensures in_range(x, r2); __admitted(); }; ghost fun in_range_shift() { @@ -437,8 +429,8 @@ a: int, b: int, s: int, - #102: in_range(x, range(a, b, s)); - ensures #101: in_range(x + k, range(a + k, b + k, s)); + in_range(x, range(a, b, s)); + ensures in_range(x + k, range(a + k, b + k, s)); __admitted(); }; ghost fun in_range_shift_extend() { @@ -448,9 +440,9 @@ a: int, b: int, s: int, - #105: in_range(x, range(a, b, s)), - #104: is_subrange(range(a + k, b + k, s), r); - ensures #103: in_range(x + k, r); + in_range(x, range(a, b, s)), + is_subrange(range(a + k, b + k, s), r); + ensures in_range(x + k, r); __admitted(); ghost(in_range_shift()[x := x, k := k, a := a, b := b, s := s]); ghost(in_range_extend()[x := x + k, r1 := range(a + k, b + k, s), r2 := r]); @@ -460,8 +452,8 @@ a: int, b: int, s: int, - #107: in_range(x, range(a, b, s)), - #106: __is_true(s >= 0); + in_range(x, range(a, b, s)), + __is_true(s >= 0); ensures lower_bound: __is_true(x >= a), upper_bound: __is_true(x < b); __admitted(); @@ -471,8 +463,8 @@ a: int, b: int, s: int, - #109: in_range(x, range(a, b, s)), - #108: __is_true(s < 0); + in_range(x, range(a, b, s)), + __is_true(s < 0); ensures lower_bound: __is_true(x > b), upper_bound: __is_true(x <= a); __admitted(); @@ -493,75 +485,75 @@ s: int, lower: __is_true(s >= 0), up_ineq: __is_true(b <= c); - ensures #110: is_subrange(range(a, b, s), range(a, c, s)); + ensures is_subrange(range(a, b, s), range(a, c, s)); __admitted(); }; ghost fun ro_split2() { requires f: _Fraction, H: HProp; - consumes #113: _RO(f, H); - produces #112: _RO(f / 2, H), - #111: _RO(f / 2, H); + consumes _RO(f, H); + produces _RO(f / 2, H), + _RO(f / 2, H); __admitted(); }; ghost fun ro_split3() { requires f: _Fraction, H: HProp; - consumes #117: _RO(f, H); - produces #116: _RO(f / 3, H), - #115: _RO(f / 3, H), - #114: _RO(f / 3, H); + consumes _RO(f, H); + produces _RO(f / 3, H), + _RO(f / 3, H), + _RO(f / 3, H); __admitted(); }; ghost fun ro_split4() { requires f: _Fraction, H: HProp; - consumes #122: _RO(f, H); - produces #121: _RO(f / 4, H), - #120: _RO(f / 4, H), - #119: _RO(f / 4, H), - #118: _RO(f / 4, H); + consumes _RO(f, H); + produces _RO(f / 4, H), + _RO(f / 4, H), + _RO(f / 4, H), + _RO(f / 4, H); __admitted(); }; ghost fun ro_allow_join2() { requires f: _Fraction, H: HProp; - consumes #124: _RO(f / 2, H); - produces #123: _RO(f - f / 2, H); + consumes _RO(f / 2, H); + produces _RO(f - f / 2, H); __admitted(); }; ghost fun ro_allow_join3() { requires f: _Fraction, H: HProp; - consumes #126: _RO(f / 3, H); - produces #125: _RO(f - f / 3 - f / 3, H); + consumes _RO(f / 3, H); + produces _RO(f - f / 3 - f / 3, H); __admitted(); }; ghost fun ro_allow_join4() { requires f: _Fraction, H: HProp; - consumes #128: _RO(f / 4, H); - produces #127: _RO(f - f / 4 - f / 4 - f / 4, H); + consumes _RO(f / 4, H); + produces _RO(f - f / 4 - f / 4 - f / 4, H); __admitted(); }; ghost fun ro_fork_group() { requires f: _Fraction, H: HProp, r: Range; - consumes #131: _RO(f, H); - produces #130: _RO(f / range_count(r), for #129 in r -> H); + consumes _RO(f, H); + produces _RO(f / range_count(r), for #_1 in r -> H); __admitted(); + }; ghost fun ro_join_group() { reverts ro_fork_group; __admitted(); }; ghost fun swap_groups() { - requires items: pure_fun(fun(#134: int, #135: int): HProp), - inner_range: Range, + requires inner_range: Range, outer_range: Range; - consumes #133: for i in outer_range -> for j in inner_range -> items(i, j); - produces #132: for j in inner_range -> for i in outer_range -> items(i, j); + consumes for i in outer_range -> for j in inner_range -> items(i, j); + produces for j in inner_range -> for i in outer_range -> items(i, j); __admitted(); }; ghost fun swap_groups_rev() { @@ -569,12 +561,11 @@ __admitted(); }; ghost fun ro_swap_groups() { - requires items: pure_fun(fun(#138: int, #139: int): HProp), - inner_range: Range, + requires inner_range: Range, outer_range: Range, f: _Fraction; - consumes #137: _RO(f, for i in outer_range -> for j in inner_range -> items(i, j)); - produces #136: _RO(f, for j in inner_range -> for i in outer_range -> items(i, j)); + consumes _RO(f, for i in outer_range -> for j in inner_range -> items(i, j)); + produces _RO(f, for j in inner_range -> for i in outer_range -> items(i, j)); __admitted(); }; ghost fun ro_swap_groups_rev() { @@ -588,26 +579,25 @@ tile_size: int, size: int, div_check: __is_true(size = tile_count * tile_size), - #142: in_range(tile_index, 0..tile_count), - #141: in_range(index, 0..tile_size); - ensures #140: in_range(tile_index * tile_size + index, 0..size); + in_range(tile_index, 0..tile_count), + in_range(index, 0..tile_size); + ensures in_range(tile_index * tile_size + index, 0..size); __admitted(); }; - ghost(assert_prop()[proof := admit(pure_fun(fun(n: int): __is_true(n = n)))][eq_refl : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(m: int, n: int, eq: __is_true(m = n)): __is_true(n = m)))][eq_sym : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(n: int): __is_true(0 = 0 * n)))][zero_mul_intro : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(n: int): __is_true(n = n + 0)))][plus_zero_intro : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(m: int, n: int, p: int): __is_true(m + n + p = m + (n + p))))][add_assoc_right : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(m: int, n: int): __is_true(m * n + n = (m + 1) * n)))][mul_add_factor : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(n): __is_true(n = n)))][eq_refl : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(m, n, eq): __is_true(n = m)))][eq_sym : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(n): __is_true(0 = 0 * n)))][zero_mul_intro : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(n): __is_true(n = n + 0)))][plus_zero_intro : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(m, n, p): __is_true(m + n + p = m + (n + p))))][add_assoc_right : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(m, n): __is_true(m * n + n = (m + 1) * n)))][mul_add_factor : proof]); ghost fun tile_divides() { requires tile_count: int, tile_size: int, size: int, - items: pure_fun(fun(#145: int): HProp), div_check: __is_true(size = tile_count * tile_size), positive_tile_size: __is_true(tile_size >= 0); - consumes #144: Group(0..size, items); - produces #143: for bi in 0..tile_count -> for i in 0..tile_size -> items(bi * tile_size + i); + consumes Group(0..size, items); + produces for bi in 0..tile_count -> for i in 0..tile_size -> items(bi * tile_size + i); __admitted(); }; ghost fun untile_divides() { @@ -618,12 +608,11 @@ requires tile_count: int, tile_size: int, size: int, - items: pure_fun(fun(#148: int): HProp), div_check: __is_true(size = tile_count * tile_size), positive_tile_size: __is_true(tile_size >= 0), f: _Fraction; - consumes #147: _RO(f, Group(0..size, items)); - produces #146: _RO(f, for bi in 0..tile_count -> for i in 0..tile_size -> items(bi * tile_size + i)); + consumes _RO(f, Group(0..size, items)); + produces _RO(f, for bi in 0..tile_count -> for i in 0..tile_size -> items(bi * tile_size + i)); __admitted(); }; ghost fun ro_untile_divides() { @@ -632,10 +621,9 @@ }; ghost fun group_collapse() { requires n: int, - m: int, - items: pure_fun(fun(#151: int, #152: int): HProp); - consumes #150: for i in 0..n -> for j in 0..m -> items(i, j); - produces #149: for ij in 0..(n * m) -> items(ij / m, ij % m); + m: int; + consumes for i in 0..n -> for j in 0..m -> items(i, j); + produces for ij in 0..(n * m) -> items(ij / m, ij % m); __admitted(); }; ghost fun group_uncollapse() { @@ -645,10 +633,9 @@ ghost fun ro_group_collapse() { requires n: int, m: int, - items: pure_fun(fun(#155: int, #156: int): HProp), f: _Fraction; - consumes #154: _RO(f, for i in 0..n -> for j in 0..m -> items(i, j)); - produces #153: _RO(f, for ij in 0..(n * m) -> items(ij / m, ij % m)); + consumes _RO(f, for i in 0..n -> for j in 0..m -> items(i, j)); + produces _RO(f, for ij in 0..(n * m) -> items(ij / m, ij % m)); __admitted(); }; ghost fun ro_group_uncollapse() { @@ -658,11 +645,10 @@ ghost fun group_focus() { requires i: int, range: Range, - items: pure_fun(fun(#160: int): HProp), bound_check: in_range(i, range); - consumes #159: Group(range, items); - produces #158: Wand(items(i), Group(range, items)), - #157: items(i); + consumes Group(range, items); + produces Wand(items(i), Group(range, items)), + items(i); __admitted(); }; ghost fun group_unfocus() { @@ -673,12 +659,11 @@ ghost fun ro_group_focus() { requires i: int, range: Range, - items: pure_fun(fun(#164: int): HProp), f: _Fraction, bound_check: in_range(i, range); - consumes #163: _RO(f, Group(range, items)); - produces #162: Wand(_RO(f, items(i)), _RO(f, Group(range, items))), - #161: _RO(f, items(i)); + consumes _RO(f, Group(range, items)); + produces Wand(_RO(f, items(i)), _RO(f, Group(range, items))), + _RO(f, items(i)); __admitted(); }; ghost fun ro_group_unfocus() { @@ -690,12 +675,11 @@ requires i: int, r: Range, r2: Range, - items: pure_fun(fun(#168: int, #169: int): HProp), f: _Fraction, bound_check: in_range(i, r); - consumes #167: _RO(f, for i2 in r2 -> for i in r -> items(i2, i)); - produces #166: Wand(_RO(f, for i2 in r2 -> items(i2, i)), _RO(f, for i2 in r2 -> for i in r -> items(i2, i))), - #165: _RO(f, for i2 in r2 -> items(i2, i)); + consumes _RO(f, for i2 in r2 -> for i in r -> items(i2, i)); + produces Wand(_RO(f, for i2 in r2 -> items(i2, i)), _RO(f, for i2 in r2 -> for i in r -> items(i2, i))), + _RO(f, for i2 in r2 -> items(i2, i)); __admitted(); }; ghost fun ro_group2_unfocus() { @@ -705,11 +689,10 @@ ghost fun group_focus_subrange() { requires sub_range: Range, big_range: Range, - items: pure_fun(fun(#173: int): HProp), bound_check: is_subrange(sub_range, big_range); - consumes #172: Group(big_range, items); - produces #171: Wand(Group(sub_range, items), Group(big_range, items)), - #170: Group(sub_range, items); + consumes Group(big_range, items); + produces Wand(Group(sub_range, items), Group(big_range, items)), + Group(sub_range, items); __admitted(); }; ghost fun group_unfocus_subrange() { @@ -719,12 +702,11 @@ ghost fun ro_group_focus_subrange() { requires sub_range: Range, big_range: Range, - items: pure_fun(fun(#177: int): HProp), f: _Fraction, bound_check: is_subrange(sub_range, big_range); - consumes #176: _RO(f, Group(big_range, items)); - produces #175: Wand(_RO(f, Group(sub_range, items)), _RO(f, Group(big_range, items))), - #174: _RO(f, Group(sub_range, items)); + consumes _RO(f, Group(big_range, items)); + produces Wand(_RO(f, Group(sub_range, items)), _RO(f, Group(big_range, items))), + _RO(f, Group(sub_range, items)); __admitted(); }; ghost fun ro_group_unfocus_subrange() { @@ -735,11 +717,10 @@ requires outer_range: Range, sub_range: Range, big_range: Range, - items: pure_fun(fun(#182: int): pure_fun(fun(#181: int): HProp)), bound_check: is_subrange(sub_range, big_range); - consumes #180: for i in outer_range -> Group(big_range, items(i)); - produces #179: Wand(for i in outer_range -> Group(sub_range, items(i)), for i in outer_range -> Group(big_range, items(i))), - #178: for i in outer_range -> Group(sub_range, items(i)); + consumes for i in outer_range -> Group(big_range, items(i)); + produces Wand(for i in outer_range -> Group(sub_range, items(i)), for i in outer_range -> Group(big_range, items(i))), + for i in outer_range -> Group(sub_range, items(i)); __admitted(); }; ghost fun group2_unfocus_subrange() { @@ -750,14 +731,13 @@ requires start: int, stop: int, step: int, - items: pure_fun(fun(#185: int): HProp), shift: int, new_start: int, new_stop: int, check_start: __is_true(new_start = start + shift), check_stop: __is_true(new_stop = stop + shift); - consumes #184: for i in range(start, stop, step) -> items(i); - produces #183: for i in range(new_start, new_stop, step) -> items(i - shift); + consumes for i in range(start, stop, step) -> items(i); + produces for i in range(new_start, new_stop, step) -> items(i - shift); __admitted(); }; ghost fun group_unshift() { @@ -768,15 +748,14 @@ requires start: int, stop: int, step: int, - items: pure_fun(fun(#188: int): HProp), shift: int, new_start: int, new_stop: int, check_start: __is_true(new_start = start + shift), check_stop: __is_true(new_stop = stop + shift), f: _Fraction; - consumes #187: _RO(f, for i in range(start, stop, step) -> items(i)); - produces #186: _RO(f, for i in range(new_start, new_stop, step) -> items(i - shift)); + consumes _RO(f, for i in range(start, stop, step) -> items(i)); + produces _RO(f, for i in range(new_start, new_stop, step) -> items(i - shift)); __admitted(); }; ghost fun ro_group_unshift() { @@ -786,15 +765,14 @@ ghost fun group_scale() { requires stop: int, step: int, - items: pure_fun(fun(#191: int): HProp), factor: int, new_step: int, new_stop: int, check_stop: __is_true(new_stop = factor * stop), check_step: __is_true(new_step = factor * step), check_factor: __is_true(factor <> 0); - consumes #190: for i in range(0, stop, step) -> items(i); - produces #189: for i in range(0, new_stop, new_step) -> items(i / factor); + consumes for i in range(0, stop, step) -> items(i); + produces for i in range(0, new_stop, new_step) -> items(i / factor); __admitted(); }; ghost fun group_unscale() { @@ -804,15 +782,14 @@ ghost fun ro_group_scale() { requires stop: int, step: int, - items: pure_fun(fun(#194: int): HProp), factor: int, new_step: int, new_stop: int, check_stop: __is_true(new_stop = factor * stop), check_step: __is_true(new_step = factor * step), f: _Fraction; - consumes #193: _RO(f, for i in range(0, stop, step) -> items(i)); - produces #192: _RO(f, for i in range(0, new_stop, new_step) -> items(i / factor)); + consumes _RO(f, for i in range(0, stop, step) -> items(i)); + produces _RO(f, for i in range(0, new_stop, new_step) -> items(i / factor)); __admitted(); }; ghost fun ro_group_unscale() { @@ -824,11 +801,10 @@ stop: int, step: int, split: int, - items: pure_fun(fun(#198: int): HProp), range_check: is_subrange(range(start, split, step), range(start, stop, step)); - consumes #197: for i in range(start, stop, step) -> items(i); - produces #196: for i in range(start, split, step) -> items(i), - #195: for i in range(split, stop, step) -> items(i); + consumes for i in range(start, stop, step) -> items(i); + produces for i in range(start, split, step) -> items(i), + for i in range(split, stop, step) -> items(i); __admitted(); }; ghost fun group_join() { @@ -840,12 +816,11 @@ stop: int, step: int, split: int, - items: pure_fun(fun(#202: int): HProp), bound_check: in_range(split, range(start, stop, step)), f: _Fraction; - consumes #201: _RO(f, for i in range(start, stop, step) -> items(i)); - produces #200: _RO(f, for i in range(start, split, step) -> items(i)), - #199: _RO(f, for i in range(split, stop, step) -> items(i)); + consumes _RO(f, for i in range(start, stop, step) -> items(i)); + produces _RO(f, for i in range(start, split, step) -> items(i)), + _RO(f, for i in range(split, stop, step) -> items(i)); __admitted(); }; ghost fun ro_group_join() { @@ -857,11 +832,9 @@ stop: int, step: int, split: int, - items: pure_fun(fun(#209: int): Prop), - bound_check: in_range(split, range(start, stop, step)), - #208: pure_fun(fun(i: int, #207: in_range(i, range(start, stop, step))): items(i)); - ensures #206: pure_fun(fun(i: int, #205: in_range(i, range(start, split, step))): items(i)), - #204: pure_fun(fun(i: int, #203: in_range(i, range(split, stop, step))): items(i)); + bound_check: in_range(split, range(start, stop, step)); + ensures pure_fun(fun(i, #_1): items(i)), + pure_fun(fun(i, #_1): items(i)); __admitted(); }; ghost fun pure_group_join() { @@ -869,14 +842,12 @@ __admitted(); }; ghost fun group_intro_zero() { - requires items: pure_fun(fun(#211: int): HProp); - produces #210: for i in 0..0 -> items(i); + produces for i in 0..0 -> items(i); __admitted(); }; ghost fun group_intro_empty() { - requires N: int, - items: pure_fun(fun(#213: int): HProp); - produces #212: for i in N..N -> items(i); + requires N: int; + produces for i in N..N -> items(i); __admitted(); }; ghost fun group_elim_zero() { @@ -889,8 +860,8 @@ }; ghost fun group_intro_one() { requires item: HProp; - consumes #215: item; - produces #214: for i in 0..1 -> item; + consumes item; + produces for i in 0..1 -> item; __admitted(); }; ghost fun group_elim_one() { @@ -898,11 +869,10 @@ __admitted(); }; ghost fun dmindex2_untile() { - requires H: pure_fun(fun(#220: pure_fun(fun(#218: int, #219: int): int)): HProp), - n1: int, + requires n1: int, n2: int; - consumes #217: H(fun(i1, i2) -> DMINDEX1(n1 * n2, i1 * n2 + i2)); - produces #216: H(fun(i1, i2) -> DMINDEX2(n1, n2, i1, i2)); + consumes H(fun(i1, i2) -> DMINDEX1(n1 * n2, i1 * n2 + i2)); + produces H(fun(i1, i2) -> DMINDEX2(n1, n2, i1, i2)); __admitted(); }; ghost fun dmindex2_tile() { @@ -910,12 +880,11 @@ __admitted(); }; ghost fun dmindex3_untile() { - requires H: pure_fun(fun(#226: pure_fun(fun(#223: int, #224: int, #225: int): int)): HProp), - n1: int, + requires n1: int, n2: int, n3: int; - consumes #222: H(fun(i1, i2, i3) -> DMINDEX2(n1 * n2, n3, i1 * n2 + i2, i3)); - produces #221: H(fun(i1, i2, i3) -> DMINDEX3(n1, n2, n3, i1, i2, i3)); + consumes H(fun(i1, i2, i3) -> DMINDEX2(n1 * n2, n3, i1 * n2 + i2, i3)); + produces H(fun(i1, i2, i3) -> DMINDEX3(n1, n2, n3, i1, i2, i3)); __admitted(); }; ghost fun dmindex3_tile() { @@ -923,13 +892,11 @@ __admitted(); }; ghost fun mindex2_unfold() { - requires T: Type, - H: pure_fun(fun(#231: pure_fun(fun(#229: int, #230: int): ptr(T))): HProp), - matrix: ptr(T), + requires matrix: ptr(T), n1: int, n2: int; - consumes #228: H(fun(i1, i2) -> matrix[MINDEX2(n1, n2, i1, i2)]); - produces #227: H(fun(i1, i2) -> matrix[i1 * n2][MINDEX1(n2, i2)]); + consumes H(fun(i1, i2) -> matrix[MINDEX2(n1, n2, i1, i2)]); + produces H(fun(i1, i2) -> matrix[i1 * n2][MINDEX1(n2, i2)]); __admitted(); }; ghost fun mindex2_fold() { @@ -937,14 +904,12 @@ __admitted(); }; ghost fun mindex3_unfold() { - requires T: Type, - H: pure_fun(fun(#237: pure_fun(fun(#234: int, #235: int, #236: int): ptr(T))): HProp), - matrix: ptr(T), + requires matrix: ptr(T), n1: int, n2: int, n3: int; - consumes #233: H(fun(i1, i2, i3) -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)]); - produces #232: H(fun(i1, i2, i3) -> matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)]); + consumes H(fun(i1, i2, i3) -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)]); + produces H(fun(i1, i2, i3) -> matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)]); __admitted(); }; ghost fun mindex3_fold() { @@ -952,14 +917,12 @@ __admitted(); }; ghost fun ro_mindex2_unfold() { - requires T: Type, - H: pure_fun(fun(#242: pure_fun(fun(#240: int, #241: int): ptr(T))): HProp), - matrix: ptr(T), + requires matrix: ptr(T), n1: int, n2: int, f: _Fraction; - consumes #239: _RO(f, H(fun(i1, i2) -> matrix[MINDEX2(n1, n2, i1, i2)])); - produces #238: _RO(f, H(fun(i1, i2) -> matrix[i1 * n2][MINDEX1(n2, i2)])); + consumes _RO(f, H(fun(i1, i2) -> matrix[MINDEX2(n1, n2, i1, i2)])); + produces _RO(f, H(fun(i1, i2) -> matrix[i1 * n2][MINDEX1(n2, i2)])); __admitted(); }; ghost fun ro_mindex2_fold() { @@ -967,61 +930,58 @@ __admitted(); }; ghost fun ro_mindex3_unfold() { - requires T: Type, - H: pure_fun(fun(#248: pure_fun(fun(#245: int, #246: int, #247: int): ptr(T))): HProp), - matrix: ptr(T), + requires matrix: ptr(T), n1: int, n2: int, n3: int, f: _Fraction; - consumes #244: _RO(f, H(fun(i1, i2, i3) -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)])); - produces #243: _RO(f, H(fun(i1, i2, i3) -> matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)])); + consumes _RO(f, H(fun(i1, i2, i3) -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)])); + produces _RO(f, H(fun(i1, i2, i3) -> matrix[i1 * n2 * n3][MINDEX2(n2, n3, i2, i3)])); __admitted(); }; ghost fun ro_mindex3_fold() { reverts ro_mindex3_unfold; __admitted(); }; - ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#362: Prop, #363: HProp): HProp))][If : x]); + ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#_1, #_2): HProp))][If : x]); ghost fun if_false_hprop_rewrite() { requires b: bool, H: HProp, H2: HProp, HP: __is_false(b); - consumes #250: If(__is_true(b), H); - produces #249: If(__is_true(b), H2); + consumes If(__is_true(b), H); + produces If(__is_true(b), H2); __admitted(); }; ghost fun if_false_hprop_drop() { requires b: bool, H: HProp, HP: __is_false(b); - consumes #251: If(__is_true(b), H); + consumes If(__is_true(b), H); __admitted(); }; ghost fun if_true_hprop_elim() { requires b: bool, H: HProp, HP: __is_true(b); - consumes #253: If(__is_true(b), H); - produces #252: H; + consumes If(__is_true(b), H); + produces H; __admitted(); }; ghost fun if_true_hprop_intro() { requires b: bool, H: HProp, HP: __is_true(b); - consumes #255: H; - produces #254: If(__is_true(b), H); + consumes H; + produces If(__is_true(b), H); __admitted(); }; ghost fun group_expand_r_if_intros() { requires n1: int, n2: int, - items: pure_fun(fun(#258: int): HProp), expand_check: __is_true(n1 <= n2); - consumes #257: for i in 0..n1 -> items(i); - produces #256: for i in 0..n2 -> If(__is_true(i < n1), items(i)); + consumes for i in 0..n1 -> items(i); + produces for i in 0..n2 -> If(__is_true(i < n1), items(i)); __admitted(); }; ghost fun group_shrink_r_if_elim() { @@ -1031,47 +991,44 @@ ghost fun group_singleton_if_intros() { requires n: int, H: HProp; - consumes #260: H; - produces #259: for i in 0..n -> If(__is_true(i = 0), H); + consumes H; + produces for i in 0..n -> If(__is_true(i = 0), H); __admitted(); }; ghost fun group_singleton_if_elim() { requires n: int, H: HProp; - consumes #262: for i in 0..n -> If(__is_true(i = 0), H); - produces #261: H; + consumes for i in 0..n -> If(__is_true(i = 0), H); + produces H; __admitted(); }; - ghost(assert_prop()[proof := admit(pure_fun(fun(b: int, e1: int, e2: int, #364: __is_true(e1 <= e2)): __is_true(b << e1 <= b << e2)))][shiftr_monotonic : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(b, e1, e2, #_1): __is_true(b << e1 <= b << e2)))][shiftr_monotonic : proof]); let __rewrite_sequence: const(int); ghost fun group_to_desyncgroup() { requires N: int, - items: pure_fun(fun(#266: int): HProp), r: Range; - consumes #265: ThreadsCtx(r), - #264: for i in 0..N -> items(i); - produces #265: ThreadsCtx(r), - #263: desync_for i in ..N -> items(i); + consumes ThreadsCtx(r), + for i in 0..N -> items(i); + produces ThreadsCtx(r), + desync_for i in ..N -> items(i); __admitted(); }; ghost fun unwrap_singleton_desyncgroup() { - requires t: int, - H: pure_fun(fun(#270: int): HProp); - consumes #269: ThreadsCtx(counted_range(t, MSIZE0())), - #268: DesyncGroup(MSIZE0(), H); - produces #269: ThreadsCtx(counted_range(t, MSIZE0())), - #267: H(0); + requires t: int; + consumes ThreadsCtx(counted_range(t, MSIZE0())), + DesyncGroup(MSIZE0(), H); + produces ThreadsCtx(counted_range(t, MSIZE0())), + H(0); __admitted(); }; ghost fun desync_tile_divides() { requires tile_count: int, tile_size: int, size: int, - items: pure_fun(fun(#273: int): HProp), div_check: __is_true(size = tile_count * tile_size), positive_tile_size: __is_true(tile_size >= 0); - consumes #272: DesyncGroup(size, items); - produces #271: desync_for bi in ..tile_count -> desync_for i in ..tile_size -> items(bi * tile_size + i); + consumes DesyncGroup(size, items); + produces desync_for bi in ..tile_count -> desync_for i in ..tile_size -> items(bi * tile_size + i); __admitted(); }; ghost fun desync_untile_divides() { @@ -1079,28 +1036,24 @@ __admitted(); }; ghost fun singleton_mindex_simplify() { - requires T: Type, - H: pure_fun(fun(#276: ptr(T)): HProp), - p: ptr(T); - consumes #275: H(p[MINDEX1(MSIZE0(), DMINDEX1(MSIZE0(), 0))]); - produces #274: H(p); + requires p: ptr(T); + consumes H(p[MINDEX1(MSIZE0(), DMINDEX1(MSIZE0(), 0))]); + produces H(p); __admitted(); }; }; { let __OPTITRUST_ENABLE_MODELS: __ghost_unit; ghost fun ro_matrix1_focus() { - requires T: Type, - matrix: ptr(T), + requires matrix: ptr(T), i: int, n: int, MT: MemType, - M: pure_fun(fun(#280: int): T), f: _Fraction, bound_check: in_range(i, 0..n); - consumes #279: _RO(f, matrix ~> Matrix1Of(n, MT, M)); - produces #278: Wand(_RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)), _RO(f, matrix ~> Matrix1Of(n, MT, M))), - #277: _RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)); + consumes _RO(f, matrix ~> Matrix1Of(n, MT, M)); + produces Wand(_RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)), _RO(f, matrix ~> Matrix1Of(n, MT, M))), + _RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)); __admitted(); ghost(ro_group_focus()[f := f, i := i, bound_check := bound_check]); }; @@ -1110,20 +1063,18 @@ ghost(close_wand()); }; ghost fun ro_matrix2_focus() { - requires T: Type, - matrix: ptr(T), + requires matrix: ptr(T), i: int, j: int, m: int, n: int, MT: MemType, - M: pure_fun(fun(#284: int, #285: int): T), f: _Fraction, bound_check_i: in_range(i, 0..m), bound_check_j: in_range(j, 0..n); - consumes #283: _RO(f, matrix ~> Matrix2Of(m, n, MT, M)); - produces #282: Wand(_RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)), _RO(f, matrix ~> Matrix2Of(m, n, MT, M))), - #281: _RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)); + consumes _RO(f, matrix ~> Matrix2Of(m, n, MT, M)); + produces Wand(_RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)), _RO(f, matrix ~> Matrix2Of(m, n, MT, M))), + _RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)); __admitted(); ghost(ro_group_focus()[f := f, i := i, bound_check := bound_check_i]); ghost(ro_group_focus()[f := f, i := j, bound_check := bound_check_j]); @@ -1134,79 +1085,68 @@ __admitted(); ghost(close_wand()); }; - fun MATRIX1_COPY_int(dest: ptr(int), src: ptr(int), length: int): unit [model, #288, #287, #286, #287, #286] { - requires model: pure_fun(fun(#289: int): int); - reads #287: src ~> Matrix1(length, model); - writes #286: dest ~> Matrix1(length, model); + fun MATRIX1_COPY_int(dest, src, length) [model] { + reads src ~> Matrix1(length, model); + writes dest ~> Matrix1(length, model); __admitted(); __ignore(memcpy(dest, src, length * sizeof(int))); }; - fun MATRIX2_COPY_int(dest: ptr(int), src: ptr(int), n1: int, n2: int): unit [model, #292, #291, #290, #291, #290] { - requires model: pure_fun(fun(#293: int, #294: int): int); - reads #291: src ~> Matrix2(n1, n2, model); - writes #290: dest ~> Matrix2(n1, n2, model); + fun MATRIX2_COPY_int(dest, src, n1, n2) [model] { + reads src ~> Matrix2(n1, n2, model); + writes dest ~> Matrix2(n1, n2, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * sizeof(int))); }; - fun MATRIX3_COPY_int(dest: ptr(int), src: ptr(int), n1: int, n2: int, n3: int): unit [model, #297, #296, #295, #296, #295] { - requires model: pure_fun(fun(#298: int, #299: int, #300: int): int); - reads #296: src ~> Matrix3(n1, n2, n3, model); - writes #295: dest ~> Matrix3(n1, n2, n3, model); + fun MATRIX3_COPY_int(dest, src, n1, n2, n3) [model] { + reads src ~> Matrix3(n1, n2, n3, model); + writes dest ~> Matrix3(n1, n2, n3, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * n3 * sizeof(int))); }; - fun MATRIX1_COPY_float(dest: ptr(f32), src: ptr(f32), length: int): unit [model, #303, #302, #301, #302, #301] { - requires model: pure_fun(fun(#304: int): f32); - reads #302: src ~> Matrix1(length, model); - writes #301: dest ~> Matrix1(length, model); + fun MATRIX1_COPY_float(dest, src, length) [model] { + reads src ~> Matrix1(length, model); + writes dest ~> Matrix1(length, model); __admitted(); __ignore(memcpy(dest, src, length * sizeof(f32))); }; - fun MATRIX2_COPY_float(dest: ptr(f32), src: ptr(f32), n1: int, n2: int): unit [model, #307, #306, #305, #306, #305] { - requires model: pure_fun(fun(#308: int, #309: int): f32); - reads #306: src ~> Matrix2(n1, n2, model); - writes #305: dest ~> Matrix2(n1, n2, model); + fun MATRIX2_COPY_float(dest, src, n1, n2) [model] { + reads src ~> Matrix2(n1, n2, model); + writes dest ~> Matrix2(n1, n2, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * sizeof(f32))); }; - fun MATRIX3_COPY_float(dest: ptr(f32), src: ptr(f32), n1: int, n2: int, n3: int): unit [model, #312, #311, #310, #311, #310] { - requires model: pure_fun(fun(#313: int, #314: int, #315: int): f32); - reads #311: src ~> Matrix3(n1, n2, n3, model); - writes #310: dest ~> Matrix3(n1, n2, n3, model); + fun MATRIX3_COPY_float(dest, src, n1, n2, n3) [model] { + reads src ~> Matrix3(n1, n2, n3, model); + writes dest ~> Matrix3(n1, n2, n3, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * n3 * sizeof(f32))); }; - fun MATRIX1_COPY_double(dest: ptr(f64), src: ptr(f64), length: int): unit [model, #318, #317, #316, #317, #316] { - requires model: pure_fun(fun(#319: int): f64); - reads #317: src ~> Matrix1(length, model); - writes #316: dest ~> Matrix1(length, model); + fun MATRIX1_COPY_double(dest, src, length) [model] { + reads src ~> Matrix1(length, model); + writes dest ~> Matrix1(length, model); __admitted(); __ignore(memcpy(dest, src, length * sizeof(f64))); }; - fun MATRIX2_COPY_double(dest: ptr(f64), src: ptr(f64), n1: int, n2: int): unit [model, #322, #321, #320, #321, #320] { - requires model: pure_fun(fun(#323: int, #324: int): f64); - reads #321: src ~> Matrix2(n1, n2, model); - writes #320: dest ~> Matrix2(n1, n2, model); + fun MATRIX2_COPY_double(dest, src, n1, n2) [model] { + reads src ~> Matrix2(n1, n2, model); + writes dest ~> Matrix2(n1, n2, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * sizeof(f64))); }; - fun MATRIX3_COPY_double(dest: ptr(f64), src: ptr(f64), n1: int, n2: int, n3: int): unit [model, #327, #326, #325, #326, #325] { - requires model: pure_fun(fun(#328: int, #329: int, #330: int): f64); - reads #326: src ~> Matrix3(n1, n2, n3, model); - writes #325: dest ~> Matrix3(n1, n2, n3, model); + fun MATRIX3_COPY_double(dest, src, n1, n2, n3) [model] { + reads src ~> Matrix3(n1, n2, n3, model); + writes dest ~> Matrix3(n1, n2, n3, model); __admitted(); __ignore(memcpy(dest, src, n1 * n2 * n3 * sizeof(f64))); }; ghost fun matrix1_span_shift() { - requires T: Type, - matrix: ptr(T), + requires matrix: ptr(T), n1: int, a: int, b: int, - MT: MemType, - M: pure_fun(fun(#333: int): T); - consumes #332: for i in a..b -> matrix[MINDEX1(n1, i)] ~> CellOf(MT); - produces #331: for i in 0..(b - a) -> matrix[a][MINDEX1(b - a, i)] ~> CellOf(MT); + MT: MemType; + consumes for i in a..b -> matrix[MINDEX1(n1, i)] ~> CellOf(MT); + produces for i in 0..(b - a) -> matrix[a][MINDEX1(b - a, i)] ~> CellOf(MT); __admitted(); }; ghost fun matrix1_span_unshift() { @@ -1214,16 +1154,14 @@ __admitted(); }; ghost fun matrix2_span_shift() { - requires T: Type, - matrix: ptr(T), + requires matrix: ptr(T), n1: int, n2: int, a: int, b: int, - MT: MemType, - M: pure_fun(fun(#336: int, #337: int): T); - consumes #335: for i in a..b -> for j in 0..n2 -> matrix[MINDEX2(n1, n2, i, j)] ~> CellOf(MT); - produces #334: for i in 0..(b - a) -> for j in 0..n2 -> matrix[a * n2][MINDEX2(b - a, n2, i, j)] ~> CellOf(MT); + MT: MemType; + consumes for i in a..b -> for j in 0..n2 -> matrix[MINDEX2(n1, n2, i, j)] ~> CellOf(MT); + produces for i in 0..(b - a) -> for j in 0..n2 -> matrix[a * n2][MINDEX2(b - a, n2, i, j)] ~> CellOf(MT); __admitted(); }; ghost fun matrix2_span_unshift() { @@ -1231,30 +1169,28 @@ __admitted(); }; ghost fun matrix3_span_shift() { - requires T: Type, - matrix: ptr(T), + requires matrix: ptr(T), n1: int, n2: int, n3: int, a: int, b: int, - MT: MemType, - M: pure_fun(fun(#340: int, #341: int, #342: int): T); - consumes #339: for i1 in a..b -> for i2 in 0..n2 -> for i3 in 0..n3 -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(MT); - produces #338: for i1 in 0..(b - a) -> for i2 in 0..n2 -> for i3 in 0..n3 -> matrix[a * n2 * n3][MINDEX3(b - a, n2, n3, i1, i2, i3)] ~> CellOf(MT); + MT: MemType; + consumes for i1 in a..b -> for i2 in 0..n2 -> for i3 in 0..n3 -> matrix[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(MT); + produces for i1 in 0..(b - a) -> for i2 in 0..n2 -> for i3 in 0..n3 -> matrix[a * n2 * n3][MINDEX3(b - a, n2, n3, i1, i2, i3)] ~> CellOf(MT); __admitted(); }; ghost fun matrix3_span_unshift() { reverts matrix3_span_shift; __admitted(); }; - ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#366: int, #367: int, #368: pure_fun(fun(#365: int): int)): int))][reduce_int_sum : x]); - ghost(assert_prop()[proof := admit(pure_fun(fun(n: int, f: pure_fun(fun(#369: int): int)): __is_true(0 = reduce_int_sum(n, n, f))))][reduce_int_sum_empty : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(a: int, b: int, f: pure_fun(fun(#370: int): int), #371: __is_true(b >= a), bp1: int, #372: __is_true(bp1 = b + 1)): __is_true(reduce_int_sum(a, b, f) + f(b) = reduce_int_sum(a, bp1, f))))][reduce_int_sum_add_right : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(a: int, b: int, f: pure_fun(fun(#373: int): int), #374: __is_true(b > a), ap1: int, #375: __is_true(ap1 = a + 1)): __is_true(reduce_int_sum(a, b, f) - f(a) = reduce_int_sum(ap1, b, f))))][reduce_int_sum_sub_left : proof]); - ghost(assert_prop()[proof := admit(pure_fun(fun(a: int, b: int, ap1: int, bp1: int, f: pure_fun(fun(#376: int): int), #377: __is_true(b >= a), #378: __is_true(ap1 = a + 1), #379: __is_true(bp1 = b + 1)): __is_true(reduce_int_sum(a, b, f) + (f(b) - f(a)) = reduce_int_sum(ap1, bp1, f))))][reduce_int_sum_slide : proof]); + ghost(assert_inhabited()[x := arbitrary(pure_fun(fun(#_1, #_2, #_3): int))][reduce_int_sum : x]); + ghost(assert_prop()[proof := admit(pure_fun(fun(n, f): __is_true(0 = reduce_int_sum(n, n, f))))][reduce_int_sum_empty : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(a, b, f, #_1, bp1, #_2): __is_true(reduce_int_sum(a, b, f) + f(b) = reduce_int_sum(a, bp1, f))))][reduce_int_sum_add_right : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(a, b, f, #_1, ap1, #_2): __is_true(reduce_int_sum(a, b, f) - f(a) = reduce_int_sum(ap1, b, f))))][reduce_int_sum_sub_left : proof]); + ghost(assert_prop()[proof := admit(pure_fun(fun(a, b, ap1, bp1, f, #_1, #_2, #_3): __is_true(reduce_int_sum(a, b, f) + (f(b) - f(a)) = reduce_int_sum(ap1, bp1, f))))][reduce_int_sum_slide : proof]); }; - fun one_fork(): unit { + fun one_fork() { letmut x = 0; letmut n = 64; letmut a; @@ -1278,30 +1214,30 @@ for i in 0..5 { strict; requires #344: _Fraction; - xconsumes #343: _RO(#344, x ~> CellOf(Any)); - xproduces #343: _RO(#344, x ~> CellOf(Any)); + xconsumes _RO(#344, x ~> CellOf(Any)); + xproduces _RO(#344, x ~> CellOf(Any)); ghost(ro_split2()[f := #_1, H := x ~> CellOf(Any)]); ghost(ro_fork_group()[f := #_1 / 2, H := x ~> CellOf(Any), r := 0..5]); ghost(ro_allow_join2()[f := #_1, H := x ~> CellOf(Any)]); for j in 0..5 { strict; requires #346: _Fraction; - xconsumes #345: _RO(#346, x ~> CellOf(Any)); - xproduces #345: _RO(#346, x ~> CellOf(Any)); + xconsumes _RO(#346, x ~> CellOf(Any)); + xproduces _RO(#346, x ~> CellOf(Any)); __ignore(x + 1); }; ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); }; __ghost_end(fork_out); }; - fun two_forks(): unit { + fun two_forks() { letmut x = 0; let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := x ~> CellOf(Any), r := 0..5])); for i in 0..5 { strict; requires #348: _Fraction; - xconsumes #347: _RO(#348, x ~> CellOf(Any)); - xproduces #347: _RO(#348, x ~> CellOf(Any)); + xconsumes _RO(#348, x ~> CellOf(Any)); + xproduces _RO(#348, x ~> CellOf(Any)); ghost(ro_split3()[f := #_1, H := x ~> CellOf(Any)]); ghost(ro_fork_group()[f := #_1 / 3, H := x ~> CellOf(Any), r := 0..5]); ghost(ro_fork_group()[f := #_1 / 3, H := x ~> CellOf(Any), r := 0..5]); @@ -1309,8 +1245,8 @@ for j in 0..5 { strict; requires #350: _Fraction; - xconsumes #349: _RO(#350, x ~> CellOf(Any)); - xproduces #349: _RO(#350, x ~> CellOf(Any)); + xconsumes _RO(#350, x ~> CellOf(Any)); + xproduces _RO(#350, x ~> CellOf(Any)); __ignore(x + 1); }; ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); @@ -1318,14 +1254,14 @@ }; __ghost_end(fork_out); }; - fun two_forks_spe_twice(): unit { + fun two_forks_spe_twice() { letmut x = 0; let fork_out: __ghost_fn = __ghost_begin(ghost(ro_fork_group()[H := x ~> CellOf(Any), r := 0..5])); for i in 0..5 { strict; requires #352: _Fraction; - xconsumes #351: _RO(#352, x ~> CellOf(Any)); - xproduces #351: _RO(#352, x ~> CellOf(Any)); + xconsumes _RO(#352, x ~> CellOf(Any)); + xproduces _RO(#352, x ~> CellOf(Any)); ghost(ro_split2()[f := #_1, H := x ~> CellOf(Any)]); ghost(ro_split2()[f := #_1 / 2, H := x ~> CellOf(Any)]); ghost(ro_fork_group()[f := #_1 / 2, H := x ~> CellOf(Any), r := 0..5]); @@ -1334,8 +1270,8 @@ for j in 0..5 { strict; requires #354: _Fraction; - xconsumes #353: _RO(#354, x ~> CellOf(Any)); - xproduces #353: _RO(#354, x ~> CellOf(Any)); + xconsumes _RO(#354, x ~> CellOf(Any)); + xproduces _RO(#354, x ~> CellOf(Any)); __ignore(x + 1); }; ghost(ro_join_group()[H := x ~> CellOf(Any), r := 0..5]); diff --git a/tests_infra/optilambda/printer_basic.ml b/tests_infra/optilambda/printer_basic.ml index 824f61528..0cbf1d995 100644 --- a/tests_infra/optilambda/printer_basic.ml +++ b/tests_infra/optilambda/printer_basic.ml @@ -60,6 +60,18 @@ let surface_writes_contract = let surface_formula_contract = { empty_fun_contract with pre = resource_set ~linear:[ (v "h", points_to_formula (term "src") (term "H")) ] () } +let generated_name_cleanup_contract = + let anon_hyp = Ast.new_var "" in + let anon_binder_hyp = Ast.new_var "" in + let anon_i = Ast.new_var "" in + let range = range (Trm.trm_int 0) (term "n") (Trm.trm_int 1) in + let group_body = app "H" [ Trm.trm_var anon_i ] in + let group_body = app "Group" [ range; Trm.trm_fun [ (anon_i, Typ.typ_int) ] Typ.typ_auto group_body ] in + { + empty_fun_contract with + pre = resource_set ~linear:[ (anon_hyp, term "Anon"); (v "named", term "Named"); (anon_binder_hyp, group_body) ] (); + } + let mixed_recovery_contract = let frac = term "f" in let read_body = term "ReadH" in @@ -272,13 +284,13 @@ let () = check "function definition" (Trm.trm_let_fun (v "f") Typ.typ_int [ tv "x" Typ.typ_int ] (Trm.trm_seq_nomarks [ Trm.trm_abort (Ret (Some (term "x"))) ])) - "fun f(x: int): int { x }"; + "fun f(x) { x }"; check "function contract" (Trm.trm_let_fun ~contract:(FunSpecContract simple_fun_contract) (v "f") Typ.typ_int [ tv "x" Typ.typ_int; tv "y" Typ.typ_int ] (Trm.trm_seq_nomarks [ Trm.trm_abort (Ret (Some (term "x"))) ])) - "fun f(x: int, y: int): int [h_req, h_in, h_ens, h_out] {\n\ + "fun f(x, y) [h_req, h_in, h_ens, h_out] {\n\ \ requires h_req: x = y;\n\ \ consumes h_in: R;\n\ \ ensures h_ens: result = x;\n\ @@ -296,9 +308,9 @@ let () = (Trm.trm_let_fun ~contract:(FunSpecContract multi_requires_contract) (v "rewrite") (Typ.typ_var (Typ.name_to_typvar "__ghost_ret")) [] (Trm.trm_seq_nomarks [])) - "ghost fun rewrite() {\n requires from: int,\n to: int,\n inside: pure_fun(fun(x: int): Prop);\n}"; + "ghost fun rewrite() {\n requires from: int,\n to: int;\n}"; - check_typ "compact Type result" (Typ.typ_pure_fun [ (v "x", Typ.typ_int) ] Typ.typ_prop) "pure_fun(fun(x: int): Prop)"; + check_typ "compact Type result" (Typ.typ_pure_fun [ (v "x", Typ.typ_int) ] Typ.typ_prop) "int -> Prop"; check "if" (Trm.trm_if @@ -348,22 +360,31 @@ let () = check "surface reads contract" (Trm.trm_let_fun ~contract:(FunSpecContract surface_reads_contract) (v "read_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - "fun read_example(): unit [f, x, x] { reads x: H; }"; + "fun read_example() [f, x, x] { reads x: H; }"; check "surface writes contract" (Trm.trm_let_fun ~contract:(FunSpecContract surface_writes_contract) (v "write_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - "fun write_example(): unit [x, x] { writes x: H; }"; + "fun write_example() [x, x] { writes x: H; }"; check "surface local formula printer in contract" (Trm.trm_let_fun ~contract:(FunSpecContract surface_formula_contract) (v "formula_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - "fun formula_example(): unit [h] { consumes h: src ~> H; }"; + "fun formula_example() [h] { consumes h: src ~> H; }"; + + check "surface generated contract names are hidden" + (Trm.trm_let_fun ~contract:(FunSpecContract generated_name_cleanup_contract) (v "generated_name_example") Typ.typ_unit [] + (Trm.trm_seq_nomarks [])) + "fun generated_name_example() [named] {\n\ + \ consumes Anon,\n\ + \ named: Named,\n\ + \ for #_1 in 0..n -> H(#_1);\n\ + }"; check "non-adjacent reads and writes recovery" (Trm.trm_let_fun ~contract:(FunSpecContract mixed_recovery_contract) (v "mixed_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - "fun mixed_example(): unit [f, read, kept, write, write, read, new_out] {\n\ + "fun mixed_example() [f, read, kept, write, write, read, new_out] {\n\ \ reads read: ReadH;\n\ \ writes write: WriteH;\n\ \ consumes kept: Kept;\n\ @@ -373,7 +394,7 @@ let () = check "alpha-equivalent group reads recovery" (Trm.trm_let_fun ~contract:(FunSpecContract alpha_group_reads_contract) (v "alpha_group_read_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - "fun alpha_group_read_example(): unit [f, read, read] {\n\ + "fun alpha_group_read_example() [f, read, read] {\n\ \ reads read: for i in 0..n -> H(i);\n\ }"; @@ -401,7 +422,15 @@ let () = (Trm.trm_seq_nomarks [])) "fun write_example(): unit [x, x] { writes x: H; }"; - let focus_expected = + let surface_focus_expected = + "fun focus_example() [f, whole, wand, focused] {\n\ + \ requires f: _Fraction;\n\ + \ consumes whole: _RO(f, Whole);\n\ + \ produces wand: Wand(_RO(f, Focused), _RO(f, Whole)),\n\ + \ focused: _RO(f, Focused);\n\ + }" + in + let explicit_focus_expected = "fun focus_example(): unit [f, whole, wand, focused] {\n\ \ requires f: _Fraction;\n\ \ consumes whole: _RO(f, Whole);\n\ @@ -412,19 +441,19 @@ let () = check "read-only focus contract stays explicit" (Trm.trm_let_fun ~contract:(FunSpecContract read_only_focus_contract) (v "focus_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - focus_expected; + surface_focus_expected; check_with_style "internal read-only focus contract stays explicit" internal_style (Trm.trm_let_fun ~contract:(FunSpecContract read_only_focus_contract) (v "focus_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - focus_expected; + explicit_focus_expected; check_with_style "typed read-only focus contract stays explicit" typed_style (Trm.trm_let_fun ~contract:(FunSpecContract read_only_focus_contract) (v "focus_example") Typ.typ_unit [] (Trm.trm_seq_nomarks [])) - focus_expected; + explicit_focus_expected; check "loop contract" (Trm.trm_for ~contract:simple_loop_contract @@ -488,6 +517,6 @@ let () = (Trm.trm_let_fun ~contract:(FunSpecContract simple_fun_contract) (v "f") Typ.typ_int [ tv "x" Typ.typ_int; tv "y" Typ.typ_int ] (Trm.trm_seq_nomarks [ Trm.trm_abort (Ret (Some (term "x"))) ])) - "fun f(x: int, y: int): int { x }"; + "fun f(x, y) { x }"; check "marks" (Mark.trm_add_mark "target" (term "x")) "@marks[target] x" diff --git a/tools/optiNLP/knowledge/optilambda.md b/tools/optiNLP/knowledge/optilambda.md index 1ea1f63f7..b2e66df29 100644 --- a/tools/optiNLP/knowledge/optilambda.md +++ b/tools/optiNLP/knowledge/optilambda.md @@ -26,9 +26,12 @@ Prompt implications: Visible OptiLambda cues: -- `fun name(args): type { ... }` describes a function. +- In Surface OptiLambda, `fun name(args) { ... }` describes a function. The + `internal` and `typed` representations may show argument and return types. - `for i in 0..n { ... }` describes a sequential loop over `i`. - Assignments, reads, writes, marks, and contract-like annotations can be used for target reasoning. +- Surface contracts hide generated resource names and type-only pure + requirements when those details are not useful for reading the trace. - Printed `.opti` text is inspection evidence only; runnable transformation scripts still target the C/C++ workflow. From 9b80ef412c303814ead085e6b8fbafcda09687a0 Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Thu, 9 Jul 2026 06:58:34 -0400 Subject: [PATCH 35/47] Add universal OptiLambda HTML diff and trace views --- lib/framework/runtime/trace.ml | 3 +- lib/optilambda/optilambda_printer.ml | 127 ++- tests_infra/optilambda/printer_basic.ml | 31 +- tools/vscode-optitrust/package.json | 54 +- .../src/commands/shortcuts.ts | 12 - .../src/commands/viewCommands.ts | 50 +- tools/vscode-optitrust/src/extension.ts | 24 - .../src/optitrust/nativeDiff.ts | 336 -------- tools/vscode-optitrust/src/optitrust/views.ts | 324 +++++++- .../syntaxes/optilambda.tmLanguage.json | 4 +- tools/web_view/diff_template.html | 104 ++- tools/web_view/optitrust_diff.css | 166 +++- tools/web_view/optitrust_interactive_code.js | 732 ++++++++++++++++++ tools/web_view/optitrust_syntax_highlight.js | 300 +++++-- tools/web_view/optitrust_syntax_highlight.mjs | 86 +- tools/web_view/optitrust_trace.js | 130 ++-- tools/web_view/trace_template.html | 2 + 17 files changed, 1828 insertions(+), 657 deletions(-) delete mode 100644 tools/vscode-optitrust/src/optitrust/nativeDiff.ts create mode 100644 tools/web_view/optitrust_interactive_code.js diff --git a/lib/framework/runtime/trace.ml b/lib/framework/runtime/trace.ml index 294b7388d..2303dfb31 100644 --- a/lib/framework/runtime/trace.ml +++ b/lib/framework/runtime/trace.ml @@ -1830,7 +1830,8 @@ let produce_diff_output_internal (step:step_tree) : unit = in prefix ^ "_" ^ side ^ suffix in - (* Generate files. *) + (* Generate only the requested pair. Other OptiLambda representations are + generated lazily by the VS Code diff webview when the user switches syntax. *) output_ast style_before (diff_filename_prefix style_before "before") ast_before; output_ast style_after (diff_filename_prefix style_after "after") ast_after; Flags.verbose_info "Generated diff files for %s" prefix diff --git a/lib/optilambda/optilambda_printer.ml b/lib/optilambda/optilambda_printer.ml index 2bfc0d02d..da0c492ba 100644 --- a/lib/optilambda/optilambda_printer.ml +++ b/lib/optilambda/optilambda_printer.ml @@ -33,7 +33,7 @@ let block_doc docs = | [] -> lbrace ^^ rbrace | _ -> surround 2 1 lbrace (semi_sep docs) rbrace -type block_item = Regular of document | FinalExpr of document +type block_item = Regular of document | FinalExpr of document | Blank type contract_clause = ContractClause of string * var list * resource_item | ContractRaw of document type read_only_formula = { read_frac : trm; read_body : trm } @@ -78,6 +78,7 @@ let code_block_doc items = let item_to_doc = function | Regular d -> d ^^ semi | FinalExpr d -> d + | Blank -> empty in match items with | [] -> lbrace ^^ rbrace @@ -156,9 +157,23 @@ let is_ghost_ret_type (ty : typ) : bool = | Trm_var v -> v.name = "__ghost_ret" && v.namespaces = [] | _ -> false +(** [is_ghost_fn_type ty] checks whether [ty] is the internal ghost-function marker. *) +let is_ghost_fn_type (ty : typ) : bool = + match ty.desc with + | Trm_var v -> v.name = "__ghost_fn" && v.namespaces = [] + | _ -> false + +(** [should_print_type_annotation style ty] keeps Surface output from exposing internal-only marker types. *) +let should_print_type_annotation (style : Optilambda_style.style) (ty : typ) : bool = + style.print_types && not (is_auto_type ty) && not (is_surface style && is_ghost_fn_type ty) + (** [typ_to_doc style ty] prints an OptiTrust type using OptiLambda syntax. *) let rec typ_to_doc (style : Optilambda_style.style) (ty : typ) : document = match ty.desc with + | Trm_apps ({ desc = Trm_var v; _ }, [ body ], [], []) when is_surface style && v.name = "__is_true" -> + formula_to_doc style body + | Trm_var v when is_surface style && v.name = "f32" -> string "float" + | Trm_var v when is_surface style && v.name = "f64" -> string "double" | Trm_var v -> var_to_doc style v | Trm_apps ({ desc = Trm_var v; _ }, args, [], []) -> begin match (v.name, args) with @@ -167,12 +182,23 @@ let rec typ_to_doc (style : Optilambda_style.style) (ty : typ) : document = let doc = typ_to_doc style ty in match ty.desc with | Trm_apps ({ desc = Trm_var v; _ }, _, [], []) when v.name = "pure_fun" && v.namespaces = [] -> parens_doc doc + | Trm_apps ({ desc = Trm_var v; _ }, [ _ ], [], []) when is_surface style && v.name = "__is_true" -> parens_doc doc | _ -> doc in let args_doc = separate (blank 1 ^^ star ^^ blank 1) (List.map arg_to_doc args) in let ret_doc = if is_type_type ret_ty then - match type_result_body_to_doc style body with + match + match body.desc with + | Trm_seq _ -> type_result_body_to_doc style body + | Trm_var _ + | Trm_arbitrary (Typ _) -> + Some (typ_to_doc style body) + | Trm_apps ({ desc = Trm_var v; _ }, _, [], []) + when List.mem v.name [ "pure_fun"; "ptr"; "const"; "array"; "fun" ] -> + Some (typ_to_doc style body) + | _ -> Some (formula_to_doc style body) + with | Some doc -> doc | None -> typ_to_doc style ret_ty else typ_to_doc style ret_ty @@ -202,7 +228,7 @@ and elem_typ_of_access_result (ty : typ) : typ = (** [typed_var_to_doc style (v, ty)] prints a variable declaration fragment. *) and typed_var_to_doc (style : Optilambda_style.style) ((v, ty) : typed_var) : document = - if style.print_types && not (is_auto_type ty) then var_to_doc style v ^^ colon ^^ blank 1 ^^ typ_to_doc style ty else var_to_doc style v + if should_print_type_annotation style ty then var_to_doc style v ^^ colon ^^ blank 1 ^^ typ_to_doc style ty else var_to_doc style v (** [surface_typed_var_to_doc style (v, ty)] hides type annotations in Surface snippets that are meant to stay C-like. *) and surface_typed_var_to_doc (style : Optilambda_style.style) ((v, ty) : typed_var) : document = @@ -460,6 +486,7 @@ and uninit_formula_body (formula : trm) : trm option = and normalize_surface_formula_binders (formula : trm) : trm = let rec erase_hidden_fun_arg_types t = match t.desc with + | Trm_apps ({ desc = Trm_var v; _ }, [ { desc = Trm_fun _; _ } ], [], []) when v.name = "pure_fun" -> t | Trm_fun (args, ret_ty, body, contract) -> let ret_ty = erase_hidden_fun_arg_types ret_ty in let body = erase_hidden_fun_arg_types body in @@ -504,6 +531,12 @@ and formula_to_doc_at (style : Optilambda_style.style) (ctx_prec : int) (formula | Trm_var v -> var_to_doc style v | Trm_lit lit -> lit_to_doc style lit | Trm_prim (ty, prim) -> prim_to_doc style ty prim + | Trm_apps ({ desc = Trm_var v; _ }, [ body ], [], []) + when is_surface style && v.name = "__is_true" && v.namespaces = [] -> + formula_to_doc_at style ctx_prec body + | Trm_apps ({ desc = Trm_var v; _ }, [ { desc = Trm_fun _; _ } ], [], []) + when is_surface style && v.name = "pure_fun" && v.namespaces = [] -> + typ_to_doc style formula | Trm_apps ({ desc = Trm_var v; _ }, [ start; stop; step ], [], []) when v.name = "range" && v.namespaces = [] -> if is_int_one step then @@ -644,6 +677,23 @@ and simplify_linear_contract (pre : resource_item list) (post : resource_item li and remove_used_fraction_requirements (used_fracs : var list) (pure : resource_item list) : resource_item list = List.filter (fun (hyp, formula) -> not (List.exists (( = ) hyp) used_fracs && is_fraction_type_formula formula)) pure +(** [surface_parallel_reads style reads] prints loop read-only resources in the same compact form as function [reads] clauses. *) +and surface_parallel_reads (style : Optilambda_style.style) (reads : resource_item list) : resource_item list * var list = + if not (is_surface style) then (reads, []) + else + List.fold_right + (fun (hyp, formula) (reads, used_fracs) -> + match read_only_formula_inv formula with + | Some ro -> + let used_fracs = + match fraction_var_of_formula ro.read_frac with + | Some frac -> frac :: used_fracs + | None -> used_fracs + in + ((hyp, ro.read_body) :: reads, used_fracs) + | None -> ((hyp, formula) :: reads, used_fracs)) + reads ([], []) + (** [is_surface_type_only_formula formula] recognizes pure contract entries that only describe types. *) and is_surface_type_only_formula (formula : trm) : bool = match formula.desc with @@ -727,12 +777,13 @@ and loop_contract_clauses (style : Optilambda_style.style) (contract : loop_cont if not style.print_contracts then [] else let strict_doc = if contract.strict then [ ContractRaw (string "strict") ] else [] in - let loop_ghosts = filter_surface_type_only_requirements style contract.loop_ghosts in - let invariant_pure = filter_surface_type_only_requirements style contract.invariant.pure in - let iter_pre_pure = filter_surface_type_only_requirements style contract.iter_contract.pre.pure in + let parallel_reads, used_fracs = surface_parallel_reads style contract.parallel_reads in + let loop_ghosts = filter_surface_type_only_requirements style (remove_used_fraction_requirements used_fracs contract.loop_ghosts) in + let invariant_pure = filter_surface_type_only_requirements style (remove_used_fraction_requirements used_fracs contract.invariant.pure) in + let iter_pre_pure = filter_surface_type_only_requirements style (remove_used_fraction_requirements used_fracs contract.iter_contract.pre.pure) in let iter_post_pure = filter_surface_type_only_requirements style contract.iter_contract.post.pure in let items = - loop_ghosts @ invariant_pure @ contract.invariant.linear @ contract.parallel_reads @ iter_pre_pure + loop_ghosts @ invariant_pure @ contract.invariant.linear @ parallel_reads @ iter_pre_pure @ contract.iter_contract.pre.linear @ iter_post_pure @ contract.iter_contract.post.linear in let used_vars = resource_items_used_vars items in @@ -740,7 +791,7 @@ and loop_contract_clauses (style : Optilambda_style.style) (contract : loop_cont @ contract_clauses ~used_vars "requires" loop_ghosts @ contract_clauses ~used_vars "requires" invariant_pure @ contract_clauses ~used_vars "preserves" contract.invariant.linear - @ contract_clauses ~used_vars "reads" contract.parallel_reads + @ contract_clauses ~used_vars "reads" parallel_reads @ contract_clauses ~used_vars "xrequires" iter_pre_pure @ contract_clauses ~used_vars "xconsumes" contract.iter_contract.pre.linear @ contract_clauses ~used_vars "xensures" iter_post_pure @@ -841,7 +892,7 @@ and let_to_doc (style : Optilambda_style.style) ((v, ty) : typed_var) (body : tr | Trm_apps ({ desc = Trm_prim (_, Prim_ref_uninit); _ }, [], [], []) -> string "letmut" ^^ blank 1 ^^ var_to_doc style v | _ -> let typed_doc = - if style.print_types && not (is_auto_type ty) then var_to_doc style v ^^ colon ^^ blank 1 ^^ typ_to_doc style ty + if should_print_type_annotation style ty then var_to_doc style v ^^ colon ^^ blank 1 ^^ typ_to_doc style ty else var_to_doc style v in string "let" ^^ blank 1 ^^ typed_doc ^^ blank 1 ^^ equals ^^ blank 1 ^^ trm_to_doc_at style 0 body @@ -865,6 +916,10 @@ and app_to_doc (style : Optilambda_style.style) ~(result_typ : typ) (f : trm) (a string "for" ^^ blank 1 ^^ range_doc ^^ blank 1 ^^ trm_to_block_doc style body | Trm_var points_to_var, [ addr; resource ] when points_to_var.name = "~>" && points_to_var.namespaces = [] -> parens_doc (trm_to_doc_at style 0 addr ^^ blank 1 ^^ string "~>" ^^ blank 1 ^^ trm_to_doc_at style 0 resource) + | Trm_var ghost_begin, [ ghost_call ] when is_surface style && ghost_begin.name = "__ghost_begin" -> + string "ghost_begin" ^^ parens_doc (trm_to_doc_at style 0 ghost_call) + | Trm_var ghost_end, [ pair ] when is_surface style && ghost_end.name = "__ghost_end" -> + string "ghost_end" ^^ parens_doc (trm_to_doc_at style 0 pair) | Trm_prim (_, Prim_binop Binop_array_access), [ base; index ] when is_internal style -> trm_to_doc_at style 10 base ^^ blank 1 ^^ string "[+]" ^^ blank 1 ^^ trm_to_doc_at style 0 index | Trm_prim (_, Prim_binop Binop_array_get), [ base; index ] when is_internal style -> @@ -932,7 +987,25 @@ and app_to_doc (style : Optilambda_style.style) ~(result_typ : typ) (f : trm) (a (** [ghost_call_to_doc style f args ghost_args ghost_bind] prints the contents of a ghost call. *) and ghost_call_to_doc (style : Optilambda_style.style) (f : trm) (args : trm list) (ghost_args : resource_item list) (ghost_bind : (var option * var) list) : document = - app_to_doc style ~result_typ:typ_auto f args ghost_args ghost_bind + if is_surface style then ( + let head_doc = + match (f.desc, args) with + | Trm_var v, [] -> var_to_doc style v + | _ -> app_to_doc style ~result_typ:typ_auto f args [] [] + in + let ghost_arg_to_doc (hyp, formula) = + dquotes (var_to_doc style hyp ^^ blank 1 ^^ string ":=" ^^ blank 1 ^^ formula_to_doc style formula) + in + let ghost_bind_to_doc (bound_opt, contract_var) = + let bound_doc = + match bound_opt with + | Some bound -> var_to_doc style bound + | None -> string "_" + in + dquotes (bound_doc ^^ blank 1 ^^ string "<-" ^^ blank 1 ^^ var_to_doc style contract_var) + in + comma_sep (head_doc :: (List.map ghost_arg_to_doc ghost_args @ List.map ghost_bind_to_doc ghost_bind))) + else app_to_doc style ~result_typ:typ_auto f args ghost_args ghost_bind (** [ghost_to_doc style t] prints ghost instructions in OptiLambda syntax. *) and ghost_to_doc (style : Optilambda_style.style) (t : trm) : document option = @@ -962,11 +1035,23 @@ and seq_to_doc (style : Optilambda_style.style) (instrs : trm mlist) (result : v (** [instrs_to_block_items style instrs] prints final [return x] as final expression [x]. *) and instrs_to_block_items (style : Optilambda_style.style) (instrs : trm list) : block_item list = + let is_function_definition instr = + match instr.desc with + | Trm_let (_, { desc = Trm_fun _; _ }) + | Trm_fun _ -> + true + | _ -> false + in let rec aux acc instrs = match instrs with | [] -> List.rev acc | [ { desc = Trm_abort (Ret (Some ret)); _ } ] -> List.rev (FinalExpr (trm_to_doc_at style 0 ret) :: acc) - | instr :: rest -> aux (Regular (trm_to_doc_at style 0 instr) :: acc) rest + | instr :: rest -> + let is_fun = is_function_definition instr in + let acc = if is_fun && acc <> [] then Blank :: acc else acc in + let acc = Regular (trm_to_doc_at style 0 instr) :: acc in + let acc = if is_fun && rest <> [] then Blank :: acc else acc in + aux acc rest in aux [] instrs @@ -1053,11 +1138,15 @@ and add_marks_to_doc (style : Optilambda_style.style) (t : trm) (doc : document) (** [trm_to_doc_at style ctx_prec t] prints [t] in an expression context. *) and trm_to_doc_at (style : Optilambda_style.style) (ctx_prec : int) (t : trm) : document = - let doc = - match ghost_to_doc style t with - | Some doc -> doc - | None -> ( - match t.desc with + match t.desc with + | Trm_apps ({ desc = Trm_var v; _ }, [ body ], [], []) when is_surface style && v.name = "__is_true" && v.namespaces = [] -> + trm_to_doc_at style ctx_prec body + | _ -> + let doc = + match ghost_to_doc style t with + | Some doc -> doc + | None -> ( + match t.desc with | Trm_var v -> var_to_doc style v | Trm_lit lit -> lit_to_doc style lit | Trm_prim (ty, prim) -> prim_to_doc style ty prim @@ -1108,9 +1197,9 @@ and trm_to_doc_at (style : Optilambda_style.style) (ctx_prec : int) (t : trm) : ^^ brackets_doc (comma_sep (List.map (fun (v, _) -> var_to_doc style v) params)) ^^ blank 1 ^^ trm_to_doc_at style 0 body end - | Trm_using_directive name -> string "using" ^^ blank 1 ^^ string name) - in - parenthesize_if (trm_precedence t < ctx_prec) (add_marks_to_doc style t doc) + | Trm_using_directive name -> string "using" ^^ blank 1 ^^ string name) + in + parenthesize_if (trm_precedence t < ctx_prec) (add_marks_to_doc style t doc) (** [trm_to_doc style t] is the main entry point for printing terms. *) and trm_to_doc (style : Optilambda_style.style) (t : trm) : document = trm_to_doc_at style 0 t diff --git a/tests_infra/optilambda/printer_basic.ml b/tests_infra/optilambda/printer_basic.ml index 0cbf1d995..ed727bbb3 100644 --- a/tests_infra/optilambda/printer_basic.ml +++ b/tests_infra/optilambda/printer_basic.ml @@ -140,6 +140,13 @@ let ghost_call_example = Trm.trm_ghost_force (Trm.ghost_call ~ghost_bind:[ (Some (v "z"), "h_out") ] (v "rewrite") [ ("h", Trm.trm_eq ~typ:Typ.typ_int (term "x") (term "y")) ]) +let arbitrary_pure_fun_ghost = + let inner_fun_ty = Typ.typ_pure_fun [ (v "i", Typ.typ_int) ] Typ.typ_f32 in + let fun_ty = Typ.typ_pure_fun [ (v "n", Typ.typ_int); (v "f", inner_fun_ty) ] Typ.typ_f32 in + Trm.trm_ghost_force + (Trm.ghost_call ~ghost_bind:[ (Some (v "reduce_sum"), "x") ] (v "assert_inhabited") + [ ("x", app "arbitrary" [ fun_ty ]) ]) + let check name trm expected = let actual = OL.trm_to_string trm in if actual <> expected then begin @@ -312,6 +319,20 @@ let () = check_typ "compact Type result" (Typ.typ_pure_fun [ (v "x", Typ.typ_int) ] Typ.typ_prop) "int -> Prop"; + check_typ "surface C-style pure_fun type" + (Typ.typ_pure_fun [ (v "n", Typ.typ_int); (v "f", Typ.typ_pure_fun [ (v "i", Typ.typ_int) ] Typ.typ_f32) ] Typ.typ_f32) + "int * (int -> float) -> float"; + + check_typ "surface pure_fun hides __is_true argument type" + (Typ.typ_pure_fun + [ (v "n", Typ.typ_int); (v "h", app "__is_true" [ Trm.trm_ge ~typ:Typ.typ_int (term "n") (Trm.trm_int 0) ]) ] + Typ.typ_prop) + "int * (n >= 0) -> Prop"; + + check "__is_true is hidden in surface" + (app "__is_true" [ Trm.trm_eq ~typ:Typ.typ_int (term "result") (term "x") ]) + "result = x"; + check "if" (Trm.trm_if (Trm.trm_lt ~typ:Typ.typ_int (term "x") (term "n")) @@ -499,7 +520,15 @@ let () = check_with_style "typed resource formula" typed_style (Trm.trm_apps (term "cell") [ typed_term "v" Typ.typ_int ]) "cell(v)"; - check "ghost call" ghost_call_example "ghost(rewrite()[h := x = y][z : h_out])"; + check "ghost call" ghost_call_example "ghost(rewrite, \"h := x = y\", \"z <- h_out\")"; + + check "surface ghost call uses C-style arguments" + arbitrary_pure_fun_ghost + "ghost(assert_inhabited, \"x := arbitrary(int * (int -> float) -> float)\", \"reduce_sum <- x\")"; + + check "surface hides __ghost_fn type" + (Trm.trm_let (tv "focusA" (Typ.typ_var (Typ.name_to_typvar "__ghost_fn"))) (term "body")) + "let focusA = body"; check_with_style "style hides types" { OL.default_style with print_types = false } (Trm.trm_let (tv "x" Typ.typ_int) (Trm.trm_int 3)) diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index 89f3e027f..94f1cd125 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -23,9 +23,6 @@ "onLanguage:optilambda", "onCommand:optitrust.hello", "onCommand:optitrust.viewDiff", - "onCommand:optitrust.viewDiffOnlyCode", - "onCommand:optitrust.viewDiffInternalSyntax", - "onCommand:optitrust.switchDiffSyntax", "onCommand:optitrust.detachView", "onCommand:optitrust.viewFullTrace", "onCommand:optitrust.viewTraceSaveStepsScript", @@ -71,19 +68,6 @@ "command": "optitrust.viewDiff", "title": "OptiTrust: View Step Diff" }, - { - "command": "optitrust.viewDiffOnlyCode", - "title": "OptiTrust: View Diff Only Code" - }, - { - "command": "optitrust.viewDiffInternalSyntax", - "title": "OptiTrust: View Diff Using Internal Syntax" - }, - { - "command": "optitrust.switchDiffSyntax", - "title": "OptiTrust: Switch Diff Syntax", - "icon": "$(replace)" - }, { "command": "optitrust.detachView", "title": "OptiTrust: Detach View", @@ -247,6 +231,11 @@ ], "description": "Default OptiLambda representation for backend-generated diff and trace views." }, + "optitrust.syntaxHighlightThemePath": { + "type": "string", + "default": "", + "description": "Optional absolute or workspace-relative path to a VS Code color theme JSON file used for OptiTrust diff and trace syntax highlighting. Leave empty to auto-detect the active theme when possible." + }, "optitrust.optinlpProvider": { "type": "string", "default": "gemini", @@ -329,18 +318,8 @@ "key": "f6", "when": "editorTextFocus && config.optitrust.enableKeybindings" }, - { - "command": "optitrust.viewDiffOnlyCode", - "key": "ctrl+f6", - "when": "editorTextFocus && config.optitrust.enableKeybindings" - }, - { - "command": "optitrust.viewDiffInternalSyntax", - "key": "ctrl+shift+f6", - "when": "editorTextFocus && config.optitrust.enableKeybindings" - }, - { - "command": "optitrust.viewFullTrace", + { + "command": "optitrust.viewFullTrace", "key": "shift+f5", "when": "editorTextFocus && config.optitrust.enableKeybindings" }, @@ -394,16 +373,6 @@ "when": "resourceScheme == file && (resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti || resourceExtname == .html || resourceExtname == .js || resourceExtname == .trace)", "group": "navigation@50" }, - { - "command": "optitrust.switchDiffSyntax", - "when": "resourceScheme == optitrust-diff", - "group": "navigation@51" - }, - { - "command": "optitrust.detachView", - "when": "resourceScheme == optitrust-diff", - "group": "navigation@52" - }, { "command": "optitrust.optinlpChat", "when": "resourceScheme == file && (resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti)", @@ -449,15 +418,6 @@ { "command": "optitrust.viewDiff" }, - { - "command": "optitrust.viewDiffOnlyCode" - }, - { - "command": "optitrust.viewDiffInternalSyntax" - }, - { - "command": "optitrust.switchDiffSyntax" - }, { "command": "optitrust.detachView" }, diff --git a/tools/vscode-optitrust/src/commands/shortcuts.ts b/tools/vscode-optitrust/src/commands/shortcuts.ts index be48d31f9..02bd12a1f 100644 --- a/tools/vscode-optitrust/src/commands/shortcuts.ts +++ b/tools/vscode-optitrust/src/commands/shortcuts.ts @@ -14,18 +14,6 @@ const SHORTCUTS: readonly ShortcutItem[] = [ detail: "OptiTrust: View Step Diff", command: "optitrust.viewDiff" }, - { - label: "Ctrl+F6", - description: "View diff only code", - detail: "OptiTrust: View Diff Only Code", - command: "optitrust.viewDiffOnlyCode" - }, - { - label: "Ctrl+Shift+F6", - description: "View diff using internal syntax", - detail: "OptiTrust: View Diff Using Internal Syntax", - command: "optitrust.viewDiffInternalSyntax" - }, { label: "Shift+F5", description: "View full trace", diff --git a/tools/vscode-optitrust/src/commands/viewCommands.ts b/tools/vscode-optitrust/src/commands/viewCommands.ts index 7f893fd82..c0298c949 100644 --- a/tools/vscode-optitrust/src/commands/viewCommands.ts +++ b/tools/vscode-optitrust/src/commands/viewCommands.ts @@ -8,12 +8,11 @@ import { appendLine } from "../optitrust/output"; import { runCommand } from "../optitrust/runner"; import { validateTransformationScript } from "../optitrust/scripts"; import { backendFlagsForViewMode, getSelectedViewMode, VIEW_MODES, ViewModeDefinition } from "../optitrust/viewMode"; -import { openNativeStepDiff } from "../optitrust/nativeDiff"; import { openHtmlView } from "../optitrust/views"; import { OptitrustWorkspace } from "../optitrust/workspace"; type ViewMode = "step_diff" | "full_trace" | "step_trace"; -type ViewOption = "diff-only-code" | "diff-internal-syntax" | "trace-save-steps-script"; +type ViewOption = "trace-save-steps-script"; interface ViewCommandSpec { readonly scriptMode: "step_diff" | "full_trace" | "step_trace"; @@ -92,32 +91,19 @@ export async function runViewCommand(workspace: OptitrustWorkspace, mode: ViewMo } function viewArgs(mode: ViewMode, selectedViewMode: ViewModeDefinition, option?: ViewOption): string[] { - if (option === "diff-only-code") { - return ["-print-only-code"]; - } - if (option === "diff-internal-syntax") { - return ["-print-optitrust-syntax"]; - } if (option === "trace-save-steps-script") { return ["-save-steps", "script"]; } // Full traces use serialized, server-backed data for in-window switching. - // Step diffs are generated lazily by the native VS Code diff integration. + // Step diffs generate the selected syntax first; the HTML diff view requests + // other syntaxes lazily when the user switches representation. if (mode === "full_trace") { return []; } return backendFlagsForViewMode(selectedViewMode); } -export function runViewDiffOnlyCode(workspace: OptitrustWorkspace): Promise { - return runViewCommand(workspace, "step_diff", "diff-only-code"); -} - -export function runViewDiffInternalSyntax(workspace: OptitrustWorkspace): Promise { - return runViewCommand(workspace, "step_diff", "diff-internal-syntax"); -} - export function runViewTraceSaveStepsScript(workspace: OptitrustWorkspace): Promise { return runViewCommand(workspace, "full_trace", "trace-save-steps-script"); } @@ -174,21 +160,6 @@ async function executeViewRequest(workspace: OptitrustWorkspace, request: Stored async function openViewResult(request: StoredViewRequest): Promise { const spec = VIEW_COMMANDS[request.mode]; - if (request.mode === "step_diff") { - await openNativeStepDiff( - { - root: request.context.root, - scriptRelativePath: request.context.relativePath, - line: request.context.line, - fileDir: request.context.fileDir, - fileBase: request.context.fileBase - }, - request.viewMode, - { markGenerated: true, useLiveView: true } - ); - return; - } - const htmlFile = path.join(request.context.fileDir, `${request.context.fileBase}${spec.htmlSuffix}`); if (await fileExists(htmlFile)) { await openHtmlView( @@ -197,7 +168,20 @@ async function openViewResult(request: StoredViewRequest): Promise { spec.viewKind, `${request.viewMode.id}:${request.option ?? "default"}:${request.context.relativePath}`, `${request.context.fileBase} ${spec.viewKind}`, - { useLiveView: true } + { + useLiveView: true, + lazyDiff: + request.mode === "step_diff" + ? { + relativePath: request.context.relativePath, + line: request.context.line + } + : undefined, + initialDiffRepresentation: + request.mode === "step_diff" + ? request.viewMode.optilambdaRepresentation ?? "cpp" + : undefined + } ); } else { appendLine(`Generated view was not found: ${htmlFile}`); diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index 41634a0db..148f5e698 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -27,13 +27,10 @@ import { showShortcuts } from "./commands/shortcuts"; import { redoLastViewCommand, runViewCommand, - runViewDiffInternalSyntax, - runViewDiffOnlyCode, runViewTraceSaveStepsScript } from "./commands/viewCommands"; import { disposeDecorations, updateDecorations } from "./optitrust/decorations"; import { detachLiveView, initializeLiveViewContext, refreshLiveViewContexts } from "./optitrust/liveView"; -import { registerNativeDiffProvider, switchNativeDiffSyntax } from "./optitrust/nativeDiff"; import { appendLine, disposeOutput } from "./optitrust/output"; import { getSelectedViewMode, updateSelectedViewMode, VIEW_MODES } from "./optitrust/viewMode"; import { findOptitrustRoot, OptitrustWorkspace } from "./optitrust/workspace"; @@ -87,7 +84,6 @@ export async function activate(context: vscode.ExtensionContext): Promise await refreshWorkspace(vscode.window.activeTextEditor?.document.uri.fsPath); optiNlpSession = new OptiNlpSessionMemory(); initializeLiveViewContext(); - registerNativeDiffProvider(context); registerOptiNlpChatParticipant(context, requireWorkspace, optiNlpSession); registerCommand(context, "optitrust.hello", async () => { @@ -127,26 +123,6 @@ export async function activate(context: vscode.ExtensionContext): Promise await runViewCommand(workspace, "step_trace"); }); - registerCommand(context, "optitrust.viewDiffOnlyCode", async () => { - const workspace = await requireWorkspace(); - if (!workspace) { - return; - } - await runViewDiffOnlyCode(workspace); - }); - - registerCommand(context, "optitrust.viewDiffInternalSyntax", async () => { - const workspace = await requireWorkspace(); - if (!workspace) { - return; - } - await runViewDiffInternalSyntax(workspace); - }); - - registerCommand(context, "optitrust.switchDiffSyntax", async () => { - await switchNativeDiffSyntax(); - }); - registerCommand(context, "optitrust.detachView", () => { if (detachLiveView()) { vscode.window.showInformationMessage("OptiTrust view detached. The next view command will open a new live view."); diff --git a/tools/vscode-optitrust/src/optitrust/nativeDiff.ts b/tools/vscode-optitrust/src/optitrust/nativeDiff.ts deleted file mode 100644 index f17fc0a47..000000000 --- a/tools/vscode-optitrust/src/optitrust/nativeDiff.ts +++ /dev/null @@ -1,336 +0,0 @@ -import * as fs from "fs/promises"; -import * as path from "path"; -import * as vscode from "vscode"; -import { appendLine } from "./output"; -import { runCommand } from "./runner"; -import { fileExists } from "./fileSystem"; -import { attachLiveView, currentLiveViewSlotId, isAttachedLiveViewUri, prepareAttachedLiveView } from "./liveView"; -import { backendFlagsForViewMode, ViewModeDefinition, VIEW_MODES } from "./viewMode"; - -const OPTITRUST_DIFF_SCHEME = "optitrust-diff"; -const nativeDiffChanges = new vscode.EventEmitter(); - -interface DiffFilePair { - readonly before: string; - readonly after: string; - readonly label: string; -} - -interface NativeDiffSession { - readonly id: string; - readonly root: string; - readonly scriptRelativePath: string; - readonly line: number; - readonly fileDir: string; - readonly fileBase: string; - readonly generatedModes: Set; -} - -export interface NativeStepDiffContext { - readonly root: string; - readonly scriptRelativePath: string; - readonly line: number; - readonly fileDir: string; - readonly fileBase: string; -} - -interface OpenNativeStepDiffOptions { - readonly viewColumn?: vscode.ViewColumn; - readonly markGenerated?: boolean; - readonly generateIfMissing?: boolean; - readonly useLiveView?: boolean; -} - -const sessions = new Map(); - -function stepDiffCandidates(fileDir: string, fileBase: string, selectedViewMode: ViewModeDefinition): DiffFilePair[] { - if (selectedViewMode.id === "optilambda.surface") { - return [ - { - before: path.join(fileDir, `${fileBase}_before.opti`), - after: path.join(fileDir, `${fileBase}_after.opti`), - label: selectedViewMode.label - }, - { - before: path.join(fileDir, `${fileBase}_before_surface.opti`), - after: path.join(fileDir, `${fileBase}_after_surface.opti`), - label: selectedViewMode.label - } - ]; - } - - if (selectedViewMode.id === "optilambda.internal" || selectedViewMode.id === "optilambda.typed") { - const representation = selectedViewMode.optilambdaRepresentation ?? "surface"; - return [ - { - before: path.join(fileDir, `${fileBase}_before_${representation}.opti`), - after: path.join(fileDir, `${fileBase}_after_${representation}.opti`), - label: selectedViewMode.label - } - ]; - } - - return [".cpp", ".c", ".cu"].map(extension => ({ - before: path.join(fileDir, `${fileBase}_before${extension}`), - after: path.join(fileDir, `${fileBase}_after${extension}`), - label: selectedViewMode.label - })); -} - -async function findExistingPair(candidates: DiffFilePair[]): Promise { - for (const candidate of candidates) { - if ((await fileExists(candidate.before)) && (await fileExists(candidate.after))) { - return candidate; - } - } - return undefined; -} - -function sessionId(fileDir: string, fileBase: string, liveSlotId?: number): string { - const baseId = path.resolve(fileDir, fileBase); - return liveSlotId === undefined ? baseId : `${baseId}::live-${liveSlotId}`; -} - -function diffUri(filePath: string, session: NativeDiffSession): vscode.Uri { - const query = new URLSearchParams({ - file: filePath, - session: session.id - }); - return vscode.Uri.from({ - scheme: OPTITRUST_DIFF_SCHEME, - path: `/${path.basename(filePath)}`, - query: query.toString() - }); -} - -function filePathFromUri(uri: vscode.Uri): string { - const filePath = new URLSearchParams(uri.query).get("file"); - if (!filePath) { - throw new Error(`Missing backing file in ${uri.toString()}`); - } - return filePath; -} - -function sessionFromUri(uri: vscode.Uri): NativeDiffSession | undefined { - const query = new URLSearchParams(uri.query); - const id = query.get("session"); - const existing = id ? sessions.get(id) : undefined; - if (existing) { - return existing; - } - - const fileDir = query.get("fileDir"); - const fileBase = query.get("fileBase"); - const root = query.get("root"); - const scriptRelativePath = query.get("scriptRelativePath"); - const line = Number(query.get("line")); - if (!id || !fileDir || !fileBase || !root || !scriptRelativePath || !Number.isInteger(line)) { - return undefined; - } - const restored = { id, root, scriptRelativePath, line, fileDir, fileBase, generatedModes: new Set() }; - sessions.set(id, restored); - return restored; -} - -function activeNativeDiffUri(): vscode.Uri | undefined { - const activeEditorUri = vscode.window.activeTextEditor?.document.uri; - if (activeEditorUri?.scheme === OPTITRUST_DIFF_SCHEME) { - return activeEditorUri; - } - - const input = vscode.window.tabGroups.activeTabGroup.activeTab?.input; - if (input instanceof vscode.TabInputTextDiff && input.modified.scheme === OPTITRUST_DIFF_SCHEME) { - return input.modified; - } - return undefined; -} - -class NativeDiffContentProvider implements vscode.TextDocumentContentProvider { - readonly onDidChange = nativeDiffChanges.event; - - async provideTextDocumentContent(uri: vscode.Uri): Promise { - return fs.readFile(filePathFromUri(uri), "utf8"); - } -} - -export function registerNativeDiffProvider(context: vscode.ExtensionContext): void { - context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(OPTITRUST_DIFF_SCHEME, new NativeDiffContentProvider())); -} - -async function generateStepDiff(session: NativeDiffSession, selectedViewMode: ViewModeDefinition): Promise { - const args = [ - "step_diff", - session.scriptRelativePath, - String(session.line), - ...backendFlagsForViewMode(selectedViewMode) - ]; - - try { - await runCommand({ - cwd: session.root, - command: path.join(session.root, "tools", "view_result.sh"), - args, - title: `OptiTrust: Generate ${selectedViewMode.label} Diff`, - env: { - OPTITRUST_NO_BROWSER: "1" - } - }); - session.generatedModes.add(selectedViewMode.id); - return true; - } catch { - return false; - } -} - -export async function openNativeStepDiff( - context: NativeStepDiffContext, - selectedViewMode: ViewModeDefinition, - options: OpenNativeStepDiffOptions = {} -): Promise { - const id = sessionId(context.fileDir, context.fileBase, options.useLiveView ? currentLiveViewSlotId() : undefined); - const existingSession = sessions.get(id); - const session: NativeDiffSession = - existingSession?.scriptRelativePath === context.scriptRelativePath && existingSession.line === context.line - ? existingSession - : { - id, - root: context.root, - scriptRelativePath: context.scriptRelativePath, - line: context.line, - fileDir: context.fileDir, - fileBase: context.fileBase, - generatedModes: new Set() - }; - sessions.set(session.id, session); - - if (options.markGenerated) { - session.generatedModes.add(selectedViewMode.id); - } - - if (options.generateIfMissing && !session.generatedModes.has(selectedViewMode.id)) { - const generated = await generateStepDiff(session, selectedViewMode); - if (!generated) { - return; - } - } - - await openExistingNativeStepDiff(session, selectedViewMode, options.viewColumn ?? vscode.ViewColumn.Beside, options.useLiveView ?? false); -} - -async function openExistingNativeStepDiff( - session: NativeDiffSession, - selectedViewMode: ViewModeDefinition, - viewColumn: vscode.ViewColumn, - useLiveView: boolean -): Promise { - const candidates = stepDiffCandidates(session.fileDir, session.fileBase, selectedViewMode); - const pair = await findExistingPair(candidates); - if (!pair) { - appendLine(`Generated native diff files were not found for ${session.fileBase} (${selectedViewMode.label}).`); - for (const candidate of candidates) { - appendLine(`Missing candidate: ${candidate.before} <-> ${candidate.after}`); - } - vscode.window.showWarningMessage(`OptiTrust command finished, but generated ${selectedViewMode.label} diff files were not found.`); - return; - } - - const beforeUri = diffUri(pair.before, session); - const afterUri = diffUri(pair.after, session); - const title = `OptiTrust Diff: ${session.fileBase} (${pair.label})`; - if (useLiveView && isAttachedLiveViewUri(beforeUri) && isAttachedLiveViewUri(afterUri)) { - const existingColumn = nativeDiffViewColumn(beforeUri, afterUri); - if (existingColumn !== undefined) { - nativeDiffChanges.fire(beforeUri); - nativeDiffChanges.fire(afterUri); - await vscode.commands.executeCommand( - "vscode.diff", - beforeUri, - afterUri, - title, - { preview: false, viewColumn: existingColumn } - ); - return; - } - } - - const targetColumn = useLiveView ? await prepareAttachedLiveView("native-diff", { replaceSameKind: true }) : viewColumn; - - if (useLiveView) { - attachLiveView({ - kind: "native-diff", - viewColumn: targetColumn, - getViewColumn: () => nativeDiffViewColumn(beforeUri, afterUri), - ownsUri: uri => uri.toString() === beforeUri.toString() || uri.toString() === afterUri.toString(), - dispose: () => closeNativeDiffTabs(beforeUri, afterUri) - }); - } - - await vscode.commands.executeCommand( - "vscode.diff", - beforeUri, - afterUri, - title, - { preview: false, viewColumn: targetColumn } - ); -} - -export async function switchNativeDiffSyntax(): Promise { - const activeUri = activeNativeDiffUri(); - if (!activeUri) { - vscode.window.showWarningMessage("Open an OptiTrust native diff before switching syntax."); - return; - } - - const session = sessionFromUri(activeUri); - if (!session) { - vscode.window.showWarningMessage("This OptiTrust diff can no longer be switched. Re-run View Step Diff."); - return; - } - - const picked = await vscode.window.showQuickPick( - VIEW_MODES.map(mode => ({ - label: mode.label, - description: mode.description, - mode - })), - { - title: "OptiTrust Diff Syntax", - placeHolder: "Select syntax for this diff" - } - ); - - if (!picked) { - return; - } - - await openNativeStepDiff(session, picked.mode, { - viewColumn: vscode.ViewColumn.Active, - generateIfMissing: true, - useLiveView: isAttachedLiveViewUri(activeUri) - }); -} - -function nativeDiffViewColumn(beforeUri: vscode.Uri, afterUri: vscode.Uri): vscode.ViewColumn | undefined { - for (const group of vscode.window.tabGroups.all) { - if (group.tabs.some(tab => isNativeDiffTab(tab, beforeUri, afterUri))) { - return group.viewColumn; - } - } - return undefined; -} - -async function closeNativeDiffTabs(beforeUri: vscode.Uri, afterUri: vscode.Uri): Promise { - const tabs = vscode.window.tabGroups.all.flatMap(group => group.tabs.filter(tab => isNativeDiffTab(tab, beforeUri, afterUri))); - if (tabs.length > 0) { - await vscode.window.tabGroups.close(tabs, true); - } -} - -function isNativeDiffTab(tab: vscode.Tab, beforeUri: vscode.Uri, afterUri: vscode.Uri): boolean { - const input = tab.input; - return ( - input instanceof vscode.TabInputTextDiff && - input.original.toString() === beforeUri.toString() && - input.modified.toString() === afterUri.toString() - ); -} diff --git a/tools/vscode-optitrust/src/optitrust/views.ts b/tools/vscode-optitrust/src/optitrust/views.ts index e303f52b0..cfbfb8d58 100644 --- a/tools/vscode-optitrust/src/optitrust/views.ts +++ b/tools/vscode-optitrust/src/optitrust/views.ts @@ -9,18 +9,38 @@ import { prepareAttachedLiveView, setActiveLiveViewContext } from "./liveView"; +import { appendLine } from "./output"; +import { runCommand } from "./runner"; +import { backendFlagsForViewMode, VIEW_MODES } from "./viewMode"; const panels = new Map(); +const panelStates = new Map(); const MAX_INLINE_ASSET_BYTES = 2 * 1024 * 1024; const LIVE_VIEW_KEY = "optitrust-live-view"; export const OPTITRUST_WEBVIEW_TYPE = "optitrustView"; interface OpenHtmlViewOptions { readonly useLiveView?: boolean; + readonly lazyDiff?: LazyDiffContext; + readonly initialDiffRepresentation?: string; } interface HtmlTransformOptions { readonly includeDetachButton?: boolean; + readonly initialDiffRepresentation?: string; +} + +interface LazyDiffContext { + readonly relativePath: string; + readonly line: number; +} + +interface PanelRuntimeState { + root: string; + htmlFile: string; + includeDetachButton?: boolean; + lazyDiff?: LazyDiffContext; + initialDiffRepresentation?: string; } function webviewKey(filePath: string, viewKind: string, metadata: string): string { @@ -39,10 +59,24 @@ async function htmlWithBase(webview: vscode.Webview, root: string, htmlFile: str const rewritten = rewriteLocalResourceUris(webview, htmlDir, inlined); const withTraceServerBase = injectTraceServerBase(root, htmlFile, rewritten); const withHighlightingConfig = await injectSyntaxHighlightingConfig(withTraceServerBase); - const withDiffSupport = injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)); + const withDiffSupport = injectDiffInitialRepresentation( + injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)), + options.initialDiffRepresentation + ); return options.includeDetachButton ? injectDetachButton(withDiffSupport) : withDiffSupport; } +function injectDiffInitialRepresentation(html: string, representation?: string): string { + if (!representation || !html.includes("diffStrings")) { + return html; + } + const script = ``; + if (html.includes("")) { + return html.replace("", `${script}\n`); + } + return `${script}\n${html}`; +} + function injectTraceServerBase(root: string, htmlFile: string, html: string): string { if (!html.includes("serialized_trace") || html.includes('id="optitrustTraceServerBaseUrl"')) { return html; @@ -188,6 +222,14 @@ interface ThemeJson { interface WebviewHighlightConfig { readonly theme?: ThemeJson; + readonly builtinTheme?: "dark-plus" | "light-plus"; + readonly requestedTheme?: string; + readonly themePath?: string; + readonly themeExtension?: string; + readonly themeLabel?: string; + readonly themeRuleCount?: number; + readonly customRuleCount?: number; + readonly resolutionStatus: "configured-theme" | "vscode-theme" | "shiki-builtin" | "fallback"; } async function injectSyntaxHighlightingConfig(html: string): Promise { @@ -195,9 +237,7 @@ async function injectSyntaxHighlightingConfig(html: string): Promise { return html; } - const config: WebviewHighlightConfig = { - theme: await activeThemeJson() - }; + const config = await activeHighlightConfig(); const script = ``; if (html.includes("")) { return html.replace("", `${script}\n`); @@ -205,16 +245,73 @@ async function injectSyntaxHighlightingConfig(html: string): Promise { return `${script}\n${html}`; } -async function activeThemeJson(): Promise { +async function activeHighlightConfig(): Promise { const activeTheme = vscode.workspace.getConfiguration("workbench").get("colorTheme", ""); - const themePath = activeTheme ? findThemePath(activeTheme) : undefined; - if (!themePath) { + const customRules = customTokenRules(activeTheme); + const configuredThemePath = syntaxHighlightThemePath(); + if (configuredThemePath) { + const theme = await activeThemeJson(activeTheme, configuredThemePath, customRules); + if (theme) { + return { + requestedTheme: activeTheme, + themePath: configuredThemePath, + themeLabel: path.basename(configuredThemePath), + customRuleCount: customRules.length, + theme, + themeRuleCount: themeRules(theme).length, + resolutionStatus: "configured-theme" + }; + } + appendLine(`OptiTrust syntax highlight: configured theme path "${configuredThemePath}" could not be loaded; continuing with automatic theme resolution.`); + } + + const resolved = activeTheme ? findTheme(activeTheme) : undefined; + const configBase = { + requestedTheme: activeTheme, + themePath: resolved?.path, + themeExtension: resolved?.extensionId, + themeLabel: resolved?.label, + customRuleCount: customRules.length + }; + if (!resolved) { + const builtinTheme = builtinShikiTheme(activeTheme); + if (builtinTheme) { + return { + ...configBase, + builtinTheme, + resolutionStatus: "shiki-builtin" + }; + } + appendLine(`OptiTrust syntax highlight: VS Code theme "${activeTheme || "(empty)"}" was not found; webviews will use Shiki fallback colors.`); + return { + ...configBase, + resolutionStatus: "fallback" + }; + } + const theme = await activeThemeJson(activeTheme, resolved.path, customRules); + return { + ...configBase, + theme, + themeRuleCount: themeRules(theme ?? {}).length, + resolutionStatus: theme ? "vscode-theme" : "fallback" + }; +} + +function syntaxHighlightThemePath(): string | undefined { + const configuredPath = vscode.workspace.getConfiguration("optitrust").get("syntaxHighlightThemePath", "").trim(); + if (!configuredPath) { return undefined; } + if (path.isAbsolute(configuredPath)) { + return configuredPath; + } + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + return workspaceRoot ? path.resolve(workspaceRoot, configuredPath) : path.resolve(configuredPath); +} +async function activeThemeJson(activeTheme: string, themePath: string, customRules: readonly TextMateRule[]): Promise { try { const theme = await loadThemeJson(themePath); - const customRules = customTokenRules(activeTheme); const tokenColors = [...themeRules(theme), ...customRules]; return { ...theme, @@ -227,21 +324,115 @@ async function activeThemeJson(): Promise { } } -function findThemePath(activeTheme: string): string | undefined { +interface ThemeResolution { + readonly path: string; + readonly extensionId: string; + readonly label?: string; +} + +function findTheme(activeTheme: string): ThemeResolution | undefined { + const names = themeLookupNames(activeTheme); for (const extension of vscode.extensions.all) { const themes = extension.packageJSON?.contributes?.themes; if (!Array.isArray(themes)) { continue; } for (const theme of themes) { - if ((theme.id === activeTheme || theme.label === activeTheme) && typeof theme.path === "string") { - return path.join(extension.extensionPath, theme.path); + if (typeof theme.path === "string" && themeMatches(theme, names)) { + return { + path: path.join(extension.extensionPath, theme.path), + extensionId: extension.id, + label: themeLabel(theme) + }; } } } return undefined; } +function builtinShikiTheme(activeTheme: string): "dark-plus" | "light-plus" | undefined { + switch (normalizeThemeName(activeTheme)) { + case "dark 2026": + case "default dark modern": + case "dark modern": + case "default dark+": + case "default dark plus": + case "dark+": + case "dark plus": + case "visual studio dark": + return "dark-plus"; + case "light 2026": + case "default light modern": + case "light modern": + case "default light+": + case "default light plus": + case "light+": + case "light plus": + case "visual studio light": + return "light-plus"; + default: + return undefined; + } +} + +function themeLabel(theme: unknown): string | undefined { + if (!isRecord(theme)) { + return undefined; + } + for (const value of [theme.label, theme.name, theme.id]) { + if (typeof value === "string") { + return value; + } + } + return undefined; +} + +function themeLookupNames(activeTheme: string): Set { + const names = new Set(); + const normalized = normalizeThemeName(activeTheme); + if (normalized) { + names.add(normalized); + names.add(normalized.replace(/^default /u, "")); + } + + const aliases: Record = { + "default dark modern": ["dark modern"], + "default light modern": ["light modern"], + "default dark+": ["dark+"], + "default light+": ["light+"], + "default dark plus": ["dark+"], + "default light plus": ["light+"], + "dark+": ["dark plus"], + "light+": ["light plus"] + }; + for (const alias of aliases[normalized] ?? []) { + names.add(alias); + } + return names; +} + +function themeMatches(theme: unknown, names: ReadonlySet): boolean { + if (!isRecord(theme)) { + return false; + } + return [ + theme.id, + theme.label, + theme.name + ].some(value => typeof value === "string" && names.has(normalizeThemeName(value))); +} + +function normalizeThemeName(name: string): string { + return name + .trim() + .replace(/^%|%$/gu, "") + .replace(/color theme label$/iu, "") + .replace(/theme label$/iu, "") + .replace(/([a-z])([A-Z])/gu, "$1 $2") + .replace(/\s+/gu, " ") + .toLowerCase(); +} + async function loadThemeJson(themePath: string, seen: Set = new Set()): Promise { const resolved = path.resolve(themePath); if (seen.has(resolved)) { @@ -388,35 +579,46 @@ function injectDetachButton(html: string): string { const detachHtml = ` + + @@ -54,6 +55,7 @@
+
From a302818ed8514c7d87101f7a37e86aba1c518eee Mon Sep 17 00:00:00 2001 From: Malih Assaad Date: Wed, 22 Jul 2026 05:32:16 -0400 Subject: [PATCH 36/47] Clean up HTML diff view interactivity --- tools/vscode-optitrust/package.json | 8 -- .../src/commands/associatedFiles.ts | 54 +------- tools/vscode-optitrust/src/extension.ts | 3 - tools/vscode-optitrust/src/optitrust/files.ts | 52 -------- .../src/optitrust/liveView.ts | 5 +- tools/vscode-optitrust/src/optitrust/views.ts | 51 +++++-- tools/web_view/diff_template.html | 10 +- tools/web_view/optitrust_interactive_code.js | 126 ++++++++++++++---- 8 files changed, 149 insertions(+), 160 deletions(-) diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index 94f1cd125..a2cc49010 100644 --- a/tools/vscode-optitrust/package.json +++ b/tools/vscode-optitrust/package.json @@ -33,7 +33,6 @@ "onCommand:optitrust.runCurrentTestAndOpenDiff", "onCommand:optitrust.openGeneratedOutput", "onCommand:optitrust.openExpectedOutput", - "onCommand:optitrust.compareOutputExpected", "onCommand:optitrust.openAssociatedFiles", "onCommand:optitrust.openUnitTestMlCppFiles", "onCommand:optitrust.selectViewSyntax", @@ -109,10 +108,6 @@ "command": "optitrust.openExpectedOutput", "title": "OptiTrust: Open Expected Output" }, - { - "command": "optitrust.compareOutputExpected", - "title": "OptiTrust: Compare Output With Expected" - }, { "command": "optitrust.openAssociatedFiles", "title": "OptiTrust: Open Associated Files", @@ -448,9 +443,6 @@ { "command": "optitrust.openExpectedOutput" }, - { - "command": "optitrust.compareOutputExpected" - }, { "command": "optitrust.openAssociatedFiles" }, diff --git a/tools/vscode-optitrust/src/commands/associatedFiles.ts b/tools/vscode-optitrust/src/commands/associatedFiles.ts index c5e0cbaad..241ec1a38 100644 --- a/tools/vscode-optitrust/src/commands/associatedFiles.ts +++ b/tools/vscode-optitrust/src/commands/associatedFiles.ts @@ -1,14 +1,13 @@ import * as path from "path"; import * as vscode from "vscode"; import { getActiveEditorContext } from "../optitrust/editor"; -import { AssociatedFile, findAssociatedFiles, OPTITRUST_C_SOURCE_EXTENSIONS, outputPairs, pickAssociatedFile } from "../optitrust/files"; +import { AssociatedFile, findAssociatedFiles, OPTITRUST_C_SOURCE_EXTENSIONS, pickAssociatedFile } from "../optitrust/files"; import { fileExists } from "../optitrust/fileSystem"; import { openFileOrHtml } from "../optitrust/views"; import { OptitrustWorkspace } from "../optitrust/workspace"; type AssociatedQuickPickItem = vscode.QuickPickItem & { readonly all?: true; - readonly pair?: Awaited>[number]; readonly file?: AssociatedFile; }; @@ -75,50 +74,16 @@ export async function openExpectedOutput(workspace: OptitrustWorkspace): Promise await openAssociated(workspace, files, "No expected output file found for the current file."); } -export async function compareOutputExpected(): Promise { - const pairs = await outputPairs(activePathOrThrow()); - if (pairs.length === 0) { - vscode.window.showInformationMessage("No generated/expected output pair found for the current file."); - return; - } - - const selected = - pairs.length === 1 - ? pairs[0] - : ( - await vscode.window.showQuickPick( - pairs.map(pair => ({ - label: pair.label, - description: `${path.basename(pair.out)} <-> ${path.basename(pair.exp)}`, - pair - })), - { placeHolder: "Select output pair to compare" } - ) - )?.pair; - - if (!selected) { - return; - } - - await vscode.commands.executeCommand( - "vscode.diff", - vscode.Uri.file(selected.out), - vscode.Uri.file(selected.exp), - `${path.basename(selected.out)} <-> ${path.basename(selected.exp)}` - ); -} - export async function openAssociatedFiles(workspace: OptitrustWorkspace): Promise { const context = getActiveEditorContext(workspace.root); const files = await findAssociatedFiles(context.filePath); - const pairs = await outputPairs(context.filePath); if (files.length === 0) { vscode.window.showInformationMessage("No associated files found for the current file."); return; } // Keep the editor-title button compact: one command opens a QuickPick that - // exposes bulk open, pair comparison, and individual file navigation. + // exposes bulk open and individual file navigation. const frequentFiles = files.filter(isFrequentAssociatedFile); const otherFiles = files.filter(file => !isFrequentAssociatedFile(file)); const items: AssociatedQuickPickItem[] = [ @@ -127,11 +92,6 @@ export async function openAssociatedFiles(workspace: OptitrustWorkspace): Promis description: `${files.length} file(s)`, all: true }, - ...pairs.map(pair => ({ - label: `Compare ${pair.label}`, - description: `${path.basename(pair.out)} <-> ${path.basename(pair.exp)}`, - pair - })), ...associatedFileGroup("Frequent files", frequentFiles), ...associatedFileGroup("Other files", otherFiles) ]; @@ -152,16 +112,6 @@ export async function openAssociatedFiles(workspace: OptitrustWorkspace): Promis return; } - if ("pair" in picked && picked.pair) { - await vscode.commands.executeCommand( - "vscode.diff", - vscode.Uri.file(picked.pair.out), - vscode.Uri.file(picked.pair.exp), - `${path.basename(picked.pair.out)} <-> ${path.basename(picked.pair.exp)}` - ); - return; - } - if ("file" in picked && picked.file) { await openFileOrHtml(workspace.root, picked.file.path, picked.file.label); } diff --git a/tools/vscode-optitrust/src/extension.ts b/tools/vscode-optitrust/src/extension.ts index 148f5e698..eb8e0a819 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -1,6 +1,5 @@ import * as vscode from "vscode"; import { - compareOutputExpected, openAssociatedFiles, openExpectedOutput, openGeneratedOutput, @@ -173,8 +172,6 @@ export async function activate(context: vscode.ExtensionContext): Promise } }); - registerCommand(context, "optitrust.compareOutputExpected", compareOutputExpected); - registerCommand(context, "optitrust.openAssociatedFiles", async () => { const workspace = await requireWorkspace(); if (workspace) { diff --git a/tools/vscode-optitrust/src/optitrust/files.ts b/tools/vscode-optitrust/src/optitrust/files.ts index a778cba0c..c694fd86b 100644 --- a/tools/vscode-optitrust/src/optitrust/files.ts +++ b/tools/vscode-optitrust/src/optitrust/files.ts @@ -1,7 +1,6 @@ import * as fs from "fs/promises"; import * as path from "path"; import * as vscode from "vscode"; -import { fileExists } from "./fileSystem"; export interface AssociatedFile { readonly label: string; @@ -9,33 +8,14 @@ export interface AssociatedFile { readonly path: string; } -export interface OutputPair { - readonly label: string; - readonly out: string; - readonly exp: string; -} - export const OPTITRUST_C_SOURCE_EXTENSIONS = [".cpp", ".cc", ".cxx", ".c"] as const; const OPTITRUST_PRIMARY_INPUT_EXTENSIONS = [...OPTITRUST_C_SOURCE_EXTENSIONS, ".opti"] as const; const C_SOURCE_EXTENSION_PRIORITY: ReadonlyMap = new Map(OPTITRUST_C_SOURCE_EXTENSIONS.map((ext, index) => [ext, index])); -const OUTPUT_EXTENSION_LABELS = new Map([ - [".cpp", "C++ output"], - [".cc", "C++ output"], - [".cxx", "C++ output"], - [".c", "C output"], - [".opti", "OptiLambda output"] -]); const KIND_ORDER: AssociatedFile["kind"][] = ["script", "input", "generated", "expected", "diff", "trace", "other"]; const OPTILAMBDA_REPRESENTATIONS = ["surface", "internal", "typed"] as const; type OptilambdaRepresentation = (typeof OPTILAMBDA_REPRESENTATIONS)[number]; -const OPTILAMBDA_REPRESENTATION_LABELS: Record = { - surface: "Surface", - internal: "Internal", - typed: "Fully-Typed" -}; - export function baseNameForAssociatedFiles(filePath: string): { dir: string; base: string } { const parsed = path.parse(filePath); const base = normalizeAssociatedBase(parsed.name, parsed.ext); @@ -166,38 +146,6 @@ function cSourcePriority(file: AssociatedFile): number { return C_SOURCE_EXTENSION_PRIORITY.get(path.extname(file.path)) ?? Number.MAX_SAFE_INTEGER; } -/** - * Detect output/expected pairs generically. VS Code's native diff command can - * then compare any supported output language without command-specific code. - */ -export async function outputPairs(filePath: string): Promise { - const { dir, base } = baseNameForAssociatedFiles(filePath); - const pairs: OutputPair[] = []; - for (const [ext, label] of OUTPUT_EXTENSION_LABELS) { - const pair = { - label, - out: path.join(dir, `${base}_out${ext}`), - exp: path.join(dir, `${base}_exp${ext}`) - }; - if ((await fileExists(pair.out)) && (await fileExists(pair.exp))) { - pairs.push(pair); - } - } - - for (const representation of OPTILAMBDA_REPRESENTATIONS) { - const pair = { - label: `OptiLambda ${OPTILAMBDA_REPRESENTATION_LABELS[representation]} output`, - out: path.join(dir, `${base}_out_${representation}.opti`), - exp: path.join(dir, `${base}_exp_${representation}.opti`) - }; - if ((await fileExists(pair.out)) && (await fileExists(pair.exp))) { - pairs.push(pair); - } - } - - return pairs; -} - export async function pickAssociatedFile(files: AssociatedFile[], placeHolder: string): Promise { if (files.length === 0) { return undefined; diff --git a/tools/vscode-optitrust/src/optitrust/liveView.ts b/tools/vscode-optitrust/src/optitrust/liveView.ts index fb05a5cf6..5cfb5fde0 100644 --- a/tools/vscode-optitrust/src/optitrust/liveView.ts +++ b/tools/vscode-optitrust/src/optitrust/liveView.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -type LiveViewKind = "native-diff" | "html"; +type LiveViewKind = "html"; interface AttachedLiveView { readonly kind: LiveViewKind; @@ -100,9 +100,6 @@ function activeEditorIsAttachedLiveView(): boolean { function activeEditorUris(): vscode.Uri[] { const input = vscode.window.tabGroups.activeTabGroup.activeTab?.input; - if (input instanceof vscode.TabInputTextDiff) { - return [input.original, input.modified]; - } if (input instanceof vscode.TabInputText) { return [input.uri]; } diff --git a/tools/vscode-optitrust/src/optitrust/views.ts b/tools/vscode-optitrust/src/optitrust/views.ts index cfbfb8d58..dc5638f41 100644 --- a/tools/vscode-optitrust/src/optitrust/views.ts +++ b/tools/vscode-optitrust/src/optitrust/views.ts @@ -28,6 +28,7 @@ interface OpenHtmlViewOptions { interface HtmlTransformOptions { readonly includeDetachButton?: boolean; readonly initialDiffRepresentation?: string; + readonly detached?: boolean; } interface LazyDiffContext { @@ -41,6 +42,7 @@ interface PanelRuntimeState { includeDetachButton?: boolean; lazyDiff?: LazyDiffContext; initialDiffRepresentation?: string; + detached?: boolean; } function webviewKey(filePath: string, viewKind: string, metadata: string): string { @@ -63,7 +65,7 @@ async function htmlWithBase(webview: vscode.Webview, root: string, htmlFile: str injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)), options.initialDiffRepresentation ); - return options.includeDetachButton ? injectDetachButton(withDiffSupport) : withDiffSupport; + return options.includeDetachButton ? injectDetachButton(withDiffSupport, options.detached ?? false) : withDiffSupport; } function injectDiffInitialRepresentation(html: string, representation?: string): string { @@ -571,11 +573,14 @@ document.addEventListener('DOMContentLoaded', function () { return `${html}\n${fallbackScript}`; } -function injectDetachButton(html: string): string { +function injectDetachButton(html: string, detached: boolean): string { if (html.includes('id="optitrustDetachViewButton"')) { return html; } + const disabled = detached ? " disabled" : ""; + const label = detached ? "Detached" : "Detach"; + const title = detached ? "This OptiTrust view is detached" : "Detach this OptiTrust view"; const detachHtml = ` - +