From abf7e6cb66a8f1ad041a01a10ab1f313403f1838 Mon Sep 17 00:00:00 2001 From: cppbear Date: Thu, 8 Oct 2026 02:30:33 +0800 Subject: [PATCH] fix: complete bytes contexts using compiler function identities --- .github/workflows/ci.yml | 4 + README.md | 2 +- docs/README.md | 1 + docs/bytes-analysis.md | 52 +++++++ docs/minimal-pipeline.md | 2 +- docs/palm-rust-unit-test-generation.md | 2 +- docs/source-integration.md | 2 +- examples/README.md | 4 +- focxt/README.md | 2 + focxt/call_chain/src/analysis/hirvisitor.rs | 8 + focxt/src/collect_context/crate_context.rs | 6 + focxt/src/collect_context/items_context.rs | 49 ++++++ focxt/src/collect_context/mod_context.rs | 8 + focxt/src/collect_context/syntax_context.rs | 160 ++++++++++++-------- focxt/src/main.rs | 1 + focxt/src/utils.rs | 19 --- scripts/check_bytes.py | 141 +++++++++++++++++ utgen/src/analysis.rs | 19 ++- 18 files changed, 388 insertions(+), 94 deletions(-) create mode 100644 docs/bytes-analysis.md create mode 100644 scripts/check_bytes.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e019d3e65..97c673a93f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,3 +44,7 @@ jobs: run: python3 scripts/check_minimal.py - name: Check generation and repair task limits run: python3 scripts/check_tasks.py + - name: Build release analysis tools + run: cargo build --workspace --release --locked + - name: Validate bytes analysis and method identities + run: python3 scripts/check_bytes.py diff --git a/README.md b/README.md index 3bcf611808..4284900e1e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ On a fresh working copy of a standalone crate, run `utgen pre-process -p ` for a custom tool build directory and `--runs 1` for one bytes copy. Rebuild the tools together: focxt consumes the source metadata emitted by the matching call-chain binary. The script prints a temporary directory containing copies, per-command logs, stage timings, and `summary.json`. Its default is two copies; dependency downloads can require network access. + +## Local Results + +Measured on macOS/Apple Silicon with the pinned compiler. Tool compilation is excluded from the analysis times below. + +| Run | Analyze | brinfo | focxt | Branch functions / contexts | Outcome | +| --- | ---: | ---: | ---: | ---: | --- | +| Previous main, release | 43.714 s | — | — | 663 / 226 | Missing context, exit 1 | +| Corrected, first copy | 34.632 s | 2.232 s | 32.297 s | 663 / 663 | Passed | +| Corrected, second copy | 34.747 s | 2.235 s | 32.396 s | 663 / 663 | Passed | + +The before/after rows perform different amounts of successful work, so they are not a speedup benchmark. Both corrected copies passed the source and artifact checks; the method-identity fixture also passed. The check is part of Linux CI alongside the existing minimal pipeline, coverage compatibility, and scheduling regressions. + +## Scope + +The 663 functions are the functions exported by the current analyzers for this pinned crate/configuration, not a claim to cover every possible Rust function or feature combination. Recovering an already selected method from parseable source does not provide general macro expansion: a macro-generated impl or source containing unexpanded substitutions may still be unsupported. Required trait methods without bodies are context declarations rather than focal functions. + +Full bytes generation, repair, model quality, and coverage experiments remain the next validation stage. This batch does not change their evaluation semantics. diff --git a/docs/minimal-pipeline.md b/docs/minimal-pipeline.md index a4fc4191f4..e58abbfa4e 100644 --- a/docs/minimal-pipeline.md +++ b/docs/minimal-pipeline.md @@ -48,7 +48,7 @@ The preprocessor parses `cfg` predicates conservatively, preserves `cfg(not(test Recursive context lookup now resolves full function names through the compiler's encoded-name index and consumes the accumulated dependencies. External functions without local artifacts remain outside this source context. -The local macOS/Apple Silicon run passed using six generation responses and one repair response. Real-model behavior, bytes-scale execution, general multi-crate support, hard process timeouts, recovery after forced termination, and explicit run/resume behavior remain later work. Git history is retained. +The local macOS/Apple Silicon run passed using six generation responses and one repair response. The [bytes analysis check](bytes-analysis.md) separately validates the larger target without a model. Real-model behavior, full bytes generation/repair, general multi-crate support, hard process timeouts, recovery after forced termination, and explicit run/resume behavior remain later work. Git history is retained. The coverage entry temporarily annotates sources with `#[coverage(off)]` and enables the nightly feature at crate roots without adding newlines. It preserves compiler flags, restores original bytes on returned errors, and performs no hashing or source fingerprint checks. `cargo llvm-cov report --json` reuses the first run's data; the first XML-producing command retains the tool's normal profile cleanup between candidates. This does not provide crash recovery or general macro expansion. See the [coverage command reference](../utgen/README.md#coverage-of-existing-tests) for scope. diff --git a/docs/palm-rust-unit-test-generation.md b/docs/palm-rust-unit-test-generation.md index d7514b2f5c..debe553ae1 100644 --- a/docs/palm-rust-unit-test-generation.md +++ b/docs/palm-rust-unit-test-generation.md @@ -635,7 +635,7 @@ utgen gen -p "$palm_example_dir" --requirement --context utgen fix -p "$palm_example_dir" ``` -切换到 integration、其他提示词或其他模型时,使用新的目标副本并重新分析,避免复用旧的生成缓存。更详细的示例见 [examples/README.md](../examples/README.md)。当前完整回归使用 [minimal 示例](minimal-pipeline.md);bytes 的完整运行仍是后续验证项。 +切换到 integration、其他提示词或其他模型时,使用新的目标副本并重新分析,避免复用旧的生成缓存。更详细的示例见 [examples/README.md](../examples/README.md)。当前完整生成与修复回归使用 [minimal 示例](minimal-pipeline.md)。[bytes 分析验收](bytes-analysis.md) 已覆盖默认配置下导出的 663 个函数及其上下文;bytes 的真实模型生成、修复和覆盖率实验仍是后续验证项。 完成后重点查看临时目标目录中的以下路径: diff --git a/docs/source-integration.md b/docs/source-integration.md index e3c80254b2..010c130ddf 100644 --- a/docs/source-integration.md +++ b/docs/source-integration.md @@ -31,4 +31,4 @@ The `comment_out_test_module` branch only handles test modules. PALM already han The `focxt_rustc_api` branch changes the entry point to `cargo focxt` and produces `focxt/name_map.json`, while its generation code still requires `focxt/impl_informations.json`. It also uses the older compiler API. Integration of that extractor requires a separate interface migration and validation. -The integration commits preserved the existing generation and repair behavior. Subsequent maintenance adds runtime API configuration, isolates real-model tests, and validates installation and basic CI; see [build validation](build-validation.md). Request concurrency controls and the unfinished batch test injection path remain follow-up work. +The integration commits preserved the existing generation and repair behavior. Subsequent maintenance adds runtime API configuration, isolates real-model tests, and validates installation and basic CI; see [build validation](build-validation.md). Subsequent checks cover [function-level task limits](minimal-pipeline.md#task-scheduling-regression) and [bytes analysis](bytes-analysis.md). The unfinished batch test injection path remains outside the active CLI. diff --git a/examples/README.md b/examples/README.md index 22dc71793e..2ed4a2a3d0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,6 +2,8 @@ Start with the [minimal pipeline](../docs/minimal-pipeline.md) for an automated, no-credentials validation. The commands below describe the larger bytes target; the complete pipeline is currently regression-tested on the minimal crate. +The [bytes analysis check](../docs/bytes-analysis.md) now validates all 663 exported functions and their context artifacts on two fresh copies. Run `cargo build --workspace --release --locked` and `python3 scripts/check_bytes.py` from the repository root. Model-based generation and repair on bytes remain a separate validation stage. + ## bytes The bundled target is [tokio-rs/bytes](https://github.com/tokio-rs/bytes), version 1.10.0. It is excluded from PALM's tool workspace and retains its own source files, tests, and license. @@ -31,7 +33,7 @@ utgen pre-process -p "$palm_example_dir" utgen analyze -p "$palm_example_dir" ``` -Analysis runs brinfo and focxt (including call-chain) on the prepared source. If a function lacks corresponding context, generation reports the missing artifact; support for larger targets is still being validated. +Analysis runs brinfo and focxt (including call-chain) on the prepared source, reports stage timings, and validates the resulting artifacts. If a function lacks corresponding context, the command reports the function and missing path. ### Generate and Repair Unit Tests diff --git a/focxt/README.md b/focxt/README.md index 0755fb91fc..17e9030c54 100644 --- a/focxt/README.md +++ b/focxt/README.md @@ -22,3 +22,5 @@ Options: ``` `focxt -c crate_path` or `focxt --crate crate_path` + +Function contexts use call-chain's compiler identities and source positions, including distinct generic trait impls and reference receivers. Recoverable macro-generated method bodies are associated with their enclosing source impl; this is not general macro expansion. Rebuild focxt and call-chain together. See the [bytes analysis validation](../docs/bytes-analysis.md) for release builds, reproducible checks, and current limits. diff --git a/focxt/call_chain/src/analysis/hirvisitor.rs b/focxt/call_chain/src/analysis/hirvisitor.rs index b9e7ecd1e9..213f776fb4 100644 --- a/focxt/call_chain/src/analysis/hirvisitor.rs +++ b/focxt/call_chain/src/analysis/hirvisitor.rs @@ -32,6 +32,9 @@ pub struct ImplInformation { pub trait_name: String, pub full_name: String, pub encoded_name: String, + pub loc: SourceInfo, + pub impl_loc: Option, + pub code: String, } pub struct VisitorData<'tcx> { @@ -243,6 +246,11 @@ impl<'tcx> Visitor<'tcx> for HirVisitor<'tcx> { trait_name, full_name: full_name, encoded_name: encoded_name, + loc: fn_source.clone(), + impl_loc: matches!(parent_kind, DefKind::Impl { .. }).then(|| { + SourceInfo::from_span(self.tcx.def_span(parent_def_id), self.tcx.sess.source_map()) + }), + code, }; let data = VisitorData { diff --git a/focxt/src/collect_context/crate_context.rs b/focxt/src/collect_context/crate_context.rs index 589aa0ddb7..5527fbd0fc 100644 --- a/focxt/src/collect_context/crate_context.rs +++ b/focxt/src/collect_context/crate_context.rs @@ -118,6 +118,12 @@ impl CrateContext { } } + pub fn bind_compiler_functions(&self, infos: &[ImplInformation]) { + for context in &self.main_mod_contexts { + context.borrow_mut().bind_compiler_functions(infos); + } + } + pub fn get_all_new_calls_and_types( &self, impl_informations: &Vec, diff --git a/focxt/src/collect_context/items_context.rs b/focxt/src/collect_context/items_context.rs index 87b2c66ad2..3694939dab 100644 --- a/focxt/src/collect_context/items_context.rs +++ b/focxt/src/collect_context/items_context.rs @@ -1,5 +1,6 @@ use std::{cell::RefCell, rc::Rc}; +use call_chain::analysis::hirvisitor::ImplInformation; use quote::ToTokens; use syn::{ ImplItemConst, ImplItemFn, ImplItemType, Item, ItemConst, ItemEnum, ItemFn, ItemImpl, @@ -751,6 +752,7 @@ impl TypeItem { #[derive(Debug, Clone)] pub struct FnItem { fn_name: Name, + encoded_name: Option, complete_name_in_file: String, item: Option, // has_items: bool, @@ -763,6 +765,7 @@ impl FnItem { pub fn new() -> Self { FnItem { fn_name: Name::none(), + encoded_name: None, complete_name_in_file: String::new(), // complete_function_name_in_file: String::new(), item: None, @@ -821,6 +824,16 @@ impl FnItem { self.fn_name.get_name() } + pub fn bind_compiler_info(&mut self, info: &ImplInformation) { + self.fn_name.insert_complete_name(&info.full_name); + self.fn_name.insert_import_name(&info.full_name); + self.encoded_name = Some(info.encoded_name.clone()); + } + + pub fn get_encoded_name(&self) -> Option { + self.encoded_name.clone() + } + pub fn get_complete_name(&self) -> String { self.fn_name.get_import_name().to_string() } @@ -854,6 +867,10 @@ impl FnItem { Item::Fn(self.item.clone().unwrap()) } + pub fn get_item(&self) -> &ItemFn { + self.item.as_ref().unwrap() + } + pub fn insert_visibility(&mut self, visibility: MyVisibility) { self.visibility = visibility; } @@ -922,6 +939,7 @@ impl ImplConstItem { #[derive(Debug, Clone)] pub struct ImplFnItem { fn_name: Name, + encoded_name: Option, complete_name_in_file: String, item: Option, // has_items: bool, @@ -934,6 +952,7 @@ impl ImplFnItem { pub fn new() -> Self { ImplFnItem { fn_name: Name::none(), + encoded_name: None, complete_name_in_file: String::new(), item: None, inside_items: Vec::new(), @@ -1042,6 +1061,16 @@ impl ImplFnItem { self.fn_name.get_name() } + pub fn bind_compiler_info(&mut self, info: &ImplInformation) { + self.fn_name.insert_complete_name(&info.full_name); + self.fn_name.insert_import_name(&info.full_name); + self.encoded_name = Some(info.encoded_name.clone()); + } + + pub fn get_encoded_name(&self) -> Option { + self.encoded_name.clone() + } + pub fn get_complete_name(&self) -> String { self.fn_name.get_import_name().to_string() } @@ -1172,6 +1201,10 @@ impl ImplItem { Item::Impl(item_impl) } + pub fn get_fns_mut(&mut self) -> &mut Vec { + &mut self.functions + } + pub fn get_fns(&self) -> &Vec { &self.functions } @@ -1510,6 +1543,7 @@ impl TraitConstItem { #[derive(Debug, Clone)] pub struct TraitFnItem { fn_name: Name, + encoded_name: Option, complete_name_in_file: String, item: Option, // has_items: bool, @@ -1521,6 +1555,7 @@ impl TraitFnItem { pub fn new() -> Self { TraitFnItem { fn_name: Name::none(), + encoded_name: None, complete_name_in_file: String::new(), item: None, inside_items: Vec::new(), @@ -1584,6 +1619,16 @@ impl TraitFnItem { self.fn_name.get_name() } + pub fn bind_compiler_info(&mut self, info: &ImplInformation) { + self.fn_name.insert_complete_name(&info.full_name); + self.fn_name.insert_import_name(&info.full_name); + self.encoded_name = Some(info.encoded_name.clone()); + } + + pub fn get_encoded_name(&self) -> Option { + self.encoded_name.clone() + } + pub fn get_complete_name(&self) -> String { self.fn_name.get_import_name().to_string() } @@ -1697,6 +1742,10 @@ impl TraitItem { Item::Trait(item_trait) } + pub fn get_fns_mut(&mut self) -> &mut Vec { + &mut self.functions + } + pub fn get_fns(&self) -> &Vec { &self.functions } diff --git a/focxt/src/collect_context/mod_context.rs b/focxt/src/collect_context/mod_context.rs index 3fcd9ac7ce..7d0eea9d32 100644 --- a/focxt/src/collect_context/mod_context.rs +++ b/focxt/src/collect_context/mod_context.rs @@ -209,6 +209,14 @@ impl ModContext { self.lib_mod = Some(Rc::clone(lib_mod)); } + pub fn bind_compiler_functions(&mut self, infos: &[ImplInformation]) { + self.syntax_context + .bind_compiler_functions(&self.mod_info.get_mod_tree().to_string(), infos); + for context in &self.sub_mods { + context.borrow_mut().bind_compiler_functions(infos); + } + } + pub fn parse_from_items( parent: &Rc>, items: &Vec, diff --git a/focxt/src/collect_context/syntax_context.rs b/focxt/src/collect_context/syntax_context.rs index 78131ae1bb..a032676e31 100644 --- a/focxt/src/collect_context/syntax_context.rs +++ b/focxt/src/collect_context/syntax_context.rs @@ -2,7 +2,7 @@ use std::{ cell::RefCell, collections::{HashMap, HashSet}, fs::{create_dir_all, File}, - io::{Read, Write}, + io::{BufReader, Read, Write}, path::PathBuf, process::exit, rc::Rc, @@ -19,8 +19,6 @@ use syn::{ Stmt, Type, TypeParamBound, UseTree as SynUseTree, Visibility, }; -use crate::utils::get_encoded_name; - use super::{ crate_context::CrateContext, items_context::{ @@ -525,7 +523,7 @@ fn get_syntax_recursively( }; let call_file = output_path.join("new_callsandtypes").join(format!("{}.json", info.encoded_name)); if let Ok(file) = File::open(call_file) { - let new_data: CallsAndTypes = serde_json::from_reader(file).unwrap(); + let new_data: CallsAndTypes = serde_json::from_reader(BufReader::new(file)).unwrap(); remain_calls.extend(new_data.calls.iter().filter(|name| !already_calls.contains(*name)).cloned()); end_data.add_data(&new_data); } @@ -995,6 +993,74 @@ impl SyntaxContext { syntax } + pub fn bind_compiler_functions(&mut self, mod_tree: &str, infos: &[ImplInformation]) { + let infos: Vec<_> = infos + .iter() + .filter(|info| info.mod_name == mod_tree) + .collect(); + let matches_function = |info: &ImplInformation, ident: &syn::Ident| { + let point = ident.span().start(); + let point = (point.line, point.column + 1); + info.fn_name == ident.to_string() + && (info.loc.get_startline(), info.loc.get_startcolumn()) <= point + && point < (info.loc.get_endline(), info.loc.get_endcolumn()) + }; + for function in &mut self.functions { + if let Some(info) = infos.iter().find(|info| { + info.impl_loc.is_none() && matches_function(info, &function.get_item().sig.ident) + }) { + function.bind_compiler_info(info); + } + } + for item in &mut self.impls { + // Match the impl token against rustc's header span, retaining the + // receiver type and generic-trait instance as part of its identity. + let point = item.get_item().impl_token.span.start(); + let point = (point.line, point.column + 1); + for info in infos.iter().filter(|info| { + info.impl_loc.as_ref().is_some_and(|loc| { + (loc.get_startline(), loc.get_startcolumn()) <= point + && point < (loc.get_endline(), loc.get_endcolumn()) + }) + }) { + if let Some(function) = item + .get_fns_mut() + .iter_mut() + .find(|function| matches_function(info, &function.get_item().sig.ident)) + { + function.bind_compiler_info(info); + } else { + // rustc already selected this method and recovered parseable + // source from its macro definition; no macro expansion here. + let parsed: syn::ImplItemFn = syn::parse_str(&info.code).unwrap(); + let mut function = ImplFnItem::new(); + function.insert_fn_name(&info.fn_name); + function + .insert_complete_name_in_file(&format!("{{impl#{}}}", item.get_impl_num())); + function.insert_visibility(parse_visibility(&parsed.vis)); + function.insert_item(&parsed); + function.bind_compiler_info(info); + item.insert_function(&function); + } + } + item.get_fns_mut() + .retain(|function| function.get_encoded_name().is_some()); + } + for item in &mut self.traits { + for function in item.get_fns_mut() { + if let Some(info) = infos.iter().find(|info| { + info.impl_loc.is_none() + && matches_function(info, &function.get_item().sig.ident) + }) { + function.bind_compiler_info(info); + } + } + item.get_fns_mut().retain(|function| { + function.get_item().default.is_none() || function.get_encoded_name().is_some() + }); + } + } + pub fn from_items(items: &Vec) -> Self { let mut syntax_context = SyntaxContext::new(); let mut impl_num: i32 = 0; @@ -1544,7 +1610,11 @@ impl SyntaxContext { fns: &mut HashMap, structs: &mut HashMap, ) { - for function_item in self.functions.iter() { + for function_item in self + .functions + .iter() + .filter(|function| function.get_encoded_name().is_some()) + { let fn_data = FnData { fn_name: function_item.get_name(), complete_fn_name: function_item.get_complete_name(), @@ -1555,7 +1625,11 @@ impl SyntaxContext { for impl_item in self.impls.iter() { let mut empty_impl_item = impl_item.clone(); empty_impl_item.clear(); - for function_item in impl_item.get_fns().iter() { + for function_item in impl_item + .get_fns() + .iter() + .filter(|function| function.get_encoded_name().is_some()) + { let fn_data = FnData { fn_name: function_item.get_name(), complete_fn_name: function_item.get_complete_name(), @@ -1567,7 +1641,11 @@ impl SyntaxContext { for trait_item in self.traits.iter() { let mut empty_trait_item = trait_item.clone(); empty_trait_item.clear(); - for function_item in trait_item.get_fns().iter() { + for function_item in trait_item + .get_fns() + .iter() + .filter(|function| function.get_encoded_name().is_some()) + { let fn_data = FnData { fn_name: function_item.get_name(), complete_fn_name: function_item.get_complete_name(), @@ -1871,13 +1949,7 @@ impl SyntaxContext { crate_context: &CrateContext, ) { for function_item in self.functions.iter() { - let encoded_name = get_encoded_name( - impl_informations, - mod_tree, - &function_item.get_name(), - &"".to_string(), - &"".to_string(), - ); + let encoded_name = function_item.get_encoded_name(); let complete_function_name = function_item.get_complete_name(); if let Some(encoded_name) = encoded_name { let call_file = @@ -1917,18 +1989,7 @@ impl SyntaxContext { } for impl_item in self.impls.iter() { for function_item in impl_item.get_fns().iter() { - let trait_name = if *impl_item.get_trait_name() != None { - &impl_item.get_trait_name().as_ref().unwrap().get_name() - } else { - &"".to_string() - }; - let encoded_name = get_encoded_name( - impl_informations, - mod_tree, - &function_item.get_name(), - &impl_item.get_struct_name().get_name(), - trait_name, - ); + let encoded_name = function_item.get_encoded_name(); let complete_function_name = function_item.get_complete_name(); if let Some(encoded_name) = encoded_name { let call_file = @@ -1998,13 +2059,7 @@ impl SyntaxContext { } for trait_item in self.traits.iter() { for function_item in trait_item.get_fns().iter() { - let encoded_name = get_encoded_name( - impl_informations, - mod_tree, - &function_item.get_name(), - &"".to_string(), - &trait_item.get_trait_name().get_name(), - ); + let encoded_name = function_item.get_encoded_name(); let complete_function_name = function_item.get_complete_name(); if let Some(encoded_name) = encoded_name { let call_file = @@ -2061,13 +2116,7 @@ impl SyntaxContext { crate_context: &CrateContext, ) { for function_item in self.functions.iter() { - let encoded_name = get_encoded_name( - impl_informations, - mod_tree, - &function_item.get_name(), - &"".to_string(), - &"".to_string(), - ); + let encoded_name = function_item.get_encoded_name(); let complete_function_name = function_item.get_complete_name(); if let Some(encoded_name) = encoded_name { let call_file = @@ -2118,18 +2167,7 @@ impl SyntaxContext { } for impl_item in self.impls.iter() { for function_item in impl_item.get_fns().iter() { - let trait_name = if *impl_item.get_trait_name() != None { - &impl_item.get_trait_name().as_ref().unwrap().get_name() - } else { - &"".to_string() - }; - let encoded_name = get_encoded_name( - impl_informations, - mod_tree, - &function_item.get_name(), - &impl_item.get_struct_name().get_name(), - trait_name, - ); + let encoded_name = function_item.get_encoded_name(); let complete_function_name = function_item.get_complete_name(); if let Some(encoded_name) = encoded_name { let call_file = output_path @@ -2208,13 +2246,7 @@ impl SyntaxContext { } for trait_item in self.traits.iter() { for function_item in trait_item.get_fns().iter() { - let encoded_name = get_encoded_name( - impl_informations, - mod_tree, - &function_item.get_name(), - &"".to_string(), - &trait_item.get_trait_name().get_name(), - ); + let encoded_name = function_item.get_encoded_name(); let complete_function_name = function_item.get_complete_name(); if let Some(encoded_name) = encoded_name { let call_file = output_path @@ -2248,11 +2280,11 @@ impl SyntaxContext { fns, structs, ); - // let rs_file_name = encoded_name.clone() + ".rs"; - // let output_file_path = output_path.join(rs_file_name); - // let mut file = File::create(output_file_path).unwrap(); - // file.write_all(syntax_context.to_string().as_bytes()) - // .unwrap(); + let rs_file_name = encoded_name.clone() + ".rs"; + let output_file_path = output_path.join(rs_file_name); + let mut file = File::create(output_file_path).unwrap(); + file.write_all(syntax_context.to_string().as_bytes()) + .unwrap(); // let directory_path = output_path.join("new_callsandtypes"); // create_dir_all(&directory_path).unwrap(); diff --git a/focxt/src/main.rs b/focxt/src/main.rs index 877cf1868b..6a0d1644b3 100644 --- a/focxt/src/main.rs +++ b/focxt/src/main.rs @@ -43,6 +43,7 @@ fn main() { crate_context.parse_crate(); crate_context.change_all_names(); + crate_context.bind_compiler_functions(&impl_informations); let mut mod_trees: HashSet = HashSet::new(); crate_context.cout_all_mod_trees_in_one_file_for_test(&mut mod_trees); diff --git a/focxt/src/utils.rs b/focxt/src/utils.rs index 88318060b8..ffe7520946 100644 --- a/focxt/src/utils.rs +++ b/focxt/src/utils.rs @@ -43,22 +43,3 @@ pub fn read_impl_informations_from_json(crate_path: &PathBuf) -> Vec, - mod_name: &String, - fn_name: &String, - struct_name: &String, - trait_name: &String, -) -> Option { - for impl_information in impl_informations.iter() { - if impl_information.mod_name.eq(mod_name) - && impl_information.fn_name.eq(fn_name) - && impl_information.struct_name.eq(struct_name) - && impl_information.trait_name.eq(trait_name) - { - return Some(impl_information.encoded_name.clone()); - } - } - None -} diff --git a/scripts/check_bytes.py b/scripts/check_bytes.py new file mode 100644 index 0000000000..50f1726dc9 --- /dev/null +++ b/scripts/check_bytes.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Validate the bundled bytes analysis in fresh copies, without a model service.""" +import argparse +import json +import os +from pathlib import Path +import platform +import re +import shutil +import signal +import subprocess +import tempfile +import time + +from check_minimal import snapshot, source_fragment + +REPO = Path(__file__).resolve().parents[1] + + +def check_artifacts(root, expected): + names = json.loads((root / 'brinfo/name_map.json').read_text()) + infos = json.loads((root / 'focxt/impl_informations.json').read_text()) + contexts = {info['full_name']: info for info in infos} + assert len(names) == expected, (len(names), expected) + assert set(names) == set(contexts) and len(infos) == expected + encoded = set(names.values()) + for directory in ['brinfo/brdata', 'focxt/callsandtypes', 'focxt/new_callsandtypes']: + assert {p.stem for p in (root / directory).glob('*.json')} == encoded, directory + assert {p.stem for p in (root / 'focxt').glob('*.rs')} == encoded + for name, key in names.items(): + info = contexts[name] + assert info['encoded_name'] == key, name + data = json.loads((root / f'brinfo/brdata/{key}.json').read_text()) + assert data['name'] == name and data['loc'] == info['loc'], name + actual = source_fragment(root, data['loc']).splitlines() + assert [line.lstrip() for line in actual] == [line.lstrip() for line in data['code']], name + assert [line.lstrip() for line in actual] == [line.lstrip() for line in info['code'].splitlines()], name + context = (root / f'focxt/{key}.rs').read_text() + assert context.strip() and re.search(r'\bfn\s+' + re.escape(info['fn_name']) + r'\b', context), name + return names, contexts + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--bin-dir', type=Path, default=REPO / 'target/release') + parser.add_argument('--runs', type=int, default=2, choices=[1, 2]) + args = parser.parse_args() + work = Path(tempfile.mkdtemp(prefix='palm-bytes-check-')).resolve() + print(f'Analysis artifacts: {work}', flush=True) + env = os.environ.copy() + env.pop('CARGO_TARGET_DIR', None) + for name in ['PALM_CONFIG', 'PALM_API_BASE', 'PALM_API_KEY', 'PALM_MODEL']: + env.pop(name, None) + env['PATH'] = str(args.bin_dir.resolve()) + os.pathsep + env['PATH'] + summary = {'platform': platform.platform(), 'bin_dir': str(args.bin_dir.resolve()), 'runs': []} + summary['rustc'] = subprocess.check_output(['rustc', '--version'], cwd=REPO, env=env, text=True).strip() + + def run(root, label, command): + log = root.parent / (root.name + '-' + label + '.log') + start = time.monotonic() + with log.open('w') as output: + process = subprocess.Popen(command, cwd=root, env=env, stdout=output, stderr=subprocess.STDOUT, + start_new_session=True) + try: + status = process.wait(timeout=300) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + raise AssertionError(f'{label}: external 300-second check limit; see {log}') + elapsed = round(time.monotonic() - start, 3) + print(f'{root.name} {label}: exit={status}, {elapsed}s', flush=True) + assert status == 0, f'{label}: see {log}\n{log.read_text()[-3000:]}' + return elapsed, log.read_text() + + fixture = work / 'identities' + (fixture / 'src').mkdir(parents=True) + (fixture / 'Cargo.toml').write_text('[package]\nname="context_fixture"\nversion="0.1.0"\nedition="2021"\n[workspace]\n') + shutil.copyfile(REPO / 'rust-toolchain.toml', fixture / 'rust-toolchain.toml') + (fixture / 'src/lib.rs').write_text('''pub struct Subject; +pub trait Probe { + fn probe(&self, input: T) -> u16; + fn tag(&self) -> u16 { 303 } +} +fn helper() -> u16 { 101 } +impl Probe for Subject { #[cfg(any())] fn probe(&self, _: u8) -> u16 { 999 } #[cfg(not(any()))] fn probe(&self, _: u8) -> u16 { helper() } } impl Probe for Subject { fn probe(&self, _: u16) -> u16 { 202 } } +macro_rules! forward { () => { fn probe(&self, _: u8) -> u16 { 404 } }; } +impl Probe for &Subject { forward!(); } +''') + before = snapshot(fixture) + run(fixture, 'analyze', ['utgen', 'analyze', '-p', str(fixture)]) + assert snapshot(fixture) == before + _, infos = check_artifacts(fixture, 5) + seen = set() + for info in infos.values(): + context = (fixture / f"focxt/{info['encoded_name']}.rs").read_text() + if info['impl_loc']: + header = source_fragment(fixture, info['impl_loc']).strip() + assert header in context, header + if header == 'impl Probe for Subject': + assert 'helper()' in context and '101' in context and '999' not in context + elif header == 'impl Probe for Subject': + assert '202' in context + else: + assert header == 'impl Probe for &Subject' + assert re.search(r'impl Probe for &Subject\s*\{\s*fn probe\([^}]+404', context) + seen.add(header) + elif info['fn_name'] == 'tag': + assert re.search(r'fn tag\([^}]+303', context) + seen.add('default method') + assert len(seen) == 4, seen + + original = snapshot(REPO / 'examples/bytes') + for number in range(1, args.runs + 1): + root = work / f'bytes-{number}' + shutil.copytree(REPO / 'examples/bytes', root, + ignore=shutil.ignore_patterns('target', 'brinfo', 'focxt', 'utgen', 'tests.bak')) + preprocess, _ = run(root, 'preprocess', ['utgen', 'pre-process', '-p', str(root)]) + prepared = snapshot(root) + elapsed, output = run(root, 'analyze', ['utgen', 'analyze', '-p', str(root)]) + assert snapshot(root) == prepared, 'analysis modified prepared sources' + names, infos = check_artifacts(root, 663) + # Verify representative default, generic-impl, and macro-generated methods. + for name, info in infos.items(): + context = (root / f"focxt/{info['encoded_name']}.rs").read_text() + if name == 'bytes::buf::buf_impl::Buf::has_remaining': + assert 'self.remaining() > 0' in context + if info['fn_name'] == 'eq' and info['mod_name'] == 'bytes::bytes': + assert 'PartialEq' in context + if info['fn_name'] == 'get_u8' and info['impl_loc'] and info['mod_name'] == 'bytes::buf::buf_impl': + assert '(**self).get_u8()' in context + stages = {name: float(seconds) for name, seconds in re.findall(r'Analysis step (\w+): ([\d.]+)s', output)} + assert set(stages) == {'clean', 'brinfo', 'focxt'}, stages + summary['runs'].append({'preprocess_seconds': preprocess, 'analyze_seconds': elapsed, + 'stages_seconds': stages, 'functions': len(names), 'contexts': len(infos)}) + (work / 'summary.json').write_text(json.dumps(summary, indent=2) + '\n') + assert snapshot(REPO / 'examples/bytes') == original + print('All bytes analysis checks passed.', flush=True) + + +if __name__ == '__main__': + main() diff --git a/utgen/src/analysis.rs b/utgen/src/analysis.rs index be2fdd7eed..99a935b421 100644 --- a/utgen/src/analysis.rs +++ b/utgen/src/analysis.rs @@ -1,5 +1,5 @@ use crate::types::{BrData, RfocxtNameInformation, TestGenInfo}; -use std::{collections::HashMap, fs, io, path::Path, process::Command}; +use std::{collections::HashMap, fs, io, path::Path, process::Command, time::Instant}; fn read_json(path: &Path) -> io::Result { let data = fs::read(path).map_err(|e| io::Error::other(format!("{}: {e}", path.display())))?; @@ -32,7 +32,12 @@ pub fn validate_analysis(project_dir: &Path, work_dir: &Path) -> io::Result<()> let file = work_dir .join("focxt") .join(format!("{}.rs", info.encoded_name)); - if fs::read_to_string(&file)?.trim().is_empty() { + let source_context = fs::read_to_string(&file).map_err(|error| { + io::Error::other(format!( + "Cannot read focal context for {name} ({}): {error}", file.display() + )) + })?; + if source_context.trim().is_empty() { return Err(io::Error::other(format!("Empty focal context for {name}"))); } let source = project_dir.join(data.loc.get_file()); @@ -73,8 +78,9 @@ pub fn validate_repair(project_dir: &Path, work_dir: &Path) -> io::Result<()> { Ok(()) } -fn run(command: &mut Command) -> io::Result<()> { +fn run(name: &str, command: &mut Command) -> io::Result<()> { let description = format!("{command:?}"); + let start = Instant::now(); let status = command .status() .map_err(|e| io::Error::other(format!("Cannot run {description}: {e}")))?; @@ -83,6 +89,7 @@ fn run(command: &mut Command) -> io::Result<()> { "{description} failed with {status}" ))); } + log::info!("Analysis step {name}: {:.3}s", start.elapsed().as_secs_f64()); Ok(()) } @@ -102,9 +109,9 @@ pub fn analyze_project(work_dir: &Path) -> io::Result<()> { } } // RUSTC_WRAPPER does not by itself invalidate Cargo's existing check cache. - run(Command::new("cargo").arg("clean").current_dir(work_dir))?; - run(Command::new("cargo").arg("brinfo").current_dir(work_dir))?; - run(Command::new("focxt") + run("clean", Command::new("cargo").arg("clean").current_dir(work_dir))?; + run("brinfo", Command::new("cargo").arg("brinfo").current_dir(work_dir))?; + run("focxt", Command::new("focxt") .arg("-c") .arg(work_dir) .current_dir(work_dir))?;