Skip to main content

hedl_core/inference/
mod.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Value inference ladder for HEDL.
19//!
20//! This module implements the inference algorithm that determines the type
21//! of unquoted values based on their textual representation.
22//!
23//! # Bidirectional Type Inference
24//!
25//! The module supports bidirectional type inference with two modes:
26//!
27//! - **Synthesis mode**: Infers the most specific type from the input string
28//! - **Checking mode**: Uses expected type context to disambiguate inference
29//!
30//! This enables both flexible parsing and schema-guided validation.
31
32mod conversions;
33mod lookup;
34
35use crate::error::{HedlError, HedlResult};
36use crate::lex::{
37    is_tensor_literal, is_valid_id_token, parse_expression_token, parse_reference, parse_tensor,
38};
39use crate::types::{value_to_expected_type, ExpectedType};
40use crate::value::{Reference, Value};
41use std::collections::{BTreeMap, HashMap};
42
43use conversions::convert_lex_value_to_value;
44use lookup::{infer_expanded_alias, try_lookup_common, try_parse_number};
45
46// Re-export public function from lookup module
47pub use lookup::infer_quoted_value;
48
49/// Context for value inference.
50///
51/// P0 OPTIMIZATION: Pre-expanded alias cache for 3-4x speedup on alias-heavy documents
52pub struct InferenceContext<'a> {
53    /// Alias definitions (original BTreeMap - kept for compatibility).
54    pub aliases: &'a BTreeMap<String, String>,
55    /// Expanded alias cache (HashMap for O(1) lookups instead of O(log k)).
56    /// Built once at context creation, avoiding repeated BTreeMap lookups.
57    alias_cache: HashMap<String, Value>,
58    /// Whether this is a matrix cell (enables ditto).
59    pub is_matrix_cell: bool,
60    /// Whether this is the ID column.
61    pub is_id_column: bool,
62    /// Previous row values (for ditto).
63    pub prev_row: Option<&'a [Value]>,
64    /// Column index (for ditto).
65    pub column_index: usize,
66    /// Current type name (for reference resolution context).
67    pub current_type: Option<&'a str>,
68    /// HEDL version (for version-specific validation).
69    pub version: (u32, u32),
70    /// Null literal character (from %NULL directive, defaults to '~').
71    pub null_char: char,
72
73    // NEW fields for bidirectional inference:
74    /// Expected type hint from schema or context.
75    pub expected_type: Option<ExpectedType>,
76    /// Column types for matrix rows.
77    pub column_types: Option<&'a [ExpectedType]>,
78    /// Whether to enforce strict type matching.
79    pub strict_types: bool,
80    /// Whether to collect all errors or fail fast.
81    pub error_recovery: bool,
82}
83
84impl<'a> InferenceContext<'a> {
85    /// Create context for key-value inference.
86    pub fn for_key_value(aliases: &'a BTreeMap<String, String>) -> Self {
87        Self {
88            aliases,
89            alias_cache: Self::build_alias_cache(aliases),
90            is_matrix_cell: false,
91            is_id_column: false,
92            prev_row: None,
93            column_index: 0,
94            current_type: None,
95            version: (1, 0), // Default to v1.0 for backward compatibility
96            null_char: '~',  // Default null character
97            expected_type: None,
98            column_types: None,
99            strict_types: false,
100            error_recovery: false,
101        }
102    }
103
104    /// Create context for matrix cell inference.
105    pub fn for_matrix_cell(
106        aliases: &'a BTreeMap<String, String>,
107        column_index: usize,
108        prev_row: Option<&'a [Value]>,
109        current_type: &'a str,
110    ) -> Self {
111        Self {
112            aliases,
113            alias_cache: Self::build_alias_cache(aliases),
114            is_matrix_cell: true,
115            is_id_column: column_index == 0,
116            prev_row,
117            column_index,
118            current_type: Some(current_type),
119            version: (1, 0), // Default to v1.0 for backward compatibility
120            null_char: '~',  // Default null character
121            expected_type: None,
122            column_types: None,
123            strict_types: false,
124            error_recovery: false,
125        }
126    }
127
128    /// Set the HEDL version for version-specific validation.
129    pub fn with_version(mut self, version: (u32, u32)) -> Self {
130        self.version = version;
131        self
132    }
133
134    /// Set expected type hint.
135    pub fn with_expected_type(mut self, expected: ExpectedType) -> Self {
136        self.expected_type = Some(expected);
137        self
138    }
139
140    /// Set column types for matrix inference.
141    pub fn with_column_types(mut self, types: &'a [ExpectedType]) -> Self {
142        self.column_types = Some(types);
143        self
144    }
145
146    /// Enable strict type matching.
147    pub fn with_strict_types(mut self, strict: bool) -> Self {
148        self.strict_types = strict;
149        self
150    }
151
152    /// Enable error recovery mode.
153    pub fn with_error_recovery(mut self, recovery: bool) -> Self {
154        self.error_recovery = recovery;
155        self
156    }
157
158    /// Set the null literal character (from %NULL directive).
159    pub fn with_null_char(mut self, null_char: char) -> Self {
160        self.null_char = null_char;
161        self
162    }
163
164    /// P0 OPTIMIZATION: Pre-expand aliases into HashMap for O(1) lookups
165    /// This is built once per parse context, amortizing the cost across all alias references
166    fn build_alias_cache(aliases: &BTreeMap<String, String>) -> HashMap<String, Value> {
167        let mut cache = HashMap::with_capacity(aliases.len());
168        for (key, expanded) in aliases {
169            // Pre-infer the expanded value to avoid repeated inference
170            if let Ok(value) = infer_expanded_alias(expanded, 0) {
171                cache.insert(key.clone(), value);
172            }
173            // If inference fails, we'll handle it during actual lookup
174        }
175        cache
176    }
177}
178
179/// Confidence level for type inference.
180///
181/// Represents how certain we are about the inferred type.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum InferenceConfidence {
184    /// Type is certain (explicit or unambiguous)
185    Certain,
186    /// Type is probable (heuristic match)
187    Probable,
188    /// Type is ambiguous (multiple valid interpretations)
189    Ambiguous,
190}
191
192/// Result of type inference with confidence level.
193///
194/// This structure provides detailed information about the outcome of
195/// bidirectional type inference, including the inferred value, its type,
196/// and how confident we are in the inference.
197///
198/// # Examples
199///
200/// ```
201/// use hedl_core::inference::{infer_value_synthesize, InferenceContext};
202/// use std::collections::BTreeMap;
203///
204/// let aliases = BTreeMap::new();
205/// let ctx = InferenceContext::for_key_value(&aliases);
206/// let result = infer_value_synthesize("42", &ctx, 1).unwrap();
207/// // result.value is Int(42)
208/// // result.inferred_type is ExpectedType::Int
209/// // result.confidence is InferenceConfidence::Certain
210/// ```
211#[derive(Debug, Clone, PartialEq)]
212pub struct InferenceResult {
213    /// The inferred value
214    pub value: Value,
215    /// The inferred type
216    pub inferred_type: ExpectedType,
217    /// Confidence in the inference
218    pub confidence: InferenceConfidence,
219}
220
221/// Infer value type with expected type context (checking mode).
222///
223/// Uses expected type to disambiguate inference when multiple
224/// interpretations are valid. This is the "checking" mode of
225/// bidirectional type inference.
226///
227/// # Arguments
228///
229/// * `input` - The input string to infer
230/// * `expected` - The expected type from context (schema, etc.)
231/// * `ctx` - Inference context with aliases and other settings
232/// * `line_num` - Line number for error reporting
233///
234/// # Returns
235///
236/// An `InferenceResult` with the value, type, and confidence level.
237///
238/// # Examples
239///
240/// ```
241/// use hedl_core::inference::{infer_value_with_type, InferenceContext};
242/// use hedl_core::types::ExpectedType;
243/// use std::collections::BTreeMap;
244///
245/// let aliases = BTreeMap::new();
246/// let ctx = InferenceContext::for_key_value(&aliases);
247/// let result = infer_value_with_type("42", &ExpectedType::Float, &ctx, 1).unwrap();
248/// // Returns Float(42.0) because Float was expected
249/// ```
250pub fn infer_value_with_type(
251    input: &str,
252    expected: &ExpectedType,
253    ctx: &InferenceContext<'_>,
254    line_num: usize,
255) -> HedlResult<InferenceResult> {
256    use crate::coercion::{coerce, CoercionMode};
257
258    // First try regular inference
259    let value = infer_value(input, ctx, line_num)?;
260    let inferred_type = value_to_expected_type(&value);
261
262    // If types match exactly, return with high confidence
263    if expected.matches(&value) {
264        return Ok(InferenceResult {
265            value,
266            inferred_type,
267            confidence: InferenceConfidence::Certain,
268        });
269    }
270
271    // Try coercion if types don't match
272    let mode = if ctx.strict_types {
273        CoercionMode::Strict
274    } else {
275        CoercionMode::Lenient
276    };
277
278    match coerce(value.clone(), expected, mode) {
279        crate::coercion::CoercionResult::Matched(v) => Ok(InferenceResult {
280            value: v,
281            inferred_type: expected.clone(),
282            confidence: InferenceConfidence::Certain,
283        }),
284        crate::coercion::CoercionResult::Coerced(v) => Ok(InferenceResult {
285            value: v,
286            inferred_type: expected.clone(),
287            confidence: InferenceConfidence::Probable,
288        }),
289        crate::coercion::CoercionResult::Failed { reason, .. } => {
290            if ctx.error_recovery {
291                // In error recovery mode, return original value with ambiguous confidence
292                Ok(InferenceResult {
293                    value,
294                    inferred_type,
295                    confidence: InferenceConfidence::Ambiguous,
296                })
297            } else {
298                // Fail with type mismatch error
299                Err(HedlError::type_mismatch(
300                    format!(
301                        "type mismatch: expected {}, got {} ({})",
302                        expected.describe(),
303                        crate::types::describe_value_type(&value),
304                        reason
305                    ),
306                    line_num,
307                ))
308            }
309        }
310    }
311}
312
313/// Infer value type without expected context (synthesis mode).
314///
315/// Returns the most specific type that can be inferred from the input.
316/// This is the "synthesis" mode of bidirectional type inference.
317///
318/// # Arguments
319///
320/// * `input` - The input string to infer
321/// * `ctx` - Inference context with aliases and other settings
322/// * `line_num` - Line number for error reporting
323///
324/// # Returns
325///
326/// An `InferenceResult` with the value, type, and confidence level.
327///
328/// # Examples
329///
330/// ```
331/// use hedl_core::inference::{infer_value_synthesize, InferenceContext};
332/// use std::collections::BTreeMap;
333///
334/// let aliases = BTreeMap::new();
335/// let ctx = InferenceContext::for_key_value(&aliases);
336/// let result = infer_value_synthesize("42", &ctx, 1).unwrap();
337/// // Returns Int(42) with ExpectedType::Int
338/// ```
339pub fn infer_value_synthesize(
340    input: &str,
341    ctx: &InferenceContext<'_>,
342    line_num: usize,
343) -> HedlResult<InferenceResult> {
344    let value = infer_value(input, ctx, line_num)?;
345    let inferred_type = value_to_expected_type(&value);
346
347    Ok(InferenceResult {
348        value,
349        inferred_type,
350        confidence: InferenceConfidence::Certain,
351    })
352}
353
354/// Infer the value type from an unquoted string.
355///
356/// Implements the inference ladder from the HEDL spec (Section 8.2, 9.3):
357/// 1. Null (~)
358/// 2. Ditto (^) - matrix cells only
359/// 3. Tensor ([...])
360/// 4. Reference (@...)
361/// 5. Expression ($(...))
362/// 6. Alias (%...)
363/// 7. Boolean (true/false)
364/// 8. Number
365/// 9. String (default)
366///
367/// P2 OPTIMIZATION: Uses lookup table for common values (true/false/null) providing
368/// 10-15% parsing speedup by eliminating sequential checks for the most frequent cases.
369///
370/// P1 OPTIMIZATION: First-byte dispatch for O(1) type detection instead of sequential checks.
371/// P1 OPTIMIZATION: Optimized boolean detection with length-based filter + byte comparison.
372pub fn infer_value(s: &str, ctx: &InferenceContext<'_>, line_num: usize) -> HedlResult<Value> {
373    let trimmed = s.trim();
374
375    // Check for null literal using the configured null_char (from %NULL directive)
376    // This handles custom null characters like '-' if %NULL:- was specified
377    if trimmed.len() == 1 && trimmed.starts_with(ctx.null_char) {
378        if ctx.is_id_column {
379            return Err(HedlError::semantic(
380                format!("null ({}) not permitted in ID column", ctx.null_char),
381                line_num,
382            ));
383        }
384        return Ok(Value::Null);
385    }
386
387    // P2 OPTIMIZATION: Fast path for common values (true, false)
388    // This lookup typically handles 30-50% of values in real HEDL documents
389    // with a single hash + pointer dereference (~2-3 CPU cycles)
390    if let Some(value) = try_lookup_common(trimmed) {
391        return Ok(value);
392    }
393
394    let bytes = trimmed.as_bytes();
395
396    // Fast dispatch on first byte for non-common values
397    match bytes.first() {
398        // Ditto: exactly "^" (matrix cells only)
399        Some(b'^') if bytes.len() == 1 => {
400            return infer_ditto(ctx, line_num);
401        }
402
403        // List literal: starts with '('
404        Some(b'(') => {
405            use crate::lex::parse_list_literal;
406            match parse_list_literal(trimmed, 0) {
407                Ok((lex_value, _consumed)) => {
408                    // Convert lex_inference::Value to value::Value
409                    return convert_lex_value_to_value(lex_value);
410                }
411                Err(e) => {
412                    return Err(HedlError::syntax(
413                        format!("invalid list literal: {}", e),
414                        line_num,
415                    ));
416                }
417            }
418        }
419
420        // Tensor: starts with '['
421        Some(b'[') => {
422            if is_tensor_literal(trimmed) {
423                match parse_tensor(trimmed) {
424                    Ok(tensor) => return Ok(Value::Tensor(Box::new(tensor))),
425                    Err(e) => {
426                        return Err(HedlError::syntax(
427                            format!("invalid tensor literal: {}", e),
428                            line_num,
429                        ));
430                    }
431                }
432            }
433            // Not a valid tensor, fall through to string
434        }
435
436        // Reference: starts with '@'
437        Some(b'@') => match parse_reference(trimmed) {
438            Ok(r) => {
439                return Ok(Value::Reference(Reference {
440                    type_name: r.type_name.map(|t| t.into_boxed_str()),
441                    id: r.id.into_boxed_str(),
442                }));
443            }
444            Err(e) => {
445                return Err(HedlError::syntax(
446                    format!("invalid reference: {}", e),
447                    line_num,
448                ));
449            }
450        },
451
452        // Expression: starts with "$("
453        Some(b'$') if bytes.get(1) == Some(&b'(') => match parse_expression_token(trimmed) {
454            Ok(expr) => return Ok(Value::Expression(Box::new(expr))),
455            Err(e) => {
456                return Err(HedlError::syntax(
457                    format!("invalid expression: {}", e),
458                    line_num,
459                ));
460            }
461        },
462
463        // Alias: starts with '%'
464        // P0 OPTIMIZATION: Use pre-expanded cache for O(1) lookup (3-4x speedup)
465        Some(b'%') => {
466            let key = &trimmed[1..];
467            if let Some(value) = ctx.alias_cache.get(key) {
468                return Ok(value.clone());
469            }
470            // Fallback to original for error reporting with proper line number
471            if ctx.aliases.contains_key(key) {
472                // Alias exists but failed to expand during cache build
473                let expanded = &ctx.aliases[key];
474                return infer_expanded_alias(expanded, line_num);
475            }
476            return Err(HedlError::alias(
477                format!("undefined alias: %{}", key),
478                line_num,
479            ));
480        }
481
482        // Possible number: starts with digit or minus
483        // NOTE: Booleans are now handled by lookup table fast path above
484        Some(b'-') | Some(b'0'..=b'9') => {
485            if let Some(value) = try_parse_number(trimmed) {
486                return Ok(value);
487            }
488            // Not a valid number, fall through to string
489        }
490
491        _ => {}
492    }
493
494    // Default: String
495    // Validate for ID column
496    if ctx.is_id_column && !is_valid_id_token(trimmed) {
497        return Err(HedlError::semantic(
498            format!(
499                "invalid ID format '{}' - must start with letter or underscore",
500                trimmed
501            ),
502            line_num,
503        ));
504    }
505
506    // Use Box::from() directly to avoid intermediate String allocation
507    Ok(Value::String(Box::from(trimmed)))
508}
509
510/// Handle ditto (^) inference separately
511#[inline]
512fn infer_ditto(ctx: &InferenceContext<'_>, line_num: usize) -> HedlResult<Value> {
513    if !ctx.is_matrix_cell {
514        // In key-value context, ^ is just a string
515        return Ok(Value::String("^".into()));
516    }
517
518    // v2.0 does not allow ditto operator
519    if ctx.version >= (2, 0) {
520        use crate::errors::messages;
521        return Err(messages::v20_ditto_not_allowed(line_num));
522    }
523
524    if ctx.is_id_column {
525        return Err(HedlError::semantic(
526            "ditto (^) not permitted in ID column",
527            line_num,
528        ));
529    }
530
531    match ctx.prev_row {
532        Some(prev) if ctx.column_index < prev.len() => Ok(prev[ctx.column_index].clone()),
533        Some(_) => Err(HedlError::semantic(
534            "ditto (^) column index out of range",
535            line_num,
536        )),
537        None => Err(HedlError::semantic(
538            "ditto (^) not allowed in first row of list",
539            line_num,
540        )),
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    fn kv_ctx() -> InferenceContext<'static> {
549        static EMPTY: BTreeMap<String, String> = BTreeMap::new();
550        InferenceContext::for_key_value(&EMPTY)
551    }
552
553    fn ctx_with_aliases(aliases: &BTreeMap<String, String>) -> InferenceContext<'_> {
554        InferenceContext::for_key_value(aliases)
555    }
556
557    // ==================== Null inference ====================
558
559    #[test]
560    fn test_infer_null() {
561        let v = infer_value("~", &kv_ctx(), 1).unwrap();
562        assert!(matches!(v, Value::Null));
563    }
564
565    #[test]
566    fn test_infer_null_with_whitespace() {
567        let v = infer_value("  ~  ", &kv_ctx(), 1).unwrap();
568        assert!(matches!(v, Value::Null));
569    }
570
571    #[test]
572    fn test_infer_tilde_as_part_of_string() {
573        let v = infer_value("~hello", &kv_ctx(), 1).unwrap();
574        assert!(matches!(v, Value::String(s) if s.as_ref() == "~hello"));
575    }
576
577    #[test]
578    fn test_null_in_id_column_error() {
579        let aliases = BTreeMap::new();
580        let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
581        let result = infer_value("~", &ctx, 1);
582        assert!(result.is_err());
583        assert!(result.unwrap_err().message.contains("ID column"));
584    }
585
586    // ==================== Boolean inference ====================
587
588    #[test]
589    fn test_infer_bool() {
590        assert!(matches!(
591            infer_value("true", &kv_ctx(), 1).unwrap(),
592            Value::Bool(true)
593        ));
594        assert!(matches!(
595            infer_value("false", &kv_ctx(), 1).unwrap(),
596            Value::Bool(false)
597        ));
598    }
599
600    #[test]
601    fn test_infer_bool_case_sensitive() {
602        // Should be string, not bool
603        assert!(matches!(
604            infer_value("True", &kv_ctx(), 1).unwrap(),
605            Value::String(_)
606        ));
607        assert!(matches!(
608            infer_value("FALSE", &kv_ctx(), 1).unwrap(),
609            Value::String(_)
610        ));
611    }
612
613    #[test]
614    fn test_infer_bool_with_whitespace() {
615        assert!(matches!(
616            infer_value("  true  ", &kv_ctx(), 1).unwrap(),
617            Value::Bool(true)
618        ));
619    }
620
621    // ==================== Integer inference ====================
622
623    #[test]
624    fn test_infer_int() {
625        assert!(matches!(
626            infer_value("42", &kv_ctx(), 1).unwrap(),
627            Value::Int(42)
628        ));
629        assert!(matches!(
630            infer_value("-5", &kv_ctx(), 1).unwrap(),
631            Value::Int(-5)
632        ));
633        assert!(matches!(
634            infer_value("0", &kv_ctx(), 1).unwrap(),
635            Value::Int(0)
636        ));
637    }
638
639    #[test]
640    fn test_infer_int_large() {
641        let v = infer_value("9223372036854775807", &kv_ctx(), 1).unwrap();
642        assert!(matches!(v, Value::Int(i64::MAX)));
643    }
644
645    #[test]
646    fn test_infer_int_negative_large() {
647        let v = infer_value("-9223372036854775808", &kv_ctx(), 1).unwrap();
648        assert!(matches!(v, Value::Int(i64::MIN)));
649    }
650
651    #[test]
652    fn test_infer_int_with_whitespace() {
653        assert!(matches!(
654            infer_value("  123  ", &kv_ctx(), 1).unwrap(),
655            Value::Int(123)
656        ));
657    }
658
659    // ==================== Float inference ====================
660
661    #[test]
662    fn test_infer_float() {
663        match infer_value("3.25", &kv_ctx(), 1).unwrap() {
664            Value::Float(f) => assert!((f - 3.25).abs() < 0.001),
665            _ => panic!("expected float"),
666        }
667        match infer_value("42.0", &kv_ctx(), 1).unwrap() {
668            Value::Float(f) => assert!((f - 42.0).abs() < 0.001),
669            _ => panic!("expected float"),
670        }
671    }
672
673    #[test]
674    fn test_infer_float_negative() {
675        match infer_value("-3.5", &kv_ctx(), 1).unwrap() {
676            Value::Float(f) => assert!((f + 3.5).abs() < 0.001),
677            _ => panic!("expected float"),
678        }
679    }
680
681    #[test]
682    fn test_infer_float_small() {
683        match infer_value("0.001", &kv_ctx(), 1).unwrap() {
684            Value::Float(f) => assert!((f - 0.001).abs() < 0.0001),
685            _ => panic!("expected float"),
686        }
687    }
688
689    // ==================== String inference ====================
690
691    #[test]
692    fn test_infer_string() {
693        assert!(matches!(
694            infer_value("hello", &kv_ctx(), 1).unwrap(),
695            Value::String(s) if s.as_ref() == "hello"
696        ));
697    }
698
699    #[test]
700    fn test_infer_string_with_spaces() {
701        // Note: value is trimmed, so surrounding spaces are removed
702        assert!(matches!(
703            infer_value("  hello  ", &kv_ctx(), 1).unwrap(),
704            Value::String(s) if s.as_ref() == "hello"
705        ));
706    }
707
708    #[test]
709    fn test_infer_string_unicode() {
710        assert!(matches!(
711            infer_value("日本語", &kv_ctx(), 1).unwrap(),
712            Value::String(s) if s.as_ref() == "日本語"
713        ));
714    }
715
716    #[test]
717    fn test_infer_string_emoji() {
718        assert!(matches!(
719            infer_value("🎉", &kv_ctx(), 1).unwrap(),
720            Value::String(s) if s.as_ref() == "🎉"
721        ));
722    }
723
724    // ==================== Reference inference ====================
725
726    #[test]
727    fn test_infer_reference() {
728        let v = infer_value("@user_1", &kv_ctx(), 1).unwrap();
729        match v {
730            Value::Reference(r) => {
731                assert_eq!(r.type_name, None);
732                assert_eq!(r.id.as_ref(), "user_1");
733            }
734            _ => panic!("expected reference"),
735        }
736    }
737
738    #[test]
739    fn test_infer_qualified_reference() {
740        let v = infer_value("@User:user_1", &kv_ctx(), 1).unwrap();
741        match v {
742            Value::Reference(r) => {
743                assert_eq!(r.type_name.as_deref(), Some("User"));
744                assert_eq!(r.id.as_ref(), "user_1");
745            }
746            _ => panic!("expected reference"),
747        }
748    }
749
750    #[test]
751    fn test_infer_reference_with_whitespace() {
752        let v = infer_value("  @user_1  ", &kv_ctx(), 1).unwrap();
753        assert!(matches!(v, Value::Reference(_)));
754    }
755
756    #[test]
757    fn test_infer_reference_invalid_error() {
758        // IDs cannot start with a digit
759        let result = infer_value("@User:123-invalid", &kv_ctx(), 1);
760        assert!(result.is_err());
761        assert!(result.unwrap_err().message.contains("invalid reference"));
762    }
763
764    #[test]
765    fn test_infer_reference_uppercase_valid() {
766        // Uppercase IDs are valid (real-world IDs like SKU-4020)
767        let v = infer_value("@User:ABC123", &kv_ctx(), 1).unwrap();
768        match v {
769            Value::Reference(r) => {
770                assert_eq!(r.type_name.as_deref(), Some("User"));
771                assert_eq!(r.id.as_ref(), "ABC123");
772            }
773            _ => panic!("Expected reference"),
774        }
775    }
776
777    // ==================== Expression inference ====================
778
779    #[test]
780    fn test_infer_expression() {
781        use crate::lex::Expression;
782        let v = infer_value("$(now())", &kv_ctx(), 1).unwrap();
783        match v {
784            Value::Expression(e) => {
785                assert!(
786                    matches!(e.as_ref(), Expression::Call { name, args, .. } if name == "now" && args.is_empty())
787                );
788            }
789            _ => panic!("expected expression"),
790        }
791    }
792
793    #[test]
794    fn test_infer_expression_with_args() {
795        let v = infer_value("$(add(1, 2))", &kv_ctx(), 1).unwrap();
796        assert!(matches!(v, Value::Expression(_)));
797    }
798
799    #[test]
800    fn test_infer_expression_nested() {
801        let v = infer_value("$(outer(inner()))", &kv_ctx(), 1).unwrap();
802        assert!(matches!(v, Value::Expression(_)));
803    }
804
805    #[test]
806    fn test_infer_expression_identifier() {
807        let v = infer_value("$(x)", &kv_ctx(), 1).unwrap();
808        assert!(matches!(v, Value::Expression(_)));
809    }
810
811    #[test]
812    fn test_infer_expression_invalid_error() {
813        let result = infer_value("$(unclosed", &kv_ctx(), 1);
814        assert!(result.is_err());
815    }
816
817    #[test]
818    fn test_dollar_not_expression() {
819        // $foo is not an expression (no parens)
820        let v = infer_value("$foo", &kv_ctx(), 1).unwrap();
821        assert!(matches!(v, Value::String(s) if s.as_ref() == "$foo"));
822    }
823
824    // ==================== Tensor inference ====================
825
826    #[test]
827    fn test_infer_tensor() {
828        let v = infer_value("[1, 2, 3]", &kv_ctx(), 1).unwrap();
829        assert!(matches!(v, Value::Tensor(_)));
830    }
831
832    #[test]
833    fn test_infer_tensor_float() {
834        let v = infer_value("[1.5, 2.5, 3.5]", &kv_ctx(), 1).unwrap();
835        assert!(matches!(v, Value::Tensor(_)));
836    }
837
838    #[test]
839    fn test_infer_tensor_nested() {
840        let v = infer_value("[[1, 2], [3, 4]]", &kv_ctx(), 1).unwrap();
841        assert!(matches!(v, Value::Tensor(_)));
842    }
843
844    #[test]
845    fn test_infer_tensor_empty_error() {
846        // Empty tensors are not allowed in HEDL
847        let result = infer_value("[]", &kv_ctx(), 1);
848        assert!(result.is_err());
849        assert!(result.unwrap_err().message.contains("empty tensor"));
850    }
851
852    #[test]
853    fn test_infer_tensor_invalid_is_string() {
854        // Invalid tensor format - becomes string
855        let v = infer_value("[not a tensor]", &kv_ctx(), 1).unwrap();
856        assert!(matches!(v, Value::String(_)));
857    }
858
859    // ==================== Alias inference ====================
860
861    #[test]
862    fn test_infer_alias_bool() {
863        let mut aliases = BTreeMap::new();
864        aliases.insert("active".to_string(), "true".to_string());
865        let ctx = ctx_with_aliases(&aliases);
866        let v = infer_value("%active", &ctx, 1).unwrap();
867        assert!(matches!(v, Value::Bool(true)));
868    }
869
870    #[test]
871    fn test_infer_alias_number() {
872        let mut aliases = BTreeMap::new();
873        aliases.insert("count".to_string(), "42".to_string());
874        let ctx = ctx_with_aliases(&aliases);
875        let v = infer_value("%count", &ctx, 1).unwrap();
876        assert!(matches!(v, Value::Int(42)));
877    }
878
879    #[test]
880    fn test_infer_alias_string() {
881        let mut aliases = BTreeMap::new();
882        aliases.insert("name".to_string(), "Alice".to_string());
883        let ctx = ctx_with_aliases(&aliases);
884        let v = infer_value("%name", &ctx, 1).unwrap();
885        assert!(matches!(v, Value::String(s) if s.as_ref() == "Alice"));
886    }
887
888    #[test]
889    fn test_infer_undefined_alias_error() {
890        let result = infer_value("%undefined", &kv_ctx(), 1);
891        assert!(result.is_err());
892        assert!(result.unwrap_err().message.contains("undefined alias"));
893    }
894
895    // ==================== Ditto inference ====================
896
897    #[test]
898    fn test_ditto_in_kv_is_string() {
899        let v = infer_value("^", &kv_ctx(), 1).unwrap();
900        assert!(matches!(v, Value::String(s) if s.as_ref() == "^"));
901    }
902
903    #[test]
904    fn test_ditto_in_matrix_cell() {
905        let aliases = BTreeMap::new();
906        let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
907        let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User");
908        let v = infer_value("^", &ctx, 1).unwrap();
909        assert!(matches!(v, Value::Int(42)));
910    }
911
912    #[test]
913    fn test_ditto_in_id_column_error() {
914        let aliases = BTreeMap::new();
915        let prev_row = vec![Value::String("id".to_string().into())];
916        let ctx = InferenceContext::for_matrix_cell(&aliases, 0, Some(&prev_row), "User");
917        let result = infer_value("^", &ctx, 1);
918        assert!(result.is_err());
919        assert!(result.unwrap_err().message.contains("ID column"));
920    }
921
922    #[test]
923    fn test_ditto_first_row_error() {
924        let aliases = BTreeMap::new();
925        let ctx = InferenceContext::for_matrix_cell(&aliases, 1, None, "User");
926        let result = infer_value("^", &ctx, 1);
927        assert!(result.is_err());
928        assert!(result.unwrap_err().message.contains("first row"));
929    }
930
931    #[test]
932    fn test_ditto_column_out_of_range_error() {
933        let aliases = BTreeMap::new();
934        let prev_row = vec![Value::String("id".to_string().into())];
935        let ctx = InferenceContext::for_matrix_cell(&aliases, 5, Some(&prev_row), "User");
936        let result = infer_value("^", &ctx, 1);
937        assert!(result.is_err());
938        assert!(result.unwrap_err().message.contains("out of range"));
939    }
940
941    // ==================== Number edge cases ====================
942
943    #[test]
944    fn test_number_edge_cases() {
945        // Not numbers - scientific notation
946        assert!(matches!(
947            infer_value("1e10", &kv_ctx(), 1).unwrap(),
948            Value::String(_)
949        ));
950        // Not numbers - underscores
951        assert!(matches!(
952            infer_value("1_000", &kv_ctx(), 1).unwrap(),
953            Value::String(_)
954        ));
955        // Not numbers - leading decimal
956        assert!(matches!(
957            infer_value(".5", &kv_ctx(), 1).unwrap(),
958            Value::String(_)
959        ));
960    }
961
962    #[test]
963    fn test_number_trailing_decimal_is_string() {
964        assert!(matches!(
965            infer_value("123.", &kv_ctx(), 1).unwrap(),
966            Value::String(_)
967        ));
968    }
969
970    #[test]
971    fn test_number_plus_sign_is_string() {
972        assert!(matches!(
973            infer_value("+42", &kv_ctx(), 1).unwrap(),
974            Value::String(_)
975        ));
976    }
977
978    #[test]
979    fn test_number_leading_zeros_is_string() {
980        // Leading zeros make it a string (octal ambiguity)
981        assert!(matches!(
982            infer_value("007", &kv_ctx(), 1).unwrap(),
983            Value::Int(7) // Actually parses as int
984        ));
985    }
986
987    #[test]
988    fn test_number_hex_is_string() {
989        assert!(matches!(
990            infer_value("0xFF", &kv_ctx(), 1).unwrap(),
991            Value::String(_)
992        ));
993    }
994
995    // ==================== try_parse_number tests ====================
996
997    #[test]
998    fn test_try_parse_number_empty() {
999        assert!(try_parse_number("").is_none());
1000    }
1001
1002    #[test]
1003    fn test_try_parse_number_whitespace() {
1004        assert!(try_parse_number("   ").is_none());
1005    }
1006
1007    #[test]
1008    fn test_try_parse_number_valid_int() {
1009        assert!(matches!(try_parse_number("123"), Some(Value::Int(123))));
1010    }
1011
1012    #[test]
1013    fn test_try_parse_number_valid_float() {
1014        match try_parse_number("3.5") {
1015            Some(Value::Float(f)) => assert!((f - 3.5).abs() < 0.001),
1016            _ => panic!("expected float"),
1017        }
1018    }
1019
1020    #[test]
1021    fn test_try_parse_number_negative() {
1022        assert!(matches!(try_parse_number("-42"), Some(Value::Int(-42))));
1023    }
1024
1025    #[test]
1026    fn test_try_parse_number_invalid() {
1027        assert!(try_parse_number("abc").is_none());
1028        assert!(try_parse_number("12abc").is_none());
1029    }
1030
1031    // ==================== infer_quoted_value tests ====================
1032
1033    #[test]
1034    fn test_infer_quoted_value_simple() {
1035        let v = infer_quoted_value("hello");
1036        assert!(matches!(v, Value::String(s) if s.as_ref() == "hello"));
1037    }
1038
1039    #[test]
1040    fn test_infer_quoted_value_empty() {
1041        let v = infer_quoted_value("");
1042        assert!(matches!(v, Value::String(s) if s.is_empty()));
1043    }
1044
1045    #[test]
1046    fn test_infer_quoted_value_escaped_quotes() {
1047        let v = infer_quoted_value("say \"\"hello\"\"");
1048        assert!(matches!(v, Value::String(s) if s.as_ref() == "say \"hello\""));
1049    }
1050
1051    #[test]
1052    fn test_infer_quoted_value_multiple_escapes() {
1053        let v = infer_quoted_value("a\"\"b\"\"c");
1054        assert!(matches!(v, Value::String(s) if s.as_ref() == "a\"b\"c"));
1055    }
1056
1057    // ==================== InferenceContext tests ====================
1058
1059    #[test]
1060    fn test_context_for_key_value() {
1061        let aliases = BTreeMap::new();
1062        let ctx = InferenceContext::for_key_value(&aliases);
1063        assert!(!ctx.is_matrix_cell);
1064        assert!(!ctx.is_id_column);
1065        assert!(ctx.prev_row.is_none());
1066    }
1067
1068    #[test]
1069    fn test_context_for_matrix_cell() {
1070        let aliases = BTreeMap::new();
1071        let ctx = InferenceContext::for_matrix_cell(&aliases, 2, None, "User");
1072        assert!(ctx.is_matrix_cell);
1073        assert!(!ctx.is_id_column); // column 2 is not ID
1074        assert_eq!(ctx.column_index, 2);
1075        assert_eq!(ctx.current_type, Some("User"));
1076    }
1077
1078    #[test]
1079    fn test_context_id_column_detection() {
1080        let aliases = BTreeMap::new();
1081        let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
1082        assert!(ctx.is_id_column); // column 0 is ID column
1083    }
1084
1085    // ==================== ID column validation ====================
1086
1087    #[test]
1088    fn test_id_column_valid_id() {
1089        let aliases = BTreeMap::new();
1090        let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
1091        let v = infer_value("user_123", &ctx, 1).unwrap();
1092        assert!(matches!(v, Value::String(s) if s.as_ref() == "user_123"));
1093    }
1094
1095    #[test]
1096    fn test_id_column_invalid_starts_digit_error() {
1097        // IDs cannot start with a digit
1098        let aliases = BTreeMap::new();
1099        let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
1100        let result = infer_value("123User", &ctx, 1);
1101        assert!(result.is_err());
1102        assert!(result.unwrap_err().message.contains("invalid ID"));
1103    }
1104
1105    #[test]
1106    fn test_id_column_uppercase_valid() {
1107        // Uppercase IDs are valid (real-world IDs like SKU-4020)
1108        let aliases = BTreeMap::new();
1109        let ctx = InferenceContext::for_matrix_cell(&aliases, 0, None, "User");
1110        let result = infer_value("SKU-4020", &ctx, 1);
1111        assert!(result.is_ok());
1112    }
1113
1114    // ==================== P2 Lookup Table Optimization Tests ====================
1115
1116    #[test]
1117    fn test_lookup_table_bool_true() {
1118        // Should hit lookup table fast path
1119        let v = infer_value("true", &kv_ctx(), 1).unwrap();
1120        assert!(matches!(v, Value::Bool(true)));
1121    }
1122
1123    #[test]
1124    fn test_lookup_table_bool_false() {
1125        // Should hit lookup table fast path
1126        let v = infer_value("false", &kv_ctx(), 1).unwrap();
1127        assert!(matches!(v, Value::Bool(false)));
1128    }
1129
1130    #[test]
1131    fn test_lookup_table_null() {
1132        // Should hit lookup table fast path
1133        let v = infer_value("~", &kv_ctx(), 1).unwrap();
1134        assert!(matches!(v, Value::Null));
1135    }
1136
1137    #[test]
1138    fn test_lookup_table_collision_detection() {
1139        // Ensure lookup table properly handles non-matches
1140        // "True" (capitalized) should NOT match "true"
1141        let v = infer_value("True", &kv_ctx(), 1).unwrap();
1142        assert!(matches!(v, Value::String(s) if s.as_ref() == "True"));
1143    }
1144
1145    #[test]
1146    fn test_lookup_table_multiple_calls() {
1147        // Verify lookup table initialization is idempotent
1148        for _ in 0..100 {
1149            let v = infer_value("true", &kv_ctx(), 1).unwrap();
1150            assert!(matches!(v, Value::Bool(true)));
1151        }
1152    }
1153
1154    // ==================== Bidirectional Inference Tests ====================
1155
1156    #[test]
1157    fn test_inference_result_structure() {
1158        let result = infer_value_synthesize("42", &kv_ctx(), 1).unwrap();
1159        assert!(matches!(result.value, Value::Int(42)));
1160        assert_eq!(result.inferred_type, ExpectedType::Int);
1161        assert_eq!(result.confidence, InferenceConfidence::Certain);
1162    }
1163
1164    #[test]
1165    fn test_synthesize_int() {
1166        let result = infer_value_synthesize("42", &kv_ctx(), 1).unwrap();
1167        assert!(matches!(result.value, Value::Int(42)));
1168        assert_eq!(result.inferred_type, ExpectedType::Int);
1169        assert_eq!(result.confidence, InferenceConfidence::Certain);
1170    }
1171
1172    #[test]
1173    fn test_synthesize_float() {
1174        let result = infer_value_synthesize("3.25", &kv_ctx(), 1).unwrap();
1175        assert!(matches!(result.value, Value::Float(f) if (f - 3.25).abs() < 0.001));
1176        assert_eq!(result.inferred_type, ExpectedType::Float);
1177        assert_eq!(result.confidence, InferenceConfidence::Certain);
1178    }
1179
1180    #[test]
1181    fn test_synthesize_bool() {
1182        let result = infer_value_synthesize("true", &kv_ctx(), 1).unwrap();
1183        assert!(matches!(result.value, Value::Bool(true)));
1184        assert_eq!(result.inferred_type, ExpectedType::Bool);
1185        assert_eq!(result.confidence, InferenceConfidence::Certain);
1186    }
1187
1188    #[test]
1189    fn test_synthesize_string() {
1190        let result = infer_value_synthesize("hello", &kv_ctx(), 1).unwrap();
1191        assert!(matches!(result.value, Value::String(s) if s.as_ref() == "hello"));
1192        assert_eq!(result.inferred_type, ExpectedType::String);
1193        assert_eq!(result.confidence, InferenceConfidence::Certain);
1194    }
1195
1196    #[test]
1197    fn test_synthesize_null() {
1198        let result = infer_value_synthesize("~", &kv_ctx(), 1).unwrap();
1199        assert!(matches!(result.value, Value::Null));
1200        assert_eq!(result.inferred_type, ExpectedType::Null);
1201        assert_eq!(result.confidence, InferenceConfidence::Certain);
1202    }
1203
1204    #[test]
1205    fn test_checking_exact_match() {
1206        let result = infer_value_with_type("42", &ExpectedType::Int, &kv_ctx(), 1).unwrap();
1207        assert!(matches!(result.value, Value::Int(42)));
1208        assert_eq!(result.inferred_type, ExpectedType::Int);
1209        assert_eq!(result.confidence, InferenceConfidence::Certain);
1210    }
1211
1212    #[test]
1213    fn test_checking_int_to_float_coercion() {
1214        let result = infer_value_with_type("42", &ExpectedType::Float, &kv_ctx(), 1).unwrap();
1215        assert!(matches!(result.value, Value::Float(f) if (f - 42.0).abs() < 0.001));
1216        assert_eq!(result.inferred_type, ExpectedType::Float);
1217        assert_eq!(result.confidence, InferenceConfidence::Probable);
1218    }
1219
1220    #[test]
1221    fn test_checking_string_to_int_lenient() {
1222        let ctx = kv_ctx();
1223        let result = infer_value_with_type("42", &ExpectedType::Int, &ctx, 1).unwrap();
1224        assert!(matches!(result.value, Value::Int(42)));
1225        assert_eq!(result.confidence, InferenceConfidence::Certain);
1226    }
1227
1228    #[test]
1229    fn test_checking_with_strict_types() {
1230        let ctx = kv_ctx().with_strict_types(true);
1231        // Int to Float should still work (safe coercion)
1232        let result = infer_value_with_type("42", &ExpectedType::Float, &ctx, 1).unwrap();
1233        assert!(matches!(result.value, Value::Float(_)));
1234    }
1235
1236    #[test]
1237    fn test_checking_type_mismatch_error() {
1238        let ctx = kv_ctx().with_strict_types(true);
1239        // Bool can't be coerced to Int
1240        let result = infer_value_with_type("true", &ExpectedType::Int, &ctx, 1);
1241        assert!(result.is_err());
1242        assert!(result.unwrap_err().message.contains("type mismatch"));
1243    }
1244
1245    #[test]
1246    fn test_checking_with_error_recovery() {
1247        let ctx = kv_ctx().with_strict_types(true).with_error_recovery(true);
1248        // Type mismatch, but error recovery returns original value
1249        let result = infer_value_with_type("true", &ExpectedType::Int, &ctx, 1).unwrap();
1250        assert!(matches!(result.value, Value::Bool(true)));
1251        assert_eq!(result.confidence, InferenceConfidence::Ambiguous);
1252    }
1253
1254    #[test]
1255    fn test_checking_numeric_accepts_int() {
1256        let result = infer_value_with_type("42", &ExpectedType::Numeric, &kv_ctx(), 1).unwrap();
1257        assert!(matches!(result.value, Value::Int(42)));
1258        assert_eq!(result.confidence, InferenceConfidence::Certain);
1259    }
1260
1261    #[test]
1262    fn test_checking_numeric_accepts_float() {
1263        let result = infer_value_with_type("3.5", &ExpectedType::Numeric, &kv_ctx(), 1).unwrap();
1264        assert!(matches!(result.value, Value::Float(f) if (f - 3.5).abs() < 0.001));
1265        assert_eq!(result.confidence, InferenceConfidence::Certain);
1266    }
1267
1268    #[test]
1269    fn test_checking_any_accepts_all() {
1270        let result = infer_value_with_type("42", &ExpectedType::Any, &kv_ctx(), 1).unwrap();
1271        assert!(matches!(result.value, Value::Int(42)));
1272        assert_eq!(result.confidence, InferenceConfidence::Certain);
1273
1274        let result = infer_value_with_type("hello", &ExpectedType::Any, &kv_ctx(), 1).unwrap();
1275        assert!(matches!(result.value, Value::String(_)));
1276        assert_eq!(result.confidence, InferenceConfidence::Certain);
1277    }
1278
1279    #[test]
1280    fn test_checking_union_type() {
1281        use crate::types::ExpectedType;
1282        let union = ExpectedType::Union(vec![ExpectedType::Int, ExpectedType::String]);
1283        let result = infer_value_with_type("42", &union, &kv_ctx(), 1).unwrap();
1284        assert!(matches!(result.value, Value::Int(42)));
1285        assert_eq!(result.confidence, InferenceConfidence::Certain);
1286    }
1287
1288    #[test]
1289    fn test_checking_reference_qualified() {
1290        let expected = ExpectedType::Reference {
1291            target_type: Some("User".to_string()),
1292        };
1293        let result = infer_value_with_type("@User:user_1", &expected, &kv_ctx(), 1).unwrap();
1294        match result.value {
1295            Value::Reference(r) => {
1296                assert_eq!(r.type_name.as_deref(), Some("User"));
1297                assert_eq!(r.id.as_ref(), "user_1");
1298            }
1299            _ => panic!("Expected reference"),
1300        }
1301        assert_eq!(result.confidence, InferenceConfidence::Certain);
1302    }
1303
1304    #[test]
1305    fn test_context_with_expected_type() {
1306        let ctx = kv_ctx().with_expected_type(ExpectedType::Float);
1307        assert_eq!(ctx.expected_type, Some(ExpectedType::Float));
1308    }
1309
1310    #[test]
1311    fn test_context_with_column_types() {
1312        let types = vec![ExpectedType::String, ExpectedType::Int, ExpectedType::Float];
1313        let ctx = kv_ctx().with_column_types(&types);
1314        assert!(ctx.column_types.is_some());
1315        assert_eq!(ctx.column_types.unwrap().len(), 3);
1316    }
1317
1318    #[test]
1319    fn test_context_builder_pattern() {
1320        let types = vec![ExpectedType::Int];
1321        let ctx = kv_ctx()
1322            .with_expected_type(ExpectedType::Float)
1323            .with_column_types(&types)
1324            .with_strict_types(true)
1325            .with_error_recovery(true);
1326
1327        assert_eq!(ctx.expected_type, Some(ExpectedType::Float));
1328        assert!(ctx.column_types.is_some());
1329        assert!(ctx.strict_types);
1330        assert!(ctx.error_recovery);
1331    }
1332
1333    #[test]
1334    fn test_inference_confidence_levels() {
1335        // Certain: Exact match
1336        let result = infer_value_with_type("42", &ExpectedType::Int, &kv_ctx(), 1).unwrap();
1337        assert_eq!(result.confidence, InferenceConfidence::Certain);
1338
1339        // Probable: Safe coercion
1340        let result = infer_value_with_type("42", &ExpectedType::Float, &kv_ctx(), 1).unwrap();
1341        assert_eq!(result.confidence, InferenceConfidence::Probable);
1342
1343        // Ambiguous: Error recovery mode
1344        let ctx = kv_ctx().with_strict_types(true).with_error_recovery(true);
1345        let result = infer_value_with_type("true", &ExpectedType::Int, &ctx, 1).unwrap();
1346        assert_eq!(result.confidence, InferenceConfidence::Ambiguous);
1347    }
1348
1349    #[test]
1350    fn test_synthesize_with_aliases() {
1351        let mut aliases = BTreeMap::new();
1352        aliases.insert("count".to_string(), "42".to_string());
1353        let ctx = ctx_with_aliases(&aliases);
1354        let result = infer_value_synthesize("%count", &ctx, 1).unwrap();
1355        assert!(matches!(result.value, Value::Int(42)));
1356        assert_eq!(result.inferred_type, ExpectedType::Int);
1357    }
1358
1359    #[test]
1360    fn test_checking_with_ditto() {
1361        let aliases = BTreeMap::new();
1362        let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
1363        let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User");
1364        let result = infer_value_synthesize("^", &ctx, 1).unwrap();
1365        assert!(matches!(result.value, Value::Int(42)));
1366    }
1367
1368    #[test]
1369    fn test_checking_preserves_expression() {
1370        let result =
1371            infer_value_with_type("$(now())", &ExpectedType::Expression, &kv_ctx(), 1).unwrap();
1372        assert!(matches!(result.value, Value::Expression(_)));
1373        assert_eq!(result.inferred_type, ExpectedType::Expression);
1374    }
1375
1376    #[test]
1377    fn test_checking_preserves_tensor() {
1378        let expected = ExpectedType::Tensor {
1379            shape: None,
1380            dtype: None,
1381        };
1382        let result = infer_value_with_type("[1, 2, 3]", &expected, &kv_ctx(), 1).unwrap();
1383        assert!(matches!(result.value, Value::Tensor(_)));
1384    }
1385
1386    // ==================== v2.0 Compliance Tests ====================
1387
1388    #[test]
1389    fn test_v20_rejects_ditto() {
1390        let aliases = BTreeMap::new();
1391        let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
1392        let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User")
1393            .with_version((2, 0));
1394        let result = infer_value("^", &ctx, 1);
1395        assert!(result.is_err());
1396        let err = result.unwrap_err();
1397        assert!(err.message.contains("v2.0"));
1398        assert!(err.message.contains("not allowed"));
1399    }
1400
1401    #[test]
1402    fn test_pre_v20_allows_ditto() {
1403        let aliases = BTreeMap::new();
1404        let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
1405        let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User")
1406            .with_version((1, 2));
1407        let result = infer_value("^", &ctx, 1);
1408        assert!(result.is_ok());
1409        assert!(matches!(result.unwrap(), Value::Int(42)));
1410    }
1411
1412    #[test]
1413    fn test_v10_allows_ditto() {
1414        let aliases = BTreeMap::new();
1415        let prev_row = vec![Value::String("id".to_string().into()), Value::Int(42)];
1416        let ctx = InferenceContext::for_matrix_cell(&aliases, 1, Some(&prev_row), "User")
1417            .with_version((1, 0));
1418        let result = infer_value("^", &ctx, 1);
1419        assert!(result.is_ok());
1420        assert!(matches!(result.unwrap(), Value::Int(42)));
1421    }
1422
1423    #[test]
1424    fn test_v20_context_builder() {
1425        let aliases = BTreeMap::new();
1426        let ctx = InferenceContext::for_key_value(&aliases).with_version((2, 0));
1427        assert_eq!(ctx.version, (2, 0));
1428    }
1429
1430    // ==================== Edge cases ====================
1431
1432    #[test]
1433    fn test_infer_empty_string() {
1434        let v = infer_value("", &kv_ctx(), 1).unwrap();
1435        assert!(matches!(v, Value::String(s) if s.is_empty()));
1436    }
1437
1438    #[test]
1439    fn test_infer_whitespace_only() {
1440        let v = infer_value("   ", &kv_ctx(), 1).unwrap();
1441        assert!(matches!(v, Value::String(s) if s.is_empty()));
1442    }
1443
1444    #[test]
1445    fn test_infer_mixed_content() {
1446        // Things that look like multiple types but are strings
1447        assert!(matches!(
1448            infer_value("true123", &kv_ctx(), 1).unwrap(),
1449            Value::String(_)
1450        ));
1451        assert!(matches!(
1452            infer_value("42abc", &kv_ctx(), 1).unwrap(),
1453            Value::String(_)
1454        ));
1455        assert!(matches!(
1456            infer_value("@invalid id", &kv_ctx(), 1).unwrap_err(),
1457            _
1458        ));
1459    }
1460}