Skip to content

Commit a24d41d

Browse files
committed
[WIP] Stitching engine
gherrit-pr-id: G443fc7f8c3623131b48be56a7a16935a9fe8f234
1 parent 66869c5 commit a24d41d

File tree

15 files changed

+414
-111
lines changed

15 files changed

+414
-111
lines changed

tools/hermes/src/desugar.rs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use anyhow::{Result, bail};
2+
use syn::FnArg;
3+
4+
#[derive(Debug)]
5+
pub struct DesugaredSpec {
6+
pub signature_args: Option<String>,
7+
pub extra_args: Vec<String>, // e.g., (h_safe : ...)
8+
pub body: String,
9+
}
10+
11+
pub fn desugar_spec(
12+
spec_content: &str,
13+
fn_name: &str,
14+
args: &[FnArg],
15+
is_stateful: bool,
16+
) -> Result<DesugaredSpec> {
17+
let mut extra_args = Vec::new();
18+
let mut signature_args = None;
19+
let lines = spec_content.lines().map(|l| l.trim()).filter(|l| !l.is_empty());
20+
21+
let mut ensures_clause = None;
22+
let mut logic_lines = Vec::new();
23+
24+
// Naive line-by-line parsing
25+
for line in lines {
26+
if let Some(req) = line.strip_prefix("requires") {
27+
extra_args.push(format!("({})", req.trim()));
28+
} else if let Some(ens) = line.strip_prefix("ensures") {
29+
if ensures_clause.is_some() {
30+
bail!("Multiple ensures clauses not supported yet");
31+
}
32+
ensures_clause = Some(ens.trim().to_string());
33+
} else if signature_args.is_none() && extra_args.is_empty() && ensures_clause.is_none() {
34+
// Check if this line looks like signature args.
35+
// It might be `(x y : Usize)` or `add_mod (x y : Usize)`.
36+
if let Some(start_idx) = line.find('(') {
37+
// Assume everything from ( onwards is args.
38+
// And ignore what's before it (likely function name).
39+
signature_args = Some(line[start_idx..].to_string());
40+
} else {
41+
// No parenthesis? treat as logic?
42+
logic_lines.push(line);
43+
}
44+
} else {
45+
logic_lines.push(line);
46+
}
47+
}
48+
49+
// Construct body
50+
// If ensures |ret, x_final| ...
51+
let body = if let Some(ens) = ensures_clause {
52+
// Parse binders |...|
53+
let (binders, logic) = parse_binders(&ens)?;
54+
let user_logic = if !logic_lines.is_empty() {
55+
// Maybe prepend logic lines?
56+
// Actually currently valid syntax is usually `ensures |...| logic`.
57+
// `logic_lines` might be empty or continuations.
58+
// Let's assume `logic` from ensures line + `logic_lines` joined.
59+
let mut full = logic;
60+
for l in logic_lines {
61+
full.push_str("\n ");
62+
full.push_str(l);
63+
}
64+
full
65+
} else {
66+
logic
67+
};
68+
69+
generate_body(fn_name, args, is_stateful, &binders, &user_logic)?
70+
} else {
71+
// No ensures clause.
72+
// check if we have logic lines
73+
if !logic_lines.is_empty() {
74+
bail!(
75+
"Raw spec lines not supported. Use 'ensures |...| ...'.\nFound: {:?}",
76+
logic_lines
77+
);
78+
} else {
79+
"True".to_string()
80+
}
81+
};
82+
83+
Ok(DesugaredSpec { signature_args, extra_args, body })
84+
}
85+
86+
fn parse_binders(ensures_content: &str) -> Result<(Vec<String>, String)> {
87+
// |ret, x_final| logic
88+
if let Some(start) = ensures_content.find('|') {
89+
if let Some(end) = ensures_content[start + 1..].find('|') {
90+
let binders_str = &ensures_content[start + 1..start + 1 + end];
91+
let logic = &ensures_content[start + 1 + end + 1..];
92+
let binders = binders_str
93+
.split(',')
94+
.map(|s| s.trim().to_string())
95+
.filter(|s| !s.is_empty())
96+
.collect();
97+
return Ok((binders, logic.trim().to_string()));
98+
}
99+
}
100+
bail!("Malformed ensures clause: missing |...| binders");
101+
}
102+
103+
fn generate_body(
104+
fn_name: &str,
105+
input_args: &[FnArg],
106+
is_stateful: bool,
107+
binders: &[String],
108+
logic: &str,
109+
) -> Result<String> {
110+
let mut out = String::new();
111+
112+
// 1. Quantifiers
113+
// "exists ret x_final,"
114+
if !binders.is_empty() {
115+
out.push_str("exists");
116+
for b in binders {
117+
if b != "_" {
118+
out.push_str(" ");
119+
out.push_str(b);
120+
} else {
121+
// If it's "_", we usually need a name for the exists,
122+
// but if the user didn't name it, maybe we auto-name it?
123+
// Or Aeneas requires a name.
124+
// Let's generate a temp name if needed or just skip?
125+
// Lean `exists _, ...` is valid but we need to bind it in the equality check.
126+
// Wait, `exists ret` -> `fwd ... = Result.ok ret`.
127+
// If user put `_`, we can't name it in the equality check easily.
128+
// Let's assume user gives names for now.
129+
out.push_str(" _");
130+
}
131+
}
132+
out.push_str(",\n ");
133+
}
134+
135+
// Capture argument names for function calls
136+
let arg_names = input_args
137+
.iter()
138+
.map(|arg| match arg {
139+
FnArg::Typed(pat) => {
140+
if let syn::Pat::Ident(pat_ident) = &*pat.pat {
141+
pat_ident.ident.to_string()
142+
} else {
143+
"_".to_string()
144+
}
145+
}
146+
FnArg::Receiver(_) => "self".to_string(),
147+
})
148+
.collect::<Vec<_>>()
149+
.join(" ");
150+
151+
// 2. Fwd Check
152+
// "fn_fwd args = Result.ok ret /\"
153+
// The first binder is usually the return value.
154+
if let Some(ret_binder) = binders.first() {
155+
let fn_call_name =
156+
if is_stateful { format!("{}_fwd", fn_name) } else { fn_name.to_string() };
157+
158+
if ret_binder != "_" {
159+
out.push_str(&format!(
160+
"{} {} = Result.ok {} /\\\n ",
161+
fn_call_name, arg_names, ret_binder
162+
));
163+
} else {
164+
// exists _, ... = Result.ok _
165+
out.push_str(&format!(
166+
"exists v, {} {} = Result.ok v /\\\n ",
167+
fn_call_name, arg_names
168+
));
169+
}
170+
}
171+
172+
// 3. Back Check (if stateful)
173+
if is_stateful {
174+
// We assume remaining binders are for mutable args in order?
175+
// User prompt: `|ret, x_final|`.
176+
// Need to know WHICH arg corresponds to `x_final`.
177+
// "If the function is "Stateful" ... append `/\ func_back args = Result.ok x_final`."
178+
// "Autonomy Rule: If ... ambiguous ... default to generating a conjunction of *all* backward functions found"
179+
// This suggests we might need to know the *names* of the back functions.
180+
// Aeneas generates `fn_back` or `fn_back0`, `fn_back1` etc.
181+
// If there is only one mutable arg, it is `fn_back`.
182+
// If detection of back function names is hard without reading Aeneas output,
183+
// we might guess `fn_back`.
184+
//
185+
// Let's assume simple case (one mutable arg) -> `fn_back`.
186+
// If multiple binders, we might need `fn_back` returning a tuple?
187+
// Aeneas: "def choose_back ... : Result (T x T)"
188+
// So `fn_back` returns a tuple if multiple.
189+
190+
if binders.len() > 1 {
191+
let back_binders = &binders[1..]; // ret is 0
192+
193+
// If just one back binder, easy.
194+
if back_binders.len() == 1 {
195+
out.push_str(&format!(
196+
"{}_back {} = Result.ok {} /\\\n ",
197+
fn_name, arg_names, back_binders[0]
198+
));
199+
} else {
200+
// Tuple return
201+
// {}_back {} = Result.ok (b1, b2, ...)
202+
let tuple_inner = back_binders.join(", ");
203+
out.push_str(&format!(
204+
"{}_back {} = Result.ok ({}) /\\\n ",
205+
fn_name, arg_names, tuple_inner
206+
));
207+
}
208+
}
209+
}
210+
211+
// 4. User Logic
212+
out.push_str(&format!("({})", logic));
213+
214+
Ok(out)
215+
}
216+
217+
#[cfg(test)]
218+
mod tests {
219+
use super::*;
220+
221+
#[test]
222+
fn test_reject_raw_spec() {
223+
let spec = "some raw logic line\n another line";
224+
let args = Vec::new();
225+
let res = desugar_spec(spec, "test_fn", &args, false);
226+
assert!(res.is_err());
227+
assert!(res.unwrap_err().to_string().contains("Raw spec lines not supported"));
228+
}
229+
230+
#[test]
231+
fn test_accept_ensures() {
232+
let spec = "ensures |ret| ret = x";
233+
let args = Vec::new();
234+
let res = desugar_spec(spec, "test_fn", &args, false);
235+
assert!(res.is_ok());
236+
}
237+
238+
#[test]
239+
fn test_accept_requires_only() {
240+
let spec = "requires h : x > 0";
241+
let args = Vec::new();
242+
let res = desugar_spec(spec, "test_fn", &args, false);
243+
assert!(res.is_ok());
244+
assert_eq!(res.unwrap().body, "True");
245+
}
246+
}

tools/hermes/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
//! - Orchestrating the execution of Charon and Aeneas.
1414
//! - Stitching generated Lean code with user-provided proofs.
1515
16+
pub mod desugar;
1617
pub mod orchestration;
1718
pub mod parser;
1819
pub mod pipeline;
20+
pub mod translator;

tools/hermes/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ fn main() -> Result<()> {
5555
args.command;
5656

5757
let crate_root = std::env::current_dir()?;
58+
std::fs::create_dir_all(&dest)?;
5859
let dest = dest.canonicalize()?;
5960
let sorry_mode = if allow_sorry { Sorry::AllowSorry } else { Sorry::RejectSorry };
6061
run_pipeline(crate_name, &crate_root, &dest, aeneas_path, manifest_path, sorry_mode)

tools/hermes/src/orchestration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub fn run_charon(crate_root: &Path, dest_file: &Path, manifest_path: Option<&Pa
2222

2323
println!("Running charon in {:?}", crate_root);
2424
let mut cmd = Command::new("charon");
25+
cmd.env_remove("CARGO_TARGET_DIR"); // Avoid deadlock with outer cargo
2526
cmd.current_dir(crate_root);
2627
cmd.arg("cargo");
2728

0 commit comments

Comments
 (0)