use super::facts::scalar_class;
use super::resolve::Scope;
use super::v1;
use crate::v2;
pub(crate) fn translate(
scope: Scope<'_>,
home: &v2::Package,
source: &str,
params: &[v2::Param],
reply_named: Option<&str>,
) -> v1::clause::Translation {
match translate_accepted(scope, home, source, params, reply_named) {
Ok(comparison) => v1::clause::Translation::Comparison(comparison),
Err(reason) => v1::clause::Translation::Refused(reason),
}
}
fn translate_accepted(
scope: Scope<'_>,
home: &v2::Package,
source: &str,
params: &[v2::Param],
reply_named: Option<&str>,
) -> Result<v1::Comparison, String> {
let (subject, op, literal) = parse(source)?;
if subject == "result" {
let named = reply_named.ok_or("`result` is only a subject on a query's `ensure` clause")?;
let class = numeric_class(scope, home, named)
.ok_or("`result` is not a named integer or float scalar")?;
if !literal_matches(&literal, class) {
return Err("the literal does not match the reply's numeric type".to_string());
}
return Ok(comparison(
op,
literal,
class,
v1::comparison::Subject::Result(true),
));
}
let [param] = params else {
return Err("a translated clause needs exactly one declared parameter".to_string());
};
if param.name != subject {
return Err("the subject is not the interaction's declared parameter".to_string());
}
let named = param_named_type(param).ok_or("the parameter is not a named type")?;
let class = numeric_class(scope, home, named)
.ok_or("the parameter is not a named integer or float scalar")?;
if !literal_matches(&literal, class) {
return Err("the literal does not match the parameter's numeric type".to_string());
}
Ok(comparison(
op,
literal,
class,
v1::comparison::Subject::Param(0),
))
}
fn comparison(
op: v1::ComparisonOp,
literal: String,
class: v1::ScalarClass,
subject: v1::comparison::Subject,
) -> v1::Comparison {
v1::Comparison {
op: op as i32,
literal,
literal_class: class as i32,
subject: Some(subject),
}
}
fn parse(source: &str) -> Result<(String, v1::ComparisonOp, String), String> {
let text = source.trim();
let comparisons = [
("<=", v1::ComparisonOp::Le),
(">=", v1::ComparisonOp::Ge),
("==", v1::ComparisonOp::Eq),
("!=", v1::ComparisonOp::Ne),
("<", v1::ComparisonOp::Lt),
(">", v1::ComparisonOp::Gt),
];
for (symbol, op) in comparisons {
let Some(position) = text.find(symbol) else {
continue;
};
let left = text[..position].trim();
let right = text[position + symbol.len()..].trim();
if is_identifier(left) && is_number(right) {
return Ok((left.to_string(), op, right.to_string()));
}
return Err("not `<subject> <comparison> <numeric literal>`".to_string());
}
Err("no accepted comparison".to_string())
}
fn is_identifier(text: &str) -> bool {
let mut chars = text.chars();
match chars.next() {
Some(first) if first.is_ascii_alphabetic() || first == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn is_number(text: &str) -> bool {
!text.is_empty()
&& text
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | 'e' | 'E'))
&& text.parse::<f64>().is_ok()
}
fn literal_matches(value: &str, class: v1::ScalarClass) -> bool {
match class {
v1::ScalarClass::Integer => !value.contains(['.', 'e', 'E']),
v1::ScalarClass::Float => true,
_ => false,
}
}
fn numeric_class(scope: Scope<'_>, home: &v2::Package, reference: &str) -> Option<v1::ScalarClass> {
let (decl, _) = scope.resolve(home, reference)?;
let v2::decl::Kind::TypeDef(td) = decl.kind.as_ref()? else {
return None;
};
match scalar_class(td) {
class @ (v1::ScalarClass::Integer | v1::ScalarClass::Float) => Some(class),
_ => None,
}
}
fn param_named_type(param: &v2::Param) -> Option<&str> {
match param.r#type.as_ref()?.kind.as_ref()? {
v2::field_type::Kind::Named(name) => Some(name),
_ => None,
}
}