Lines
71.88 %
Functions
33.33 %
Branches
100 %
//! Static inference of the `PairElement` for a `(cons car cdr)`
//! form. Single source of truth so CONS picks the most-specific
//! homogeneous element when both arms agree, and widens to
//! `PairElement::AnyRef` when they disagree (ADR-0025 escape hatch).
use crate::ast::{Expr, PairElement, WasmType};
use crate::compiler::expr::format_expr;
use crate::error::{Error, Result};
/// Infers the `PairElement` for a runtime CONS argument list. Lookup
/// order: car's WasmType → cdr's PairRef element → car's literal shape
/// (Number → Ratio, Bool → I32). When the car and cdr disagree on
/// element type the result widens to `PairElement::AnyRef` instead of
/// erroring, preserving heterogeneous CL `cons` semantics. Integer
/// `Number` literals default to Ratio; consumers that need I32 bridge
/// via an existing typed local or host-fn return value.
pub(super) fn infer_pair_element(car: &Expr, cdr: &Expr) -> Result<PairElement> {
let car_ty = car.wasm_type();
let cdr_ty = cdr.wasm_type();
let cdr_elem = match cdr_ty {
Some(WasmType::PairRef(elem)) => Some(elem),
_ => None,
};
let car_elem = match car_ty {
Some(ty) => PairElement::from_wasm_type(ty).ok_or_else(|| {
Error::Compile(format!(
"CONS: car type {ty} cannot ride a $pair cell (nested pairs land in a follow-up slice)"
))
})?,
None => match literal_pair_element(car) {
Some(elem) => elem,
None => match cdr_elem {
None => {
return Err(Error::Compile(format!(
"CONS: cannot infer element type from constant car {} — \
supply a typed cdr (a pair) or a typed runtime car",
format_expr(car)
)));
}
},
Ok(match cdr_elem {
Some(cdr_elem) => car_elem.widen(cdr_elem),
None => car_elem,
})
/// Maps a literal car expression to its natural `PairElement`, before
/// falling back to the cdr's element. Without this precedence, a
/// literal `String` car against a typed `pair<ratio>` cdr was silently
/// classified as Ratio and then refused at codegen ("expected ratio,
/// got string"). The widening contract requires it to widen to AnyRef.
///
/// A numeric literal — integral or fractional — is a dimensionless `Ratio`,
/// NEVER a count: arithmetic accepts `Ratio` and refuses `I32` ("indices"),
/// so reclassifying an integer literal as `I32` here would be an illegal
/// implicit Ratio→count conversion (a `5` stored in a list would come back as
/// a count and arithmetic would reject it). Genuine runtime counts reach the
/// `I32` slot via their `WasmType` in `infer_pair_element`, not this literal
/// path. `Bool` keeps its own truth-value slot.
pub(super) fn literal_pair_element(car: &Expr) -> Option<PairElement> {
match car {
Expr::Number(_) => Some(PairElement::Ratio),
Expr::Bool(_) => Some(PairElement::Bool),
Expr::String(_) => Some(PairElement::StringRef),