harn-parser 0.8.6

Parser, AST, and type checker for the Harn programming language
Documentation
//! Type rules for binary operators.
//!
//! Pure helper that maps `(operator, left_type, right_type)` to the inferred
//! result type without touching scope or emitting diagnostics. The
//! diagnostic-emitting binary-op checker lives on `TypeChecker` itself
//! (`check_binops`) — see `inference::binary_ops`.

use crate::ast::*;

use super::scope::InferredType;

fn dict_like(ty: &TypeExpr) -> bool {
    matches!(ty, TypeExpr::Named(n) if n == "dict")
        || matches!(ty, TypeExpr::DictType(..))
        || matches!(ty, TypeExpr::Shape(_))
}

/// Infer the result type of a binary operation.
pub(super) fn infer_binary_op_type(
    op: &str,
    left: &InferredType,
    right: &InferredType,
) -> InferredType {
    match op {
        "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
            Some(TypeExpr::Named("bool".into()))
        }
        "+" => match (left, right) {
            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
                match (l.as_str(), r.as_str()) {
                    ("int", "int") => Some(TypeExpr::Named("int".into())),
                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
                    ("string", "string") => Some(TypeExpr::Named("string".into())),
                    ("list", "list") => Some(TypeExpr::Named("list".into())),
                    ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
                    _ => None,
                }
            }
            // dict + dict shallow-merges. Recognize parameterized and shape
            // forms so a `dict<string, V> + Shape{...}` chain (the common
            // connector/agent option-projection idiom) keeps the dict-shaped
            // typing instead of falling through to `None` and downgrading
            // every subsequent inference step to untyped.
            (Some(l), Some(r)) if dict_like(l) && dict_like(r) => {
                Some(TypeExpr::Named("dict".into()))
            }
            _ => None,
        },
        "-" | "/" | "%" => match (left, right) {
            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
                match (l.as_str(), r.as_str()) {
                    ("int", "int") => Some(TypeExpr::Named("int".into())),
                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
                    _ => None,
                }
            }
            _ => None,
        },
        "**" => match (left, right) {
            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
                match (l.as_str(), r.as_str()) {
                    ("int", "int") => Some(TypeExpr::Named("int".into())),
                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
                    _ => None,
                }
            }
            _ => None,
        },
        "*" => match (left, right) {
            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
                match (l.as_str(), r.as_str()) {
                    ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
                    ("int", "int") => Some(TypeExpr::Named("int".into())),
                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
                    _ => None,
                }
            }
            _ => None,
        },
        "??" => match (left, right) {
            // Union containing nil: strip nil, use non-nil members
            (Some(TypeExpr::Union(members)), _) => {
                let non_nil: Vec<_> = members
                    .iter()
                    .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
                    .cloned()
                    .collect();
                if non_nil.len() == 1 {
                    Some(non_nil[0].clone())
                } else if non_nil.is_empty() {
                    right.clone()
                } else {
                    Some(TypeExpr::Union(non_nil))
                }
            }
            // Left is nil: result is always the right side
            (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
            // Left is a known non-nil type: right is unreachable, preserve left
            (Some(l), _) => Some(l.clone()),
            // Unknown left: use right as best guess
            (None, _) => right.clone(),
        },
        "|>" => None,
        _ => None,
    }
}