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
38705
pub(super) fn infer_pair_element(car: &Expr, cdr: &Expr) -> Result<PairElement> {
18
38705
    let car_ty = car.wasm_type();
19
38705
    let cdr_ty = cdr.wasm_type();
20
38705
    let cdr_elem = match cdr_ty {
21
18252
        Some(WasmType::PairRef(elem)) => Some(elem),
22
20453
        _ => None,
23
    };
24
38705
    let car_elem = match car_ty {
25
36854
        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
1851
        None => match literal_pair_element(car) {
31
1638
            Some(elem) => elem,
32
213
            None => match cdr_elem {
33
213
                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
38705
    Ok(match cdr_elem {
45
18252
        Some(cdr_elem) => car_elem.widen(cdr_elem),
46
20453
        None => car_elem,
47
    })
48
38705
}
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
3981
pub(super) fn literal_pair_element(car: &Expr) -> Option<PairElement> {
64
3981
    match car {
65
3126
        Expr::Number(_) => Some(PairElement::Ratio),
66
1
        Expr::Bool(_) => Some(PairElement::Bool),
67
215
        Expr::String(_) => Some(PairElement::StringRef),
68
639
        _ => None,
69
    }
70
3981
}