1
//! Static inference of the `PairElement` for a `(cons car cdr)`
2
//! form. Single source of truth so CONS picks the most-specific
3
//! homogeneous element when both arms agree, and widens to
4
//! `PairElement::AnyRef` when they disagree (ADR-0025 escape hatch).
5

            
6
use crate::ast::{Expr, PairElement, WasmType};
7
use crate::compiler::expr::format_expr;
8
use crate::error::{Error, Result};
9

            
10
/// Infers the `PairElement` for a runtime CONS argument list. Lookup
11
/// order: car's WasmType → cdr's PairRef element → car's literal shape
12
/// (Number → Ratio, Bool → I32). When the car and cdr disagree on
13
/// element type the result widens to `PairElement::AnyRef` instead of
14
/// erroring, preserving heterogeneous CL `cons` semantics. Integer
15
/// `Number` literals default to Ratio; consumers that need I32 bridge
16
/// via an existing typed local or host-fn return value.
17
37118
pub(super) fn infer_pair_element(car: &Expr, cdr: &Expr) -> Result<PairElement> {
18
37118
    let car_ty = car.wasm_type();
19
37118
    let cdr_ty = cdr.wasm_type();
20
37118
    let cdr_elem = match cdr_ty {
21
17529
        Some(WasmType::PairRef(elem)) => Some(elem),
22
19589
        _ => None,
23
    };
24
37118
    let car_elem = match car_ty {
25
35345
        Some(ty) => PairElement::from_wasm_type(ty).ok_or_else(|| {
26
            Error::Compile(format!(
27
                "CONS: car type {ty} cannot ride a $pair cell (nested pairs land in a follow-up slice)"
28
            ))
29
        })?,
30
1773
        None => match literal_pair_element(car) {
31
1569
            Some(elem) => elem,
32
204
            None => match cdr_elem {
33
204
                Some(elem) => elem,
34
                None => {
35
                    return Err(Error::Compile(format!(
36
                        "CONS: cannot infer element type from constant car {} — \
37
                         supply a typed cdr (a pair) or a typed runtime car",
38
                        format_expr(car)
39
                    )));
40
                }
41
            },
42
        },
43
    };
44
37118
    Ok(match cdr_elem {
45
17529
        Some(cdr_elem) => car_elem.widen(cdr_elem),
46
19589
        None => car_elem,
47
    })
48
37118
}
49

            
50
/// Maps a literal car expression to its natural `PairElement`, before
51
/// falling back to the cdr's element. Without this precedence, a
52
/// literal `String` car against a typed `pair<ratio>` cdr was silently
53
/// classified as Ratio and then refused at codegen ("expected ratio,
54
/// got string"). The widening contract requires it to widen to AnyRef.
55
///
56
/// A numeric literal — integral or fractional — is a dimensionless `Ratio`,
57
/// NEVER a count: arithmetic accepts `Ratio` and refuses `I32` ("indices"),
58
/// so reclassifying an integer literal as `I32` here would be an illegal
59
/// implicit Ratio→count conversion (a `5` stored in a list would come back as
60
/// a count and arithmetic would reject it). Genuine runtime counts reach the
61
/// `I32` slot via their `WasmType` in `infer_pair_element`, not this literal
62
/// path. `Bool` keeps its own truth-value slot.
63
3813
pub(super) fn literal_pair_element(car: &Expr) -> Option<PairElement> {
64
3813
    match car {
65
2994
        Expr::Number(_) => Some(PairElement::Ratio),
66
1
        Expr::Bool(_) => Some(PairElement::Bool),
67
206
        Expr::String(_) => Some(PairElement::StringRef),
68
612
        _ => None,
69
    }
70
3813
}