use super::*;
use crate::ir::{EscapePairSpec, StringEscapeRoundtripPin};
pub(super) fn detect_string_escape_roundtrip(
law: &crate::ast::VerifyLaw,
inputs: &ProofLowerInputs,
fn_contracts: &std::collections::HashMap<crate::ir::FnId, crate::ir::FnContract>,
) -> Option<crate::ir::ProofStrategy> {
use crate::ast::{Expr, Literal, Pattern, Stmt};
if law.when.is_some() || law.givens.len() != 1 || law.givens[0].type_name != "String" {
return None;
}
let given = law.givens[0].name.as_str();
const SKELETON_RESERVED: &[&str] = &["hdata", "hval", "hpos", "cs"];
if SKELETON_RESERVED.contains(&given) {
return None;
}
fn ident_of(e: &Spanned<Expr>) -> Option<&str> {
match &e.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.as_str()),
_ => None,
}
}
fn call_of(e: &Spanned<Expr>) -> Option<(String, &[Spanned<Expr>])> {
match &e.node {
Expr::FnCall(callee, args) => {
Some((expr_to_dotted_name(&callee.node)?, args.as_slice()))
}
Expr::TailCall(data) => Some((data.target.clone(), data.args.as_slice())),
_ => None,
}
}
fn ctor_of(e: &Spanned<Expr>) -> Option<(String, Vec<&Spanned<Expr>>)> {
match &e.node {
Expr::FnCall(callee, args) => {
let name = expr_to_dotted_name(&callee.node)?;
let leaf = name.rsplit('.').next()?;
if !leaf.chars().next().is_some_and(|c| c.is_uppercase()) {
return None;
}
Some((name, args.iter().collect()))
}
Expr::Constructor(name, payload) => {
let args: Vec<&Spanned<Expr>> = match payload.as_deref() {
None => Vec::new(),
Some(Spanned {
node: Expr::Tuple(items),
..
}) => items.iter().collect(),
Some(single) => vec![single],
};
Some((name.clone(), args))
}
_ => None,
}
}
fn is_ident(e: &Spanned<Expr>, name: &str) -> bool {
ident_of(e) == Some(name)
}
fn is_int_lit(e: &Spanned<Expr>, v: i64) -> bool {
matches!(&e.node, Expr::Literal(Literal::Int(n)) if *n == v)
}
fn is_plus_lit(e: &Spanned<Expr>, name: &str, v: i64) -> bool {
matches!(&e.node, Expr::BinOp(crate::ast::BinOp::Add, l, r)
if ident_of(l) == Some(name) && is_int_lit(r, v))
}
fn str_lit(e: &Spanned<Expr>) -> Option<&str> {
match &e.node {
Expr::Literal(Literal::Str(s)) => Some(s.as_str()),
_ => None,
}
}
fn single_char(s: &str) -> Option<char> {
let mut it = s.chars();
let c = it.next()?;
it.next().is_none().then_some(c)
}
let resolve_user_fn = |name: &str| -> Option<&FnDef> {
let fd = inputs.find_fn_def_by_call_name(name)?;
(fd.effects.is_empty() && fd.name != "main").then_some(fd)
};
fn single_match(fd: &FnDef) -> Option<(&Spanned<Expr>, &[crate::ast::MatchArm])> {
let [Stmt::Expr(body)] = fd.body.stmts() else {
return None;
};
let Expr::Match { subject, arms } = &body.node else {
return None;
};
Some((subject, arms.as_slice()))
}
fn match_of(e: &Spanned<Expr>) -> Option<(&Spanned<Expr>, &[crate::ast::MatchArm])> {
let Expr::Match { subject, arms } = &e.node else {
return None;
};
Some((subject, arms.as_slice()))
}
fn charat_match<'b>(
subject: &Spanned<Expr>,
arms: &'b [crate::ast::MatchArm],
s_param: &str,
pos_param: &str,
) -> Option<(String, &'b crate::ast::MatchArm)> {
let (callee, args) = call_of(subject)?;
if callee != "String.charAt"
|| args.len() != 2
|| !is_ident(&args[0], s_param)
|| !is_ident(&args[1], pos_param)
|| arms.len() != 2
{
return None;
}
arms.iter()
.any(|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.None" && b.is_empty()))
.then_some(())?;
let some_arm = arms.iter().find(
|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.Some" && b.len() == 1),
)?;
let Pattern::Constructor(_, binders) = &some_arm.pattern else {
return None;
};
Some((binders[0].clone(), some_arm))
}
fn bool_match(arms: &[crate::ast::MatchArm]) -> Option<(&Spanned<Expr>, &Spanned<Expr>)> {
if arms.len() != 2 {
return None;
}
let t = arms
.iter()
.find(|a| matches!(&a.pattern, Pattern::Literal(Literal::Bool(true))))?;
let f = arms
.iter()
.find(|a| matches!(&a.pattern, Pattern::Literal(Literal::Bool(false))))?;
Some((&t.body, &f.body))
}
fn list_concat_snoc(e: &Spanned<Expr>) -> Option<(&Spanned<Expr>, &Spanned<Expr>)> {
let (callee, args) = call_of(e)?;
if callee != "List.concat" || args.len() != 2 {
return None;
}
let Expr::List(items) = &args[1].node else {
return None;
};
if items.len() != 1 {
return None;
}
Some((&args[0], &items[0]))
}
fn param_types(fd: &FnDef, expected: &[&str]) -> bool {
fd.params.len() == expected.len()
&& fd
.params
.iter()
.zip(expected)
.all(|((_, ty), want)| ty.trim() == *want)
}
let (lhs_callee, lhs_args) = call_of(&law.lhs)?;
let subject = lhs_args.first()?;
let Expr::BinOp(crate::ast::BinOp::Add, open_prod, term_lit) = &subject.node else {
return None;
};
let term_str = str_lit(term_lit)?;
let terminator = single_char(term_str)?;
let Expr::BinOp(crate::ast::BinOp::Add, open_lit, prod_call) = &open_prod.node else {
return None;
};
single_char(str_lit(open_lit)?)?;
let (producer_name, prod_args) = call_of(prod_call)?;
if prod_args.len() != 1 || !is_ident(&prod_args[0], given) {
return None;
}
let (ok_ctor, rhs_args) = ctor_of(&law.rhs)?;
if rhs_args.len() != 2 {
return None;
}
let (str_ctor, str_args) = ctor_of(rhs_args[0])?;
if str_args.len() != 1 || !is_ident(str_args[0], given) {
return None;
}
{
let Expr::BinOp(crate::ast::BinOp::Add, len_call, two) = &rhs_args[1].node else {
return None;
};
if !is_int_lit(two, 2) {
return None;
}
let (len_callee, len_args) = call_of(len_call)?;
if len_callee != "String.len" || len_args.len() != 1 || len_args[0].node != prod_call.node {
return None;
}
}
let entry_fd = resolve_user_fn(&lhs_callee)?;
let scan_name: String = if lhs_args.len() == 2 {
if !is_int_lit(&lhs_args[1], 1) || !param_types(entry_fd, &["String", "Int"]) {
return None;
}
let [Stmt::Expr(body)] = entry_fd.body.stmts() else {
return None;
};
let (scan, scan_args) = call_of(body)?;
(scan_args.len() == 4
&& is_ident(&scan_args[0], &entry_fd.params[0].0)
&& is_ident(&scan_args[1], &entry_fd.params[1].0)
&& is_ident(&scan_args[2], &entry_fd.params[1].0)
&& matches!(&scan_args[3].node, Expr::List(items) if items.is_empty()))
.then_some(())?;
scan
} else if lhs_args.len() == 4 {
(is_int_lit(&lhs_args[1], 1)
&& is_int_lit(&lhs_args[2], 1)
&& matches!(&lhs_args[3].node, Expr::List(items) if items.is_empty()))
.then_some(())?;
lhs_callee.clone()
} else {
return None;
};
let scan_fd = resolve_user_fn(&scan_name)?;
if !param_types(scan_fd, &["String", "Int", "Int", "List<String>"]) {
return None;
}
let (sc_s, sc_pos, sc_st, sc_ch) = (
scan_fd.params[0].0.as_str(),
scan_fd.params[1].0.as_str(),
scan_fd.params[2].0.as_str(),
scan_fd.params[3].0.as_str(),
);
let (scan_subject, scan_arms) = single_match(scan_fd)?;
let (c_name, some_arm) = charat_match(scan_subject, scan_arms, sc_s, sc_pos)?;
let (c_subject, c_arms) = match_of(&some_arm.body)?;
if !is_ident(c_subject, &c_name) || c_arms.len() != 3 {
return None;
}
if !matches!(&c_arms[2].pattern, Pattern::Wildcard) {
return None;
}
let mut finish_name: Option<String> = None;
let mut escape_name: Option<String> = None;
let mut term_arm_char: Option<char> = None;
let mut esc_arm_char: Option<char> = None;
for arm in &c_arms[0..2] {
let Pattern::Literal(Literal::Str(lit)) = &arm.pattern else {
return None;
};
let ch = single_char(lit)?;
let (callee, args) = call_of(&arm.body)?;
if args.len() == 4
&& is_ident(&args[0], sc_s)
&& is_plus_lit(&args[1], sc_pos, 1)
&& is_ident(&args[2], sc_st)
&& is_ident(&args[3], sc_ch)
{
(finish_name.is_none()).then_some(())?;
finish_name = Some(callee);
term_arm_char = Some(ch);
} else if args.len() == 5
&& is_ident(&args[0], sc_s)
&& is_plus_lit(&args[1], sc_pos, 1)
&& is_ident(&args[2], sc_pos)
&& is_ident(&args[3], sc_st)
&& is_ident(&args[4], sc_ch)
{
(escape_name.is_none()).then_some(())?;
escape_name = Some(callee);
esc_arm_char = Some(ch);
} else {
return None;
}
}
let finish_name = finish_name?;
let escape_name = escape_name?;
if term_arm_char? != terminator {
return None;
}
let escape_char = esc_arm_char?;
if escape_char == terminator {
return None;
}
let validate_name = {
let (callee, args) = call_of(&c_arms[2].body)?;
(args.len() == 5
&& is_ident(&args[0], sc_s)
&& is_ident(&args[1], sc_pos)
&& is_ident(&args[2], sc_st)
&& is_ident(&args[3], sc_ch)
&& is_ident(&args[4], &c_name))
.then_some(())?;
callee
};
let finish_fd = resolve_user_fn(&finish_name)?;
if !param_types(finish_fd, &["String", "Int", "Int", "List<String>"]) {
return None;
}
{
let (f_s, f_next, f_st, f_ch) = (
finish_fd.params[0].0.as_str(),
finish_fd.params[1].0.as_str(),
finish_fd.params[2].0.as_str(),
finish_fd.params[3].0.as_str(),
);
let [
Stmt::Binding(seg_name, _, slice_expr),
Stmt::Binding(all_name, _, join_expr),
Stmt::Expr(ok_expr),
] = finish_fd.body.stmts()
else {
return None;
};
{
let (callee, args) = call_of(slice_expr)?;
(callee == "String.slice"
&& args.len() == 3
&& is_ident(&args[0], f_s)
&& is_ident(&args[1], f_st)
&& matches!(&args[2].node, Expr::BinOp(crate::ast::BinOp::Sub, l, r)
if ident_of(l) == Some(f_next) && is_int_lit(r, 1)))
.then_some(())?;
}
{
let (callee, args) = call_of(join_expr)?;
(callee == "String.join" && args.len() == 2 && str_lit(&args[1]) == Some(""))
.then_some(())?;
let (list, item) = list_concat_snoc(&args[0])?;
(is_ident(list, f_ch) && is_ident(item, seg_name)).then_some(())?;
}
{
let (arm_ok, ok_args) = ctor_of(ok_expr)?;
(arm_ok == ok_ctor && ok_args.len() == 2 && is_ident(ok_args[1], f_next))
.then_some(())?;
let (arm_str, str_args) = ctor_of(ok_args[0])?;
(arm_str == str_ctor && str_args.len() == 1 && is_ident(str_args[0], all_name))
.then_some(())?;
}
}
let validate_fd = resolve_user_fn(&validate_name)?;
if !param_types(
validate_fd,
&["String", "Int", "Int", "List<String>", "String"],
) {
return None;
}
let control_threshold = {
let (v_s, v_pos, v_st, v_ch, v_c) = (
validate_fd.params[0].0.as_str(),
validate_fd.params[1].0.as_str(),
validate_fd.params[2].0.as_str(),
validate_fd.params[3].0.as_str(),
validate_fd.params[4].0.as_str(),
);
let (subject, arms) = single_match(validate_fd)?;
let Expr::BinOp(crate::ast::BinOp::Lt, code_call, t_lit) = &subject.node else {
return None;
};
let Expr::Literal(Literal::Int(threshold)) = &t_lit.node else {
return None;
};
{
let (callee, args) = call_of(code_call)?;
(callee == "Char.toCode" && args.len() == 1 && is_ident(&args[0], v_c)).then_some(())?;
}
let (_, false_body) = bool_match(arms)?;
let (callee, args) = call_of(false_body)?;
(callee == scan_name
&& args.len() == 4
&& is_ident(&args[0], v_s)
&& is_plus_lit(&args[1], v_pos, 1)
&& is_ident(&args[2], v_st)
&& is_ident(&args[3], v_ch))
.then_some(())?;
*threshold
};
if !(1..=256).contains(&control_threshold) {
return None;
}
let escape_fd = resolve_user_fn(&escape_name)?;
if !param_types(escape_fd, &["String", "Int", "Int", "Int", "List<String>"]) {
return None;
}
let mut consumer_pairs: Vec<(char, String)> = Vec::new();
let mut unicode_hop: Option<(char, String)> = None;
{
let (e_s, e_pos, e_sp, e_st, e_ch) = (
escape_fd.params[0].0.as_str(),
escape_fd.params[1].0.as_str(),
escape_fd.params[2].0.as_str(),
escape_fd.params[3].0.as_str(),
escape_fd.params[4].0.as_str(),
);
let [
Stmt::Binding(seg_name, _, slice_expr),
Stmt::Binding(base_name, _, base_expr),
Stmt::Expr(dispatch),
] = escape_fd.body.stmts()
else {
return None;
};
{
let (callee, args) = call_of(slice_expr)?;
(callee == "String.slice"
&& args.len() == 3
&& is_ident(&args[0], e_s)
&& is_ident(&args[1], e_st)
&& is_ident(&args[2], e_sp))
.then_some(())?;
}
{
let (list, item) = list_concat_snoc(base_expr)?;
(is_ident(list, e_ch) && is_ident(item, seg_name)).then_some(())?;
}
let (d_subject, d_arms) = match_of(dispatch)?;
let (c2_name, some_arm) = charat_match(d_subject, d_arms, e_s, e_pos)?;
let (l_subject, l_arms) = match_of(&some_arm.body)?;
if !is_ident(l_subject, &c2_name) || l_arms.is_empty() {
return None;
}
if !matches!(&l_arms.last()?.pattern, Pattern::Wildcard) {
return None;
}
for arm in &l_arms[..l_arms.len() - 1] {
let Pattern::Literal(Literal::Str(lit)) = &arm.pattern else {
return None;
};
let letter = single_char(lit)?;
let (callee, args) = call_of(&arm.body)?;
if callee == scan_name {
(args.len() == 4
&& is_ident(&args[0], e_s)
&& is_plus_lit(&args[1], e_pos, 1)
&& is_plus_lit(&args[2], e_pos, 1))
.then_some(())?;
let (list, item) = list_concat_snoc(&args[3])?;
(is_ident(list, base_name)).then_some(())?;
let decoded = str_lit(item)?;
consumer_pairs.push((letter, decoded.to_string()));
} else if args.len() == 4
&& is_ident(&args[0], e_s)
&& is_plus_lit(&args[1], e_pos, 1)
&& is_plus_lit(&args[2], e_pos, 1)
&& is_ident(&args[3], base_name)
{
unicode_hop.is_none().then_some(())?;
unicode_hop = Some((letter, callee));
} else {
return None;
}
}
}
let (unicode_letter, unicode_name) = unicode_hop?;
let unicode_fd = resolve_user_fn(&unicode_name)?;
if !param_types(unicode_fd, &["String", "Int", "Int", "List<String>"]) {
return None;
}
let (read_hex_name, codepoint_name) = {
let (u_s, u_pos, u_e, u_ch) = (
unicode_fd.params[0].0.as_str(),
unicode_fd.params[1].0.as_str(),
unicode_fd.params[2].0.as_str(),
unicode_fd.params[3].0.as_str(),
);
let (subject, arms) = single_match(unicode_fd)?;
let (rh_name, rh_args) = call_of(subject)?;
(rh_args.len() == 4
&& is_ident(&rh_args[0], u_s)
&& is_ident(&rh_args[1], u_pos)
&& is_int_lit(&rh_args[2], 0)
&& is_int_lit(&rh_args[3], 0))
.then_some(())?;
if arms.len() != 2 {
return None;
}
arms.iter()
.any(|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.None" && b.is_empty()))
.then_some(())?;
let some_arm = arms.iter().find(
|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.Some" && b.len() == 1),
)?;
let Pattern::Constructor(_, binders) = &some_arm.pattern else {
return None;
};
let (cp_name, cp_args) = call_of(&some_arm.body)?;
(cp_args.len() == 5
&& is_ident(&cp_args[0], u_s)
&& is_plus_lit(&cp_args[1], u_pos, 4)
&& is_ident(&cp_args[2], u_e)
&& is_ident(&cp_args[3], u_ch)
&& is_ident(&cp_args[4], &binders[0]))
.then_some(())?;
(rh_name, cp_name)
};
let codepoint_fd = resolve_user_fn(&codepoint_name)?;
if !param_types(
codepoint_fd,
&["String", "Int", "Int", "List<String>", "Int"],
) {
return None;
}
let (high_surrogate_name, low_surrogate_name, apply_name) = {
let (p_s, p_pos, p_e, p_ch, p_cp) = (
codepoint_fd.params[0].0.as_str(),
codepoint_fd.params[1].0.as_str(),
codepoint_fd.params[2].0.as_str(),
codepoint_fd.params[3].0.as_str(),
codepoint_fd.params[4].0.as_str(),
);
let (subject, arms) = single_match(codepoint_fd)?;
let (hi_name, hi_args) = call_of(subject)?;
(hi_args.len() == 1 && is_ident(&hi_args[0], p_cp)).then_some(())?;
let (_, false_body) = bool_match(arms)?;
let (lo_subject, lo_arms) = match_of(false_body)?;
let (lo_name, lo_args) = call_of(lo_subject)?;
(lo_args.len() == 1 && is_ident(&lo_args[0], p_cp)).then_some(())?;
let (_, lo_false) = bool_match(lo_arms)?;
let (app_name, app_args) = call_of(lo_false)?;
(app_args.len() == 5
&& is_ident(&app_args[0], p_s)
&& is_ident(&app_args[1], p_pos)
&& is_ident(&app_args[2], p_e)
&& is_ident(&app_args[3], p_ch)
&& is_ident(&app_args[4], p_cp))
.then_some(())?;
(hi_name, lo_name, app_name)
};
let apply_fd = resolve_user_fn(&apply_name)?;
if !param_types(apply_fd, &["String", "Int", "Int", "List<String>", "Int"]) {
return None;
}
{
let (a_s, a_pos, _a_e, a_ch, a_cp) = (
apply_fd.params[0].0.as_str(),
apply_fd.params[1].0.as_str(),
apply_fd.params[2].0.as_str(),
apply_fd.params[3].0.as_str(),
apply_fd.params[4].0.as_str(),
);
let (subject, arms) = single_match(apply_fd)?;
let (callee, args) = call_of(subject)?;
(callee == "Char.fromCode" && args.len() == 1 && is_ident(&args[0], a_cp)).then_some(())?;
if arms.len() != 2 {
return None;
}
let some_arm = arms.iter().find(
|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.Some" && b.len() == 1),
)?;
let Pattern::Constructor(_, binders) = &some_arm.pattern else {
return None;
};
let (sc_callee, sc_args) = call_of(&some_arm.body)?;
(sc_callee == scan_name
&& sc_args.len() == 4
&& is_ident(&sc_args[0], a_s)
&& is_ident(&sc_args[1], a_pos)
&& is_ident(&sc_args[2], a_pos))
.then_some(())?;
let (list, item) = list_concat_snoc(&sc_args[3])?;
(is_ident(list, a_ch) && is_ident(item, &binders[0])).then_some(())?;
}
let surrogate_min = |name: &str| -> Option<i64> {
let fd = resolve_user_fn(name)?;
if !param_types(fd, &["Int"]) || fd.return_type.trim() != "Bool" {
return None;
}
let (subject, arms) = single_match(fd)?;
let Expr::BinOp(crate::ast::BinOp::Gte, l, r) = &subject.node else {
return None;
};
if !is_ident(l, &fd.params[0].0) {
return None;
}
let Expr::Literal(Literal::Int(min)) = &r.node else {
return None;
};
let (_, false_body) = bool_match(arms)?;
matches!(&false_body.node, Expr::Literal(Literal::Bool(false))).then_some(*min)
};
let high_surrogate_min = surrogate_min(&high_surrogate_name)?;
let low_surrogate_min = surrogate_min(&low_surrogate_name)?;
if control_threshold > high_surrogate_min || control_threshold > low_surrogate_min {
return None;
}
let read_hex_fd = resolve_user_fn(&read_hex_name)?;
if !param_types(read_hex_fd, &["String", "Int", "Int", "Int"]) {
return None;
}
let hex_val_name = {
let (r_s, r_pos, r_acc, r_count) = (
read_hex_fd.params[0].0.as_str(),
read_hex_fd.params[1].0.as_str(),
read_hex_fd.params[2].0.as_str(),
read_hex_fd.params[3].0.as_str(),
);
let (subject, arms) = single_match(read_hex_fd)?;
let Expr::BinOp(crate::ast::BinOp::Eq, l, r) = &subject.node else {
return None;
};
(is_ident(l, r_count) && is_int_lit(r, 4)).then_some(())?;
let (true_body, false_body) = bool_match(arms)?;
{
let (done_ctor, done_args) = ctor_of(true_body)?;
(done_ctor == "Option.Some" && done_args.len() == 1 && is_ident(done_args[0], r_acc))
.then_some(())?;
}
let (ca_subject, ca_arms) = match_of(false_body)?;
let (c3_name, some_arm) = charat_match(ca_subject, ca_arms, r_s, r_pos)?;
let (hv_subject, hv_arms) = match_of(&some_arm.body)?;
let (hv_name, hv_args) = call_of(hv_subject)?;
(hv_args.len() == 1 && is_ident(&hv_args[0], &c3_name)).then_some(())?;
if hv_arms.len() != 2 {
return None;
}
hv_arms
.iter()
.any(|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.None" && b.is_empty()))
.then_some(())?;
let v_arm = hv_arms.iter().find(
|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.Some" && b.len() == 1),
)?;
let Pattern::Constructor(_, v_binders) = &v_arm.pattern else {
return None;
};
let (rec_name, rec_args) = call_of(&v_arm.body)?;
(rec_name == read_hex_name
&& rec_args.len() == 4
&& is_ident(&rec_args[0], r_s)
&& is_plus_lit(&rec_args[1], r_pos, 1)
&& matches!(&rec_args[2].node, Expr::BinOp(crate::ast::BinOp::Add, ml, vr)
if matches!(&ml.node, Expr::BinOp(crate::ast::BinOp::Mul, al, sixteen)
if ident_of(al) == Some(r_acc) && is_int_lit(sixteen, 16))
&& ident_of(vr) == Some(&v_binders[0]))
&& is_plus_lit(&rec_args[3], r_count, 1))
.then_some(())?;
hv_name
};
let hex_val_fd = resolve_user_fn(&hex_val_name)?;
if !param_types(hex_val_fd, &["String"]) || hex_val_fd.return_type.trim() != "Option<Int>" {
return None;
}
let producer_fd = resolve_user_fn(&producer_name)?;
if !param_types(producer_fd, &["String"]) || producer_fd.return_type.trim() != "String" {
return None;
}
let fold_name = {
let [Stmt::Expr(body)] = producer_fd.body.stmts() else {
return None;
};
let (fold, fold_args) = call_of(body)?;
(fold_args.len() == 2 && str_lit(&fold_args[1]) == Some("")).then_some(())?;
let (chars_callee, chars_args) = call_of(&fold_args[0])?;
(chars_callee == "String.chars"
&& chars_args.len() == 1
&& is_ident(&chars_args[0], &producer_fd.params[0].0))
.then_some(())?;
fold
};
let fold_fd = resolve_user_fn(&fold_name)?;
if !param_types(fold_fd, &["List<String>", "String"]) || fold_fd.return_type.trim() != "String"
{
return None;
}
let classifier_name = {
let (chars_p, acc_p) = (fold_fd.params[0].0.as_str(), fold_fd.params[1].0.as_str());
let (subject, arms) = single_match(fold_fd)?;
if !is_ident(subject, chars_p) || arms.len() != 2 {
return None;
}
let nil_arm = arms
.iter()
.find(|a| matches!(&a.pattern, Pattern::EmptyList))?;
is_ident(&nil_arm.body, acc_p).then_some(())?;
let cons_arm = arms
.iter()
.find(|a| matches!(&a.pattern, Pattern::Cons(_, _)))?;
let Pattern::Cons(h, t) = &cons_arm.pattern else {
return None;
};
let (rec, rec_args) = call_of(&cons_arm.body)?;
(rec == fold_name && rec_args.len() == 2 && is_ident(&rec_args[0], t)).then_some(())?;
let Expr::BinOp(crate::ast::BinOp::Add, acc_side, cls_call) = &rec_args[1].node else {
return None;
};
is_ident(acc_side, acc_p).then_some(())?;
let (cls, cls_args) = call_of(cls_call)?;
(cls_args.len() == 1 && is_ident(&cls_args[0], h)).then_some(())?;
cls
};
let classifier_fd = resolve_user_fn(&classifier_name)?;
if !param_types(classifier_fd, &["String"]) || classifier_fd.return_type.trim() != "String" {
return None;
}
let mut pairs: Vec<EscapePairSpec> = Vec::new();
let control_name = {
let (subject, arms) = single_match(classifier_fd)?;
if !is_ident(subject, &classifier_fd.params[0].0) || arms.len() < 2 {
return None;
}
if !matches!(&arms.last()?.pattern, Pattern::Wildcard) {
return None;
}
for arm in &arms[..arms.len() - 1] {
let Pattern::Literal(Literal::Str(lit)) = &arm.pattern else {
return None;
};
let decoded = single_char(lit)?;
let esc_out = str_lit(&arm.body)?;
let mut out_chars = esc_out.chars();
(out_chars.next() == Some(escape_char)).then_some(())?;
let letter = out_chars.next()?;
out_chars.next().is_none().then_some(())?;
pairs.push(EscapePairSpec {
decoded,
letter,
from_control_ladder: false,
});
}
let (ctl, ctl_args) = call_of(&arms.last()?.body)?;
(ctl_args.len() == 1 && is_ident(&ctl_args[0], &classifier_fd.params[0].0)).then_some(())?;
ctl
};
let classifier_set: Vec<char> = pairs.iter().map(|p| p.decoded).collect();
{
let mut seen = classifier_set.clone();
seen.sort_unstable();
seen.dedup();
if seen.len() != classifier_set.len() {
return None;
}
}
if !classifier_set.contains(&terminator) || !classifier_set.contains(&escape_char) {
return None;
}
if ((terminator as i64) < control_threshold) || ((escape_char as i64) < control_threshold) {
return None;
}
let control_fd = resolve_user_fn(&control_name)?;
if !param_types(control_fd, &["String"]) || control_fd.return_type.trim() != "String" {
return None;
}
let control_escape_name = {
let c_param = control_fd.params[0].0.as_str();
let [Stmt::Binding(code_name, _, code_expr), Stmt::Expr(ladder)] = control_fd.body.stmts()
else {
return None;
};
{
let (callee, args) = call_of(code_expr)?;
(callee == "Char.toCode" && args.len() == 1 && is_ident(&args[0], c_param))
.then_some(())?;
}
let mut cursor = ladder;
loop {
let (subject, arms) = match_of(cursor)?;
match &subject.node {
Expr::BinOp(crate::ast::BinOp::Eq, l, r) => {
is_ident(l, code_name).then_some(())?;
let Expr::Literal(Literal::Int(code)) = &r.node else {
return None;
};
if *code < 0 || *code >= control_threshold {
return None;
}
let decoded = char::from_u32(*code as u32)?;
if classifier_set.contains(&decoded)
|| pairs
.iter()
.any(|p| p.from_control_ladder && p.decoded == decoded)
{
return None;
}
let (true_body, false_body) = bool_match(arms)?;
let esc_out = str_lit(true_body)?;
let mut out_chars = esc_out.chars();
(out_chars.next() == Some(escape_char)).then_some(())?;
let letter = out_chars.next()?;
out_chars.next().is_none().then_some(())?;
pairs.push(EscapePairSpec {
decoded,
letter,
from_control_ladder: true,
});
cursor = false_body;
}
Expr::BinOp(crate::ast::BinOp::Lt, l, r) => {
(is_ident(l, code_name) && is_int_lit(r, control_threshold)).then_some(())?;
let (true_body, false_body) = bool_match(arms)?;
is_ident(false_body, c_param).then_some(())?;
let (cce, cce_args) = call_of(true_body)?;
(cce_args.len() == 2
&& is_ident(&cce_args[0], c_param)
&& is_ident(&cce_args[1], code_name))
.then_some(())?;
break cce;
}
_ => return None,
}
}
};
let control_escape_fd = resolve_user_fn(&control_escape_name)?;
if !param_types(control_escape_fd, &["String", "Int"])
|| control_escape_fd.return_type.trim() != "String"
{
return None;
}
{
let (cc_c, cc_code) = (
control_escape_fd.params[0].0.as_str(),
control_escape_fd.params[1].0.as_str(),
);
let (subject, arms) = single_match(control_escape_fd)?;
let (callee, args) = call_of(subject)?;
(callee == "Byte.toHex" && args.len() == 1 && is_ident(&args[0], cc_code)).then_some(())?;
if arms.len() != 2 {
return None;
}
let ok_arm = arms.iter().find(
|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Result.Ok" && b.len() == 1),
)?;
let err_arm = arms
.iter()
.find(|a| matches!(&a.pattern, Pattern::Constructor(n, _) if n == "Result.Err"))?;
is_ident(&err_arm.body, cc_c).then_some(())?;
let Pattern::Constructor(_, ok_binders) = &ok_arm.pattern else {
return None;
};
let Expr::BinOp(crate::ast::BinOp::Add, prefix, hex) = &ok_arm.body.node else {
return None;
};
is_ident(hex, &ok_binders[0]).then_some(())?;
let prefix_str = str_lit(prefix)?;
let want: String = [escape_char, unicode_letter, '0', '0'].iter().collect();
(prefix_str == want).then_some(())?;
}
for pair in &pairs {
let decoded_str: String = pair.decoded.to_string();
let hit = consumer_pairs
.iter()
.find(|(letter, _)| *letter == pair.letter);
match hit {
Some((_, dec)) if *dec == decoded_str => {}
_ => return None,
}
if pair.letter == unicode_letter {
return None;
}
}
if pairs.is_empty() {
return None;
}
let contract_of = |fd: &FnDef| -> Option<&crate::ir::FnContract> {
let key = match inputs.fn_owning_scope(fd) {
Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &fd.name),
None => crate::ir::FnKey::entry(&fd.name),
};
inputs
.symbol_table
.fn_id_of(&key)
.and_then(|id| fn_contracts.get(&id))
};
for fd in [
scan_fd,
escape_fd,
validate_fd,
unicode_fd,
codepoint_fd,
apply_fd,
] {
let contract = contract_of(fd)?;
if !matches!(
contract.recursion,
Some(crate::ir::RecursionContract::Fuel {
fuel_metric: crate::ir::FuelMetric::Lex { .. },
})
) {
return None;
}
}
{
let contract = contract_of(read_hex_fd)?;
let Some(crate::ir::RecursionContract::Fuel {
fuel_metric:
crate::ir::FuelMetric::BoundMinusParamNatAbsPlusOne {
ref param,
ref bound,
},
}) = contract.recursion
else {
return None;
};
if *param != read_hex_fd.params[3].0 {
return None;
}
if !matches!(
&bound.node,
crate::ir::hir::ResolvedExpr::Literal(Literal::Int(4))
) {
return None;
}
}
for name in [
&fold_name,
&classifier_name,
&control_name,
&control_escape_name,
&hex_val_name,
&high_surrogate_name,
&low_surrogate_name,
] {
let fd = resolve_user_fn(name)?;
if let Some(contract) = contract_of(fd)
&& matches!(
contract.recursion,
Some(crate::ir::RecursionContract::Fuel {
fuel_metric: crate::ir::FuelMetric::Lex { .. },
})
)
{
return None;
}
}
Some(crate::ir::ProofStrategy::StringEscapeRoundtrip(Box::new(
StringEscapeRoundtripPin {
scan_fn: scan_name,
escape_fn: escape_name,
validate_fn: validate_name,
finish_fn: finish_name,
unicode_fn: unicode_name,
codepoint_fn: codepoint_name,
apply_fn: apply_name,
read_hex_fn: read_hex_name,
hex_val_fn: hex_val_name,
high_surrogate_fn: high_surrogate_name,
low_surrogate_fn: low_surrogate_name,
producer_fn: producer_name,
fold_fn: fold_name,
classifier_fn: classifier_name,
control_fn: control_name,
control_escape_fn: control_escape_name,
ok_ctor,
str_ctor,
terminator,
escape_char,
unicode_letter,
pairs,
control_threshold,
high_surrogate_min,
low_surrogate_min,
},
)))
}