online-dsl-forge 0.3.3

A bounded parser and runtime for in-memory DSL expressions.
Documentation
use std::collections::{BTreeMap, HashSet};

use crate::parser::{AstExpression, Diagnostic, SourceSpan};
use crate::sema::schema::{ExpressionFunction, ExpressionFunctionScope, SignatureMatch};
use crate::sema::verified::{VerifiedExprKind, VerifiedExpression};

use super::support::{ExprAnalysis, LocalBinding, function_calls, substitute_expression};
use super::{AnalyzeState, ExpressionFunctionMode};

type FunctionKey = (ExpressionFunctionScope, String);

impl<'a> AnalyzeState<'a> {
  pub(super) fn current_function_scope(&self) -> ExpressionFunctionScope {
    self
      .active_functions
      .last()
      .map(|(scope, _)| *scope)
      .unwrap_or(self.analyzer.expression_function_scope)
  }

  pub(super) fn validate_function_graph(&mut self) {
    for diagnostic in self.schema.expression_function_diagnostics() {
      self.diagnostics.push(diagnostic.diagnostic());
    }

    for function in self.schema.expression_functions() {
      self
        .analyzer
        .dialect
        .validate(&function.expression, &mut self.diagnostics);
      self.validate_function_signature(function);
    }

    let mut permanent = HashSet::new();
    let mut temporary = HashSet::new();
    for function in self.schema.expression_functions() {
      self.validate_function_node(function, &mut permanent, &mut temporary);
    }
  }

  pub(super) fn analyze_expression_function(
    &mut self,
    function: &ExpressionFunction,
    args: &[AstExpression],
    span: SourceSpan,
    depth: usize,
  ) -> ExprAnalysis {
    if self.analyzer.expression_function_mode == ExpressionFunctionMode::CallFrame {
      return self.analyze_expression_function_call_frame(function, args, span, depth);
    }

    if function.params.len() != args.len() {
      self.diagnostics.push(Diagnostic::new(
        format!(
          "function {} does not accept {} arguments",
          function.name,
          args.len()
        ),
        span,
      ));
      return ExprAnalysis::leaf(
        VerifiedExpression::new(
          VerifiedExprKind::FunctionCall {
            name: function.name.clone(),
            args: Vec::new(),
          },
          span,
        ),
        None,
      );
    }

    let key = function_key(function);
    if self.active_functions.contains(&key) {
      self.diagnostics.push(Diagnostic::new(
        format!("recursive expression function {}", function.name),
        span,
      ));
      return ExprAnalysis::leaf(VerifiedExpression::new(VerifiedExprKind::Null, span), None);
    }

    let replacements = function
      .params
      .iter()
      .cloned()
      .zip(args.iter().cloned())
      .collect::<BTreeMap<_, _>>();
    let substituted = substitute_expression(&function.expression, &replacements);
    self.active_functions.push(key);
    let mut analysis = self.analyze_expression(&substituted, depth + 1);
    self.active_functions.pop();
    analysis.cost += 1;
    analysis.nodes += 1;
    analysis
  }

  fn analyze_expression_function_call_frame(
    &mut self,
    function: &ExpressionFunction,
    args: &[AstExpression],
    span: SourceSpan,
    depth: usize,
  ) -> ExprAnalysis {
    if function.params.len() != args.len() {
      self.diagnostics.push(Diagnostic::new(
        format!(
          "function {} does not accept {} arguments",
          function.name,
          args.len()
        ),
        span,
      ));
      return ExprAnalysis::leaf(
        VerifiedExpression::new(
          VerifiedExprKind::ExpressionFunctionCall {
            name: function.name.clone(),
            params: function.params.clone(),
            args: Vec::new(),
            body: Box::new(VerifiedExpression::new(VerifiedExprKind::Null, span)),
          },
          span,
        ),
        None,
      );
    }

    let key = function_key(function);
    if self.active_functions.contains(&key) {
      self.diagnostics.push(Diagnostic::new(
        format!("recursive expression function {}", function.name),
        span,
      ));
      return ExprAnalysis::leaf(VerifiedExpression::new(VerifiedExprKind::Null, span), None);
    }

    let args_analysis = self.analyze_args(args, depth);
    let bindings = function
      .params
      .iter()
      .cloned()
      .zip(args_analysis.bindings.iter().cloned())
      .collect::<BTreeMap<String, LocalBinding>>();

    self.active_functions.push(key);
    self.local_bindings.push(bindings);
    let body = self.analyze_expression(&function.expression, depth + 1);
    self.local_bindings.pop();
    self.active_functions.pop();
    let origin = body.origin;
    let path = body.path.clone();

    ExprAnalysis::new(
      VerifiedExpression::new(
        VerifiedExprKind::ExpressionFunctionCall {
          name: function.name.clone(),
          params: function.params.clone(),
          args: args_analysis.exprs,
          body: Box::new(body.expr),
        },
        span,
      ),
      origin,
      path,
      args_analysis.body_need.merge(body.body_need),
      args_analysis.nodes + body.nodes + 1,
      args_analysis.cost + body.cost + 1,
    )
    .with_mitigation_payload(args_analysis.mitigation_payload || body.mitigation_payload)
  }

