online-dsl-forge 0.3.3

A bounded parser and runtime for in-memory DSL expressions.
Documentation
use super::ast::{AstExpression, BinaryOp, ExprKind};

pub fn format_expression(expression: &AstExpression) -> String {
  format_with_parent(expression, 0, ChildSide::Root)
}

#[derive(Clone, Copy, Eq, PartialEq)]
enum ChildSide {
  Root,
  Left,
  Right,
  Unary,
  Receiver,
}

fn format_with_parent(
  expression: &AstExpression,
  parent_precedence: u8,
  side: ChildSide,
) -> String {
  let own_precedence = precedence(expression);
  let mut output = match &expression.kind {
    ExprKind::Null => "null".to_string(),
    ExprKind::Bool { value } => value.to_string(),
    ExprKind::Int { value } => value.to_string(),
    ExprKind::Float { value } => format_float(*value),
    ExprKind::String { value } => format!("\"{}\"", escape_string(value)),
    ExprKind::Array { items } => {
      let items = items
        .iter()
        .map(format_expression)
        .collect::<Vec<_>>()
        .join(", ");
      format!("[{items}]")
    }
    ExprKind::Identifier { name } => name.clone(),
    ExprKind::Member { receiver, name } => {
      format!(
        "{}.{}",
        format_with_parent(receiver, own_precedence, ChildSide::Receiver),
        name
      )
    }
    ExprKind::FunctionCall { name, args } => format!("{name}({})", format_args(args)),
    ExprKind::MethodCall {
      receiver,
      name,
      args,
    } => format!(
      "{}.{}({})",
      format_with_parent(receiver, own_precedence, ChildSide::Receiver),
      name,
      format_args(args)
    ),
    ExprKind::Unary { op, expr } => {
      format!(
        "{}{}",
        op.as_str(),
        format_with_parent(expr, own_precedence, ChildSide::Unary)
      )
    }
    ExprKind::Binary { left, op, right } => format!(
      "{} {} {}",
      format_with_parent(left, own_precedence, ChildSide::Left),
      op.as_str(),
      format_with_parent(right, own_precedence, ChildSide::Right)
    ),
  };

  if needs_parentheses(own_precedence, parent_precedence, side) {
    output = format!("({output})");
  }
  output
}

fn format_args(args: &[AstExpression]) -> String {
  args
    .iter()
    .map(format_expression)
    .collect::<Vec<_>>()
    .join(", ")
}

fn precedence(expression: &AstExpression) -> u8 {
  match &expression.kind {
    ExprKind::Binary { op, .. } => binary_precedence(*op),
    ExprKind::Unary { .. } => 7,
    ExprKind::Member { .. } | ExprKind::FunctionCall { .. } | ExprKind::MethodCall { .. } => 8,
    ExprKind::Null
    | ExprKind::Bool { .. }
    | ExprKind::Int { .. }
    | ExprKind::Float { .. }
    | ExprKind::String { .. }
    | ExprKind::Array { .. }
    | ExprKind::Identifier { .. } => 9,
  }
}

fn binary_precedence(op: BinaryOp) -> u8 {
  match op {
    BinaryOp::Or => 1,
    BinaryOp::And => 2,
    BinaryOp::Eq | BinaryOp::Ne => 3,
    BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => 4,
    BinaryOp::Add | BinaryOp::Sub => 5,
    BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => 6,
  }
}

fn needs_parentheses(own: u8, parent: u8, side: ChildSide) -> bool {
  if matches!(side, ChildSide::Root) {
    return false;
  }
  own < parent || (side == ChildSide::Right && own == parent)
}

fn escape_string(value: &str) -> String {
  let mut escaped = String::new();
  for ch in value.chars() {
    match ch {
      '\\' => escaped.push_str("\\\\"),
      '"' => escaped.push_str("\\\""),
      '\n' => escaped.push_str("\\n"),
      '\r' => escaped.push_str("\\r"),
      '\t' => escaped.push_str("\\t"),
      other => escaped.push(other),
    }
  }
  escaped
}

fn format_float(value: f64) -> String {
  let mut output = value.to_string();
  if value.is_finite() && !output.contains('.') && !output.contains('e') && !output.contains('E') {
    output.push_str(".0");
  }
  output
}

#[cfg(test)]
mod tests {
  use crate::parse_expression;

  use super::format_expression;

  #[test]
  fn preserves_right_nested_binary_shape() {
    let ast = parse_expression("1 - (2 - 3)").expect("expression should parse");
    assert_eq!(format_expression(&ast), "1 - (2 - 3)");
  }

  #[test]
  fn normalizes_strings() {
    let ast = parse_expression("'a\\nb'").expect("expression should parse");
    assert_eq!(format_expression(&ast), "\"a\\nb\"");
  }
}