mod conversions;
mod lookup;
use crate::error::{HedlError, HedlResult};
use crate::lex::{
is_tensor_literal, is_valid_id_token, parse_expression_token, parse_reference, parse_tensor,
};
use crate::types::{value_to_expected_type, ExpectedType};
use crate::value::{Reference, Value};
use std::collections::{BTreeMap, HashMap};
use conversions::convert_lex_value_to_value;
use lookup::{infer_expanded_alias, try_lookup_common, try_parse_number};
pub use lookup::infer_quoted_value;
pub struct InferenceContext<'a> {
pub aliases: &'a BTreeMap<String, String>,
alias_cache: HashMap<String, Value>,
pub is_matrix_cell: bool,
pub is_id_column: bool,
pub prev_row: Option<&'a [Value]>,
pub column_index: usize,
pub current_type: Option<&'a str>,
pub version: (u32, u32),
pub null_char: char,
pub expected_type: Option<ExpectedType>,
pub column_types: Option<&'a [ExpectedType]>,
pub strict_types: bool,
pub error_recovery: bool,
}
impl<'a> InferenceContext<'a> {
pub fn for_key_value(aliases: &'a BTreeMap<String, String>) -> Self {
Self {
aliases,
alias_cache: Self::build_alias_cache(aliases),
is_matrix_cell: false,
is_id_column: false,
prev_row: None,
column_index: 0,
current_type: None,
version: (1, 0), null_char: '~', expected_type: None,
column_types: None,
strict_types: false,
error_recovery: false,
}
}
pub fn for_matrix_cell(
aliases: &'a BTreeMap<String, String>,
column_index: usize,
prev_row: Option<&'a [Value]>,
current_type: &'a str,
) -> Self {
Self {
aliases,
alias_cache: Self::build_alias_cache(aliases),
is_matrix_cell: true,
is_id_column: column_index == 0,
prev_row,
column_index,
current_type: Some(current_type),
version: (1, 0), null_char: '~', expected_type: None,
column_types: None,
strict_types: false,
error_recovery: false,
}
}
pub fn with_version(mut self, version: (u32, u32)) -> Self {
self.version = version;
self
}
pub fn with_expected_type(mut self, expected: ExpectedType) -> Self {
self.expected_type = Some(expected);
self
}
pub fn with_column_types(mut self, types: &'a [ExpectedType]) -> Self {
self.column_types = Some(types);
self
}
pub fn with_strict_types(mut self, strict: bool) -> Self {
self.strict_types = strict;
self
}
pub fn with_error_recovery(mut self, recovery: bool) -> Self {
self.error_recovery = recovery;
self
}
pub fn with_null_char(mut self, null_char: char) -> Self {
self.null_char = null_char;
self
}
fn build_alias_cache(aliases: &BTreeMap<String, String>) -> HashMap<String, Value> {
let mut cache = HashMap::with_capacity(aliases.len());
for (key, expanded) in aliases {
if let Ok(value) = infer_expanded_alias(expanded, 0) {
cache.insert(key.clone(), value);
}
}
cache
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InferenceConfidence {
Certain,
Probable,
Ambiguous,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InferenceResult {
pub value: Value,
pub inferred_type: ExpectedType,
pub confidence: InferenceConfidence,
}
pub fn infer_value_with_type(
input: &str,
expected: &ExpectedType,
ctx: &InferenceContext<'_>,
line_num: usize,
) -> HedlResult<InferenceResult> {
use crate::coercion::{coerce, CoercionMode};
let value = infer_value(input, ctx, line_num)?;
let inferred_type = value_to_expected_type(&value);
if expected.matches(&value) {
return Ok(InferenceResult {
value,
inferred_type,
confidence: InferenceConfidence::Certain,
});
}
let mode = if ctx.strict_types {
CoercionMode::Strict
} else {
CoercionMode::Lenient
};
match coerce(value.clone(), expected, mode) {
crate::coercion::CoercionResult::Matched(v) => Ok(InferenceResult {
value: v,
inferred_type: expected.clone(),
confidence: InferenceConfidence::Certain,
}),
crate::coercion::CoercionResult::Coerced(v) => Ok(InferenceResult {
value: v,
inferred_type: expected.clone(),
confidence: InferenceConfidence::Probable,
}),
crate::coercion::CoercionResult::Failed { reason, .. } => {
if ctx.error_recovery {
Ok(InferenceResult {
value,
inferred_type,
confidence: InferenceConfidence::Ambiguous,
})
} else {
Err(HedlError::type_mismatch(
format!(
"type mismatch: expected {}, got {} ({})",
expected.describe(),
crate::types::describe_value_type(&value),
reason
),
line_num,
))
}
}
}
}
pub fn infer_value_synthesize(
input: &str,
ctx: &InferenceContext<'_>,
line_num: usize,
) -> HedlResult<InferenceResult> {
let value = infer_value(input, ctx, line_num)?;
let inferred_type = value_to_expected_type(&value);
Ok(InferenceResult {
value,
inferred_type,
confidence: InferenceConfidence::Certain,
})
}
pub fn infer_value(s: &str, ctx: &InferenceContext<'_>, line_num: usize) -> HedlResult<Value> {
let trimmed = s.trim();
if trimmed.len() == 1 && trimmed.starts_with(ctx.null_char) {
if ctx.is_id_column {
return Err(HedlError::semantic(
format!("null ({}) not permitted in ID column", ctx.null_char),
line_num,
));
}
return Ok(Value::Null);
}
if let Some(value) = try_lookup_common(trimmed) {
return Ok(value);
}
let bytes = trimmed.as_bytes();
match bytes.first() {
Some(b'^') if bytes.len() == 1 => {
return infer_ditto(ctx, line_num);
}
Some(b'(') => {
use crate::lex::parse_list_literal;
match parse_list_literal(trimmed, 0) {
Ok((lex_value, _consumed)) => {
return convert_lex_value_to_value(lex_value);
}
Err(e) => {
return Err(HedlError::syntax(
format!("invalid list literal: {}", e),
line_num,
));
}
}
}
Some(b'[') => {
if is_tensor_literal(trimmed) {
match parse_tensor(trimmed) {
Ok(tensor) => return Ok(Value::Tensor(Box::new(tensor))),
Err(e) => {
return Err(HedlError::syntax(
format!("invalid tensor literal: {}", e),
line_num,
));
}
}
}
}
Some(b'@') => match parse_reference(trimmed) {
Ok(r) => {
return Ok(Value::Reference(Reference {
type_name: r.type_name.map(|t| t.into_boxed_str()),
id: r.id.into_boxed_str(),
}));
}
Err(e) => {
return Err(HedlError::syntax(
format!("invalid reference: {}", e),
line_num,
));
}
},
Some(b'$') if bytes.get(1) == Some(&b'(') => match parse_expression_token(trimmed) {
Ok(expr) => return Ok(Value::Expression(Box::new(expr))),
Err(e) => {
return Err(HedlError::syntax(
format!("invalid expression: {}", e),
line_num,
));
}
},
Some(b'%') => {
let key = &trimmed[1..];
if let Some(value) = ctx.alias_cache.get(key) {
return Ok(value.clone());
}
if ctx.aliases.contains_key(key) {
let expanded = &ctx.aliases[key];
return infer_expanded_alias(expanded, line_num);
}
return Err(HedlError::alias(
format!("undefined alias: %{}", key),
line_num,
));
}
Some(b'-') | Some(b'0'..=b'9') => {
if let Some(value) = try_parse_number(trimmed) {
return Ok(value);
}
}
_ => {}
}
if ctx.is_id_column && !is_valid_id_token(trimmed) {
return Err(HedlError::semantic(
format!(
"invalid ID format '{}' - must start with letter or underscore",
trimmed
),
line_num,
));
}
Ok(Value::String(Box::from(trimmed)))
}
#[inline]
fn infer_ditto(ctx: &InferenceContext<'_>, line_num: usize) -> HedlResult<Value> {
if !ctx.is_matrix_cell {
return Ok(Value::String("^".into()));
}
if ctx.version >= (2, 0) {
use crate::errors::messages;
return Err(messages::v20_ditto_not_allowed(line_num));
}
if ctx.is_id_column {
return Err(HedlError::semantic(
"ditto (^) not permitted in ID column",
line_num,
));
}
match ctx.prev_row {
Some(prev) if ctx.column_index < prev.len() => Ok(prev[ctx.column_index].clone()),
Some(_) => Err(HedlError::semantic(
"ditto (^) column index out of range",
line_num,
)),
None => Err(HedlError::semantic(
"ditto (^) not allowed in first row of list",
line_num,
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kv_ctx() -> InferenceContext<'static> {
static EMPTY: BTreeMap<String, String> = BTreeMap::new();
InferenceContext::for_key_value(&EMPTY)
}
fn ctx_with_aliases(aliases: &BTreeMap<String, String>) -> InferenceContext<'_> {
InferenceContext::for_key_value(aliases)
}
#[test]
fn test_infer_null() {
let v = infer_value("~", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Null));
}
#[test]
fn test_infer_null_with_whitespace() {
let v = infer_value(" ~ ", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Null));
}
#[test]
fn test_infer_tilde_as_part_of_string() {
let v = infer_value("~hello", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::String(s) if s.as_ref() == "~hello"));
}
#[test]
fn test_null_in_id_column_error() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
let result = infer_value("~", &ctx, 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("ID column"));
}
#[test]
fn test_infer_bool() {
assert!(matches!(
infer_value("true", &kv_ctx(), 1).unwrap(),
Value::Bool(true)
));
assert!(matches!(
infer_value("false", &kv_ctx(), 1).unwrap(),
Value::Bool(false)
));
}
#[test]
fn test_infer_bool_case_sensitive() {
assert!(matches!(
infer_value("True", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
assert!(matches!(
infer_value("FALSE", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
}
#[test]
fn test_infer_bool_with_whitespace() {
assert!(matches!(
infer_value(" true ", &kv_ctx(), 1).unwrap(),
Value::Bool(true)
));
}
#[test]
fn test_infer_int() {
assert!(matches!(
infer_value("42", &kv_ctx(), 1).unwrap(),
Value::Int(42)
));
assert!(matches!(
infer_value("-5", &kv_ctx(), 1).unwrap(),
Value::Int(-5)
));
assert!(matches!(
infer_value("0", &kv_ctx(), 1).unwrap(),
Value::Int(0)
));
}
#[test]
fn test_infer_int_large() {
let v = infer_value("9223372036854775807", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Int(i64::MAX)));
}
#[test]
fn test_infer_int_negative_large() {
let v = infer_value("-9223372036854775808", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Int(i64::MIN)));
}
#[test]
fn test_infer_int_with_whitespace() {
assert!(matches!(
infer_value(" 123 ", &kv_ctx(), 1).unwrap(),
Value::Int(123)
));
}
#[test]
fn test_infer_float() {
match infer_value("3.25", &kv_ctx(), 1).unwrap() {
Value::Float(f) => assert!((f - 3.25).abs() < 0.001),
_ => panic!("expected float"),
}
match infer_value("42.0", &kv_ctx(), 1).unwrap() {
Value::Float(f) => assert!((f - 42.0).abs() < 0.001),
_ => panic!("expected float"),
}
}
#[test]
fn test_infer_float_negative() {
match infer_value("-3.5", &kv_ctx(), 1).unwrap() {
Value::Float(f) => assert!((f + 3.5).abs() < 0.001),
_ => panic!("expected float"),
}
}
#[test]
fn test_infer_float_small() {
match infer_value("0.001", &kv_ctx(), 1).unwrap() {
Value::Float(f) => assert!((f - 0.001).abs() < 0.0001),
_ => panic!("expected float"),
}
}
#[test]
fn test_infer_string() {
assert!(matches!(
infer_value("hello", &kv_ctx(), 1).unwrap(),
Value::String(s) if s.as_ref() == "hello"
));
}
#[test]
fn test_infer_string_with_spaces() {
assert!(matches!(
infer_value(" hello ", &kv_ctx(), 1).unwrap(),
Value::String(s) if s.as_ref() == "hello"
));
}
#[test]
fn test_infer_string_unicode() {
assert!(matches!(
infer_value("日本語", &kv_ctx(), 1).unwrap(),
Value::String(s) if s.as_ref() == "日本語"
));
}
#[test]
fn test_infer_string_emoji() {
assert!(matches!(
infer_value("🎉", &kv_ctx(), 1).unwrap(),
Value::String(s) if s.as_ref() == "🎉"
));
}
#[test]
fn test_infer_reference() {
let v = infer_value("@user_1", &kv_ctx(), 1).unwrap();
match v {
Value::Reference(r) => {
assert_eq!(r.type_name, None);
assert_eq!(r.id.as_ref(), "user_1");
}
_ => panic!("expected reference"),
}
}
#[test]
fn test_infer_qualified_reference() {
let v = infer_value("@User:user_1", &kv_ctx(), 1).unwrap();
match v {
Value::Reference(r) => {
assert_eq!(r.type_name.as_deref(), Some("User"));
assert_eq!(r.id.as_ref(), "user_1");
}
_ => panic!("expected reference"),
}
}
#[test]
fn test_infer_reference_with_whitespace() {
let v = infer_value(" @user_1 ", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Reference(_)));
}
#[test]
fn test_infer_reference_invalid_error() {
let result = infer_value("@User:123-invalid", &kv_ctx(), 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("invalid reference"));
}
#[test]
fn test_infer_reference_uppercase_valid() {
let v = infer_value("@User:ABC123", &kv_ctx(), 1).unwrap();
match v {
Value::Reference(r) => {
assert_eq!(r.type_name.as_deref(), Some("User"));
assert_eq!(r.id.as_ref(), "ABC123");
}
_ => panic!("Expected reference"),
}
}
#[test]
fn test_infer_expression() {
use crate::lex::Expression;
let v = infer_value("$(now())", &kv_ctx(), 1).unwrap();
match v {
Value::Expression(e) => {
assert!(
matches!(e.as_ref(), Expression::Call { name, args, .. } if name == "now" && args.is_empty())
);
}
_ => panic!("expected expression"),
}
}
#[test]
fn test_infer_expression_with_args() {
let v = infer_value("$(add(1, 2))", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Expression(_)));
}
#[test]
fn test_infer_expression_nested() {
let v = infer_value("$(outer(inner()))", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Expression(_)));
}
#[test]
fn test_infer_expression_identifier() {
let v = infer_value("$(x)", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Expression(_)));
}
#[test]
fn test_infer_expression_invalid_error() {
let result = infer_value("$(unclosed", &kv_ctx(), 1);
assert!(result.is_err());
}
#[test]
fn test_dollar_not_expression() {
let v = infer_value("$foo", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::String(s) if s.as_ref() == "$foo"));
}
#[test]
fn test_infer_tensor() {
let v = infer_value("[1, 2, 3]", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Tensor(_)));
}
#[test]
fn test_infer_tensor_float() {
let v = infer_value("[1.5, 2.5, 3.5]", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Tensor(_)));
}
#[test]
fn test_infer_tensor_nested() {
let v = infer_value("[[1, 2], [3, 4]]", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Tensor(_)));
}
#[test]
fn test_infer_tensor_empty_error() {
let result = infer_value("[]", &kv_ctx(), 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("empty tensor"));
}
#[test]
fn test_infer_tensor_invalid_is_string() {
let v = infer_value("[not a tensor]", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::String(_)));
}
#[test]
fn test_infer_alias_bool() {
let mut aliases = BTreeMap::new();
aliases.insert("active".to_string(), "true".to_string());
let ctx = ctx_with_aliases(&aliases);
let v = infer_value("%active", &ctx, 1).unwrap();
assert!(matches!(v, Value::Bool(true)));
}
#[test]
fn test_infer_alias_number() {
let mut aliases = BTreeMap::new();
aliases.insert("count".to_string(), "42".to_string());
let ctx = ctx_with_aliases(&aliases);
let v = infer_value("%count", &ctx, 1).unwrap();
assert!(matches!(v, Value::Int(42)));
}
#[test]
fn test_infer_alias_string() {
let mut aliases = BTreeMap::new();
aliases.insert("name".to_string(), "Alice".to_string());
let ctx = ctx_with_aliases(&aliases);
let v = infer_value("%name", &ctx, 1).unwrap();
assert!(matches!(v, Value::String(s) if s.as_ref() == "Alice"));
}
#[test]
fn test_infer_undefined_alias_error() {
let result = infer_value("%undefined", &kv_ctx(), 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("undefined alias"));
}
#[test]
fn test_ditto_in_kv_is_string() {
let v = infer_value("^", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::String(s) if s.as_ref() == "^"));
}
#[test]
fn test_ditto_in_matrix_cell() {
let aliases = BTreeMap::new();
let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User");
let v = infer_value("^", &ctx, 1).unwrap();
assert!(matches!(v, Value::Int(42)));
}
#[test]
fn test_ditto_in_id_column_error() {
let aliases = BTreeMap::new();
let prev_row = vec![Value::String("id".to_string().into())];
let ctx = InferenceContext::for_matrix_cell(&aliases, 0, Some(&prev_row), "User");
let result = infer_value("^", &ctx, 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("ID column"));
}
#[test]
fn test_ditto_first_row_error() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_matrix_cell(&aliases, 1, None, "User");
let result = infer_value("^", &ctx, 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("first row"));
}
#[test]
fn test_ditto_column_out_of_range_error() {
let aliases = BTreeMap::new();
let prev_row = vec![Value::String("id".to_string().into())];
let ctx = InferenceContext::for_matrix_cell(&aliases, 5, Some(&prev_row), "User");
let result = infer_value("^", &ctx, 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("out of range"));
}
#[test]
fn test_number_edge_cases() {
assert!(matches!(
infer_value("1e10", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
assert!(matches!(
infer_value("1_000", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
assert!(matches!(
infer_value(".5", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
}
#[test]
fn test_number_trailing_decimal_is_string() {
assert!(matches!(
infer_value("123.", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
}
#[test]
fn test_number_plus_sign_is_string() {
assert!(matches!(
infer_value("+42", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
}
#[test]
fn test_number_leading_zeros_is_string() {
assert!(matches!(
infer_value("007", &kv_ctx(), 1).unwrap(),
Value::Int(7) ));
}
#[test]
fn test_number_hex_is_string() {
assert!(matches!(
infer_value("0xFF", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
}
#[test]
fn test_try_parse_number_empty() {
assert!(try_parse_number("").is_none());
}
#[test]
fn test_try_parse_number_whitespace() {
assert!(try_parse_number(" ").is_none());
}
#[test]
fn test_try_parse_number_valid_int() {
assert!(matches!(try_parse_number("123"), Some(Value::Int(123))));
}
#[test]
fn test_try_parse_number_valid_float() {
match try_parse_number("3.5") {
Some(Value::Float(f)) => assert!((f - 3.5).abs() < 0.001),
_ => panic!("expected float"),
}
}
#[test]
fn test_try_parse_number_negative() {
assert!(matches!(try_parse_number("-42"), Some(Value::Int(-42))));
}
#[test]
fn test_try_parse_number_invalid() {
assert!(try_parse_number("abc").is_none());
assert!(try_parse_number("12abc").is_none());
}
#[test]
fn test_infer_quoted_value_simple() {
let v = infer_quoted_value("hello");
assert!(matches!(v, Value::String(s) if s.as_ref() == "hello"));
}
#[test]
fn test_infer_quoted_value_empty() {
let v = infer_quoted_value("");
assert!(matches!(v, Value::String(s) if s.is_empty()));
}
#[test]
fn test_infer_quoted_value_escaped_quotes() {
let v = infer_quoted_value("say \"\"hello\"\"");
assert!(matches!(v, Value::String(s) if s.as_ref() == "say \"hello\""));
}
#[test]
fn test_infer_quoted_value_multiple_escapes() {
let v = infer_quoted_value("a\"\"b\"\"c");
assert!(matches!(v, Value::String(s) if s.as_ref() == "a\"b\"c"));
}
#[test]
fn test_context_for_key_value() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_key_value(&aliases);
assert!(!ctx.is_matrix_cell);
assert!(!ctx.is_id_column);
assert!(ctx.prev_row.is_none());
}
#[test]
fn test_context_for_matrix_cell() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_matrix_cell(&aliases, 2, None, "User");
assert!(ctx.is_matrix_cell);
assert!(!ctx.is_id_column); assert_eq!(ctx.column_index, 2);
assert_eq!(ctx.current_type, Some("User"));
}
#[test]
fn test_context_id_column_detection() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
assert!(ctx.is_id_column); }
#[test]
fn test_id_column_valid_id() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
let v = infer_value("user_123", &ctx, 1).unwrap();
assert!(matches!(v, Value::String(s) if s.as_ref() == "user_123"));
}
#[test]
fn test_id_column_invalid_starts_digit_error() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
let result = infer_value("123User", &ctx, 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("invalid ID"));
}
#[test]
fn test_id_column_uppercase_valid() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
let result = infer_value("SKU-4020", &ctx, 1);
assert!(result.is_ok());
}
#[test]
fn test_lookup_table_bool_true() {
let v = infer_value("true", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Bool(true)));
}
#[test]
fn test_lookup_table_bool_false() {
let v = infer_value("false", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Bool(false)));
}
#[test]
fn test_lookup_table_null() {
let v = infer_value("~", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Null));
}
#[test]
fn test_lookup_table_collision_detection() {
let v = infer_value("True", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::String(s) if s.as_ref() == "True"));
}
#[test]
fn test_lookup_table_multiple_calls() {
for _ in 0..100 {
let v = infer_value("true", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::Bool(true)));
}
}
#[test]
fn test_inference_result_structure() {
let result = infer_value_synthesize("42", &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.inferred_type, ExpectedType::Int);
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_synthesize_int() {
let result = infer_value_synthesize("42", &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.inferred_type, ExpectedType::Int);
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_synthesize_float() {
let result = infer_value_synthesize("3.25", &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Float(f) if (f - 3.25).abs() < 0.001));
assert_eq!(result.inferred_type, ExpectedType::Float);
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_synthesize_bool() {
let result = infer_value_synthesize("true", &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Bool(true)));
assert_eq!(result.inferred_type, ExpectedType::Bool);
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_synthesize_string() {
let result = infer_value_synthesize("hello", &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::String(s) if s.as_ref() == "hello"));
assert_eq!(result.inferred_type, ExpectedType::String);
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_synthesize_null() {
let result = infer_value_synthesize("~", &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Null));
assert_eq!(result.inferred_type, ExpectedType::Null);
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_checking_exact_match() {
let result = infer_value_with_type("42", &ExpectedType::Int, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.inferred_type, ExpectedType::Int);
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_checking_int_to_float_coercion() {
let result = infer_value_with_type("42", &ExpectedType::Float, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Float(f) if (f - 42.0).abs() < 0.001));
assert_eq!(result.inferred_type, ExpectedType::Float);
assert_eq!(result.confidence, InferenceConfidence::Probable);
}
#[test]
fn test_checking_string_to_int_lenient() {
let ctx = kv_ctx();
let result = infer_value_with_type("42", &ExpectedType::Int, &ctx, 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_checking_with_strict_types() {
let ctx = kv_ctx().with_strict_types(true);
let result = infer_value_with_type("42", &ExpectedType::Float, &ctx, 1).unwrap();
assert!(matches!(result.value, Value::Float(_)));
}
#[test]
fn test_checking_type_mismatch_error() {
let ctx = kv_ctx().with_strict_types(true);
let result = infer_value_with_type("true", &ExpectedType::Int, &ctx, 1);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("type mismatch"));
}
#[test]
fn test_checking_with_error_recovery() {
let ctx = kv_ctx().with_strict_types(true).with_error_recovery(true);
let result = infer_value_with_type("true", &ExpectedType::Int, &ctx, 1).unwrap();
assert!(matches!(result.value, Value::Bool(true)));
assert_eq!(result.confidence, InferenceConfidence::Ambiguous);
}
#[test]
fn test_checking_numeric_accepts_int() {
let result = infer_value_with_type("42", &ExpectedType::Numeric, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_checking_numeric_accepts_float() {
let result = infer_value_with_type("3.5", &ExpectedType::Numeric, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Float(f) if (f - 3.5).abs() < 0.001));
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_checking_any_accepts_all() {
let result = infer_value_with_type("42", &ExpectedType::Any, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.confidence, InferenceConfidence::Certain);
let result = infer_value_with_type("hello", &ExpectedType::Any, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::String(_)));
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_checking_union_type() {
use crate::types::ExpectedType;
let union = ExpectedType::Union(vec![ExpectedType::Int, ExpectedType::String]);
let result = infer_value_with_type("42", &union, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_checking_reference_qualified() {
let expected = ExpectedType::Reference {
target_type: Some("User".to_string()),
};
let result = infer_value_with_type("@User:user_1", &expected, &kv_ctx(), 1).unwrap();
match result.value {
Value::Reference(r) => {
assert_eq!(r.type_name.as_deref(), Some("User"));
assert_eq!(r.id.as_ref(), "user_1");
}
_ => panic!("Expected reference"),
}
assert_eq!(result.confidence, InferenceConfidence::Certain);
}
#[test]
fn test_context_with_expected_type() {
let ctx = kv_ctx().with_expected_type(ExpectedType::Float);
assert_eq!(ctx.expected_type, Some(ExpectedType::Float));
}
#[test]
fn test_context_with_column_types() {
let types = vec![ExpectedType::String, ExpectedType::Int, ExpectedType::Float];
let ctx = kv_ctx().with_column_types(&types);
assert!(ctx.column_types.is_some());
assert_eq!(ctx.column_types.unwrap().len(), 3);
}
#[test]
fn test_context_builder_pattern() {
let types = vec![ExpectedType::Int];
let ctx = kv_ctx()
.with_expected_type(ExpectedType::Float)
.with_column_types(&types)
.with_strict_types(true)
.with_error_recovery(true);
assert_eq!(ctx.expected_type, Some(ExpectedType::Float));
assert!(ctx.column_types.is_some());
assert!(ctx.strict_types);
assert!(ctx.error_recovery);
}
#[test]
fn test_inference_confidence_levels() {
let result = infer_value_with_type("42", &ExpectedType::Int, &kv_ctx(), 1).unwrap();
assert_eq!(result.confidence, InferenceConfidence::Certain);
let result = infer_value_with_type("42", &ExpectedType::Float, &kv_ctx(), 1).unwrap();
assert_eq!(result.confidence, InferenceConfidence::Probable);
let ctx = kv_ctx().with_strict_types(true).with_error_recovery(true);
let result = infer_value_with_type("true", &ExpectedType::Int, &ctx, 1).unwrap();
assert_eq!(result.confidence, InferenceConfidence::Ambiguous);
}
#[test]
fn test_synthesize_with_aliases() {
let mut aliases = BTreeMap::new();
aliases.insert("count".to_string(), "42".to_string());
let ctx = ctx_with_aliases(&aliases);
let result = infer_value_synthesize("%count", &ctx, 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
assert_eq!(result.inferred_type, ExpectedType::Int);
}
#[test]
fn test_checking_with_ditto() {
let aliases = BTreeMap::new();
let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User");
let result = infer_value_synthesize("^", &ctx, 1).unwrap();
assert!(matches!(result.value, Value::Int(42)));
}
#[test]
fn test_checking_preserves_expression() {
let result =
infer_value_with_type("$(now())", &ExpectedType::Expression, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Expression(_)));
assert_eq!(result.inferred_type, ExpectedType::Expression);
}
#[test]
fn test_checking_preserves_tensor() {
let expected = ExpectedType::Tensor {
shape: None,
dtype: None,
};
let result = infer_value_with_type("[1, 2, 3]", &expected, &kv_ctx(), 1).unwrap();
assert!(matches!(result.value, Value::Tensor(_)));
}
#[test]
fn test_v20_rejects_ditto() {
let aliases = BTreeMap::new();
let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User")
.with_version((2, 0));
let result = infer_value("^", &ctx, 1);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains("v2.0"));
assert!(err.message.contains("not allowed"));
}
#[test]
fn test_pre_v20_allows_ditto() {
let aliases = BTreeMap::new();
let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User")
.with_version((1, 2));
let result = infer_value("^", &ctx, 1);
assert!(result.is_ok());
assert!(matches!(result.unwrap(), Value::Int(42)));
}
#[test]
fn test_v10_allows_ditto() {
let aliases = BTreeMap::new();
let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User")
.with_version((1, 0));
let result = infer_value("^", &ctx, 1);
assert!(result.is_ok());
assert!(matches!(result.unwrap(), Value::Int(42)));
}
#[test]
fn test_v20_context_builder() {
let aliases = BTreeMap::new();
let ctx = InferenceContext::for_key_value(&aliases).with_version((2, 0));
assert_eq!(ctx.version, (2, 0));
}
#[test]
fn test_infer_empty_string() {
let v = infer_value("", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::String(s) if s.is_empty()));
}
#[test]
fn test_infer_whitespace_only() {
let v = infer_value(" ", &kv_ctx(), 1).unwrap();
assert!(matches!(v, Value::String(s) if s.is_empty()));
}
#[test]
fn test_infer_mixed_content() {
assert!(matches!(
infer_value("true123", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
assert!(matches!(
infer_value("42abc", &kv_ctx(), 1).unwrap(),
Value::String(_)
));
assert!(matches!(
infer_value("@invalid id", &kv_ctx(), 1).unwrap_err(),
_
));
}
}