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/lib/framework/runtime/run.ml b/lib/framework/runtime/run.ml index e0f558f74..ee2059b2c 100644 --- a/lib/framework/runtime/run.ml +++ b/lib/framework/runtime/run.ml @@ -2,6 +2,13 @@ (* Debug *) (******************************************************************************) include Tools +open Ast +open Trm + +(** [absolute_path path] normalizes [path] against the current working directory. *) +let absolute_path (path : string) : string = + 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) (** [set_exn_backtrace b]: based on [b] enable or disable backtracing in case an exception was thrown *) let set_exn_backtrace (b : bool) : unit = @@ -42,18 +49,71 @@ 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 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" + +(** [inline_parser basepath inline] parses an inlined C/C++ source and makes + declarations from explicitly inlined files behave as main-file code. *) +let inline_parser (basepath : string) (inline : string list) : Trace.parser = + let inlined_paths = + List.map (fun filename -> absolute_path (Filename.concat basepath filename)) inline + in + let is_inlined_file filename = + let filename = absolute_path filename in + List.exists ((=) filename) inlined_paths + in + let flatten_inlined_includes (t : trm) : trm = + match t.desc with + | Trm_seq (instrs, result) -> + let instrs = + Mlist.to_list instrs + |> List.concat_map (fun instr -> + match trm_include_inv instr, instr.desc with + | Some filename, Trm_seq (included_instrs, None) when is_inlined_file filename -> + Mlist.to_list included_instrs + | _ -> + [instr]) + |> Mlist.of_list + in + trm_alter ~desc:(Trm_seq (instrs, result)) t + | _ -> + t + in + fun filename -> + let header, ast = Trace.parse filename in + header, flatten_inlined_includes ast + (** [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 +172,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) ?(parser : Trace.parser 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 +209,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 ?parser ~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 +305,9 @@ let script_cpp ?(filename : string option) ?(prepro : string list = []) ?(inline *) (* Handles on-the-fly inlining *) - let filename = + let filename, parser = match inline with - | [] -> filename + | [] -> filename, None | _ -> let program_basename = get_program_basename () in let basepath = Filename.dirname program_basename in @@ -259,10 +319,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 (inline_parser basepath inline) in - script ?filename ~capture_show_in_batch ~extension:".cpp" ~check_exit_at_end ?prefix f) + script ?filename ?parser ~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 7d0d57407..21c8875dd 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 *) @@ -1333,8 +1333,10 @@ let invalidate () : unit = (** [get_initial_ast filename]: gets the initial ast before applying any trasformations [filename] - filename of the source code returns header and ast. *) -let get_initial_ast (filename : string) : (string * trm) = - parse filename +let get_initial_ast ?(parser : parser option) (filename : string) : (string * trm) = + match parser with + | None -> parse filename + | Some parser -> parser filename (** [init f]: initializes the trace with the contents of the file [f]. This operation should be the first in a transformation script. @@ -1342,7 +1344,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) ?(parser : parser 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 +1376,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 ?parser filename) in + let header = Option.value ~default:parsed_header header in let context = { extension; prefix; header } in the_trace.next_step_id <- 0; @@ -1815,19 +1818,25 @@ 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 + (* 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 (** [produce_trace_output step] is an auxiliary function for [produce_output_and_exit] *) let produce_trace_output (step:step_tree) : unit = 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..813f051c4 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 @@ -32,11 +33,43 @@ let block_doc docs = | [] -> lbrace ^^ rbrace | _ -> 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 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 } +(** [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 @@ -45,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 @@ -67,6 +101,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" @@ -96,12 +157,53 @@ 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 + | "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 + | 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 + 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 + 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) @@ -126,7 +228,11 @@ 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 = + 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 = @@ -274,12 +380,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]]. *) @@ -335,6 +447,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 @@ -363,18 +482,150 @@ 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_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 + 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]. *) +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; _ }, [ 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 + 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 (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 + | 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 (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 ^^ trm_to_doc_at style 0 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. *) +(** [simplify_linear_contract pre post] recovers user-facing [reads], [writes], and [preserves] clauses from desugared + linear resources. It only recovers preserved [_RO] resources, [Uninit] writes, and unchanged resources with the same + hypothesis name; transformations that reshape resources remain explicit. *) and simplify_linear_contract (pre : resource_item list) (post : resource_item list) : - resource_item list * resource_item list * resource_item list * resource_item list * var list = + resource_item list * resource_item list * resource_item list * resource_item list * resource_item list * var list = let rec find_remove pred before = function | [] -> None | item :: rest -> @@ -383,15 +634,15 @@ and simplify_linear_contract (pre : resource_item list) (post : resource_item li | None -> find_remove pred (item :: before) rest end in - let rec aux kept_pre reads writes used_fracs remaining_post = function - | [] -> (List.rev kept_pre, remaining_post, List.rev reads, List.rev writes, List.rev used_fracs) + let rec aux kept_pre reads writes preserves used_fracs remaining_post = function + | [] -> (List.rev kept_pre, remaining_post, List.rev reads, List.rev writes, List.rev preserves, List.rev used_fracs) | ((pre_hyp, pre_formula) as pre_item) :: rest -> begin match read_only_formula_inv pre_formula with | Some pre_ro -> 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 @@ -402,74 +653,127 @@ and simplify_linear_contract (pre : resource_item list) (post : resource_item li | Some frac -> frac :: used_fracs | None -> used_fracs in - aux kept_pre ((pre_hyp, post_ro.read_body) :: reads) writes used_fracs remaining_post rest - | None -> aux (pre_item :: kept_pre) reads writes used_fracs remaining_post rest + aux kept_pre ((pre_hyp, post_ro.read_body) :: reads) writes preserves used_fracs remaining_post rest + | None -> aux (pre_item :: kept_pre) reads writes preserves used_fracs remaining_post rest end | None -> - let pred (post_hyp, post_formula) = + let write_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 in - begin match find_remove pred [] remaining_post with + begin match find_remove write_pred [] remaining_post with | Some (post_formula, remaining_post) -> - aux kept_pre reads ((pre_hyp, post_formula) :: writes) used_fracs remaining_post rest - | None -> aux (pre_item :: kept_pre) reads writes used_fracs remaining_post rest + aux kept_pre reads ((pre_hyp, post_formula) :: writes) preserves used_fracs remaining_post rest + | None -> + let preserve_pred (post_hyp, post_formula) = + if pre_hyp = post_hyp && same_formula pre_formula post_formula then Some post_formula else None + in + begin match find_remove preserve_pred [] remaining_post with + | Some (post_formula, remaining_post) -> + aux kept_pre reads writes ((pre_hyp, post_formula) :: preserves) used_fracs remaining_post rest + | None -> aux (pre_item :: kept_pre) reads writes preserves used_fracs remaining_post rest + end end end in - aux [] [] [] [] post pre + aux [] [] [] [] [] post pre (** [remove_used_fraction_requirements used_fracs pure] drops generated [_Fraction] hypotheses that only support recovered [reads] clauses. *) 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 + | 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 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 rec aux cur_keyword cur_items acc clauses = + 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 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 - | 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 + | [] -> List.rev (flush_groups groups order acc) + | ContractRaw doc :: rest -> aux [] [] (doc :: flush_groups groups order acc) rest + | 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 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 = if not style.print_contracts then [] else - let consumes, produces, reads, writes, used_fracs = simplify_linear_contract contract.pre.linear contract.post.linear in + let consumes, produces, reads, writes, preserves, 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 @ preserves @ 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 "preserves" preserves + @ 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 = @@ -483,15 +787,32 @@ 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 parallel_reads, used_fracs = surface_parallel_reads style contract.parallel_reads in + let xconsumes, xproduces, xreads, xwrites, xpreserves, xused_fracs = + simplify_linear_contract contract.iter_contract.pre.linear contract.iter_contract.post.linear + in + let used_fracs = used_fracs @ xused_fracs 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 @ parallel_reads @ iter_pre_pure + @ xreads @ xwrites @ xpreserves @ xconsumes @ iter_post_pure @ xproduces + 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 "srequires" invariant_pure + @ contract_clauses ~used_vars "spreserves" contract.invariant.linear + @ contract_clauses ~used_vars "sreads" parallel_reads + @ contract_clauses ~used_vars "xrequires" iter_pre_pure + @ contract_clauses ~used_vars "xreads" xreads + @ contract_clauses ~used_vars "xwrites" xwrites + @ contract_clauses ~used_vars "xpreserves" xpreserves + @ contract_clauses ~used_vars "xconsumes" xconsumes + @ contract_clauses ~used_vars "xensures" iter_post_pure + @ contract_clauses ~used_vars "xproduces" xproduces (** [fun_spec_items spec] collects resources mentioned by a function spec. *) and fun_spec_items (spec : fun_spec) : resource_item list = @@ -508,8 +829,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 = @@ -556,10 +880,10 @@ and fun_def_to_doc (style : Optilambda_style.style) ?(type_params = []) (name : else empty in let contract_docs = contract_clauses_to_docs style (fun_spec_clauses style spec) in - let contract_summary_doc = if is_ghost then empty else contract_summary_to_doc style (fun_spec_items spec) in + let contract_summary_doc = if is_ghost || is_surface style 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 @@ -584,7 +908,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 @@ -608,6 +932,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 -> @@ -658,6 +986,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)) @@ -670,7 +1003,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 = @@ -700,11 +1051,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 @@ -742,7 +1105,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 @@ -792,11 +1154,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 @@ -847,15 +1213,37 @@ 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 +(** [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) diff --git a/lib/optilambda/optilambda_syntax.md b/lib/optilambda/optilambda_syntax.md index b21bb8741..f206c91da 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 @@ -102,7 +108,7 @@ v.x Function definitions: ```optilambda -fun f[A](x: A, y: B): A [h1, h2] { +fun f[A](x: A, y: B): A { requires h1: x = y; produces h2: y = x; @@ -110,6 +116,10 @@ fun f[A](x: A, y: B): A [h1, h2] { } ``` +Surface function headers print argument and return types, but omit contract-name +summaries. Contract details are printed as clauses in the function body. Hidden +details remain available in the Internal and Fully-Typed representations. + Ghost functions hide the internal `__ghost_ret` return type in Surface syntax: ```optilambda @@ -126,6 +136,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 @@ -142,7 +170,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 +197,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 @@ -318,6 +343,10 @@ preserves Loop/body clauses: ```text +srequires +sreads +smodifies +spreserves xrequires xensures xreads @@ -325,6 +354,7 @@ xwrites xmodifies xpreserves xconsumes +xproduces ``` Examples: @@ -341,23 +371,40 @@ Resource points-to formulas use infix notation in all three representations: (src ~> Matrix1(length, model)) ``` -Function contracts recover compact `reads` and `writes` clauses in all three -representations when the desugared resources match the safe user-facing -patterns: +Function and loop contracts recover compact `reads`, `writes`, and `preserves` +families in all three representations when the desugared resources match the +safe user-facing patterns: ```optilambda reads h: H writes h: H +preserves h: H +sreads h: H +xreads h: H +xwrites h: H +xpreserves h: H ``` `reads` means that the same fractional read-only permission `_RO(f, H)` is present in both the precondition and the postcondition. `writes` means that an 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`. +`preserves` means that the same named resource `H` is consumed and produced +unchanged. The `s*` forms describe shared loop resources, while the `x*` forms +describe exclusive per-iteration loop resources. Transformations that change the +resource shape, split or join fractions, or produce a `Wand(...)` stay explicit +as `consumes` / `produces` or `xconsumes` / `xproduces`. -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..30f8a2840 100644 --- a/tests_infra/optilambda/printcpp.cpp +++ b/tests_infra/optilambda/printcpp.cpp @@ -291,9 +291,32 @@ void arrow() { #include - - - +void loop_contract_clause_examples() { + __pure(); + int n = 64; + int* a; + int* b; + int read_value = 0; + int kept_value = 0; + int written_value; + int shared_sum = 0; + int shared_tmp = 0; + + for (int k = 0; k < n; k++) { + __srequires("n_nonneg: n >= 0"); + __sreads("for i in 0..n -> &a[MINDEX1(n, i)] ~~> A(i)"); + __spreserves("&shared_sum ~~> reduce_int_sum(0, k, fun i -> A(i))"); + __smodifies("&shared_tmp ~~> k"); + __xrequires("k_nonneg: k >= 0"); + __xensures("k_done: k + 1 > 0"); + __xreads("&read_value ~~> 0"); + __xpreserves("&kept_value ~~> 0"); + __xwrites("&written_value ~~> k"); + __xconsumes("input: IterInput(k)"); + __xproduces("output: IterOutput(k)"); + written_value = read_value + b[MINDEX1(n, k)]; + } +} void one_fork () { __pure(); @@ -360,4 +383,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..184a75c3c 100644 --- a/tests_infra/optilambda/printcpp_exp.opti +++ b/tests_infra/optilambda/printcpp_exp.opti @@ -16,20 +16,27 @@ type vect3 = vect2; type int2 = array(int, 2); type intstar = ptr(int); + fun addr_array_cell(): unit { letmut p; letmut n = p[0]; }; + + fun initlist(): unit { letmut v1 = {1, 2}; letmut v2 = {1, 2}; letmut p = {1, 2}; letmut n = (p)[0]; }; + + fun f(n: int): int { let __res: int = n; __res }; + + fun test_loop(): unit { letmut a = 0; for i in 0..10 { __ignore(post_incr(a)); }; @@ -42,18 +49,24 @@ }; let z: int = x + y; }; + + fun stack_var(): unit { letmut r = 3; r = r + 1 + 2; - (+=)(r, 2); + r += 2; __ignore(post_incr(r)); letmut s = f(r); }; + + fun stack_array(): unit { letmut t = array(5, 6); letmut a = t[0]; t[1] = a + 2; }; + + fun stack_struct(): unit { letmut v = {5, 6}; letmut a = v.x; @@ -63,11 +76,15 @@ letmut p1 = {v, v}; letmut p2 = {v, {7, 8}}; }; + + fun references(): unit { letmut a = 3; let b: ptr(int) = a; b = b + 4; }; + + fun constants(): unit { let a: int = 3; let b: int = a + 3; @@ -75,11 +92,15 @@ let v: vect = {0, 1}; letmut d = v.x; }; + + fun const_pointers(): unit { letmut a = 3; letmut b = a; let c: int = b + 4; }; + + fun nonconst_pointers(): unit { letmut a = 3; letmut b = a; @@ -87,12 +108,18 @@ letmut c = 3; b = c; }; + + fun main(): int {}; + + fun h(x: int): int { letmut y = x + 1; let __res: int = y; __res }; + + fun immutable_stack_ptr(): int { letmut x = 3; letmut y = f(x); @@ -104,6 +131,8 @@ let __res: int = p + q + r; __res }; + + fun immutable_stack_array(): int { letmut x = 3; letmut y = 4; @@ -115,12 +144,16 @@ let __res: int = t[0]; __res }; + + fun immutable_stack_var(): int { let a: int = 4; let r: int = 3; let s: int = r + 1; r }; + + fun mutable_stack_var(): int { letmut r = 3; r = r + 1; @@ -128,6 +161,8 @@ let __res: int = r; __res }; + + fun mutable_stack_array(): int { letmut x = 3; letmut y = 4; @@ -137,6 +172,8 @@ let __res: int = w[0]; __res }; + + fun access_encoding(): unit { let a: vect = {0, 1}; let b: vect = a; @@ -144,10 +181,14 @@ let ax: int = a.x; let cy: int = c.y; }; + + fun foo(v: vect): int { let __res: int = v.x; __res }; + + fun mutable_var_encoding(): int { let a: vect = {0, 1}; letmut ax = foo(a); @@ -156,10 +197,12 @@ let __res: int = cx; __res }; + record vectpair { fst: vect; snd: vect }; + fun lvalue_encoding(): unit { letmut p; (p).x = 2; @@ -170,277 +213,378 @@ letmut v; v = 4; }; + + fun arrow(): unit { 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 { + fun __ghost_begin(#2, #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 { - let __res: __ghost_fn = g; + + + fun __ghost_end(#5): unit {}; + + + fun __with_reverse(g, g_rev): __ghost_fn { + let __res = g; __res }; - fun __reverts(#6: __ghost_fn): unit {}; + + + fun __reverts(#6): unit {}; + + ghost fun __clear(#7: __ghost_args) {}; + + ghost fun assert_inhabited() { - requires T: Type, - x: T; + requires x: T; ensures x: T; }; + + ghost fun define() {}; + + ghost fun assert_prop() { requires P: Prop, proof: P; ensures proof: P; }; + + ghost fun assert_eq() { requires x: int, y: int, - eq: __is_true(x = y); + eq: x = y; }; + + ghost fun assert_alias() {}; + + ghost fun rewrite_prop() { requires from: int, to: int, - inside: pure_fun(fun(#71: int): Prop), - by: __is_true(from = to), - #70: inside(from); + by: from = to, + 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); + by: from = 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); + by: from = to, + 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); + by: from = 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() { requires x: f32; - ensures out: __is_true(x = x); + ensures out: x = x; __admitted(); }; + + ghost fun eq_sym_float() { requires x: f32, y: f32, - H: __is_true(x = y); - ensures out: __is_true(y = x); + H: x = y; + ensures out: y = x; __admitted(); }; + + ghost fun assume() { requires P: Prop; ensures H: P; __admitted(); }; + + ghost fun to_prove() { requires P: Prop; ensures H: P; __admitted(); }; + + 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(int * int -> n - d + d = n)", "z_cancel_minus_plus <- proof"); + ghost(assert_prop, "proof := admit(int * int -> n + d - d = n)", "z_cancel_plus_minus <- proof"); + + fun wrap_z_cancel_minus_plus(m: int): int { requires n: int, d: int, - #86: __is_true(m = n - d + d); - ensures #85: __is_true(_Res = n); + m = n - d + d; + ensures _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) -> _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]); + + ghost(assert_prop, "proof := admit(float * float -> n - d + d = n)", "r_cancel_minus_plus <- proof"); + ghost(assert_prop, "proof := admit(float * float -> n + d - d = n)", "r_cancel_plus_minus <- proof"); + fun MINDEX0(): int { let __res: int = 0; __res }; + + fun MINDEX1(N1: int, i1: int): int { let __res: int = i1; __res }; + + fun MINDEX2(N1: int, N2: int, i1: int, i2: int): int { let __res: int = i1 * N2 + i2; __res }; + + fun MINDEX3(N1: int, N2: int, N3: int, i1: int, i2: int, i3: int): int { 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 { 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 { let __res: int = i1 * N2 * N3 * N4 * N5 + i2 * N3 * N4 * N5 + i3 * N4 * N5 + i4 * N5 + i5; __res }; + + fun DMINDEX0(): int { let __res: int = 0; __res }; + + fun DMINDEX1(N1: int, i1: int): int { let __res: int = 0; __res }; + + fun DMINDEX2(N1: int, N2: int, i1: int, i2: int): int { let __res: int = 0; __res }; + + fun DMINDEX3(N1: int, N2: int, N3: int, i1: int, i2: int, i3: int): int { let __res: int = 0; __res }; + + fun DMINDEX4(N1: int, N2: int, N3: int, N4: int, i1: int, i2: int, i3: int, i4: int): int { 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 { let __res: int = 0; __res }; + + fun MSIZE0(): usize { let __res: int = 1; __res }; + + fun MSIZE1(N1: int): usize { let __res: usize = cast(N1); __res }; + + fun MSIZE2(N1: int, N2: int): usize { let __res: usize = cast(N1) * cast(N2); __res }; + + fun MSIZE3(N1: int, N2: int, N3: int): usize { let __res: usize = cast(N1) * cast(N2) * cast(N3); __res }; + + fun MSIZE4(N1: int, N2: int, N3: int, N4: int): usize { 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 { let __res: usize = cast(N1) * cast(N2) * cast(N3) * cast(N4) * cast(N5); __res }; + + fun exact_div(n: int, b: int): int { __admitted(); let __res: int = n / b; __res }; + + fun min(a: int, b: int): int { __admitted(); if (a < b) { a } else { b } }; + + fun max(a: int, b: int): int { __admitted(); if (a > b) { a } else { b } }; - fun maxf(a: f32, b: f32): f32 { + + + fun maxf(a: float, b: float): float { __admitted(); if (a > b) { a } else { b } }; + + fun ANY(maxValue: int): int { 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(HProp * HProp -> 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() { reverts hide; - ghost(close_wand()); + ghost(close_wand); }; + + ghost fun wand_simplify() { 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(int * Range -> Prop)", "in_range <- x"); + ghost(assert_inhabited, "x := arbitrary(Range * Range -> Prop)", "is_subrange <- x"); + ghost(assert_inhabited, "x := arbitrary(Range -> 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() { requires x: int, k: int, 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() { requires x: int, k: int, @@ -448,947 +592,1045 @@ 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]); + 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"); }; + + ghost fun in_range_bounds() { requires x: int, a: int, b: int, s: int, - #107: in_range(x, range(a, b, s)), - #106: __is_true(s >= 0); - ensures lower_bound: __is_true(x >= a), - upper_bound: __is_true(x < b); + in_range(x, range(a, b, s)), + s >= 0; + ensures lower_bound: x >= a, + upper_bound: x < b; __admitted(); }; + + ghost fun in_range_bounds_rev() { requires x: int, a: int, b: int, s: int, - #109: in_range(x, range(a, b, s)), - #108: __is_true(s < 0); - ensures lower_bound: __is_true(x > b), - upper_bound: __is_true(x <= a); + in_range(x, range(a, b, s)), + s < 0; + ensures lower_bound: x > b, + upper_bound: x <= a; __admitted(); }; + + ghost fun bounds_to_in_range() { requires x: int, a: int, b: int, - lower_bound: __is_true(x >= a), - upper_bound: __is_true(x < b); - ensures range_check: in_range(x, range(a, b, 1)); + lower_bound: x >= a, + upper_bound: x < b; + ensures range_check: in_range(x, a..b); __admitted(); }; + + ghost fun expand_subrange() { requires a: int, b: int, c: int, 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)); + lower: s >= 0, + up_ineq: b <= c; + 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(__frac_div(f, 2), H), - #111: _RO(__frac_div(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(__frac_div(f, 3), H), - #115: _RO(__frac_div(f, 3), H), - #114: _RO(__frac_div(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(__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); + 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(__frac_div(f, 2), H); - produces #123: _RO(__frac_sub(f, __frac_div(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(__frac_div(f, 3), H); - produces #125: _RO(__frac_sub(__frac_sub(f, __frac_div(f, 3)), __frac_div(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(__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 _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(__frac_div(f, range_count(r)), Group(r, fun(#129: int) { - 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: 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 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() { reverts swap_groups; __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, 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 _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() { reverts ro_swap_groups; __admitted(); }; + + ghost fun tiled_index_in_range() { requires tile_index: int, index: int, tile_count: int, 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)); - __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]); + div_check: size = tile_count * tile_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(int -> n = n)", "eq_refl <- proof"); + ghost(assert_prop, "proof := admit(int * int * (m = n) -> n = m)", "eq_sym <- proof"); + ghost(assert_prop, "proof := admit(int -> 0 = 0 * n)", "zero_mul_intro <- proof"); + ghost(assert_prop, "proof := admit(int -> n = n + 0)", "plus_zero_intro <- proof"); + ghost(assert_prop, "proof := admit(int * int * int -> m + n + p = m + (n + p))", "add_assoc_right <- proof"); + ghost(assert_prop, "proof := admit(int * int -> 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(range(0, size, 1), items); - produces #143: for bi in 0..tile_count { - for i in 0..tile_size { items(bi * tile_size + i) } - }; + div_check: size = tile_count * tile_size, + positive_tile_size: tile_size >= 0; + 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() { reverts tile_divides; __admitted(); }; + + ghost fun ro_tile_divides() { 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), + div_check: size = tile_count * tile_size, + positive_tile_size: 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 _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() { reverts ro_tile_divides; __admitted(); }; + + 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() { reverts group_collapse; __admitted(); }; + + 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() { reverts ro_group_collapse; __admitted(); }; + + 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() { reverts group_focus; __admitted(); - ghost(close_wand()); + ghost(close_wand); }; + + 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() { reverts ro_group_focus; __admitted(); - ghost(close_wand()); + ghost(close_wand); }; + + ghost fun ro_group2_focus() { 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, 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 _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() { reverts ro_group2_focus; - ghost(close_wand()); + ghost(close_wand); }; + + 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() { reverts group_focus_subrange; - ghost(close_wand()); + ghost(close_wand); }; + + 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() { reverts ro_group_focus_subrange; - ghost(close_wand()); + ghost(close_wand); }; + + ghost fun group2_focus_subrange() { 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: 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 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() { reverts group2_focus_subrange; __admitted(); }; + + ghost fun group_shift() { 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) - }; + check_start: new_start = start + shift, + check_stop: new_stop = stop + 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() { reverts group_shift; __admitted(); }; + + ghost fun ro_group_shift() { 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), + check_start: new_start = start + shift, + check_stop: 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() { reverts ro_group_shift; __admitted(); }; + + 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) - }; + check_stop: new_stop = factor * stop, + check_step: new_step = factor * step, + check_factor: factor <> 0; + 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() { reverts group_scale; __admitted(); }; + + 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), + check_stop: new_stop = factor * stop, + check_step: 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() { reverts ro_group_scale; __admitted(); }; + + ghost fun group_split() { requires start: int, 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() { reverts group_split; __admitted(); }; + + ghost fun ro_group_split() { requires start: int, 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() { reverts ro_group_split; __admitted(); }; + + ghost fun pure_group_split() { requires start: int, 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 int * in_range(i, range(start, split, step)) -> items(i), + int * in_range(i, range(split, stop, step)) -> items(i); __admitted(); }; + + ghost fun pure_group_join() { reverts pure_group_split; __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() { reverts group_intro_zero; __admitted(); }; + + ghost fun group_elim_empty() { reverts group_intro_empty; __admitted(); }; + + 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() { reverts group_intro_one; __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() { reverts dmindex2_untile; __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() { reverts dmindex3_untile; __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() { reverts mindex2_unfold; __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() { reverts mindex3_unfold; __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() { reverts ro_mindex2_unfold; __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(Prop * HProp -> 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(b, H); + produces If(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(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; + HP: b; + consumes If(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); + HP: b; + consumes H; + produces If(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)) }; + expand_check: n1 <= n2; + consumes for i in 0..n1 -> items(i); + produces for i in 0..n2 -> If(i < n1, items(i)); __admitted(); }; + + ghost fun group_shrink_r_if_elim() { reverts group_expand_r_if_intros; __admitted(); }; + + 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(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(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(int * int * int * (e1 <= e2) -> 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: DesyncGroup(N, fun(i: int) { items(i) }); + preserves ThreadsCtx(r); + consumes for i in 0..N -> items(i); + produces 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; + preserves ThreadsCtx(counted_range(t, MSIZE0())); + consumes DesyncGroup(MSIZE0(), H); + produces 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: DesyncGroup(tile_count, fun(bi: int) { - DesyncGroup(tile_size, fun(i: int) { items(bi * tile_size + i) }) - }); + div_check: size = tile_count * tile_size, + positive_tile_size: tile_size >= 0; + 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() { reverts desync_tile_divides; __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, 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 _RO(f, for i1 in 0..n -> matrix[MINDEX1(n, i1)] ~> CellOf(MT)(M(i1))); + produces Wand(_RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)(M(i))), _RO(f, for i1 in 0..n -> matrix[MINDEX1(n, i1)] ~> CellOf(MT)(M(i1)))), + _RO(f, matrix[MINDEX1(n, i)] ~> CellOf(MT)(M(i))); __admitted(); - ghost(ro_group_focus()[f := f, i := i, bound_check := bound_check]); + ghost(ro_group_focus, "f := f", "i := i", "bound_check := bound_check"); }; + + ghost fun ro_matrix1_unfocus() { reverts ro_matrix1_focus; __admitted(); - ghost(close_wand()); + 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, 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 _RO(f, for i1 in 0..m -> for i2 in 0..n -> matrix[MINDEX2(m, n, i1, i2)] ~> CellOf(MT)(M(i1, i2))); + produces Wand(_RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)(M(i, j))), _RO(f, for i1 in 0..m -> for i2 in 0..n -> matrix[MINDEX2(m, n, i1, i2)] ~> CellOf(MT)(M(i1, i2)))), + _RO(f, matrix[MINDEX2(m, n, i, j)] ~> CellOf(MT)(M(i, j))); __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]); - ghost(wand_simplify()); + 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"); + ghost(wand_simplify); }; + + ghost fun ro_matrix2_unfocus() { reverts ro_matrix2_focus; __admitted(); - ghost(close_wand()); + 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: ptr(int), src: ptr(int), length: int): unit { + reads for i1 in 0..length -> src[MINDEX1(length, i1)] ~> CellOf(Any)(model(i1)); + writes for i1 in 0..length -> dest[MINDEX1(length, i1)] ~> CellOf(Any)(model(i1)); __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: ptr(int), src: ptr(int), n1: int, n2: int): unit { + reads for i1 in 0..n1 -> for i2 in 0..n2 -> src[MINDEX2(n1, n2, i1, i2)] ~> CellOf(Any)(model(i1, i2)); + writes for i1 in 0..n1 -> for i2 in 0..n2 -> dest[MINDEX2(n1, n2, i1, i2)] ~> CellOf(Any)(model(i1, i2)); __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: ptr(int), src: ptr(int), n1: int, n2: int, n3: int): unit { + reads for i1 in 0..n1 -> for i2 in 0..n2 -> for i3 in 0..n3 -> src[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(Any)(model(i1, i2, i3)); + writes for i1 in 0..n1 -> for i2 in 0..n2 -> for i3 in 0..n3 -> dest[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(Any)(model(i1, i2, i3)); __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: ptr(float), src: ptr(float), length: int): unit { + reads for i1 in 0..length -> src[MINDEX1(length, i1)] ~> CellOf(Any)(model(i1)); + writes for i1 in 0..length -> dest[MINDEX1(length, i1)] ~> CellOf(Any)(model(i1)); __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: ptr(float), src: ptr(float), n1: int, n2: int): unit { + reads for i1 in 0..n1 -> for i2 in 0..n2 -> src[MINDEX2(n1, n2, i1, i2)] ~> CellOf(Any)(model(i1, i2)); + writes for i1 in 0..n1 -> for i2 in 0..n2 -> dest[MINDEX2(n1, n2, i1, i2)] ~> CellOf(Any)(model(i1, i2)); __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: ptr(float), src: ptr(float), n1: int, n2: int, n3: int): unit { + reads for i1 in 0..n1 -> for i2 in 0..n2 -> for i3 in 0..n3 -> src[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(Any)(model(i1, i2, i3)); + writes for i1 in 0..n1 -> for i2 in 0..n2 -> for i3 in 0..n3 -> dest[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(Any)(model(i1, i2, i3)); __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: ptr(double), src: ptr(double), length: int): unit { + reads for i1 in 0..length -> src[MINDEX1(length, i1)] ~> CellOf(Any)(model(i1)); + writes for i1 in 0..length -> dest[MINDEX1(length, i1)] ~> CellOf(Any)(model(i1)); __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: ptr(double), src: ptr(double), n1: int, n2: int): unit { + reads for i1 in 0..n1 -> for i2 in 0..n2 -> src[MINDEX2(n1, n2, i1, i2)] ~> CellOf(Any)(model(i1, i2)); + writes for i1 in 0..n1 -> for i2 in 0..n2 -> dest[MINDEX2(n1, n2, i1, i2)] ~> CellOf(Any)(model(i1, i2)); __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: ptr(double), src: ptr(double), n1: int, n2: int, n3: int): unit { + reads for i1 in 0..n1 -> for i2 in 0..n2 -> for i3 in 0..n3 -> src[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(Any)(model(i1, i2, i3)); + writes for i1 in 0..n1 -> for i2 in 0..n2 -> for i3 in 0..n3 -> dest[MINDEX3(n1, n2, n3, i1, i2, i3)] ~> CellOf(Any)(model(i1, i2, i3)); __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)(M(i)); + produces for i in 0..(b - a) -> matrix[a][MINDEX1(b - a, i)] ~> CellOf(MT)(M(i + a)); __admitted(); }; + + ghost fun matrix1_span_unshift() { reverts matrix1_span_shift; __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)(M(i, j)); + produces for i in 0..(b - a) -> for j in 0..n2 -> matrix[a * n2][MINDEX2(b - a, n2, i, j)] ~> CellOf(MT)(M(i + a, j)); __admitted(); }; + + ghost fun matrix2_span_unshift() { reverts matrix2_span_shift; __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)(M(i1, i2, i3)); + 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)(M(i1 + a, i2, i3)); __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(int * int * (int -> int) -> int)", "reduce_int_sum <- x"); + ghost(assert_prop, "proof := admit(int * (int -> int) -> 0 = reduce_int_sum(n, n, f))", "reduce_int_sum_empty <- proof"); + ghost(assert_prop, "proof := admit(int * int * (int -> int) * (b >= a) * int * (bp1 = b + 1) -> 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(int * int * (int -> int) * (b > a) * int * (ap1 = a + 1) -> 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(int * int * int * int * (int -> int) * (b >= a) * (ap1 = a + 1) * (bp1 = b + 1) -> reduce_int_sum(a, b, f) + (f(b) - f(a)) = reduce_int_sum(ap1, bp1, f))", "reduce_int_sum_slide <- proof"); + }; + + fun loop_contract_clause_examples(): unit { + letmut n = 64; + letmut a; + letmut b; + letmut read_value = 0; + letmut kept_value = 0; + letmut written_value; + letmut shared_sum = 0; + letmut shared_tmp = 0; + for k in 0..n { + srequires n_nonneg: *n >= 0; + spreserves shared_sum ~> CellOf(Any)(reduce_int_sum(0, k, fun(i) -> A(i))), + shared_tmp ~> CellOf(Any)(k); + sreads for i in 0..(*n) -> (*a)[MINDEX1(*n, i)] ~> CellOf(Any)(A(i)); + xrequires k_nonneg: k >= 0; + xreads read_value ~> CellOf(Any)(0); + xwrites written_value ~> CellOf(Any)(k); + xpreserves kept_value ~> CellOf(Any)(0); + xconsumes input: IterInput(k); + xensures k_done: k + 1 > 0; + xproduces output: IterOutput(k); + written_value = read_value + (b)[MINDEX1(n, k)]; + }; }; + + 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] { + let fork_out = ghost_begin(ghost(ro_fork_group, "H := x ~> CellOf(Any)(0)", "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] { + xreads x ~> CellOf(Any)(0); + ghost(ro_split2, "f := #_1", "H := x ~> CellOf(Any)(0)"); + ghost(ro_fork_group, "f := #_1 / 2", "H := x ~> CellOf(Any)(0)", "r := 0..5"); + ghost(ro_allow_join2, "f := #_1", "H := x ~> CellOf(Any)(0)"); + for j in 0..5 { strict; - requires #346: _Fraction; - xconsumes #345: _RO(#346, (x ~> CellOf(Any))); - xproduces #345: _RO(#346, (x ~> CellOf(Any))); + xreads x ~> CellOf(Any)(0); __ignore(x + 1); }; - ghost(ro_join_group()[H := (x ~> CellOf(Any)), r := range(0, 5, 1)]); + ghost(ro_join_group, "H := x ~> CellOf(Any)(0)", "r := 0..5"); }; - __ghost_end(fork_out); + 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_begin(ghost(ro_fork_group, "H := x ~> CellOf(Any)(0)", "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] { + xreads x ~> CellOf(Any)(0); + ghost(ro_split3, "f := #_1", "H := x ~> CellOf(Any)(0)"); + ghost(ro_fork_group, "f := #_1 / 3", "H := x ~> CellOf(Any)(0)", "r := 0..5"); + ghost(ro_fork_group, "f := #_1 / 3", "H := x ~> CellOf(Any)(0)", "r := 0..5"); + ghost(ro_allow_join3, "f := #_1", "H := x ~> CellOf(Any)(0)"); + for j in 0..5 { strict; - requires #350: _Fraction; - xconsumes #349: _RO(#350, (x ~> CellOf(Any))); - xproduces #349: _RO(#350, (x ~> CellOf(Any))); + xreads x ~> CellOf(Any)(0); __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)(0)", "r := 0..5"); + ghost(ro_join_group, "H := x ~> CellOf(Any)(0)", "r := 0..5"); }; - __ghost_end(fork_out); + 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_begin(ghost(ro_fork_group, "H := x ~> CellOf(Any)(0)", "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] { + xreads x ~> CellOf(Any)(0); + ghost(ro_split2, "f := #_1", "H := x ~> CellOf(Any)(0)"); + ghost(ro_split2, "f := #_1 / 2", "H := x ~> CellOf(Any)(0)"); + ghost(ro_fork_group, "f := #_1 / 2", "H := x ~> CellOf(Any)(0)", "r := 0..5"); + ghost(ro_fork_group, "f := #_1 / 2 / 2", "H := x ~> CellOf(Any)(0)", "r := 0..5"); + ghost(ro_allow_join2, "f := #_1 / 2", "H := x ~> CellOf(Any)(0)"); + for j in 0..5 { strict; - requires #354: _Fraction; - xconsumes #353: _RO(#354, (x ~> CellOf(Any))); - xproduces #353: _RO(#354, (x ~> CellOf(Any))); + xreads x ~> CellOf(Any)(0); __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)(0)", "r := 0..5"); + ghost(ro_allow_join2, "f := #_1", "H := x ~> CellOf(Any)(0)"); + ghost(ro_join_group, "H := x ~> CellOf(Any)(0)", "r := 0..5"); }; - __ghost_end(fork_out); + ghost_end(fork_out); }; } diff --git a/tests_infra/optilambda/printer_basic.ml b/tests_infra/optilambda/printer_basic.ml index 09cafc4a0..e9dc7ee72 100644 --- a/tests_infra/optilambda/printer_basic.ml +++ b/tests_infra/optilambda/printer_basic.ml @@ -57,6 +57,63 @@ let surface_writes_contract = post = resource_set ~linear:[ (v "x", body) ] (); } +let preserves_contract = + { + pre = resource_set ~linear:[ (v "ctx", term "Ctx"); (v "changed", term "Old") ] (); + post = resource_set ~linear:[ (v "ctx", term "Ctx"); (v "changed_out", term "New") ] (); + } + +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 + 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 @@ -85,10 +142,55 @@ let simple_loop_contract = }; } +let detailed_loop_contract = + let shared_frac = term "sf" in + let iter_frac = term "xf" in + { + empty_loop_contract with + invariant = + resource_set + ~pure:[ (v "s_inv", Trm.trm_le ~typ:Typ.typ_int (Trm.trm_int 0) (term "i")) ] + ~linear:[ (v "s_ctx", term "SharedCtx"); (v "s_tmp", term "SharedTmp") ] + (); + parallel_reads = [ (v "s_read", read_only_formula shared_frac (term "SharedRead")) ]; + iter_contract = + { + pre = + resource_set + ~pure:[ (v "xf", term "_Fraction") ] + ~linear: + [ + (v "x_read", read_only_formula iter_frac (term "IterRead")); + (v "x_write", uninit_formula (term "IterWrite")); + (v "x_keep", term "IterKeep"); + (v "x_in", term "IterIn"); + ] + (); + post = + resource_set + ~pure:[ (v "x_ens", Trm.trm_gt ~typ:Typ.typ_int (Trm.trm_add ~typ:Typ.typ_int (term "i") (Trm.trm_int 1)) (Trm.trm_int 0)) ] + ~linear: + [ + (v "x_read", read_only_formula iter_frac (term "IterRead")); + (v "x_write", term "IterWrite"); + (v "x_keep", term "IterKeep"); + (v "x_out", term "IterOut"); + ] + (); + }; + } + 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 @@ -239,7 +341,7 @@ 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 [h_req, h_in, h_ens, h_out] {\n\ + "fun f(x: int, y: int): int {\n\ \ requires h_req: x = y;\n\ \ consumes h_in: R;\n\ \ ensures h_ens: result = x;\n\ @@ -257,9 +359,23 @@ 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_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 @@ -309,12 +425,52 @@ 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(): unit { 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(): unit { writes x: H; }"; + + check "surface preserves contract" + (Trm.trm_let_fun ~contract:(FunSpecContract preserves_contract) (v "preserve_example") Typ.typ_unit [] + (Trm.trm_seq_nomarks [])) + "fun preserve_example(): unit {\n\ + \ preserves ctx: Ctx;\n\ + \ consumes changed: Old;\n\ + \ produces changed_out: New;\n\ + }"; + + 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 { 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(): unit {\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 {\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 {\n\ + \ reads read: for i in 0..n -> H(i);\n\ + }"; check_with_style "internal reads contract" internal_style @@ -340,7 +496,35 @@ let () = (Trm.trm_seq_nomarks [])) "fun write_example(): unit [x, x] { writes x: H; }"; - let focus_expected = + check_with_style "internal preserves contract" + internal_style + (Trm.trm_let_fun ~contract:(FunSpecContract preserves_contract) (v "preserve_example") Typ.typ_unit [] + (Trm.trm_seq_nomarks [])) + "fun preserve_example(): unit [ctx, changed, ctx, changed_out] {\n\ + \ preserves ctx: Ctx;\n\ + \ consumes changed: Old;\n\ + \ produces changed_out: New;\n\ + }"; + + check_with_style "typed preserves contract" + typed_style + (Trm.trm_let_fun ~contract:(FunSpecContract preserves_contract) (v "preserve_example") Typ.typ_unit [] + (Trm.trm_seq_nomarks [])) + "fun preserve_example(): unit [ctx, changed, ctx, changed_out] {\n\ + \ preserves ctx: Ctx;\n\ + \ consumes changed: Old;\n\ + \ produces changed_out: New;\n\ + }"; + + let surface_focus_expected = + "fun focus_example(): unit {\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\ @@ -351,33 +535,58 @@ 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 { 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\ - \ requires h_loop: i < n,\n\ - \ h_inv: 0 <= i;\n\ + "for i in 0..n {\n\ + \ requires h_loop: i < n;\n\ + \ srequires h_inv: 0 <= i;\n\ \ xrequires h_xreq: i < n;\n\ \ xproduces h_xprod: Done;\n\ \ 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 "loop shared and exclusive contract clauses" + (Trm.trm_for ~contract:detailed_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 {\n\ + \ srequires s_inv: 0 <= i;\n\ + \ spreserves s_ctx: SharedCtx,\n\ + \ s_tmp: SharedTmp;\n\ + \ sreads s_read: SharedRead;\n\ + \ xreads x_read: IterRead;\n\ + \ xwrites x_write: IterWrite;\n\ + \ xpreserves x_keep: IterKeep;\n\ + \ xconsumes x_in: IterIn;\n\ + \ xensures x_ens: i + 1 > 0;\n\ + \ xproduces x_out: IterOut;\n\ + \ x = x + 1;\n\ + }"; + + 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"; @@ -402,7 +611,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/optiNLP/README.md b/tools/optiNLP/README.md new file mode 100644 index 000000000..e1a73b429 --- /dev/null +++ b/tools/optiNLP/README.md @@ -0,0 +1,297 @@ +# OptiNLP + +OptiNLP helps an AI assistant generate OptiTrust targets and transformation +scripts from natural-language requests. It combines prompt assets in this +directory with the VS Code extension and a small CLI. + +OptiNLP currently supports three workflows: + +1. generate an OptiTrust target; +2. generate a transformation script from an explicit command; +3. generate a full transformation script from the active source file. + +Private internship notes under `practice/` may be used as background while +designing prompts, but generated OptiNLP artifacts must not be written there or +copy private text from there. + +## Directory Guide + +```text +tools/optiNLP/ + README.md + prompts/ + 01_target_generator.md + 02_command_to_script.md + 03_code_to_full_script.md + knowledge/ + target_description.md + targets.md + script_patterns.md + transformations.md + optilambda.md + eval/ + target_cases.md + command_to_script_cases.md + code_to_script_cases.md + target_prompt_smoke.md +``` + +- `prompts/`: mode-specific instructions sent to the AI provider. +- `knowledge/`: stable OptiTrust context loaded with the prompts. +- `eval/`: manual test cases for checking whether prompt outputs are good. + +The VS Code and CLI implementation lives under: + +```text +tools/vscode-optitrust/src/optinlp/ +tools/vscode-optitrust/src/commands/optinlp*.ts +``` + +## Setup + +OptiNLP is part of the OptiTrust VS Code extension. For the general extension +requirements, `.vsix` packaging, installation commands, and workspace detection +rules, read: + +```text +tools/vscode-optitrust/README.md +``` + +After the extension setup is complete, compile it from the extension directory: + +```bash +cd tools/vscode-optitrust +npm run compile +``` + +For local OptiNLP or extension development: + +```bash +npm run dev:extension +``` + +The CLI uses the compiled extension output, so run `npm run compile` again after +changing TypeScript files. + +## Provider Setup + +OptiNLP supports these providers: + +- `gemini`, the default provider; +- `openai`; +- `mock`, for deterministic local testing without an API key. + +In VS Code, use the command palette: + +```text +OptiTrust: OptiNLP Select Provider +OptiTrust: OptiNLP Set Model +OptiTrust: OptiNLP Set API Key +``` + +Provider settings are: + +```json +"optitrust.optinlpProvider": "gemini", +"optitrust.optinlpModel": "", +"optitrust.optinlpUseProviderSession": true +``` + +For CLI usage, API keys are read from the environment: + +```bash +export GEMINI_API_KEY=... +export OPENAI_API_KEY=... +``` + +To test without a remote provider: + +```bash +export OPTINLP_PROVIDER=mock +``` + +## VS Code Usage + +Open the OptiTrust repository in VS Code, then open a C/C++ source file or an +OptiTrust transformation script. + +Main commands: + +```text +OptiTrust: Open OptiNLP Chat +OptiTrust: OptiNLP Generate Target +OptiTrust: OptiNLP Generate Script +OptiTrust: OptiNLP Generate Full Transformation +OptiTrust: OptiNLP Suggest Target At Cursor +OptiTrust: OptiNLP Clear Session +``` + +Native chat participant: + +```text +@optinlp /target target the second loop named i +@optinlp /script unroll the loop i +@optinlp /full generate a complete transformation script for this file +@optinlp /config +@optinlp /clear +@optinlp /help +``` + +When possible, OptiNLP uses the active editor and associated source files as +context. Generated target results can be inserted into the editor. Generated +scripts can be opened as new editor documents. + +## CLI Usage + +Compile the extension first: + +```bash +cd tools/vscode-optitrust +npm run compile +``` + +Then run: + +```bash +npm run optinlp -- target --file ../../tests/loop/unroll/loop_unroll.cpp --request "target the loop i" +npm run optinlp -- script --file ../../tests/loop/unroll/loop_unroll.cpp --request "unroll the loop i" +npm run optinlp -- full --file ../../tests/loop/unroll/loop_unroll.cpp --request "generate a complete transformation script for this file" +``` + +Useful options: + +```text +--json +--provider gemini|mock|openai +--model MODEL_NAME +--root /path/to/optitrust +--session-summary "..." +``` + +Example with the mock provider: + +```bash +npm run optinlp -- target \ + --provider mock \ + --file ../../tests/loop/unroll/loop_unroll.cpp \ + --request "target the loop i" +``` + +## Modes + +### Target Generation + +Input: + +```text +target the second loop named i +``` + +Expected kind of output: + +```ocaml +[occIndex 1; cFor "i"] +``` + +Prompt: + +```text +prompts/01_target_generator.md +``` + +### Command To Script + +Input: + +```text +unroll the loop i +``` + +Expected kind of output: + +```ocaml +open Optitrust +open Target + +let _ = Run.script_cpp (fun _ -> + !! Loop.unroll [cFor "i"]; +) +``` + +Prompt: + +```text +prompts/02_command_to_script.md +``` + +### Full Script Generation + +Input: + +```text +generate a complete transformation script for this file +``` + +Expected output: + +- code summary; +- candidate transformations; +- full generated script; +- assumptions; +- validation steps. + +Prompt: + +```text +prompts/03_code_to_full_script.md +``` + +## Testing + +Run OptiNLP TypeScript tests: + +```bash +cd tools/vscode-optitrust +npm run test:optinlp +``` + +Check prompt-kit formatting: + +```bash +git diff --check -- tools/optiNLP +``` + +Manual prompt evaluation: + +1. Choose a prompt from `prompts/`. +2. Include the relevant files from `knowledge/`. +3. Run one case from `eval/`. +4. Compare the AI output with the expected target, script, or candidate result. + +The evaluation files are intentionally small and readable so prompt failures can +be diagnosed by hand. + +## Updating Prompts + +When changing a prompt: + +1. update the matching file in `prompts/`; +2. update or add examples in `eval/`; +3. update `knowledge/` if the prompt depends on new OptiTrust APIs; +4. run `git diff --check -- tools/optiNLP`; +5. run `npm run test:optinlp` from `tools/vscode-optitrust` if the output schema + or prompt sections changed. + +Keep prompt section names stable when possible. The VS Code integration parses +known Markdown sections to offer editor actions such as inserting targets or +opening generated scripts. + +## Current Limitations + +- OptiNLP is only as reliable as its prompts, knowledge files, and provider + output. +- Generated scripts still need normal OptiTrust validation. +- OptiLambda is currently a printer-oriented representation; do not generate + `Run.script_opti` unless parser support is added later. +- The `ollama` provider is listed as a possible provider id in code but is not + implemented yet. 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..f0fdb1989 --- /dev/null +++ b/tools/optiNLP/eval/code_to_script_cases.md @@ -0,0 +1,134 @@ +# Code To Full Script Evaluation Cases + +Use these cases to manually test `prompts/03_code_to_full_script.md`. + +## Case 1: Simple Loop + +Input: + +```c +void f(int n) { + for (int i = 0; i < n; i++) { + work(i); + } +} +``` + +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 `Full Transformation Script` section must contain a complete `.ml` script, not just + `[cFor "i"]`. + +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. +- Emit the complete script in the `Full Transformation Script` section. + +## 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 full-script behavior: + +- 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. +- Emit the complete script in the `Full Transformation Script` section. + +## 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. + +## 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/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..072140cfb --- /dev/null +++ b/tools/optiNLP/eval/target_cases.md @@ -0,0 +1,431 @@ +# 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`. + +## 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 new file mode 100644 index 000000000..db13d07ef --- /dev/null +++ b/tools/optiNLP/eval/target_prompt_smoke.md @@ -0,0 +1,64 @@ +# 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"]`; +- 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. + +## 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; +- 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 + +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. + +## 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..6172b8406 --- /dev/null +++ b/tools/optiNLP/knowledge/optilambda.md @@ -0,0 +1,41 @@ +# 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. + +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`. +- 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`. +- For now, generated runnable scripts should use `Run.script_cpp`. + +Visible OptiLambda cues: + +- 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. +- Function contracts use clauses such as `reads`, `writes`, `preserves`, + `consumes`, and `produces`. Loop contracts distinguish shared resources with + `sreads` / `spreserves` and per-iteration exclusive resources with + `xreads`, `xwrites`, `xpreserves`, `xconsumes`, and `xproduces`. +- 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 new file mode 100644 index 000000000..0da2f5ffe --- /dev/null +++ b/tools/optiNLP/knowledge/script_patterns.md @@ -0,0 +1,210 @@ +# OptiTrust Script Patterns + +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 +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. + +## 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: + +```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"]; +) +``` + +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 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. + +## 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/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 new file mode 100644 index 000000000..4bf38a480 --- /dev/null +++ b/tools/optiNLP/knowledge/targets.md @@ -0,0 +1,640 @@ +# 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. + +## 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. + +## 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`. +- 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. +- 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 + +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. + +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 +that operate at a position rather than directly on a node: + +```ocaml +tBefore +tAfter +tFirst +tLast +tBetweenAll +tSpan [START_TARGET] [STOP_TARGET] +tSpanSeq [SEQ_TARGET] +tSpanAround [INSTR_TARGET] +``` + +Examples: + +```ocaml +[tBefore; cVarDef "x"] +[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 +inserting, moving, fissioning, or selecting a boundary. + +## Structural Selectors + +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: + +```ocaml +[cTopFunDef "main"; cCall "foo"] +[cFunBody "main"; cFor "i"] +[cFor "i"; cArrayWrite "A"] +[cFor "i" ~body:[cArrayWrite "out"]] +[cCall "foo" ~args:[[cVar "x"]]] +``` + +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. +- 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 + +- 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 + +String-based selectors are useful when a more semantic selector is unavailable: + +```ocaml +sInstr "x++;" +sExpr "i + 1" +sInstrRegexp "A\\[.*\\]" +sExprRegexp "MINDEX.*" +[cIf ~cond:[sExpr "x < n"] (); dThen] +``` + +Prefer semantic constructors such as `cFor`, `cCall`, `cVarDef`, `cArrayRead`, +`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. + +## 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: + +- quote exact identifiers as OCaml strings; +- use current OptiTrust target constructors only; +- 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/knowledge/transformations.md b/tools/optiNLP/knowledge/transformations.md new file mode 100644 index 000000000..34c3be458 --- /dev/null +++ b/tools/optiNLP/knowledge/transformations.md @@ -0,0 +1,237 @@ +# OptiTrust Transformation Knowledge + +This file is a compact orientation map for prompt generation. It is not a full +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 + +- `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 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 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 new file mode 100644 index 000000000..3fa0c2118 --- /dev/null +++ b/tools/optiNLP/prompts/01_target_generator.md @@ -0,0 +1,339 @@ +# 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. + +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: + +- 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. + +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. +- 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 + 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, 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. 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 + +- 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"] +``` + +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"]] +``` + +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: + +````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, what it matches, and whether it +depends on fragile text or expression matching. + +## 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 +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 +``` + +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..714b69e2d --- /dev/null +++ b/tools/optiNLP/prompts/02_command_to_script.md @@ -0,0 +1,136 @@ +# 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. + +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 + 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. +- Use the module names and argument order shown in the knowledge/examples. +- 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_full_script.md b/tools/optiNLP/prompts/03_code_to_full_script.md new file mode 100644 index 000000000..46076bd55 --- /dev/null +++ b/tools/optiNLP/prompts/03_code_to_full_script.md @@ -0,0 +1,146 @@ +# Prompt 3: OptiTrust Code To Full Script + +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. + +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 + +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. + +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 + 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 the examples and knowledge in the current + request. +- Prefer semantic targets over line numbers. +- 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. +- 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 + +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. 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 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 + +````markdown +## Code Summary +Short structural summary of the input code. + +## Candidate Transformations +| Rank | Transformation | Target | Why it may apply | Risk | +| --- | --- | --- | --- | --- | + +## Recommended First Candidate +Explain the chosen full-file script strategy. + +## Full Transformation 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. + +## 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. 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/.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/README.md b/tools/vscode-optitrust/README.md index fd234898e..cfe1adcac 100644 --- a/tools/vscode-optitrust/README.md +++ b/tools/vscode-optitrust/README.md @@ -2,46 +2,40 @@ VS Code support for the in-tree OptiTrust development workflow. -The extension does not reimplement OptiTrust. It provides an editor interface over -the existing project tools, especially `tools/view_result.sh`, `tools/_last_view_result.sh`, -and `./tester`. Normal OCaml editing remains owned by the standard OCaml extension -and OCaml-LSP. +The extension does not reimplement OptiTrust. It provides an editor interface +over the existing project tools, especially `tools/view_result.sh`, +`tools/_last_view_result.sh`, the trace server, and `./tester`. Normal OCaml +editing remains owned by the standard OCaml extension and OCaml-LSP. ## Features - Detects whether the opened workspace is an OptiTrust repository. -- Remembers the detected OptiTrust project root. -- Warns clearly when the workspace is not supported. -- Runs a step diff for the transformation at the current cursor line. -- Runs a full standalone transformation trace for the current script. -- Runs a step trace for the transformation at the current cursor line. -- Re-runs the last OptiTrust view command. -- Runs the current OptiTrust test. -- Displays command output in a dedicated `OptiTrust` output panel. -- Shows the exact command executed by the extension. -- Shows progress notifications for long-running commands. -- Reports command failures with readable VS Code messages. -- Detects OptiTrust transformation scripts before running transformation commands. -- Passes the current 1-based cursor line to OptiTrust commands. -- Supports scripts in `tests`, `case_studies`, and configured user script folders. -- Opens generated output files for the current script or test. -- Opens expected output files for the current test. -- Compares generated output against expected output. -- Discovers associated files using suffix and extension rules. -- Provides a top-right editor button for associated files. -- Supports related `.ml`, `.cpp`, `.c`, `.opti`, `.html`, `.js`, and `.trace` files. -- Provides default OptiTrust keybindings that can be disabled. -- Removes the need to copy OptiTrust shortcuts into `keybindings.json`. +- Runs step diffs, full traces, and step traces from the current cursor line. +- Opens one universal HTML step-diff view for C/C++ and OptiLambda output. +- Uses OptiLambda Surface as the default step-diff representation. +- Switches generated diff and trace panels between C/C++, Surface, Internal, + and Fully-Typed OptiLambda. +- Lazily generates missing OptiLambda diff representations when a panel switch + requests them. +- Keeps generated diff and trace text searchable inside the webview. +- Groups ghost and contract blocks behind compact `G*` and `C*` buttons. +- Shows context diffs inside ghost and contract popups in step diff views. +- Shows Surface OptiLambda hover details for variables and supported syntax + nodes. +- Reuses an attached live diff/trace panel and lets you detach a panel when you + want to keep it. +- Applies VS Code-styled popup, hover, diff, and syntax-highlight colors in + generated views. - Registers `.opti` as the OptiLambda file extension. -- Adds OptiLambda bracket matching and `//` comments. -- Adds OptiLambda syntax highlighting with theme-friendly TextMate scopes. -- Shows generated diff and trace views inside VS Code webviews. -- Reuses an existing diff/trace panel for the same file and view metadata. -- Lets generated diffs switch between C/C++ and OptiLambda representations in the panel. -- Lets standalone traces switch between C/C++ and OptiLambda representations in the panel. -- Supports server-backed trace requests with `syntax=cpp` or `syntax=optilambda&repr=...`. -- Provides a health check for the OptiTrust installation. -- Supports local `.vsix` packaging and manual installation. +- Adds OptiLambda bracket matching, `//` comments, and TextMate syntax + highlighting for opened `.opti` files. +- Opens generated, expected, and other associated files from the active file. +- Runs current tests and reruns the last OptiTrust test selection. +- Displays command output and exact backend commands in the `OptiTrust` output + panel. +- Provides a health check for the local OptiTrust installation. +- Provides OptiNLP commands and a native VS Code Chat participant named + `@optinlp`. ## Requirements @@ -55,19 +49,39 @@ and OCaml-LSP. - `llvm-config-15` - The standard OCaml VS Code extension is recommended for `.ml` editing. -## Install From `.vsix` +## Setup -From `tools/vscode-optitrust`: +Start from a working OptiTrust checkout and open the repository root: + +```bash +cd /path/to/optitrust +code . +``` + +Build the extension from its own folder: ```bash +cd tools/vscode-optitrust npm install npm run compile -npm run package ``` -Then install the generated package: +Run it in a development Extension Host: + +```bash +npm run dev:extension +``` + +This opens a separate VS Code window with the local extension loaded. In that +new window, open the OptiTrust repository root if it is not already open. + +To install a local `.vsix` instead: ```bash +cd tools/vscode-optitrust +npm install +npm run compile +npm run package code --install-extension optitrust-0.0.1.vsix ``` @@ -77,7 +91,21 @@ For VSCodium: codium --install-extension optitrust-0.0.1.vsix ``` -After installing, reload VS Code and open the OptiTrust repository root. +After installing the `.vsix`, reload VS Code and open the OptiTrust repository +root. Run this command to confirm that the extension activated: + +```text +OptiTrust: Verify Extension Loaded +``` + +Then run: + +```text +OptiTrust: Health Check +``` + +The health check verifies workspace detection, required tools, the backend build +for the runner and trace server, and trace-server reachability. ## Workspace Detection @@ -89,8 +117,8 @@ The extension detects the OptiTrust root using repository-level files such as: - `tools/view_result.sh` - `lib/optitrust.ml` -The current MVP targets in-tree OptiTrust development. External or off-tree -OptiTrust installations are not supported yet. +The current extension targets in-tree OptiTrust development. External or +off-tree OptiTrust installations are not supported yet. If automatic detection is not enough, set: @@ -100,9 +128,10 @@ If automatic detection is not enough, set: ## Main Workflows -### View A Step Diff +### View Step Diff -Open an OptiTrust script, place the cursor on or near a transformation line, then run: +Open an OptiTrust `.ml` transformation script, place the cursor on or near a +transformation line, then run: ```text OptiTrust: View Step Diff @@ -114,15 +143,31 @@ Default keybinding: F6 ``` -The generated diff opens inside VS Code. When OptiLambda payloads are available, -the panel can switch in place between: +The step diff opens in the universal OptiTrust HTML diff view. This is the only +user-facing step-diff view; the old native `vscode.diff` path is not used for +step diffs because it cannot host interactive HTML controls. + +The step diff starts in OptiLambda Surface mode by default. Use the selector in +the diff toolbar to switch between: -- C/C++, -- OptiLambda Surface, -- OptiLambda Internal, -- OptiLambda Fully-Typed. +- `C/C++` +- `OptiLambda Surface` +- `OptiLambda Internal` +- `OptiLambda Fully-Typed` -### View A Full Trace +When a representation is not already available, the panel asks the backend to +generate it on demand and updates in place. + +The diff view supports: + +- side-by-side code diff rendering, +- searchable generated text, +- ghost and contract group buttons, +- contract and ghost popup diffs, +- Surface variable and syntax-node hover popups, +- the toolbar `Detach` button. + +### View Full Trace Run: @@ -136,11 +181,11 @@ Default keybinding: Shift+F5 ``` -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. +The full trace opens in an OptiTrust webview. The trace tree, step navigation, +syntax selector, grouped ghost/contract controls, hovers, and search all remain +inside the same panel. -### View A Step Trace +### View Step Trace Run: @@ -154,19 +199,100 @@ Default keybinding: Shift+F6 ``` -For server-backed traces, the selected syntax is sent to the trace server as: +This opens a trace focused on the transformation step at the cursor line. + +### Trace With Saved Step Script + +Run: ```text -syntax=cpp -syntax=optilambda&repr=surface -syntax=optilambda&repr=internal -syntax=optilambda&repr=typed +OptiTrust: View Trace Save Steps Script ``` -### Run The Current Test +Default keybinding: + +```text +Ctrl+F5 +``` + +This uses the existing OptiTrust `-save-steps script` trace mode. + +### Redo The Last View Command + +Run: + +```text +OptiTrust: Redo Last View Command +``` + +Default keybinding: + +```text +F5 +``` + +This reruns the last diff or trace command with the latest cursor and file +context. + +### 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` or the panel `Detach` button to keep the current +view. Once detached, the button changes to `Detached`, becomes disabled, and +later diff/trace commands update a new attached panel instead. + +### Search In Diff And Trace Views + +Use `Ctrl+F` inside generated diff and trace webviews. The search operates on +real DOM text, so generated code, diff lines, visible popup content, and trace +text remain searchable. + +### Ghost And Contract Groups + +Generated OptiLambda views compact consecutive ghost and contract blocks: + +- square `G1`, `G2`, ... buttons represent ghost groups, +- round `C1`, `C2`, ... buttons represent contract groups. + +Click a group button to open its popup. Click the same button again to close it. +In step diffs, group popups can show a context diff between the old and new +group content, highlighting removed lines in red and added lines in green. + +### Surface Hover Details + +Surface OptiLambda views attach VS Code-styled hover popups to supported +variables and syntax nodes. Use them for type and skeleton information without +displaying every type inline. + +Internal and Fully-Typed representations currently get ghost and contract +grouping only. + +### Select Default Diff/Trace Syntax Run: +```text +OptiTrust: Select Diff/Trace Syntax +``` + +This writes workspace settings for commands that need to request one syntax from +the backend up front. Available modes are: + +- `C/C++` +- `OptiLambda Surface` +- `OptiLambda Internal` +- `OptiLambda Fully-Typed` + +The `F6` step-diff workflow intentionally opens Surface by default and then +lets the toolbar switch representations in the panel. + +### Run Tests + +Run the current OptiTrust test: + ```text OptiTrust: Run Current Test ``` @@ -183,9 +309,31 @@ The extension runs: ./tester run -with-ignored ``` -Output appears in the `OptiTrust` output panel. +Rerun the last tried tests: + +```text +OptiTrust: Rerun Last-Tried Tests +``` + +Default keybinding: + +```text +F10 +``` -### Open Associated Files +Run the current test through the OptiTrust diff workflow: + +```text +OptiTrust: Run Current Test And Open Diff +``` + +Default keybinding: + +```text +Ctrl+Shift+F10 +``` + +### Associated Files Use the editor-title button in the top-right of supported files, or run: @@ -193,51 +341,75 @@ Use the editor-title button in the top-right of supported files, or run: OptiTrust: Open Associated Files ``` -The QuickPick menu can: +The QuickPick can open one associated file or all associated files. Supported +related file types include `.ml`, `.cpp`, `.c`, `.opti`, `.html`, `.js`, and +`.trace`. -- open all associated files, -- open one associated file, -- compare generated and expected outputs when a pair exists. +You can also run: + +```text +OptiTrust: Open Generated Output +OptiTrust: Open Expected Output +OptiTrust: Open Unit Test ML And CPP Files +``` + +`Open Unit Test ML And CPP Files` has this default keybinding: + +```text +Alt+Shift+F10 +``` ## Commands | Command | Description | | --- | --- | | `OptiTrust: Verify Extension Loaded` | Checks that the extension activates. | -| `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 Step Diff` | Shows the universal HTML diff for the transformation at the cursor line. | +| `OptiTrust: Detach View` | Keeps the current OptiTrust view open and removes it from future live updates. | +| `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. | | `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. | +| `OptiTrust: Run Current Test And Open Diff` | Runs the current test through `tester rundiff`. | | `OptiTrust: Open Generated Output` | Opens generated output related to the current file. | | `OptiTrust: Open Expected Output` | Opens expected output related to the current file. | -| `OptiTrust: Compare Output With Expected` | Opens a VS Code diff for generated vs expected output. | | `OptiTrust: Open Associated Files` | Opens the associated-files QuickPick menu. | | `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: Select Diff/Trace Syntax` | Selects the default backend-requested 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. | +| `OptiTrust: OptiNLP Set Gemini API Key` | Stores the Gemini API key used by OptiNLP. | +| `OptiTrust: OptiNLP Set OpenAI API Key` | Stores the OpenAI API key used by OptiNLP. | +| `OptiTrust: OptiNLP Set API Key` | Stores an API key for the currently selected OptiNLP provider. | +| `OptiTrust: OptiNLP Select Provider` | Selects the OptiNLP provider. | +| `OptiTrust: OptiNLP Set Model` | Sets an optional OptiNLP model override. | +| `OptiTrust: OptiNLP Clear Session` | Clears OptiNLP in-memory session context. | ## Default Keybindings | Keybinding | Command | | --- | --- | | `F6` | View step diff | -| `Ctrl+F6` | View diff only code | -| `Ctrl+Shift+F6` | View diff using internal syntax | | `Shift+F5` | View full trace | | `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 | | `Alt+Shift+F10` | Open unit test ML and CPP files | +On macOS, `Ctrl+F10` and `Ctrl+Shift+F10` are contributed as `Cmd+F10` and +`Cmd+Shift+F10`. + Disable all contributed keybindings with: ```json @@ -251,7 +423,11 @@ Disable all contributed keybindings with: "optitrust.rootOverride": "", "optitrust.scriptFolders": [], "optitrust.viewSyntax": "cpp", -"optitrust.optilambdaRepresentation": "surface" +"optitrust.optilambdaRepresentation": "surface", +"optitrust.syntaxHighlightThemePath": "", +"optitrust.optinlpProvider": "gemini", +"optitrust.optinlpModel": "", +"optitrust.optinlpUseProviderSession": true ``` `optitrust.scriptFolders` accepts workspace-relative folders for user-created @@ -268,10 +444,9 @@ transformation scripts. - `internal` - `typed` -Generated step diffs and standalone full traces keep the normal OptiTrust UI and -provide an in-panel selector for C/C++ and the three OptiLambda representations. -The settings are mainly used for server-backed views and commands that need to -request one syntax up front. +`optitrust.syntaxHighlightThemePath` accepts an absolute or workspace-relative +path to a VS Code color theme JSON file. Leave it empty to let the extension +try to resolve the active VS Code theme automatically. ## OptiLambda Support @@ -283,11 +458,11 @@ It provides: - line comments with `//`, - bracket matching, -- basic syntax highlighting, -- theme-friendly TextMate scopes, -- generated/expected `.opti` comparison support, +- TextMate syntax highlighting for opened `.opti` files, - representation-specific `.opti` artifact discovery, -- OptiLambda display in diff and trace panels. +- OptiLambda display in diff and trace panels, +- grouped ghost and contract rendering in generated HTML views, +- Surface hover details in generated HTML views. The current representation model is: @@ -295,8 +470,51 @@ The current representation model is: - Internal: explicit internal operations such as `get`, `set`, and `ref`. - Fully-Typed: explicit internal operations with type parameters when available. -The extension is display-oriented. Parsing OptiLambda back into the OptiTrust AST -is a future backend milestone. +The extension is display-oriented. Parsing OptiLambda back into the OptiTrust +AST is a future backend milestone. + +## 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. ## Health Check @@ -327,6 +545,29 @@ It also builds: dune build tools/runner/optitrust_runner.exe tools/trace_server/trace_server.exe ``` +## Development Commands + +From `tools/vscode-optitrust`: + +```bash +npm install +npm run compile +npm run dev:extension +npm run package +``` + +Useful scripts: + +- `npm run compile`: rebuilds the webview highlighter bundle and TypeScript + extension output. +- `npm run dev:extension`: starts a local VS Code Extension Host. +- `npm run watch`: watches TypeScript changes. +- `npm run package`: creates `optitrust-0.0.1.vsix`. +- `npm run test:optinlp`: compiles and runs OptiNLP tests. + +After changing webview highlighting code, run `npm run compile` so +`tools/web_view/optitrust_syntax_highlight.js` is regenerated. + ## Notes On Webviews And Theme Colors Generated diff and trace views are opened inside VS Code webviews. The extension @@ -334,16 +575,35 @@ rewrites local resources and inlines local scripts/styles so generated OptiTrust HTML can run under VS Code webview security rules. The panels use VS Code theme variables for backgrounds, borders, fonts, line -numbers, and diff colors. C/C++ syntax highlighting is still produced by the -existing Highlight.js/diff2html pipeline, with dark/light webview overrides so it -matches VS Code themes more closely. +numbers, popups, and diff colors. Syntax highlighting is produced by the +webview highlighter bundle, which tries to resolve the active VS Code theme and +falls back to Shiki built-in themes when the active theme cannot be loaded. + +VS Code webviews do not expose the exact TextMate token colors of every +installed editor theme. If the automatic theme lookup is not good enough, set +`optitrust.syntaxHighlightThemePath` to the JSON file for the theme you want the +webviews to use. -VS Code webviews do not expose the exact TextMate token colors of every installed -editor theme. For that reason, syntax colors are theme-compatible but may not be -pixel-identical to a custom editor theme. +The `OptiTrust` output panel reports theme-resolution information when +generated views are opened. In the webview DOM, highlighted containers also +include diagnostic `data-optitrust-*` attributes such as the highlighter, +resolved theme, and theme source. ## Troubleshooting +If the extension does not activate: + +- open the OptiTrust repository root, not only `tools/vscode-optitrust`, +- run `OptiTrust: Verify Extension Loaded`, +- check that VS Code is version 1.90 or newer. + +If workspace detection fails: + +- run `OptiTrust: Health Check`, +- confirm that `dune-project`, `optitrust.opam`, `tester`, + `tools/view_result.sh`, and `lib/optitrust.ml` exist under the root, +- set `optitrust.rootOverride` to the absolute OptiTrust path if needed. + If generated views open blank: - run `OptiTrust: Health Check`, @@ -351,6 +611,19 @@ If generated views open blank: - regenerate the diff or trace, - open the generated `_diff.html` or `_trace.html` file directly if needed. +If syntax switching in a diff panel gets stuck: + +- inspect the `OptiTrust` output panel for the backend command and error, +- make sure the backend can generate OptiLambda output, +- rebuild the backend and regenerate the diff. + +If colors in generated views look wrong: + +- inspect the `OptiTrust` output panel for the requested and resolved theme, +- set `optitrust.syntaxHighlightThemePath` to the desired VS Code theme JSON + file, +- run `npm run compile` after changing highlighter source files. + If `npm install` fails on WSL paths: - run `npm install` inside WSL/Linux, @@ -373,9 +646,11 @@ If a large trace stays on `Loading the trace ...`: ## Known Limitations -- The MVP supports in-tree OptiTrust development only. +- The extension supports in-tree OptiTrust development only. - `.ml` files remain OCaml files; this extension does not replace OCaml-LSP. - Diff and trace generation still depends on existing OptiTrust scripts. -- The exact syntax colors in webviews may differ from custom editor themes. +- Exact syntax colors in webviews may differ from custom editor themes. +- Internal and Fully-Typed OptiLambda currently have ghost and contract grouping + but not the full Surface hover feature set. - OptiLambda parsing is not implemented in this extension pass. - Fully-Typed output quality depends on type information available in the AST. diff --git a/tools/vscode-optitrust/package.json b/tools/vscode-optitrust/package.json index c078c60e7..15eb6aa80 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", @@ -23,8 +23,7 @@ "onLanguage:optilambda", "onCommand:optitrust.hello", "onCommand:optitrust.viewDiff", - "onCommand:optitrust.viewDiffOnlyCode", - "onCommand:optitrust.viewDiffInternalSyntax", + "onCommand:optitrust.detachView", "onCommand:optitrust.viewFullTrace", "onCommand:optitrust.viewTraceSaveStepsScript", "onCommand:optitrust.viewStepTrace", @@ -34,13 +33,30 @@ "onCommand:optitrust.runCurrentTestAndOpenDiff", "onCommand:optitrust.openGeneratedOutput", "onCommand:optitrust.openExpectedOutput", - "onCommand:optitrust.compareOutputExpected", "onCommand:optitrust.openAssociatedFiles", "onCommand:optitrust.openUnitTestMlCppFiles", "onCommand:optitrust.selectViewSyntax", - "onCommand:optitrust.healthCheck" + "onCommand:optitrust.healthCheck", + "onCommand:optitrust.showShortcuts", + "onCommand:optitrust.optinlpChat", + "onCommand:optitrust.optinlpGenerateTarget", + "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" ], "main": "./out/extension.js", + "bin": { + "optinlp": "./out/optinlp/cli.js" + }, "contributes": { "commands": [ { @@ -52,12 +68,9 @@ "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.detachView", + "title": "OptiTrust: Detach View", + "icon": "$(debug-disconnect)" }, { "command": "optitrust.viewFullTrace", @@ -95,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", @@ -115,6 +124,57 @@ { "command": "optitrust.healthCheck", "title": "OptiTrust: Health Check" + }, + { + "command": "optitrust.showShortcuts", + "title": "OptiTrust: Show Shortcuts", + "icon": "$(keyboard)" + }, + { + "command": "optitrust.optinlpChat", + "title": "OptiTrust: Open OptiNLP Chat", + "icon": "$(comment-discussion)" + }, + { + "command": "optitrust.optinlpGenerateTarget", + "title": "OptiTrust: OptiNLP Generate Target" + }, + { + "command": "optitrust.optinlpGenerateScript", + "title": "OptiTrust: OptiNLP Generate Script" + }, + { + "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" + }, + { + "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" } ], "configuration": { @@ -165,27 +225,96 @@ "Use explicit OptiLambda operations with type parameters." ], "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", + "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 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." } } }, + "configurationDefaults": { + "diffEditor.renderSideBySide": true + }, + "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", "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" }, @@ -204,6 +333,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", @@ -231,8 +365,45 @@ "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.optinlpChat", + "when": "resourceScheme == file && (resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti)", + "group": "navigation@55" + }, + { + "command": "optitrust.optinlpSuggestTargetAtCursor", + "when": "resourceScheme == file && resourceExtname == .ml", + "group": "navigation@60" + }, + { + "command": "optitrust.showShortcuts", + "when": "resourceScheme == file && (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", + "group": "optinlp@1" + }, + { + "command": "optitrust.optinlpGenerateScript", + "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", + "group": "optinlp@2" + }, + { + "command": "optitrust.optinlpGenerateFullTransformation", + "when": "resourceExtname == .ml || resourceExtname == .cpp || resourceExtname == .c || resourceExtname == .opti", + "group": "optinlp@3" } ], "commandPalette": [ @@ -243,10 +414,7 @@ "command": "optitrust.viewDiff" }, { - "command": "optitrust.viewDiffOnlyCode" - }, - { - "command": "optitrust.viewDiffInternalSyntax" + "command": "optitrust.detachView" }, { "command": "optitrust.viewFullTrace" @@ -275,9 +443,6 @@ { "command": "optitrust.openExpectedOutput" }, - { - "command": "optitrust.compareOutputExpected" - }, { "command": "optitrust.openAssociatedFiles" }, @@ -289,6 +454,42 @@ }, { "command": "optitrust.healthCheck" + }, + { + "command": "optitrust.showShortcuts" + }, + { + "command": "optitrust.optinlpChat" + }, + { + "command": "optitrust.optinlpGenerateTarget" + }, + { + "command": "optitrust.optinlpGenerateScript" + }, + { + "command": "optitrust.optinlpGenerateFullTransformation" + }, + { + "command": "optitrust.optinlpSuggestTargetAtCursor" + }, + { + "command": "optitrust.optinlpSetGeminiApiKey" + }, + { + "command": "optitrust.optinlpSetOpenAiApiKey" + }, + { + "command": "optitrust.optinlpSetConfiguredApiKey" + }, + { + "command": "optitrust.optinlpSelectProvider" + }, + { + "command": "optitrust.optinlpSetModel" + }, + { + "command": "optitrust.optinlpClearSession" } ] }, @@ -316,12 +517,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": "^3.9.2", "esbuild": "^0.28.1", 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..2ef8ebbd2 --- /dev/null +++ b/tools/vscode-optitrust/scripts/run_extension_dev_host.sh @@ -0,0 +1,56 @@ +#!/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" + +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 run_code --help 2>&1 | grep -q -- "--extensionDevelopmentPath"; then + echo "Opening VS Code Extension Development Host..." + run_code --new-window --extensionDevelopmentPath="$extension_dir" "$repo_root" + install_dev_vsix +else + echo "The '$code_cmd' CLI does not support --extensionDevelopmentPath." + install_dev_vsix + echo "Opening OptiTrust with the installed extension..." + run_code --new-window "$repo_root" +fi diff --git a/tools/vscode-optitrust/src/commands/associatedFiles.ts b/tools/vscode-optitrust/src/commands/associatedFiles.ts index f306dd190..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 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, 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; }; @@ -20,15 +19,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) { @@ -47,7 +37,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) ); } @@ -84,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[] = [ @@ -136,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) ]; @@ -161,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); } @@ -183,13 +124,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/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/optinlpCommands.ts b/tools/vscode-optitrust/src/commands/optinlpCommands.ts new file mode 100644 index 000000000..a7a86fb60 --- /dev/null +++ b/tools/vscode-optitrust/src/commands/optinlpCommands.ts @@ -0,0 +1,499 @@ +// 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, + 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: "marked selection" | "full file" | "associated source" | "target-at-cursor context"; +} + +export interface OptiNlpGenerationOutcome { + readonly mode: OptiNlpMode; + readonly userRequest: string; + readonly sourceLabel: SourceContext["label"]; + readonly result: OptiNlpProviderResult; +} + +export interface OptiNlpGenerationOptions { + readonly renderToOutput?: boolean; + readonly editor?: vscode.TextEditor; + readonly throwProviderErrors?: boolean; + readonly sourceContext?: SourceContext; + readonly filePath?: string; + readonly language?: string; + readonly cancellationToken?: vscode.CancellationToken; +} + +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 provider = configuredOptiNlpProvider(); + 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; + } +} + +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`, + 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 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); + } +} + +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: OptiNlpGenerationOptions = { renderToOutput: true } +): Promise { + const resolvedMode = resolveRequestedMode(mode, userRequest); + const editorContext = getActiveEditorContext(workspace.root, options.editor); + const sourceContext = options.sourceContext ?? (await getSourceContext(editorContext.editor)); + if (!sourceContext) { + 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: canOmitStableSource && stableSource ? stableSourceOmittedText(stableSource.label) : sourceContext.text, + filePath, + language: options.language ?? inferLanguage(editorContext.filePath), + 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 + }; + + let result: OptiNlpProviderResult; + const definition = modeDefinition(resolvedMode); + try { + result = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `OptiNLP: ${definition.label}`, + cancellable: true + }, + (_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; + } + appendHeader("OptiNLP Error"); + appendLine(error.userMessage); + if (error.technicalDetail) { + appendLine(`Detail: ${error.technicalDetail}`); + } + showOutput(); + vscode.window.showErrorMessage(`OptiNLP: ${error.userMessage}`); + return undefined; + } + throw error; + } finally { + cancellationSubscription?.dispose(); + } + + 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 + }; +} + +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) { + return selectedContext; + } + + 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 fullFileSourceContextFromEditor(editor); +} + +function selectedSourceContextFromEditor(editor: vscode.TextEditor): SourceContext | 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 { + 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({ + 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 provider = configuredOptiNlpProvider(); + const model = configuredOptiNlpModel() || 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 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 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`, + language: "ocaml" + }); + await vscode.window.showTextDocument(document, { preview: false, viewColumn: vscode.ViewColumn.Beside }); +} diff --git a/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts new file mode 100644 index 000000000..2f6269bed --- /dev/null +++ b/tools/vscode-optitrust/src/commands/optinlpTargetAtCursor.ts @@ -0,0 +1,280 @@ +// 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 { 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"; +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 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 fileExists(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"); +} + +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) { + 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 chatPrompt = targetChatPrompt(collected); + const request = targetRequest(collected); + const pending = setPendingOptiNlpChatRequest({ + mode: "target", + chatPrompt, + userRequest: request, + sourceContext: { + text: targetSourceContext(collected), + label: "target-at-cursor context" + }, + filePath: collected.sourceRelativePath, + language: "cpp+optilambda+ocaml", + targetInsertionFilePath: collected.scriptPath + }); + + await vscode.commands.executeCommand("optitrust.optinlpChat", { + query: `@optinlp /target ${chatPrompt} [context:${pending.id}]`, + preserveExisting: true + }); +} diff --git a/tools/vscode-optitrust/src/commands/shortcuts.ts b/tools/vscode-optitrust/src/commands/shortcuts.ts new file mode 100644 index 000000000..02bd12a1f --- /dev/null +++ b/tools/vscode-optitrust/src/commands/shortcuts.ts @@ -0,0 +1,84 @@ +// 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: "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); + } +} diff --git a/tools/vscode-optitrust/src/commands/viewCommands.ts b/tools/vscode-optitrust/src/commands/viewCommands.ts index 2ba2432ef..c0298c949 100644 --- a/tools/vscode-optitrust/src/commands/viewCommands.ts +++ b/tools/vscode-optitrust/src/commands/viewCommands.ts @@ -3,41 +3,38 @@ 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"; -import { backendFlagsForViewMode, getSelectedViewMode, ViewModeDefinition } from "../optitrust/viewMode"; +import { backendFlagsForViewMode, getSelectedViewMode, VIEW_MODES, ViewModeDefinition } from "../optitrust/viewMode"; 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 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 = { step_diff: { - mode: "step_diff", scriptMode: "step_diff", title: "OptiTrust: View Step Diff", viewKind: "diff", htmlSuffix: "_diff.html" }, 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", scriptMode: "step_trace", title: "OptiTrust: View Step Trace", viewKind: "step-trace", @@ -45,17 +42,27 @@ const VIEW_COMMANDS: Record = { } }; -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } +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]; + +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) { @@ -66,15 +73,79 @@ export async function runViewCommand(workspace: OptitrustWorkspace, mode: ViewMo markExecutedLine(context.editor, context.line); const selectedViewMode = getSelectedViewMode(); - const extraArgs = viewArgs(spec.mode, selectedViewMode, option); - const args = [spec.scriptMode, context.relativePath, String(context.line), ...extraArgs]; + const initialDiffViewMode = option === undefined ? DEFAULT_STEP_DIFF_VIEW_MODE : selectedViewMode; + const commandViewMode = mode === "step_diff" ? initialDiffViewMode : selectedViewMode; + + 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[] { + if (option === "trace-save-steps-script") { + return ["-save-steps", "script"]; + } + + // Full traces use serialized, server-backed data for in-window switching. + // 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 runViewTraceSaveStepsScript(workspace: OptitrustWorkspace): Promise { + return runViewCommand(workspace, "full_trace", "trace-save-steps-script"); +} + +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", + 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: spec.title, + title: titleOverride ?? spec.title, env: { OPTITRUST_NO_BROWSER: "1" } @@ -83,57 +154,113 @@ export async function runViewCommand(workspace: OptitrustWorkspace, mode: ViewMo 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}`); + lastViewRequest = request; + await openViewResult(request); +} + +async function openViewResult(request: StoredViewRequest): Promise { + const spec = VIEW_COMMANDS[request.mode]; + 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, + 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}`); vscode.window.showWarningMessage(`OptiTrust command finished, but generated view was not found: ${path.basename(htmlFile)}`); } } -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"]; +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; } - if (option === "trace-save-steps-script") { - return ["-save-steps", "script"]; + + const args = parseLastViewResultArgs(content); + if (args.length < 3) { + return undefined; } - // 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") { - return []; + const mode = modeFromScriptMode(args[0]); + const line = Number(args[2]); + if (!mode || !Number.isInteger(line)) { + return undefined; } - return backendFlagsForViewMode(selectedViewMode); + + 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) + }; } -export function runViewDiffOnlyCode(workspace: OptitrustWorkspace): Promise { - return runViewCommand(workspace, "step_diff", "diff-only-code"); +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) : []; } -export function runViewDiffInternalSyntax(workspace: OptitrustWorkspace): Promise { - return runViewCommand(workspace, "step_diff", "diff-internal-syntax"); +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; } -export function runViewTraceSaveStepsScript(workspace: OptitrustWorkspace): Promise { - return runViewCommand(workspace, "full_trace", "trace-save-steps-script"); +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; } -export async function redoLastViewCommand(workspace: OptitrustWorkspace): Promise { - 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" - }); - } catch { - return; +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 bf88245ce..eb8e0a819 100644 --- a/tools/vscode-optitrust/src/extension.ts +++ b/tools/vscode-optitrust/src/extension.ts @@ -1,27 +1,43 @@ import * as vscode from "vscode"; import { - compareOutputExpected, openAssociatedFiles, openExpectedOutput, openGeneratedOutput, openUnitTestMlCppFiles } from "./commands/associatedFiles"; import { runHealthCheck } from "./commands/healthCheck"; +import { registerOptiNlpChatParticipant } from "./commands/optinlpChatParticipant"; +import { + clearOptiNlpSession, + generateOptiNlpScript, + generateOptiNlpTarget, + generateOptiNlpFullTransformation, + insertTargetAtCursorInFile, + openOcamlDocument, + selectOptiNlpProvider, + setOptiNlpConfiguredProviderApiKey, + setOptiNlpGeminiApiKey, + setOptiNlpModel, + setOptiNlpOpenAiApiKey +} from "./commands/optinlpCommands"; +import { suggestOptiNlpTargetAtCursor } from "./commands/optinlpTargetAtCursor"; import { rerunLastTests, runCurrentTest, runCurrentTestAndOpenDiff } from "./commands/runTests"; +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 { 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); @@ -50,11 +66,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}`); @@ -65,6 +81,9 @@ 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(); + registerOptiNlpChatParticipant(context, requireWorkspace, optiNlpSession); registerCommand(context, "optitrust.hello", async () => { const workspace = await requireWorkspace(); @@ -103,20 +122,12 @@ 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; + 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."); } - await runViewDiffOnlyCode(workspace); - }); - - registerCommand(context, "optitrust.viewDiffInternalSyntax", async () => { - const workspace = await requireWorkspace(); - if (!workspace) { - return; - } - await runViewDiffInternalSyntax(workspace); }); registerCommand(context, "optitrust.redoLastViewCommand", async () => { @@ -161,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) { @@ -202,8 +211,84 @@ export async function activate(context: vscode.ExtensionContext): Promise } }); + registerCommand(context, "optitrust.showShortcuts", showShortcuts); + + registerCommand(context, "optitrust.optinlpChat", async (options: unknown) => { + await openOptiNlpChat(openChatOptions(options)); + }); + + 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.optinlpGenerateFullTransformation", async () => { + const workspace = await requireWorkspace(); + if (workspace && optiNlpSession) { + await generateOptiNlpFullTransformation(context, workspace, optiNlpSession); + } + }); + + 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); + }); + + registerCommand(context, "optitrust.optinlpSetOpenAiApiKey", async () => { + 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); + } + }); + 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); @@ -225,7 +310,60 @@ 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(); + optiNlpSession = undefined; } diff --git a/tools/vscode-optitrust/src/optinlp/assets.ts b/tools/vscode-optitrust/src/optinlp/assets.ts new file mode 100644 index 000000000..5beb51255 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/assets.ts @@ -0,0 +1,133 @@ +// 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"; +import { OptiNlpMode, OptiNlpProviderRequest } from "./providerTypes"; + +export interface OptiNlpAssets { + readonly promptText: string; + readonly knowledgeText: string; + readonly stableContextKey: string; + readonly stableContextLabel: 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, + stableContextKey: assets.stableContextKey, + stableContextLabel: assets.stableContextLabel, + 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")) + ]); + + 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, + 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); + 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..e3b9bcbba --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/cli.test.ts @@ -0,0 +1,148 @@ +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"; +import { markSelectedRangeInText } from "./sourceContext"; + +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.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 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(); + 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 testCliFullRequest(): Promise { + await withTempSource(async filePath => { + const stdout = new MemoryWritable(); + const stderr = new MemoryWritable(); + const exitCode = await runOptiNlpCli( + ["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_full_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 testMarkedSelectionContext(); + await testCliTargetMarkdown(); + await testCliScriptJson(); + await testCliFullRequest(); + 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..2c97a3b37 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/cli.ts @@ -0,0 +1,181 @@ +#!/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("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", "full"] + .map(command => { + const definition = modeFromCliCommand(command); + const placeholder = definition ? modeDefinition(definition).placeholder : "..."; + return ` optinlp ${command} --file path --request "${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/geminiProvider.ts b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts new file mode 100644 index 000000000..b4a586fea --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/geminiProvider.ts @@ -0,0 +1,182 @@ +// 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 { parseOptiNlpMarkdownResultSafely } 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 generateFullScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_full_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), + signal: request.abortSignal + }); + } 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."); + } + + const structured = parseOptiNlpMarkdownResultSafely(request.mode, markdownOutput); + + 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..c8725b21d --- /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_full_script": + return provider.generateFullScript(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..9de7b2c1f --- /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 generateFullScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_full_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_full_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.", + "", + "## Full Transformation 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..110b71bbd --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/modes.ts @@ -0,0 +1,97 @@ +// 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" | "full"; +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: ["target_description.md", "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: ["target_description.md", "targets.md", "script_patterns.md", "transformations.md", "optilambda.md"] + }, + { + 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_full_script.md", + knowledgeFiles: ["target_description.md", "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 { + return resolveRequestedMode(mode, request); +} + +export function resolveRequestedMode(mode: OptiNlpUiMode, request: string): OptiNlpMode { + if (mode === "target") { + return "target"; + } + if (isFullFileScriptRequest(request)) { + return "code_to_full_script"; + } + 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_full_script"; + } + 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; +} diff --git a/tools/vscode-optitrust/src/optinlp/openaiProvider.ts b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts new file mode 100644 index 000000000..06e54338f --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/openaiProvider.ts @@ -0,0 +1,194 @@ +// 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 { parseOptiNlpMarkdownResultSafely } 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 id?: string; + readonly output?: readonly OpenAiOutputItem[]; + readonly output_text?: string; + readonly error?: { + readonly message?: string; + }; +} + +export class OpenAiProvider implements OptiNlpProvider { + readonly name = "openai"; + readonly supportsProviderSession = true; + 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 generateFullScript(request: OptiNlpProviderRequest): Promise { + return this.generate(requestWithMode(request, "code_to_full_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 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.", + "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: providerSessionEnabled, + previous_response_id: providerSessionEnabled ? request.previousProviderResponseId : undefined + }; + + let response: Response; + try { + response = await this.fetchImpl(this.endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}` + }, + 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); + } + + 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."); + } + + const structured = parseOptiNlpMarkdownResultSafely(request.mode, markdownOutput); + + return { + provider: this.name, + model: this.model, + markdownOutput, + providerResponseId: rawResponse.id, + 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/provider.test.ts b/tools/vscode-optitrust/src/optinlp/provider.test.ts new file mode 100644 index 000000000..7c73b7b97 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/provider.test.ts @@ -0,0 +1,386 @@ +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 fullScriptResult = await provider.generateFullScript(sampleRequest); + assert.strictEqual(fullScriptResult.structured?.kind, "code_to_full_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: validFullScriptMarkdown }] } }] }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + + const provider = new GeminiProvider({ apiKey: "test-key", model: "gemini-test", fetchImpl }); + const result = await provider.generateFullScript(sampleRequest); + + assert.strictEqual(result.provider, "gemini"); + assert.strictEqual(result.model, "gemini-test"); + assert.strictEqual(result.markdownOutput, validFullScriptMarkdown); + assert.strictEqual(result.structured?.kind, "code_to_full_script"); + assert.ok(result.rawResponse); +} + +async function testGeminiUnstructuredResponseStillDisplays(): 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 }); + const result = await provider.generateTarget(sampleRequest); + + assert.strictEqual(result.markdownOutput, "## Intent\nGenerated."); + assert.strictEqual(result.structured, undefined); +} + +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; 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_test", 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.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." }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + + const provider = new OpenAiProvider({ apiKey: "test-key", fetchImpl }); + const result = await provider.generateTarget(sampleRequest); + + assert.strictEqual(result.markdownOutput, "## Intent\nGenerated."); + assert.strictEqual(result.structured, undefined); +} + +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 parsedFullScript = parseOptiNlpMarkdownResult("code_to_full_script", validFullScriptMarkdown); + assert.strictEqual(parsedFullScript.kind, "code_to_full_script"); + assert.strictEqual(parsedFullScript.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 validFullScriptMarkdown = [ + "## 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.", + "", + "## Full Transformation 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 testGeminiUnstructuredResponseStillDisplays(); + await testOpenAiPromptConstruction(); + await testOpenAiMissingApiKey(); + await testOpenAiEmptyResponse(); + await testOpenAiProviderException(); + await testOpenAiSuccessfulResponse(); + await testOpenAiPreviousResponseId(); + await testOpenAiUnstructuredResponseStillDisplays(); + await testMarkdownSchemaParsing(); + console.log("OptiNLP provider tests passed."); +} + +void main().catch(error => { + console.error(error); + process.exitCode = 1; +}); 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..11bddc435 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/providerFactory.ts @@ -0,0 +1,36 @@ +// 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 { 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", "openai"]; + +export interface OptiNlpProviderFactoryOptions { + readonly provider?: OptiNlpProviderId; + readonly gemini?: GeminiProviderOptions; + readonly mock?: MockProviderOptions; + readonly openai?: OpenAiProviderOptions; +} + +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": + return new OpenAiProvider(options.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..4e4b7e79c --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/providerTypes.ts @@ -0,0 +1,50 @@ +// Provider-neutral OptiNLP request/result contracts. CLI, VS Code commands, +// 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"; + +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 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; + readonly rawResponse?: unknown; +} + +export interface OptiNlpProvider { + readonly name: string; + readonly model: string; + readonly supportsProviderSession?: boolean; + + generateTarget(request: OptiNlpProviderRequest): Promise; + generateScript(request: OptiNlpProviderRequest): Promise; + generateFullScript(request: OptiNlpProviderRequest): Promise; +} + +export function requestWithMode(request: OptiNlpProviderRequest, mode: OptiNlpMode): OptiNlpProviderRequest { + return { ...request, mode }; +} diff --git a/tools/vscode-optitrust/src/optinlp/resultActions.ts b/tools/vscode-optitrust/src/optinlp/resultActions.ts new file mode 100644 index 000000000..7179ac2bc --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/resultActions.ts @@ -0,0 +1,50 @@ +// Utilities for turning structured OptiNLP results into editor actions. +// VS Code commands and native chat 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_full_script": + 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/resultSchemas.ts b/tools/vscode-optitrust/src/optinlp/resultSchemas.ts new file mode 100644 index 000000000..34eaad9c9 --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/resultSchemas.ts @@ -0,0 +1,263 @@ +// 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 FullScriptResult { + readonly kind: "code_to_full_script"; + readonly codeSummary: string; + readonly candidateTransformations: readonly CandidateTransformation[]; + readonly recommendedFirstCandidate: string; + readonly fullScript: string; + readonly validation: string; + readonly missingInformation?: string; +} + +export type OptiNlpStructuredResult = TargetResult | ScriptResult | FullScriptResult; + +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 fullScriptResultSchema = { + type: "object", + required: ["kind", "codeSummary", "candidateTransformations", "recommendedFirstCandidate", "fullScript", "validation"], + properties: { + kind: { const: "code_to_full_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" }, + fullScript: { 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_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; + } +} + +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 parseFullScriptResult(markdown: string): FullScriptResult { + return { + 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") + }; +} + +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] + })); +} 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..69b15646c --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.test.ts @@ -0,0 +1,155 @@ +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 fullScriptResult(): OptiNlpProviderResult { + return { + provider: "mock", + model: "mock-model", + markdownOutput: "markdown", + structured: { + kind: "code_to_full_script", + codeSummary: "A loop.", + candidateTransformations: [ + { + rank: "High", + transformation: "Loop.unroll", + target: "[cFor \"i\"]", + whyItMayApply: "Local loop.", + risk: "Needs validation." + } + ], + recommendedFirstCandidate: "Try unrolling.", + fullScript: "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_full_script", userRequest: "generate full transformation" }, fullScriptResult()); + assert.match(memory.summary() ?? "", /Previous full script:/u); + + memory.clear(); + assert.strictEqual(memory.summary(), undefined); + 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."); +} + +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..6e002318f --- /dev/null +++ b/tools/vscode-optitrust/src/optinlp/sessionMemory.ts @@ -0,0 +1,201 @@ +// 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 fullScript?: string; + readonly assumptions: readonly string[]; + readonly validation?: string; +} + +export interface OptiNlpSessionSnapshot { + readonly turns: readonly OptiNlpSessionTurn[]; + readonly acceptedAssumptions: readonly string[]; + readonly lastValidation?: OptiNlpValidationRecord; + readonly stableContextCount: number; +} + +export interface OptiNlpSessionMemoryOptions { + readonly maxTurns?: number; +} + +export class OptiNlpSessionMemory { + private readonly maxTurns: number; + 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); + } + + 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 { + 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, + stableContextCount: this.stableContexts.size + }; + } + + clear(): void { + 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 { + 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.fullScript) { + lines.push(`Previous full script: ${truncateOneLine(lastTurn.fullScript, 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 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 { + mode: request.mode, + userRequest: request.userRequest, + filePath: request.filePath, + language: request.language, + provider: result.provider, + model: result.model, + target: targetFrom(structured), + script: scriptFrom(structured), + fullScript: fullScriptFrom(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_full_script": + return result.candidateTransformations[0]?.target; + } +} + +function scriptFrom(result: OptiNlpStructuredResult | undefined): string | undefined { + return result?.kind === "command_to_script" ? result.generatedScript : 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[] { + 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))}...`; +} 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/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."); } 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/files.ts b/tools/vscode-optitrust/src/optitrust/files.ts index aa686a4fe..c694fd86b 100644 --- a/tools/vscode-optitrust/src/optitrust/files.ts +++ b/tools/vscode-optitrust/src/optitrust/files.ts @@ -8,32 +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 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" -}; - -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); @@ -87,7 +69,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 +85,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,42 +132,18 @@ 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"]) { - const pair = { - label: labels.get(ext) ?? `${ext.slice(1).toUpperCase()} output`, - out: path.join(dir, `${base}_out${ext}`), - exp: path.join(dir, `${base}_exp${ext}`) - }; - if ((await exists(pair.out)) && (await exists(pair.exp))) { - pairs.push(pair); - } - } +export async function findAssociatedCSourceFile(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]; +} - 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 exists(pair.out)) && (await exists(pair.exp))) { - pairs.push(pair); - } - } +function compareCSourcePriority(a: AssociatedFile, b: AssociatedFile): number { + return cSourcePriority(a) - cSourcePriority(b) || a.label.localeCompare(b.label); +} - return pairs; +function cSourcePriority(file: AssociatedFile): number { + return C_SOURCE_EXTENSION_PRIORITY.get(path.extname(file.path)) ?? Number.MAX_SAFE_INTEGER; } export async function pickAssociatedFile(files: AssociatedFile[], placeHolder: string): Promise { diff --git a/tools/vscode-optitrust/src/optitrust/liveView.ts b/tools/vscode-optitrust/src/optitrust/liveView.ts new file mode 100644 index 000000000..5cfb5fde0 --- /dev/null +++ b/tools/vscode-optitrust/src/optitrust/liveView.ts @@ -0,0 +1,114 @@ +import * as vscode from "vscode"; + +type LiveViewKind = "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.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/views.ts b/tools/vscode-optitrust/src/optitrust/views.ts index 74986a94f..dc5638f41 100644 --- a/tools/vscode-optitrust/src/optitrust/views.ts +++ b/tools/vscode-optitrust/src/optitrust/views.ts @@ -1,9 +1,49 @@ 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"; +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; + readonly detached?: boolean; +} + +interface LazyDiffContext { + readonly relativePath: string; + readonly line: number; +} + +interface PanelRuntimeState { + root: string; + htmlFile: string; + includeDetachButton?: boolean; + lazyDiff?: LazyDiffContext; + initialDiffRepresentation?: string; + detached?: boolean; +} function webviewKey(filePath: string, viewKind: string, metadata: string): string { return `${path.resolve(filePath)}::${viewKind}::${metadata}`; @@ -14,13 +54,44 @@ 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, 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 withHighlightingConfig = await injectSyntaxHighlightingConfig(rewritten); - return injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)); + const withTraceServerBase = injectTraceServerBase(root, htmlFile, rewritten); + const withHighlightingConfig = await injectSyntaxHighlightingConfig(withTraceServerBase); + const withDiffSupport = injectDiffInitialRepresentation( + injectDiffFallback(injectDiffWebviewStyle(withHighlightingConfig)), + options.initialDiffRepresentation + ); + return options.includeDetachButton ? injectDetachButton(withDiffSupport, options.detached ?? false) : 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; + } + + 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}`; } /** @@ -153,6 +224,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 { @@ -160,9 +239,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`); @@ -170,16 +247,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, @@ -192,21 +326,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)) { @@ -345,27 +573,230 @@ 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, 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 = ` + + +`; + + 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 { + let key = options.useLiveView ? LIVE_VIEW_KEY : webviewKey(htmlFile, viewKind, metadata); const existing = panels.get(key); + const state: PanelRuntimeState = { + root, + htmlFile, + includeDetachButton: options.useLiveView, + lazyDiff: options.lazyDiff, + initialDiffRepresentation: options.initialDiffRepresentation, + detached: false + }; + panelStates.set(key, state); if (existing) { - existing.webview.html = await htmlWithBase(existing.webview, htmlFile); - // Reopening an existing view should refresh it in place. Passing - // ViewColumn.Beside here moves the tab back next to the active editor, - // which is disruptive when users place diff/trace panels on another group - // or screen. - existing.reveal(existing.viewColumn, true); + existing.title = title; + if (!options.useLiveView) { + existing.reveal(existing.viewColumn, true); + } + existing.webview.html = await htmlWithBase(existing.webview, root, htmlFile, { + includeDetachButton: options.useLiveView, + initialDiffRepresentation: state.initialDiffRepresentation, + detached: state.detached + }); 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, + retainContextWhenHidden: true, localResourceRoots: [vscode.Uri.file(root), vscode.Uri.file(path.dirname(htmlFile))] }); - panel.onDidDispose(() => panels.delete(key)); - panel.webview.html = await htmlWithBase(panel.webview, htmlFile); + const liveView = options.useLiveView + ? { + kind: "html" as const, + viewColumn, + getViewColumn: () => panel.viewColumn, + detach: () => { + const detachedKey = webviewKey(htmlFile, viewKind, `${metadata}:detached:${Date.now()}`); + const currentState = panelStates.get(key); + panels.delete(key); + panelStates.delete(key); + key = detachedKey; + if (currentState) { + currentState.detached = true; + panelStates.set(key, currentState); + } + panels.set(key, panel); + }, + dispose: () => panel.dispose() + } + : undefined; + + const messageSubscription = panel.webview.onDidReceiveMessage(async (message: unknown) => { + if (!isRecord(message)) { + return; + } + if (message.type === "optitrust.detachView") { + 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."); + } + return; + } + if (message.type === "optitrust.generateDiffRepresentation") { + const representation = typeof message.representation === "string" ? message.representation : ""; + const currentState = panelStates.get(key); + const lazyDiff = currentState?.lazyDiff; + if (!lazyDiff) { + const warning = "This OptiTrust diff cannot generate another syntax. Re-run View Step Diff."; + void panel.webview.postMessage({ type: "optitrust.diffGenerationFailed", representation, message: warning }); + vscode.window.showWarningMessage(warning); + return; + } + const viewMode = VIEW_MODES.find(mode => + representation === "cpp" ? mode.id === "cpp" : mode.optilambdaRepresentation === representation + ); + if (!viewMode) { + const warning = `Unknown OptiTrust diff syntax: ${representation}`; + void panel.webview.postMessage({ type: "optitrust.diffGenerationFailed", representation, message: warning }); + vscode.window.showWarningMessage(warning); + return; + } + try { + await runCommand({ + cwd: currentState.root, + command: path.join(currentState.root, "tools", "view_result.sh"), + args: [ + "step_diff", + lazyDiff.relativePath, + String(lazyDiff.line), + ...backendFlagsForViewMode(viewMode) + ], + title: `OptiTrust: Generate ${viewMode.label} Diff`, + env: { + OPTITRUST_NO_BROWSER: "1" + } + }); + } catch { + const warning = `Failed to generate ${viewMode.label} diff.`; + appendLine(warning); + void panel.webview.postMessage({ type: "optitrust.diffGenerationFailed", representation, message: warning }); + return; + } + currentState.initialDiffRepresentation = representation; + panel.webview.html = await htmlWithBase(panel.webview, currentState.root, currentState.htmlFile, { + includeDetachButton: currentState.includeDetachButton, + initialDiffRepresentation: currentState.initialDiffRepresentation, + detached: currentState.detached + }); + } + }); + + panel.onDidDispose(() => { + messageSubscription.dispose(); + panels.delete(key); + panelStates.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, + initialDiffRepresentation: state.initialDiffRepresentation, + detached: state.detached + }); panels.set(key, panel); + if (liveView) { + attachLiveView(liveView); + } } export async function openFileOrHtml(root: string, filePath: string, title?: string): Promise { 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); } } diff --git a/tools/vscode-optitrust/syntaxes/optilambda.tmLanguage.json b/tools/vscode-optitrust/syntaxes/optilambda.tmLanguage.json index bdabef6a9..628385008 100644 --- a/tools/vscode-optitrust/syntaxes/optilambda.tmLanguage.json +++ b/tools/vscode-optitrust/syntaxes/optilambda.tmLanguage.json @@ -72,7 +72,7 @@ }, { "name": "storage.modifier.ghost.optilambda", - "match": "\\bghost\\b" + "match": "\\b(?:ghost|ghost_begin|ghost_end)\\b" }, { "name": "storage.modifier.mutable.optilambda", @@ -96,7 +96,7 @@ "patterns": [ { "name": "entity.other.attribute-name.contract.optilambda", - "match": "\\b(?:requires|consumes|ensures|produces|reads|writes|pure|modifies|preserves|xrequires|xconsumes|xensures|xproduces|xreads|xwrites|xmodifies|xpreserves|strict|reverts)\\b" + "match": "\\b(?:requires|consumes|ensures|produces|reads|writes|pure|modifies|preserves|srequires|sreads|smodifies|spreserves|xrequires|xconsumes|xensures|xproduces|xreads|xwrites|xmodifies|xpreserves|strict|reverts)\\b" } ] }, @@ -104,7 +104,7 @@ "patterns": [ { "name": "support.type.optilambda", - "match": "\\b(?:Type|Prop|HProp|int|usize|f32|f64|bool|ptr|array|pure_fun|MemType|_Fraction)\\b" + "match": "\\b(?:Type|Prop|HProp|int|usize|f32|f64|float|double|bool|ptr|array|pure_fun|MemType|_Fraction)\\b" } ] }, diff --git a/tools/web_view/diff_template.html b/tools/web_view/diff_template.html index da782190b..31069ffd7 100644 --- a/tools/web_view/diff_template.html +++ b/tools/web_view/diff_template.html @@ -18,10 +18,14 @@ + + @@ -32,21 +33,32 @@ -
-
-
-
-
-
-
-
Loading the trace {TRACEJSFILE}...
-
-
-
- +
+
+
{INSERT_TITLE}
+
+ +
+
+
Trace
+
+
+
+
+
+
+ Step Details +
+
+
Loading the trace {TRACEJSFILE}...
+
+
+
+ + +
+
-
-