Skip to content

Commit 3f9641d

Browse files
committed
Fix type ambiguity in spec generation for shadowed type names
When a Rust function uses a type via `use` import (e.g., `use std::sync::atomic::Ordering`), the spec generator only sees the short name "Ordering" from the syn AST. This conflicts with Lean's native `Ordering` (core.cmp.Ordering) brought in by `open Aeneas.Std`, causing type mismatches in the generated Pre/Post structs. After Aeneas generates Funs.lean (which contains fully-qualified type names in def signatures), parse those signatures and use the Aeneas types as overrides in spec generation. Falls back to the existing map_type behavior when no Aeneas type info is available.
1 parent be6f199 commit 3f9641d

4 files changed

Lines changed: 234 additions & 6 deletions

File tree

anneal/src/aeneas.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,20 @@ pub fn generate_lean_workspace(roots: &LockedRoots, artifacts: &[AnnealArtifact]
346346
let slug = artifact.artifact_slug();
347347
let output_dir = lean_generated_root.join(&slug);
348348

349+
// Parse parameter types from Aeneas-generated Funs.lean to use
350+
// fully-qualified type names in spec generation. We undo the
351+
// `show` -> `show1` patch so parameter names match the Rust source.
352+
let funs_path = output_dir.join("Funs.lean");
353+
let funs_types = if funs_path.exists() {
354+
let content = std::fs::read_to_string(&funs_path)
355+
.with_context(|| format!("Failed to read {}", funs_path.display()))?;
356+
crate::funs_types::parse_funs_types(&content)
357+
} else {
358+
crate::funs_types::FunsTypeMap::new()
359+
};
360+
349361
// Generate Anneal specs
350-
let generated = generate::generate_artifact(artifact);
362+
let generated = generate::generate_artifact_with_funs_types(artifact, &funs_types);
351363
let specs_path = output_dir.join(artifact.lean_spec_file_name());
352364
let map_path = output_dir.join(format!("{}.lean.map", artifact.artifact_slug()));
353365

anneal/src/funs_types.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
//! Parses Aeneas-generated `Funs.lean` to extract fully-qualified parameter types.
2+
//!
3+
//! Anneal's spec generator re-derives types from the Rust AST, losing qualification
4+
//! for `use`-imported names (e.g., `Ordering` instead of `core.sync.atomic.Ordering`).
5+
//! This module provides a lookup table from the Aeneas output as a corrective.
6+
7+
use std::collections::HashMap;
8+
9+
/// Function name → `[(param_name, lean_type)]`.
10+
pub type FunsTypeMap = HashMap<String, Vec<(String, String)>>;
11+
12+
/// Parses `def` signatures from `Funs.lean`, extracting explicit parameter types.
13+
/// Skips implicit `{}` parameters. Returns an empty map on parse failure.
14+
pub fn parse_funs_types(content: &str) -> FunsTypeMap {
15+
let mut map = FunsTypeMap::new();
16+
let lines: Vec<&str> = content.lines().collect();
17+
18+
for i in 0..lines.len() {
19+
let Some(rest) = lines[i].trim().strip_prefix("def ") else { continue };
20+
let name = rest.split([' ', '(', '{', ':']).next().unwrap_or("");
21+
if name.is_empty() {
22+
continue;
23+
}
24+
25+
// Collect signature lines until `:=`
26+
let mut sig = String::from(rest);
27+
for j in (i + 1)..lines.len() {
28+
if sig.contains(":=") {
29+
break;
30+
}
31+
sig.push(' ');
32+
sig.push_str(lines[j].trim());
33+
}
34+
35+
let params = extract_params(&sig);
36+
if !params.is_empty() {
37+
// patch_funs renames `show` → `show1` to avoid a Lean keyword clash;
38+
// undo that so param names match the Rust source during lookup.
39+
let params = params
40+
.into_iter()
41+
.map(|(n, t)| (if n == "show1" { "show".to_string() } else { n }, t))
42+
.collect();
43+
map.insert(name.to_string(), params);
44+
}
45+
}
46+
map
47+
}
48+
49+
/// Extracts `(name : type)` bindings, skipping `{implicit}` params and stopping at `:`.
50+
fn extract_params(sig: &str) -> Vec<(String, String)> {
51+
let mut params = Vec::new();
52+
let mut chars = sig.chars().peekable();
53+
54+
while let Some(&c) = chars.peek() {
55+
match c {
56+
'(' => {
57+
chars.next();
58+
if let Some(pair) = parse_binding(&collect_delimited(&mut chars, ')')) {
59+
params.push(pair);
60+
}
61+
}
62+
'{' => {
63+
chars.next();
64+
collect_delimited(&mut chars, '}');
65+
}
66+
':' => break,
67+
_ => {
68+
chars.next();
69+
}
70+
}
71+
}
72+
params
73+
}
74+
75+
/// Reads chars until the matching `close` delimiter, handling nesting.
76+
fn collect_delimited(chars: &mut std::iter::Peekable<std::str::Chars>, close: char) -> String {
77+
let open = if close == ')' { '(' } else { '{' };
78+
let mut depth = 1u32;
79+
let mut buf = String::new();
80+
for c in chars.by_ref() {
81+
if c == open {
82+
depth += 1;
83+
} else if c == close {
84+
depth -= 1;
85+
if depth == 0 {
86+
return buf;
87+
}
88+
}
89+
buf.push(c);
90+
}
91+
buf
92+
}
93+
94+
/// Splits `"name : type"` on the first ` : `.
95+
fn parse_binding(s: &str) -> Option<(String, String)> {
96+
let s = s.trim();
97+
let i = s.find(" : ")?;
98+
let (name, ty) = (s[..i].trim(), s[i + 3..].trim());
99+
(!name.is_empty() && !ty.is_empty()).then(|| (name.to_string(), ty.to_string()))
100+
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use super::*;
105+
106+
#[test]
107+
fn simple() {
108+
let m = parse_funs_types("def hash_key (k : Std.Usize) : Result Std.Usize := do\n");
109+
assert_eq!(m["hash_key"], [("k".into(), "Std.Usize".into())]);
110+
}
111+
112+
#[test]
113+
fn multiline() {
114+
let m = parse_funs_types(
115+
"def frame.AtomicFrameState.load\n\
116+
\x20 (self : frame.AtomicFrameState) (order : core.sync.atomic.Ordering) :\n\
117+
\x20 Result frame.FrameState\n\
118+
\x20 := do\n",
119+
);
120+
let p = &m["frame.AtomicFrameState.load"];
121+
assert_eq!(p[0], ("self".into(), "frame.AtomicFrameState".into()));
122+
assert_eq!(p[1], ("order".into(), "core.sync.atomic.Ordering".into()));
123+
}
124+
125+
#[test]
126+
fn skips_implicits() {
127+
let m = parse_funs_types(
128+
"def HashMap.alloc {T : Type} (slots : alloc.vec.Vec T) (n : Std.Usize) :\n\
129+
\x20 Result Unit := do\n",
130+
);
131+
let p = &m["HashMap.alloc"];
132+
assert_eq!(p.len(), 2);
133+
assert_eq!(p[0].0, "slots");
134+
assert_eq!(p[1], ("n".into(), "Std.Usize".into()));
135+
}
136+
137+
#[test]
138+
fn multiple_ordering_params() {
139+
let m = parse_funs_types(
140+
"def f.compare_exchange\n\
141+
\x20 (self : f.T) (expected : f.S)\n\
142+
\x20 (success : core.sync.atomic.Ordering)\n\
143+
\x20 (failure : core.sync.atomic.Ordering) :\n\
144+
\x20 Result Unit := do\n",
145+
);
146+
let p = &m["f.compare_exchange"];
147+
assert_eq!(p.len(), 4);
148+
assert_eq!(p[2].1, "core.sync.atomic.Ordering");
149+
assert_eq!(p[3].1, "core.sync.atomic.Ordering");
150+
}
151+
152+
#[test]
153+
fn trait_instance_no_params() {
154+
let m = parse_funs_types("def Foo.Clone : core.clone.Clone Foo := {\n clone := x\n}\n");
155+
assert!(m.get("Foo.Clone").map_or(true, |p| p.is_empty()));
156+
}
157+
}

anneal/src/generate.rs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,19 @@ pub fn generate_item(
236236
item: &crate::parse::ParsedLeanItem<crate::parse::hkd::Safe>,
237237
builder: &mut LeanBuilder,
238238
naming_context: &NamingContext,
239+
funs_types: &crate::funs_types::FunsTypeMap,
239240
) {
241+
let namespace = naming_context.item_namespace(item);
240242
match &item.item {
241-
ParsedItem::Function(func) => {
242-
generate_function(&func.item, &func.anneal, builder, &item.source_file, naming_context)
243-
}
243+
ParsedItem::Function(func) => generate_function_with_funs_types(
244+
&func.item,
245+
&func.anneal,
246+
builder,
247+
&item.source_file,
248+
naming_context,
249+
funs_types,
250+
&namespace,
251+
),
244252
ParsedItem::Type(ty) => {
245253
generate_type(&ty.item, &ty.anneal, builder, &item.source_file, naming_context)
246254
}
@@ -259,6 +267,13 @@ pub fn generate_item(
259267
/// 3. Iterates over all items in the artifact, generating code for each.
260268
/// 4. Wraps items in their respective module namespaces.
261269
pub fn generate_artifact(artifact: &crate::scanner::AnnealArtifact) -> GeneratedArtifact {
270+
generate_artifact_with_funs_types(artifact, &Default::default())
271+
}
272+
273+
pub fn generate_artifact_with_funs_types(
274+
artifact: &crate::scanner::AnnealArtifact,
275+
funs_types: &crate::funs_types::FunsTypeMap,
276+
) -> GeneratedArtifact {
262277
let mut builder = LeanBuilder::new();
263278
builder.push_str("-- This file is automatically generated by Anneal.\n");
264279
builder.push_str("-- Do not edit manually.\n\n");
@@ -293,7 +308,7 @@ pub fn generate_artifact(artifact: &crate::scanner::AnnealArtifact) -> Generated
293308
builder.push_str(&format!("namespace {}\n\n", namespace));
294309
}
295310

296-
generate_item(item, &mut builder, &naming_context);
311+
generate_item(item, &mut builder, &naming_context, funs_types);
297312
builder.push('\n');
298313

299314
if !namespace.is_empty() {
@@ -571,6 +586,26 @@ fn generate_function(
571586
builder: &mut LeanBuilder,
572587
source_file: &std::path::Path,
573588
naming_context: &NamingContext,
589+
) {
590+
generate_function_with_funs_types(
591+
func,
592+
block,
593+
builder,
594+
source_file,
595+
naming_context,
596+
&Default::default(),
597+
"",
598+
)
599+
}
600+
601+
fn generate_function_with_funs_types(
602+
func: &FunctionItem<crate::parse::hkd::Safe>,
603+
block: &FunctionAnnealBlock<crate::parse::hkd::Safe>,
604+
builder: &mut LeanBuilder,
605+
source_file: &std::path::Path,
606+
naming_context: &NamingContext,
607+
funs_types: &crate::funs_types::FunsTypeMap,
608+
item_namespace: &str,
574609
) {
575610
let (fn_name, fn_span, impl_struct_name, generic_params, generic_bounds, dict_args) = match func
576611
{
@@ -599,7 +634,30 @@ fn generate_function(
599634
(n.inner.sig.ident.clone(), n.inner.sig.name_span, None, p, b, d)
600635
}
601636
};
602-
let args = extract_args_metadata(func, &impl_struct_name);
637+
let mut args = extract_args_metadata(func, &impl_struct_name);
638+
639+
// Override parameter types from Aeneas-generated Funs.lean when available.
640+
// This ensures fully-qualified type names are used, preventing ambiguity
641+
// with Lean builtins (e.g., `Ordering` vs `core.sync.atomic.Ordering`).
642+
if !funs_types.is_empty() {
643+
// Build the Aeneas function name: namespace.fn_name
644+
let aeneas_fn_name = if item_namespace.is_empty() {
645+
fn_name.to_string()
646+
} else {
647+
format!("{}.{}", item_namespace, fn_name)
648+
};
649+
650+
if let Some(aeneas_params) = funs_types.get(&aeneas_fn_name) {
651+
for arg in args.iter_mut() {
652+
if let Some((_, aeneas_type)) =
653+
aeneas_params.iter().find(|(name, _)| *name == arg.name)
654+
{
655+
arg.lean_type = aeneas_type.clone();
656+
}
657+
}
658+
}
659+
}
660+
603661
let has_return_value = !is_unit_return(func);
604662

605663
builder.push_str(&format!("namespace {}\n\n", fn_name));

anneal/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod aeneas;
22
mod charon;
33
mod diagnostics;
44
mod errors;
5+
mod funs_types;
56
mod generate;
67
mod parse;
78
mod resolve;

0 commit comments

Comments
 (0)