expr.rs raw

//! The binding expression language: s-expression data interpreted as a
//! small, pure expression grammar, plus string interpolation templates.
//!
//! Grammar (all forms are sexpr lists, so spans come from the reader):
//!   literals: `42`, `2.5`, `true`, `false`, `"text"`, `"{name} text"`
//!   paths:    `count`, `form.busy`
//!   unary:    `(not x)`, `(- x)`
//!   binary:   `(+ a b c…)` `- * / %` `< <= > >=` `= !=` `and or`
//!             (n-ary forms left-fold)
//!   choice:   `(if cond then else)`
//!
//! No loops, no calls: real logic lives in Rust behind named handlers.

use crate::diagnostics::Diagnostic;
use crate::ir::Literal;
use crate::sexpr::{Sexpr, Span};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BinOp {
    Add,
    Sub,
    Mul,
    Div,
    Rem,
    Lt,
    Le,
    Gt,
    Ge,
    Eq,
    Ne,
    And,
    Or,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum UnOp {
    Not,
    Neg,
}

#[derive(Clone, Debug, PartialEq)]
pub enum Expr {
    Int(i64, Span),
    Float(f64, Span),
    Bool(bool, Span),
    /// A string literal, possibly with `{name}` interpolations.
    Str(Template, Span),
    /// `count`, or a record field access like `todo.label` (one level:
    /// records are flat).
    Path(Vec<String>, Span),
    Unary(UnOp, Box<Expr>, Span),
    Binary(BinOp, Box<Expr>, Box<Expr>, Span),
    If(Box<Expr>, Box<Expr>, Box<Expr>, Span),
    /// `(list a b c)` — a list of element expressions.
    List(Vec<Expr>, Span),
    /// `(name arg…)` with a non-operator lowercase head: a handler
    /// invocation. Only valid as an event-wire value; validation rejects
    /// it anywhere else.
    Call(String, Vec<Expr>, Span),
    /// `(Todo :id 1 :label "hi")` — a record literal (capitalized head,
    /// keyword fields), mirroring instance syntax.
    RecordLit(String, Vec<(String, Expr)>, Span),
    /// A recognized data form — `(path …)`, `(svg-path "…")`, `(asset "…")` —
    /// already reduced to its literal by the reader.
    ///
    /// These forms are *data*, not computation: they are read straight from
    /// the s-expression by the same parser the theme language uses, which is
    /// why they arrive here already finished rather than as a `Call` to be
    /// interpreted later. One parser per form, two surface languages.
    Form(Literal, Span),
}

impl Expr {
    pub fn span(&self) -> Span {
        match self {
            Expr::Int(_, s)
            | Expr::Float(_, s)
            | Expr::Bool(_, s)
            | Expr::Str(_, s)
            | Expr::Path(_, s)
            | Expr::Unary(_, _, s)
            | Expr::Binary(_, _, _, s)
            | Expr::If(_, _, _, s)
            | Expr::List(_, s)
            | Expr::Call(_, _, s)
            | Expr::RecordLit(_, _, s)
            | Expr::Form(_, s) => *s,
        }
    }

    /// Whether the expression is a compile-time constant (no paths).
    pub fn is_literal(&self) -> bool {
        match self {
            Expr::Int(..) | Expr::Float(..) | Expr::Bool(..) => true,
            Expr::Str(template, _) => template.refs().next().is_none(),
            Expr::Path(..) => false,
            Expr::Unary(_, inner, _) => inner.is_literal(),
            Expr::Binary(_, lhs, rhs, _) => lhs.is_literal() && rhs.is_literal(),
            Expr::If(c, t, e, _) => c.is_literal() && t.is_literal() && e.is_literal(),
            Expr::List(items, _) => items.iter().all(Expr::is_literal),
            Expr::Call(..) => false,
            Expr::RecordLit(_, fields, _) => fields.iter().all(|(_, v)| v.is_literal()),
            // A data form is finished at read time; that is the whole point.
            Expr::Form(..) => true,
        }
    }

    /// All names referenced by the expression (paths and interpolations).
    pub fn referenced_names(&self, out: &mut Vec<(String, Span)>) {
        match self {
            Expr::Int(..) | Expr::Float(..) | Expr::Bool(..) => {}
            Expr::Str(template, _) => {
                for (name, span) in template.refs() {
                    out.push((name.to_owned(), span));
                }
            }
            Expr::Path(segments, span) => out.push((segments.join("."), *span)),
            Expr::Unary(_, inner, _) => inner.referenced_names(out),
            Expr::Binary(_, lhs, rhs, _) => {
                lhs.referenced_names(out);
                rhs.referenced_names(out);
            }
            Expr::If(c, t, e, _) => {
                c.referenced_names(out);
                t.referenced_names(out);
                e.referenced_names(out);
            }
            Expr::List(items, _) => {
                for item in items {
                    item.referenced_names(out);
                }
            }
            // A data form is closed: an asset path is fixed at build time and
            // a path's coordinates are numbers, so neither reads a name.
            Expr::Form(..) => {}
            // A call's head is a handler, not a value reference; only the
            // arguments read names.
            Expr::Call(_, args, _) => {
                for arg in args {
                    arg.referenced_names(out);
                }
            }
            Expr::RecordLit(_, fields, _) => {
                for (_, value) in fields {
                    value.referenced_names(out);
                }
            }
        }
    }
}

/// A string with `{name}` interpolations. `{{` escapes a literal brace.
#[derive(Clone, Debug, PartialEq)]
pub struct Template {
    pub segments: Vec<Segment>,
}

#[derive(Clone, Debug, PartialEq)]
pub enum Segment {
    Literal(String),
    /// An interpolated name, with its span in the source file.
    Ref(String, Span),
}

impl Template {
    pub fn refs(&self) -> impl Iterator<Item = (&str, Span)> {
        self.segments.iter().filter_map(|s| match s {
            Segment::Ref(name, span) => Some((name.as_str(), *span)),
            Segment::Literal(_) => None,
        })
    }
}

/// Interpret one sexpr value as an expression.
pub fn from_sexpr(value: &Sexpr, source: &str) -> Result<Expr, Diagnostic> {
    match value {
        Sexpr::Int(v, span) => Ok(Expr::Int(*v, *span)),
        Sexpr::Float(v, span) => Ok(Expr::Float(*v, *span)),
        Sexpr::Str(_, span) => Ok(Expr::Str(parse_template(*span, source)?, *span)),
        Sexpr::Symbol(name, span) => match name.as_str() {
            "true" => Ok(Expr::Bool(true, *span)),
            "false" => Ok(Expr::Bool(false, *span)),
            _ => Ok(Expr::Path(
                name.split('.').map(str::to_owned).collect(),
                *span,
            )),
        },
        Sexpr::Keyword(name, span) => Err(Diagnostic::new(
            format!("expected an expression, found keyword `:{name}`"),
            *span,
        )),
        Sexpr::List(items, span) => from_list(items, *span, source),
    }
}

fn from_list(items: &[Sexpr], span: Span, source: &str) -> Result<Expr, Diagnostic> {
    let Some(head) = items.first() else {
        return Err(Diagnostic::new("empty list is not an expression", span));
    };
    let Some(op) = head.as_symbol() else {
        return Err(Diagnostic::new(
            format!(
                "an expression list must start with an operator symbol, found {}",
                head.kind_name()
            ),
            head.span(),
        ));
    };
    // Data forms first: `(path …)` and `(asset …)` are values the shared
    // s-expression parsers read whole, not operators to evaluate.
    if let Some(result) = crate::path::parse_path_form(items, span) {
        return result.map(|cmds| Expr::Form(Literal::Path(cmds), span));
    }
    if let Some(result) = crate::asset::parse_asset_form(items, span) {
        return result.map(|path| Expr::Form(Literal::Asset(path), span));
    }
    if let Some(result) = crate::rich::parse_rich_form(items, span) {
        return result.map(|runs| Expr::Form(Literal::Rich(runs), span));
    }

    let args = &items[1..];
    match op {
        "if" => {
            if args.len() != 3 {
                return Err(Diagnostic::new(
                    format!(
                        "`if` takes exactly 3 arguments (condition, then, else), found {}",
                        args.len()
                    ),
                    span,
                ));
            }
            Ok(Expr::If(
                Box::new(from_sexpr(&args[0], source)?),
                Box::new(from_sexpr(&args[1], source)?),
                Box::new(from_sexpr(&args[2], source)?),
                span,
            ))
        }
        "list" => {
            let items = args
                .iter()
                .map(|item| from_sexpr(item, source))
                .collect::<Result<Vec<_>, _>>()?;
            Ok(Expr::List(items, span))
        }
        "not" => {
            if args.len() != 1 {
                return Err(Diagnostic::new(
                    format!("`not` takes exactly 1 argument, found {}", args.len()),
                    span,
                ));
            }
            Ok(Expr::Unary(
                UnOp::Not,
                Box::new(from_sexpr(&args[0], source)?),
                span,
            ))
        }
        "-" if args.len() == 1 => Ok(Expr::Unary(
            UnOp::Neg,
            Box::new(from_sexpr(&args[0], source)?),
            span,
        )),
        _ => {
            let Some(op_kind) = binary_op(op) else {
                // A capitalized head is a record literal (keyword fields,
                // mirroring instance syntax); a lowercase one is a handler
                // invocation. Whether either is legal here — and whether
                // the names resolve — is validation's decision.
                if op.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
                    let mut fields = Vec::new();
                    let mut rest = args.iter();
                    while let Some(item) = rest.next() {
                        let Sexpr::Keyword(name, kw_span) = item else {
                            return Err(Diagnostic::new(
                                "record literals take `:field value` pairs",
                                item.span(),
                            ));
                        };
                        let value = rest.next().ok_or_else(|| {
                            Diagnostic::new(format!("`:{name}` needs a value"), *kw_span)
                        })?;
                        fields.push((name.clone(), from_sexpr(value, source)?));
                    }
                    return Ok(Expr::RecordLit(op.to_owned(), fields, span));
                }
                let args_exprs = args
                    .iter()
                    .map(|arg| from_sexpr(arg, source))
                    .collect::<Result<Vec<_>, _>>()?;
                return Ok(Expr::Call(op.to_owned(), args_exprs, span));
            };
            let op = op_kind;
            if args.len() < 2 {
                return Err(Diagnostic::new(
                    "binary operators take at least 2 arguments",
                    span,
                ));
            }
            let mut expr = from_sexpr(&args[0], source)?;
            for arg in &args[1..] {
                let rhs = from_sexpr(arg, source)?;
                expr = Expr::Binary(op, Box::new(expr), Box::new(rhs), span);
            }
            Ok(expr)
        }
    }
}