  fn validate_function_signature(&mut self, function: &ExpressionFunction) {
    if !valid_oxirule_identifier(&function.name) || is_top_level_oxirule_object(&function.name) {
      self.diagnostics.push(Diagnostic::new(
        format!("function name {} must be a valid identifier", function.name),
        function.expression.span,
      ));
    }

    let mut params = HashSet::new();
    for param in &function.params {
      if !valid_oxirule_identifier(param) || is_top_level_oxirule_object(param) {
        self.diagnostics.push(Diagnostic::new(
          format!(
            "function {} parameter {param} must be a valid identifier",
            function.name
          ),
          function.expression.span,
        ));
      }
      if !params.insert(param.as_str()) {
        self.diagnostics.push(Diagnostic::new(
          format!(
            "function {} contains duplicate parameter {param}",
            function.name
          ),
          function.expression.span,
        ));
      }
    }
  }

  fn validate_function_node(
    &mut self,
    function: &ExpressionFunction,
    permanent: &mut HashSet<FunctionKey>,
    temporary: &mut HashSet<FunctionKey>,
  ) {
    let key = function_key(function);
    if permanent.contains(&key) {
      return;
    }
    if !temporary.insert(key.clone()) {
      self.diagnostics.push(Diagnostic::new(
        format!("recursive expression function {}", function.name),
        function.expression.span,
      ));
      return;
    }

    for call in function_calls(&function.expression) {
      let Some(callee) = self
        .schema
        .expression_function_for_scope(&call.name, function.scope)
      else {
        self.validate_host_function_call(&call.name, call.arity, call.span);
        continue;
      };
      if callee.params.len() != call.arity {
        self.diagnostics.push(Diagnostic::new(
          format!(
            "function {} does not accept {} arguments",
            call.name, call.arity
          ),
          call.span,
        ));
      }
      self.validate_function_node(callee, permanent, temporary);
    }

    temporary.remove(&key);
    permanent.insert(key);
  }

  fn validate_host_function_call(&mut self, name: &str, arity: usize, span: SourceSpan) {
    match self.schema.function_accepts(name, arity) {
      SignatureMatch::Matches => {}
      SignatureMatch::Unknown if self.analyzer.options.allow_unknown_functions => {}
      SignatureMatch::Unknown => self
        .diagnostics
        .push(Diagnostic::new(format!("unknown function {name}"), span)),
      SignatureMatch::ArityMismatch => self.diagnostics.push(Diagnostic::new(
        format!("function {name} does not accept {arity} arguments"),
        span,
      )),
    }
  }
}

fn function_key(function: &ExpressionFunction) -> FunctionKey {
  (function.scope, function.name.clone())
}

fn valid_oxirule_identifier(identifier: &str) -> bool {
  let mut chars = identifier.chars();
  let Some(first) = chars.next() else {
    return false;
  };
  (first.is_ascii_alphabetic() || first == '_')
    && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
    && !is_reserved_identifier(identifier)
}

fn is_reserved_identifier(identifier: &str) -> bool {
  matches!(
    identifier,
    "if"
      | "else"
      | "for"
      | "while"
      | "do"
      | "switch"
      | "let"
      | "const"
      | "function"
      | "import"
      | "export"
      | "new"
      | "try"
      | "catch"
      | "throw"
      | "await"
      | "return"
      | "true"
      | "false"
      | "null"
  )
}

fn is_top_level_oxirule_object(identifier: &str) -> bool {
  matches!(
    identifier,
    "Context" | "Request" | "DynamicPolicy" | "Response" | "Stream"
  )
}