use crate::ast::{Expr, Literal, Spanned, VerifyBlock};
use crate::codegen::CodegenContext;
use super::expr::emit_expr_legacy;
pub(super) fn ground_truth_rhs(
vb: &VerifyBlock,
ctx: &CodegenContext,
global_case_idx: usize,
) -> Option<String> {
let key = (
crate::codegen::common::verify_block_counter_key(vb),
global_case_idx,
);
let repr = ctx.sample_expected.get(&key)?;
let expr = parse_literal_expr(repr)?;
if !is_safe_literal_expr(&expr) {
return None;
}
Some(emit_expr_legacy(&expr, ctx, None))
}
fn parse_literal_expr(repr: &str) -> Option<Spanned<Expr>> {
let mut lexer = crate::lexer::Lexer::new(repr);
let tokens = lexer.tokenize().ok()?;
let mut parser = crate::parser::Parser::new(tokens);
let items = parser.parse().ok()?;
match items.as_slice() {
[crate::ast::TopLevel::Stmt(crate::ast::Stmt::Expr(expr))] => Some(expr.clone()),
_ => None,
}
}
fn is_safe_literal_expr(expr: &Spanned<Expr>) -> bool {
match &expr.node {
Expr::Literal(Literal::Int(_) | Literal::Str(_) | Literal::Bool(_) | Literal::Unit) => true,
Expr::Literal(Literal::Float(_)) => false,
Expr::Neg(inner) => is_safe_literal_expr(inner),
Expr::List(items) | Expr::Tuple(items) => items.iter().all(is_safe_literal_expr),
Expr::MapLiteral(_) => false,
Expr::Attr(_, _) => is_capitalized_dotted_path(expr),
Expr::FnCall(callee, args) => {
is_capitalized_dotted_path(callee) && args.iter().all(is_safe_literal_expr)
}
Expr::Constructor(name, arg) => {
name.split('.').count() >= 2
&& name.split('.').all(starts_capitalized)
&& arg.as_deref().is_none_or(is_safe_literal_expr)
}
_ => false,
}
}
fn is_capitalized_dotted_path(expr: &Spanned<Expr>) -> bool {
fn walk(expr: &Spanned<Expr>, segments: &mut usize) -> bool {
match &expr.node {
Expr::Ident(name) => {
*segments += 1;
starts_capitalized(name)
}
Expr::Attr(base, field) => {
*segments += 1;
starts_capitalized(field) && walk(base, segments)
}
_ => false,
}
}
let mut segments = 0;
walk(expr, &mut segments) && segments >= 2
}
fn starts_capitalized(s: &str) -> bool {
s.chars().next().is_some_and(|c| c.is_ascii_uppercase())
}
#[cfg(test)]
mod tests {
use super::*;
fn parses_safe(repr: &str) -> bool {
parse_literal_expr(repr).is_some_and(|e| is_safe_literal_expr(&e))
}
#[test]
fn whitelists_ground_truth_repr_shapes() {
assert!(parses_safe("210"));
assert!(parses_safe("-5"));
assert!(parses_safe("true"));
assert!(parses_safe("\"hello\""));
assert!(parses_safe("[1, 2, 3]"));
assert!(parses_safe("[]"));
assert!(parses_safe("(1, \"a\")"));
assert!(parses_safe("Result.Ok(3)"));
assert!(parses_safe("Result.Err(\"boom\")"));
assert!(parses_safe("Option.Some([1])"));
assert!(parses_safe("Option.None"));
assert!(parses_safe("Color.Rgb(1, 2, 3)"));
}
#[test]
fn declines_unsafe_or_unparseable_shapes() {
assert!(!parses_safe("1.5"));
assert!(!parses_safe("[1.5]"));
assert!(!parses_safe("Red"));
assert!(!parses_safe("Rgb(1, 2, 3)"));
assert!(!parses_safe("<fn foo>"));
assert!(!parses_safe("Vector[1, 2]"));
assert!(!parses_safe("User(name: \"x\")"));
assert!(!parses_safe("f(1)"));
assert!(!parses_safe("1 + 2"));
assert!(!parses_safe("{\"a\": 1}"));
assert!(!parses_safe("{\"a\" => 1}"));
assert!(!parses_safe("Option.Some({\"a\" => 1})"));
}
}