fn binary_op(symbol: &str) -> Option<BinOp> {
    Some(match symbol {
        "+" => BinOp::Add,
        "-" => BinOp::Sub,
        "*" => BinOp::Mul,
        "/" => BinOp::Div,
        "%" => BinOp::Rem,
        "<" => BinOp::Lt,
        "<=" => BinOp::Le,
        ">" => BinOp::Gt,
        ">=" => BinOp::Ge,
        "=" => BinOp::Eq,
        "!=" => BinOp::Ne,
        "and" => BinOp::And,
        "or" => BinOp::Or,
        _ => return None,
    })
}

/// Scan a string literal for `{name}` interpolations.
///
/// Works on the raw source slice (the string's span, quotes included) so
/// each `Ref` carries a file-accurate span for diagnostics; escapes are
/// processed inline, matching the reader's escape set.
fn parse_template(span: Span, source: &str) -> Result<Template, Diagnostic> {
    let raw = &source[span.start + 1..span.end - 1];
    let base = span.start + 1;
    let bytes = raw.as_bytes();
    let mut segments = Vec::new();
    let mut literal = String::new();
    let mut i = 0;
    while i < bytes.len() {
        match bytes[i] {
            b'\\' => {
                // The reader validated the escape already.
                let escaped = match bytes.get(i + 1) {
                    Some(b'n') => '\n',
                    Some(b't') => '\t',
                    Some(b'\\') => '\\',
                    Some(b'"') => '"',
                    Some(b'{') => '{',
                    _ => unreachable!("reader validated escapes"),
                };
                literal.push(escaped);
                i += 2;
            }
            b'{' if bytes.get(i + 1) == Some(&b'{') => {
                literal.push('{');
                i += 2;
            }
            b'{' => {
                let open = i;
                let close = raw[i..].find('}').map(|off| i + off).ok_or_else(|| {
                    Diagnostic::new(
                        "unclosed `{` in string interpolation (use `{{` or `\\{` for a literal brace)",
                        Span::new(base + open, base + open + 1),
                    )
                })?;
                let name = raw[i + 1..close].trim();
                let name_span = Span::new(base + i + 1, base + close);
                if name.is_empty() {
                    return Err(Diagnostic::new("empty interpolation `{}`", name_span));
                }
                if !name
                    .chars()
                    .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
                {
                    return Err(Diagnostic::new(
                        format!(
                            "interpolations hold a name like `count` or `todo.label`, found `{name}`"
                        ),
                        name_span,
                    ));
                }
                if !literal.is_empty() {
                    segments.push(Segment::Literal(std::mem::take(&mut literal)));
                }
                segments.push(Segment::Ref(name.to_owned(), name_span));
                i = close + 1;
            }
            b'}' if bytes.get(i + 1) == Some(&b'}') => {
                literal.push('}');
                i += 2;
            }
            _ => {
                let ch = raw[i..].chars().next().expect("in bounds");
                literal.push(ch);
                i += ch.len_utf8();
            }
        }
    }
    if !literal.is_empty() {
        segments.push(Segment::Literal(literal));
    }
    Ok(Template { segments })
}

#[cfg(test)]
mod tests;