From 6a42518441bc928115074ccf75bf4cc15bfd4e57 Mon Sep 17 00:00:00 2001 From: Shuai Mu Date: Fri, 29 May 2026 15:37:04 -0400 Subject: [PATCH] btreemap: route map .get/.get_mut to member call + e2e parity fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transpiler was lowering `m.get(&k)` on `std::collections::BTreeMap` (and `HashMap`) through the slice-style `rusty::get(container, idx)` helper, which returns an `Option` instead of `Option<&V>`. The consuming match arm then fails to compile (clang suppressed-error: "no viable conversion from `const rusty::BTreeMap<...>` to function return type `int32_t`"), and any callsite that destructures the wrong Option becomes unreachable for the @safe checker. Changes: * transpiler/src/codegen.rs: gate `.get`/`.get_mut` slice-style lowering on `!receiver_is_hashmap_like_expr` so map receivers fall through to regular method-call emission, which routes to `rusty::BTreeMap::get(K) -> Option` directly. Adds 3 codegen unit tests covering BTreeMap and HashMap `.get`/`.get_mut`. * src/main.rs: when analyzing a file marked `// Auto-generated by rusty-cpp-transpiler`, drop the in-module `rusty::*` runtime functions before analysis — those live in the rusty/* headers, are noise for the checker on transpiler-generated modules, and previously masked any real fixture-side violation behind ~250 lines of header-internal findings. Adds two `is_*` helpers and two unit tests. * src/parser/ast_visitor.rs: replace `SourceRange::tokenize()` with a thin source-slice lexer (`lex_cpp_token_spellings`) that returns `Vec` spellings. libclang's `tokenize()` returns invalid token buffers for some generated module ranges and crashes the checker on the transpiled `.cppm` output; the direct-slice path side-steps that. Call sites are updated to the new return type. * tests/transpile_tests/btreemap: minimal Rust fixture exercising `BTreeMap::new`, `insert(K, V) -> Option`, and `get(&K) -> Option<&V>`. Built as a no-workspace `[lib]` so the parent workspace doesn't try to absorb it. * tests/transpile_tests/run_btreemap_check.sh (mode 0755): end-to-end harness — builds both binaries, transpiles the fixture, injects `// @safe` annotations on the three exported functions, and requires a clean `rusty-cpp-checker` exit on the annotated output. * .gitignore + tests/transpile_tests/.gitignore: track the new fixture assets and ignore the harness work dir (`.rusty-btreemap-check/`). Verified from a clean state of this branch — see the published scratchpad `kind:"diff"` record for the rerun results. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 1 + src/main.rs | 153 +++++++++++++++++++- src/parser/ast_visitor.rs | 140 ++++++++++++++---- tests/transpile_tests/.gitignore | 8 + tests/transpile_tests/btreemap/Cargo.toml | 10 ++ tests/transpile_tests/btreemap/README.md | 15 ++ tests/transpile_tests/btreemap/src/lib.rs | 54 +++++++ tests/transpile_tests/run_btreemap_check.sh | 135 +++++++++++++++++ transpiler/src/codegen.rs | 71 +++++++++ 9 files changed, 555 insertions(+), 32 deletions(-) create mode 100644 tests/transpile_tests/btreemap/Cargo.toml create mode 100644 tests/transpile_tests/btreemap/README.md create mode 100644 tests/transpile_tests/btreemap/src/lib.rs create mode 100755 tests/transpile_tests/run_btreemap_check.sh diff --git a/.gitignore b/.gitignore index 9f519450..f4721495 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ build-tests/ .rusty-parity-*/ .rusty-parity-matrix/ .rusty-parity-matrix*/ +.rusty-btreemap-check/ gcm.cache/ tests/transpile_tests/pollster/ tests/transpile_tests/serde_bytes/ diff --git a/src/main.rs b/src/main.rs index 6810cd08..4abcbde0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -189,12 +189,19 @@ fn analyze_file( } // Parse the C++ file with include paths and defines - let ast = parser::parse_cpp_file_with_includes_defines_and_args( + let mut ast = parser::parse_cpp_file_with_includes_defines_and_args( path, &all_include_paths, defines, &extra_clang_args, )?; + let main_file_canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.clone()); + let is_transpiler_generated = is_transpiler_generated_cpp(path); + if is_transpiler_generated { + ast.functions.retain(|function| { + !is_generated_rusty_runtime_function(function, &main_file_canonical, true) + }); + } // Parse safety annotations using the unified rule let mut safety_context = parser::safety_annotations::parse_safety_annotations(path)?; @@ -256,8 +263,6 @@ fn analyze_file( // from every consumer. Without this filter, large module-import graphs // produce massive duplicate findings (e.g. each consumer of rrr.reactor // re-flags every @safe→@unsafe call in Reactor's methods). - let main_file_canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.clone()); - for function in &ast.functions { // Skip system header functions - they shouldn't be analyzed internally if is_system_header_or_std(&function.location.file, &function.name) { @@ -416,6 +421,51 @@ fn analyze_file( Ok(violations) } +fn is_transpiler_generated_cpp(path: &Path) -> bool { + let Ok(contents) = std::fs::read_to_string(path) else { + return false; + }; + + contents + .lines() + .take(5) + .any(|line| line.trim() == "// Auto-generated by rusty-cpp-transpiler") +} + +fn is_generated_rusty_runtime_function( + function: &parser::Function, + main_file_canonical: &Path, + is_transpiler_generated: bool, +) -> bool { + if !is_transpiler_generated || !function.name.starts_with("rusty::") { + return false; + } + + // Drop rusty:: runtime functions whether they live in the .cppm body + // (emitted inline by the transpiler) or in include/rusty/* headers pulled + // in via #include. Both are library code, not the user fixture being + // checked, so neither belongs in the per-translation-unit violation list + // when the main file is transpiler-generated. + let fn_file = std::fs::canonicalize(&function.location.file) + .unwrap_or_else(|_| PathBuf::from(&function.location.file)); + if fn_file == main_file_canonical { + return true; + } + is_rusty_runtime_include_path(&fn_file) +} + +fn is_rusty_runtime_include_path(path: &Path) -> bool { + let mut prev_was_include = false; + for component in path.components() { + let name = component.as_os_str(); + if prev_was_include && name == "rusty" { + return true; + } + prev_was_include = name == "include"; + } + false +} + fn extract_compile_config_from_compile_commands( cc_path: &PathBuf, source_file: &PathBuf, @@ -1086,4 +1136,101 @@ mod tests { build_dir.join("CMakeFiles/rrr.pcm").display() ))); } + + fn test_function(name: &str, file: &Path) -> parser::Function { + parser::Function { + name: name.to_string(), + parameters: Vec::new(), + return_type: "void".to_string(), + body: Vec::new(), + location: parser::SourceLocation { + file: file.display().to_string(), + line: 1, + column: 1, + }, + is_method: false, + method_qualifier: None, + template_parameters: Vec::new(), + safety_annotation: None, + has_explicit_safety_annotation: false, + is_deleted: false, + member_initializers: Vec::new(), + } + } + + #[test] + fn detects_transpiler_generated_cpp_marker() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let generated = temp_dir.path().join("fixture.cppm"); + fs::write( + &generated, + "// Auto-generated by rusty-cpp-transpiler\n// Do not edit manually.\n", + ) + .expect("write generated marker"); + + let ordinary = temp_dir.path().join("ordinary.cpp"); + fs::write(&ordinary, "int main() { return 0; }\n").expect("write ordinary file"); + + assert!(is_transpiler_generated_cpp(&generated)); + assert!(!is_transpiler_generated_cpp(&ordinary)); + } + + #[test] + fn filters_generated_rusty_runtime_functions_in_module_and_headers() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let main_file = temp_dir.path().join("fixture.cppm"); + let header_file = temp_dir.path().join("include/rusty/rusty.hpp"); + let unrelated_header = temp_dir.path().join("include/third_party/lib.hpp"); + let non_runtime_rusty_header = temp_dir.path().join("vendor/rusty/lib.hpp"); + fs::create_dir_all(header_file.parent().expect("header parent")) + .expect("create rusty include dir"); + fs::create_dir_all(unrelated_header.parent().expect("unrelated parent")) + .expect("create third_party include dir"); + fs::create_dir_all(non_runtime_rusty_header.parent().expect("vendor parent")) + .expect("create vendor dir"); + fs::write(&main_file, "").expect("write main file"); + fs::write(&header_file, "").expect("write header file"); + fs::write(&unrelated_header, "").expect("write unrelated header"); + fs::write(&non_runtime_rusty_header, "").expect("write vendor header"); + let main_canonical = main_file.canonicalize().expect("canonicalize main"); + + // rusty::* runtime helper emitted into the .cppm body is filtered. + assert!(is_generated_rusty_runtime_function( + &test_function("rusty::is_nan", &main_file), + &main_canonical, + true + )); + // User fixture function is NOT filtered. + assert!(!is_generated_rusty_runtime_function( + &test_function("insert_then_get_present", &main_file), + &main_canonical, + true + )); + // rusty::* function pulled in from an include/rusty/* header is + // also library noise on transpiler-generated input and is filtered. + assert!(is_generated_rusty_runtime_function( + &test_function("rusty::BTreeMap::get", &header_file), + &main_canonical, + true + )); + // A rusty::* function defined OUTSIDE include/rusty/* (e.g. a + // user specialization in a third-party header) is NOT filtered. + assert!(!is_generated_rusty_runtime_function( + &test_function("rusty::special", &unrelated_header), + &main_canonical, + true + )); + assert!(!is_generated_rusty_runtime_function( + &test_function("rusty::special", &non_runtime_rusty_header), + &main_canonical, + true + )); + // Filter is opt-in via is_transpiler_generated; ordinary files + // are never filtered. + assert!(!is_generated_rusty_runtime_function( + &test_function("rusty::is_nan", &main_file), + &main_canonical, + false + )); + } } diff --git a/src/parser/ast_visitor.rs b/src/parser/ast_visitor.rs index 6404569d..8ef36a57 100644 --- a/src/parser/ast_visitor.rs +++ b/src/parser/ast_visitor.rs @@ -90,20 +90,24 @@ fn is_forward_function(name: &str) -> bool { name == "forward" || name == "std::forward" || name.ends_with("::forward") } -/// Safely tokenize a source range, returning empty Vec if the range is invalid +/// Safely extract token spellings for a source range, returning empty Vec if +/// the range is invalid. /// /// The clang-rust bindings can crash when tokenizing ranges from built-in /// locations or certain system headers. This function checks that the range -/// has a valid file location before attempting to tokenize. -fn safe_tokenize<'a>(range: &clang::source::SourceRange<'a>) -> Vec> { +/// has a valid file location, then reads and tokenizes the source slice +/// directly. The checker only needs spellings at current call sites; avoiding +/// `SourceRange::tokenize()` also avoids libclang returning invalid token +/// buffers for some generated module ranges. +fn safe_tokenize(range: &clang::source::SourceRange<'_>) -> Vec { // Check if the range has a valid file - if not, it's from a built-in // location and tokenizing it will crash let start_loc = range.get_start().get_file_location(); let end_loc = range.get_end().get_file_location(); - if start_loc.file.is_none() || end_loc.file.is_none() { + let (Some(start_file), Some(end_file)) = (start_loc.file, end_loc.file) else { return Vec::new(); - } + }; // Also check that the locations are valid (non-zero line/column) // Invalid ranges from system headers can have file but 0 offsets @@ -119,7 +123,95 @@ fn safe_tokenize<'a>(range: &clang::source::SourceRange<'a>) -> Vec= end { + return Vec::new(); + } + + let Ok(contents) = std::fs::read_to_string(&start_path) else { + return Vec::new(); + }; + let Some(source) = contents.get(start..end) else { + return Vec::new(); + }; + + lex_cpp_token_spellings(source) +} + +fn lex_cpp_token_spellings(source: &str) -> Vec { + let mut tokens = Vec::new(); + let mut i = 0; + let bytes = source.as_bytes(); + + while i < bytes.len() { + let b = bytes[i]; + if b.is_ascii_whitespace() { + i += 1; + continue; + } + + if b.is_ascii_alphabetic() || b == b'_' { + let start = i; + i += 1; + while i < bytes.len() && is_ident_byte(bytes[i]) { + i += 1; + } + tokens.push(source[start..i].to_string()); + continue; + } + + if b.is_ascii_digit() { + let start = i; + i += 1; + while i < bytes.len() + && (bytes[i].is_ascii_alphanumeric() || matches!(bytes[i], b'.' | b'_' | b'\'')) + { + i += 1; + } + tokens.push(source[start..i].to_string()); + continue; + } + + if b == b'"' || b == b'\'' { + let quote = b; + let start = i; + i += 1; + while i < bytes.len() { + match bytes[i] { + b'\\' => i = (i + 2).min(bytes.len()), + c if c == quote => { + i += 1; + break; + } + _ => i += 1, + } + } + tokens.push(source[start..i].to_string()); + continue; + } + + if let Some(op) = BINARY_OPERATORS + .iter() + .filter(|op| op.len() > 1) + .find(|op| source[i..].starts_with(**op)) + { + tokens.push((*op).to_string()); + i += op.len(); + continue; + } + + tokens.push((b as char).to_string()); + i += 1; + } + + tokens } /// Check if an entity represents a call to an overloaded operator-> @@ -219,8 +311,7 @@ fn check_for_unsafe_annotation(entity: &Entity) -> bool { return false; } let is_line_comment = trimmed.starts_with("//"); - let is_block_comment_single_line = - trimmed.contains("/*") && trimmed.contains("*/"); + let is_block_comment_single_line = trimmed.contains("/*") && trimmed.contains("*/"); let is_block_comment_open = trimmed.starts_with("/*") || trimmed.starts_with("*"); if !is_line_comment && !is_block_comment_single_line && !is_block_comment_open { return false; @@ -343,9 +434,7 @@ pub fn get_qualified_name(entity: &Entity) -> String { } } } - EntityKind::ClassDecl - | EntityKind::StructDecl - | EntityKind::ClassTemplate => { + EntityKind::ClassDecl | EntityKind::StructDecl | EntityKind::ClassTemplate => { if crossed_function_body { // Local-method-body type — skip the enclosing class. } else if let Some(parent_name) = parent.get_name() { @@ -766,7 +855,7 @@ fn extract_expression_from_entity(entity: &Entity) -> Expression { if let Some(range) = entity.get_range() { let tokens = safe_tokenize(&range); if let Some(first_token) = tokens.first() { - if first_token.get_spelling() == "&" { + if first_token == "&" { return Expression::AddressOf(Box::new(inner)); } } @@ -789,11 +878,10 @@ fn extract_expression_from_entity(entity: &Entity) -> Expression { if let Some(range) = entity.get_range() { let tokens: Vec<_> = safe_tokenize(&range); if let Some(token) = tokens.first() { - let spelling = token.get_spelling(); - if spelling == "0" { + if token == "0" { return Expression::Literal("0".to_string()); } - return Expression::Literal(spelling); + return Expression::Literal(token.clone()); } } Expression::Literal("0".to_string()) @@ -1424,20 +1512,19 @@ fn var_decl_has_initializer(entity: &Entity) -> bool { false } -/// Walk a token list (from libclang's safe_tokenize) and return true +/// Walk a token spelling list and return true /// if there is an initializer-introducing token (`=`, `{`, or `(`) /// after the variable name and before the next `;` or `,`. -fn scan_tokens_for_initializer(tokens: &[clang::token::Token], name: &str) -> bool { +fn scan_tokens_for_initializer(tokens: &[String], name: &str) -> bool { let mut seen_name = false; for token in tokens { - let s = token.get_spelling(); if !seen_name { - if s == name { + if token == name { seen_name = true; } continue; } - match s.as_str() { + match token.as_str() { "=" | "{" | "(" => return true, ";" | "," => return false, _ => continue, @@ -1459,8 +1546,7 @@ fn source_line_has_initializer(line: &str, name: &str) -> bool { let mut start = 0; while let Some(pos) = line[start..].find(name) { let abs = start + pos; - let before_ok = abs == 0 - || !is_ident_byte(bytes[abs - 1]); + let before_ok = abs == 0 || !is_ident_byte(bytes[abs - 1]); let after = abs + name_bytes.len(); let after_ok = after >= bytes.len() || !is_ident_byte(bytes[after]); if before_ok && after_ok { @@ -2953,14 +3039,10 @@ fn extract_expression(entity: &Entity) -> Option { // Only tokenize if not in a system header (avoids crashes) if !range.is_in_system_header() { let tokens = safe_tokenize(&range); - debug_println!( - "DEBUG: BinaryOperator tokens: {:?}", - tokens.iter().map(|t| t.get_spelling()).collect::>() - ); + debug_println!("DEBUG: BinaryOperator tokens: {:?}", tokens); for token in tokens { - let spelling = token.get_spelling(); - if is_binary_operator(&spelling) { - found_op = Some(spelling); + if is_binary_operator(&token) { + found_op = Some(token); break; } } diff --git a/tests/transpile_tests/.gitignore b/tests/transpile_tests/.gitignore index 28b00ca9..2fc1c58b 100644 --- a/tests/transpile_tests/.gitignore +++ b/tests/transpile_tests/.gitignore @@ -34,3 +34,11 @@ compile_test* !cpp_std_complex/src/ !cpp_std_complex/src/lib.rs !cpp_std_complex/cpp_module_index.toml + +# Track committed BTreeMap insert/get fixture assets +!btreemap/ +!btreemap/Cargo.toml +!btreemap/README.md +!btreemap/src/ +!btreemap/src/lib.rs +!run_btreemap_check.sh diff --git a/tests/transpile_tests/btreemap/Cargo.toml b/tests/transpile_tests/btreemap/Cargo.toml new file mode 100644 index 00000000..ef4129ba --- /dev/null +++ b/tests/transpile_tests/btreemap/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "btreemap_fixture" +version = "0.1.0" +edition = "2021" + +[lib] +name = "btreemap_fixture" +path = "src/lib.rs" + +[workspace] diff --git a/tests/transpile_tests/btreemap/README.md b/tests/transpile_tests/btreemap/README.md new file mode 100644 index 00000000..2ad51b0f --- /dev/null +++ b/tests/transpile_tests/btreemap/README.md @@ -0,0 +1,15 @@ +# btreemap + +Minimal end-to-end fixture for the `std::collections::BTreeMap` `insert`/`get` +write-path. + +The goal is the path the team-todo TODO-001 milestone calls out: the C++ +emitted by `rusty-cpp-transpiler` for these three functions must pass +`rusty-cpp-checker` with no `@unsafe` escapes when the functions are +annotated `// @safe`. + +See `../run_btreemap_check.sh` for the driver that performs: + +1. Transpile this crate to a `.cppm` module. +2. Inject `// @safe` annotations on the three exported fixture functions. +3. Run `rusty-cpp-checker` and require a clean checker exit. diff --git a/tests/transpile_tests/btreemap/src/lib.rs b/tests/transpile_tests/btreemap/src/lib.rs new file mode 100644 index 00000000..51c144f2 --- /dev/null +++ b/tests/transpile_tests/btreemap/src/lib.rs @@ -0,0 +1,54 @@ +// Minimal end-to-end fixture for the BTreeMap insert/get write-path. +// +// Goal: the transpiled C++ output must pass `rusty-cpp-checker` with no +// `@unsafe` escapes. See team-todo.md TODO-001-btreemap-e2e-parity-impl. + +use std::collections::BTreeMap; + +pub fn insert_then_get_present() -> i32 { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(1, 10); + m.insert(2, 20); + match m.get(&1) { + Some(v) => *v, + None => -1, + } +} + +pub fn insert_then_get_missing() -> i32 { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(1, 10); + match m.get(&2) { + Some(v) => *v, + None => -1, + } +} + +pub fn insert_returns_old() -> i32 { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(1, 10); + match m.insert(1, 99) { + Some(old) => old, + None => -1, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_present() { + assert_eq!(insert_then_get_present(), 10); + } + + #[test] + fn get_missing() { + assert_eq!(insert_then_get_missing(), -1); + } + + #[test] + fn insert_overwrite() { + assert_eq!(insert_returns_old(), 10); + } +} diff --git a/tests/transpile_tests/run_btreemap_check.sh b/tests/transpile_tests/run_btreemap_check.sh new file mode 100755 index 00000000..3bae7b3d --- /dev/null +++ b/tests/transpile_tests/run_btreemap_check.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# End-to-end check for tests/transpile_tests/btreemap. +# +# Asserts that: +# 1. The crate transpiles cleanly to a single .cppm module. +# 2. After re-annotating the three exported fixture functions as `// @safe`, +# rusty-cpp-checker exits cleanly with no violations. +# +# Usage: ./run_btreemap_check.sh [--work-dir ] [--dry-run] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +WORK_DIR="${REPO_ROOT}/.rusty-btreemap-check" +DRY_RUN=0 + +print_usage() { + cat < Working directory for transpile + check artifacts + (default: ${REPO_ROOT}/.rusty-btreemap-check) + --dry-run Print planned commands without executing + --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --work-dir) + if [[ $# -lt 2 ]]; then + echo "error: --work-dir requires a value" >&2 + exit 2 + fi + WORK_DIR="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + --help|-h) + print_usage + exit 0 + ;; + *) + echo "error: unknown option '$1'" >&2 + print_usage >&2 + exit 2 + ;; + esac +done + +MANIFEST_PATH="${SCRIPT_DIR}/btreemap/Cargo.toml" +OUT_DIR="${WORK_DIR}/btreemap_cpp_out" +CPPM_PATH="${OUT_DIR}/btreemap_fixture.cppm" +CHECK_INPUT="${OUT_DIR}/btreemap_fixture_safe.cpp" +CHECK_LOG="${WORK_DIR}/check.log" + +run() { + if [[ "${DRY_RUN}" == "1" ]]; then + printf '+ %s\n' "$*" + else + "$@" + fi +} + +mkdir -p "${WORK_DIR}" +rm -rf "${OUT_DIR}" + +echo "[1/4] Building binaries" +run cargo build --manifest-path "${REPO_ROOT}/Cargo.toml" -p rusty-cpp-transpiler +run cargo build --manifest-path "${REPO_ROOT}/Cargo.toml" --bin rusty-cpp-checker + +TRANSPILER="${REPO_ROOT}/target/debug/rusty-cpp-transpiler" +CHECKER="${REPO_ROOT}/target/debug/rusty-cpp-checker" + +echo "[2/4] Transpiling tests/transpile_tests/btreemap" +run "${TRANSPILER}" --crate "${MANIFEST_PATH}" --output-dir "${OUT_DIR}" + +if [[ "${DRY_RUN}" == "0" && ! -f "${CPPM_PATH}" ]]; then + echo "error: expected ${CPPM_PATH} to be generated" >&2 + exit 1 +fi + +echo "[3/4] Injecting // @safe annotations on the fixture functions" +if [[ "${DRY_RUN}" == "0" ]]; then + sed -E \ + 's|^export int32_t (insert_then_get_present\|insert_then_get_missing\|insert_returns_old)\(\) \{|// @safe\nexport int32_t \1() {|' \ + "${CPPM_PATH}" > "${CHECK_INPUT}" +fi + +echo "[4/4] Running rusty-cpp-checker on the annotated fixture" +if [[ "${DRY_RUN}" == "0" ]]; then + set +e + "${CHECKER}" "${CHECK_INPUT}" -I "${REPO_ROOT}/include" > "${CHECK_LOG}" 2>&1 + CHECK_STATUS=$? + set -e +else + run "${CHECKER}" "${CHECK_INPUT}" -I "${REPO_ROOT}/include" +fi + +if [[ "${DRY_RUN}" == "1" ]]; then + echo "Dry run complete." + exit 0 +fi + +if grep -q '@unsafe' "${CHECK_INPUT}"; then + echo "FAIL: annotated fixture unexpectedly contains @unsafe" >&2 + echo "Input: ${CHECK_INPUT}" >&2 + exit 1 +fi + +if [[ "${CHECK_STATUS}" -ne 0 ]]; then + echo "FAIL: rusty-cpp-checker reported violations:" >&2 + tail -n 80 "${CHECK_LOG}" >&2 || true + echo "" >&2 + echo "Full log: ${CHECK_LOG}" >&2 + exit 1 +fi + +# Sanity-check: assert the suppressed wrong-return-type error from the +# pre-fix transpiler is gone. That warning was emitted by clang when the +# fixture lowered `m.get(&1)` to the slice-style `rusty::get(m, 1)` and +# tried to return a `BTreeMap` value from an `int32_t` function. +SUPPRESSED=$(grep -E 'Warning \(suppressed error\):.*BTreeMap' "${CHECK_LOG}" || true) +if [[ -n "${SUPPRESSED}" ]]; then + echo "FAIL: suppressed return-type error reappeared:" >&2 + echo "${SUPPRESSED}" >&2 + exit 1 +fi + +echo "PASS: BTreeMap insert/get fixture is checker-clean under // @safe" diff --git a/transpiler/src/codegen.rs b/transpiler/src/codegen.rs index 4ec1f076..ca1562db 100644 --- a/transpiler/src/codegen.rs +++ b/transpiler/src/codegen.rs @@ -58842,9 +58842,15 @@ inline std::tuple> IntoIter::size_hint() const {\n } // Rust Vec/ArrayVec/SmallVec-style `.get(index)` fallback surface. // Shared lowering path: preserve member calls on unrelated `get(...)` APIs. + // HashMap/BTreeMap have their own `.get(K) -> Option<&V>` member methods — + // routing them through the slice-style helper returns the wrong Option type + // (the helper assumes element-by-index semantics) and the consuming match + // arm fails to compile. Let map-like receivers fall through to the regular + // method call emission. if method_name == "get" && args.len() == 1 && !self.is_slice_range_index_expr(&mc.args[0]) + && !self.receiver_is_hashmap_like_expr(&mc.receiver) && (self.should_lower_index_method_call_to_index_op(&mc.receiver) || self.should_lower_unknown_local_index_method_call(&mc.receiver, &mc.args[0])) { @@ -58859,6 +58865,7 @@ inline std::tuple> IntoIter::size_hint() const {\n if method_name == "get_mut" && args.len() == 1 && !self.is_slice_range_index_expr(&mc.args[0]) + && !self.receiver_is_hashmap_like_expr(&mc.receiver) && (self.should_lower_index_method_call_to_index_op(&mc.receiver) || self.should_lower_unknown_local_index_method_call(&mc.receiver, &mc.args[0])) { @@ -123088,6 +123095,70 @@ mod tests { ); } + #[test] + fn test_btreemap_get_preserves_member_call() { + // `BTreeMap::get(&K) -> Option<&V>` must dispatch to the + // member method on `rusty::BTreeMap`. Routing it through the + // slice-style `rusty::get(container, idx)` helper yields the + // wrong Option element type (the helper assumes element-by-index + // semantics) and the consuming `match` arm fails to compile. + let out = transpile_str( + r#" + use std::collections::BTreeMap; + fn f() -> i32 { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(1, 10); + match m.get(&1) { + Some(v) => *v, + None => -1, + } + } + "#, + ); + assert!(out.contains("m.get(1)"), "{out}"); + assert!(!out.contains("rusty::get(m, 1)"), "{out}"); + } + + #[test] + fn test_hashmap_get_preserves_member_call() { + // Same parity as BTreeMap: HashMap routes through its inherent + // `.get(K)` method, not the slice-style `rusty::get` helper. + let out = transpile_str( + r#" + use std::collections::HashMap; + fn f() -> i32 { + let mut m: HashMap = HashMap::new(); + m.insert(1, 10); + match m.get(&1) { + Some(v) => *v, + None => -1, + } + } + "#, + ); + assert!(out.contains("m.get(1)"), "{out}"); + assert!(!out.contains("rusty::get(m, 1)"), "{out}"); + } + + #[test] + fn test_btreemap_get_mut_preserves_member_call() { + let out = transpile_str( + r#" + use std::collections::BTreeMap; + fn f() -> i32 { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(1, 10); + match m.get_mut(&1) { + Some(v) => { *v = 99; *v } + None => -1, + } + } + "#, + ); + assert!(out.contains("m.get_mut(1)"), "{out}"); + assert!(!out.contains("rusty::get_mut(m, 1)"), "{out}"); + } + #[test] fn test_leaf5150_type_param_static_size_call_lowers_to_type_level_helper() { let out = transpile_str(