Skip to main content

online_dsl_forge/sema/
analyzer.rs

1mod body_need;
2mod functions;
3mod phase;
4mod support;
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use crate::parser::{
9  AstExpression, BinaryOp, Diagnostic, DiagnosticReport, ExprKind, SourceSpan, UnaryOp,
10};
11use serde::{Deserialize, Serialize};
12
13use crate::sema::dialect::ExpressionDialect;
14use crate::sema::profile::{
15  BodyNeedSummary, Determinism, RegexPolicy, SecurityProfile, SecurityProfileId,
16};
17use crate::sema::schema::{
18  CapabilityMeta, CapabilityTicket, ExpressionFunctionScope, RuntimeSchema, SignatureMatch,
19};
20use crate::sema::verified::{
21  CompiledExpression, CompiledRegexCache, RegexLiteral, VerifiedExprKind, VerifiedExpression,
22  VerifiedProgram, VerifiedProgramParts,
23};
24use support::{
25  ArgsAnalysis, ExprAnalysis, LocalBinding, ObjectOrigin, member_origin, string_literal,
26};
27
28#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)]
29pub struct CompileOptions {
30  pub allow_unknown_variables: bool,
31  pub allow_unknown_functions: bool,
32  pub allow_unknown_methods: bool,
33}
34
35#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)]
36pub enum ExpressionFunctionMode {
37  #[default]
38  Inline,
39  CallFrame,
40}
41
42#[derive(Debug, Clone)]
43pub struct Analyzer {
44  profile: SecurityProfile,
45  options: CompileOptions,
46  dialect: ExpressionDialect,
47  expression_function_scope: ExpressionFunctionScope,
48  expression_function_mode: ExpressionFunctionMode,
49}
50
51impl Analyzer {
52  pub fn new(profile: SecurityProfile) -> Self {
53    Self {
54      profile,
55      options: CompileOptions::default(),
56      dialect: ExpressionDialect::default(),
57      expression_function_scope: ExpressionFunctionScope::Local,
58      expression_function_mode: ExpressionFunctionMode::default(),
59    }
60  }
61
62  pub fn with_options(mut self, options: CompileOptions) -> Self {
63    self.options = options;
64    self
65  }
66
67  pub fn with_dialect(mut self, dialect: ExpressionDialect) -> Self {
68    self.dialect = dialect;
69    self
70  }
71
72  pub fn with_expression_function_scope(mut self, scope: ExpressionFunctionScope) -> Self {
73    self.expression_function_scope = scope;
74    self
75  }
76
77  pub fn with_expression_function_mode(mut self, mode: ExpressionFunctionMode) -> Self {
78    self.expression_function_mode = mode;
79    self
80  }
81
82  pub fn analyze(
83    &self,
84    expression: &AstExpression,
85    schema: &RuntimeSchema,
86  ) -> Result<VerifiedProgram, DiagnosticReport> {
87    let mut state = AnalyzeState::new(self, schema);
88    self.dialect.validate(expression, &mut state.diagnostics);
89    state.validate_function_graph();
90    let mut analysis = state.analyze_expression(expression, 0);
91    state.merge_body_access_for_exposed_origin(&mut analysis.body_need, analysis.origin);
92    state.validate_program_bounds(&analysis, expression.span);
93
94    if state.diagnostics.is_empty() {
95      Ok(VerifiedProgram::new(VerifiedProgramParts {
96        ast: expression.clone(),
97        root: analysis.expr,
98        profile: self.profile.clone(),
99        body_need: analysis.body_need,
100        static_cost_upper_bound: analysis.cost,
101        regex_literals: state.regex_literals,
102        regex_cache: state.regex_cache,
103        required_capabilities: state.required_capabilities,
104        required_capability_metadata: state.required_capability_metadata,
105      }))
106    } else {
107      Err(DiagnosticReport::new(state.diagnostics))
108    }
109  }
110}
111
112pub fn compile_expression(
113  expression: &AstExpression,
114  schema: &RuntimeSchema,
115  options: CompileOptions,
116) -> Result<CompiledExpression, DiagnosticReport> {
117  Analyzer::new(SecurityProfile::generic_safe())
118    .with_options(options)
119    .analyze(expression, schema)
120    .map(CompiledExpression::new)
121}
122
123struct AnalyzeState<'a> {
124  analyzer: &'a Analyzer,
125  schema: &'a RuntimeSchema,
126  diagnostics: Vec<Diagnostic>,
127  regex_literals: Vec<RegexLiteral>,
128  regex_cache: CompiledRegexCache,
129  required_capabilities: BTreeSet<CapabilityTicket>,
130  required_capability_metadata: BTreeMap<CapabilityTicket, CapabilityMeta>,
131  active_functions: Vec<(ExpressionFunctionScope, String)>,
132  local_bindings: Vec<BTreeMap<String, LocalBinding>>,
133}
134
135impl<'a> AnalyzeState<'a> {
136  fn new(analyzer: &'a Analyzer, schema: &'a RuntimeSchema) -> Self {
137    Self {
138      analyzer,
139      schema,
140      diagnostics: Vec::new(),
141      regex_literals: Vec::new(),
142      regex_cache: CompiledRegexCache::default(),
143      required_capabilities: BTreeSet::new(),
144      required_capability_metadata: BTreeMap::new(),
145      active_functions: Vec::new(),
146      local_bindings: Vec::new(),
147    }
148  }
149
150  fn validate_program_bounds(&mut self, analysis: &ExprAnalysis, span: SourceSpan) {
151    if analysis.nodes > self.analyzer.profile.max_ast_nodes {
152      self
153        .diagnostics
154        .push(Diagnostic::new("AST node limit exceeded", span));
155    }
156    if analysis.cost > self.analyzer.profile.max_cost_units {
157      self
158        .diagnostics
159        .push(Diagnostic::new("static cost limit exceeded", span));
160    }
161    if let Some(limit) = self.analyzer.profile.body_access_limit
162      && !limit.allows(analysis.body_need)
163    {
164      self.diagnostics.push(Diagnostic::new(
165        "body access limit exceeded by profile",
166        span,
167      ));
168    }
169    if matches!(self.analyzer.profile.id, SecurityProfileId::MitigationField)
170      && (analysis.mitigation_payload || analysis.body_need.reads_payload())
171    {
172      self.diagnostics.push(Diagnostic::new(
173        "MitigationField cannot read request, response, or stream body bytes",
174        span,
175      ));
176    }
177  }
178
179  fn analyze_expression(&mut self, expression: &AstExpression, depth: usize) -> ExprAnalysis {
180    if depth > self.analyzer.profile.max_call_depth {
181      self.diagnostics.push(Diagnostic::new(
182        "semantic call depth limit exceeded",
183        expression.span,
184      ));
185    }
186
187    match &expression.kind {
188      ExprKind::Null => ExprAnalysis::leaf(
189        VerifiedExpression::new(VerifiedExprKind::Null, expression.span),
190        None,
191      ),
192      ExprKind::Bool { value } => ExprAnalysis::leaf(
193        VerifiedExpression::new(VerifiedExprKind::Bool(*value), expression.span),
194        None,
195      ),
196      ExprKind::Int { value } => ExprAnalysis::leaf(
197        VerifiedExpression::new(VerifiedExprKind::Int(*value), expression.span),
198        None,
199      ),
200      ExprKind::Float { value } => ExprAnalysis::leaf(
201        VerifiedExpression::new(VerifiedExprKind::Float(*value), expression.span),
202        None,
203      ),
204      ExprKind::String { value } => ExprAnalysis::leaf(
205        VerifiedExpression::new(VerifiedExprKind::String(value.clone()), expression.span),
206        None,
207      ),
208      ExprKind::Identifier { name } => self.analyze_identifier(name, expression.span),
209      ExprKind::Array { items } => self.analyze_array(items, expression.span, depth),
210      ExprKind::Member { receiver, name } => {
211        self.analyze_member(receiver, name, expression.span, depth)
212      }
213      ExprKind::FunctionCall { name, args } => {
214        self.analyze_function_call(name, args, expression.span, depth)
215      }
216      ExprKind::MethodCall {
217        receiver,
218        name,
219        args,
220      } => self.analyze_method_call(receiver, name, args, expression.span, depth),
221      ExprKind::Unary { op, expr } => self.analyze_unary(*op, expr, expression.span, depth),
222      ExprKind::Binary { left, op, right } => {
223        self.analyze_binary(left, *op, right, expression.span, depth)
224      }
225    }
226  }
227
228  fn analyze_identifier(&mut self, name: &str, span: SourceSpan) -> ExprAnalysis {
229    if let Some(binding) = self.local_binding(name).cloned() {
230      let mut analysis = ExprAnalysis::leaf(
231        VerifiedExpression::new(VerifiedExprKind::Identifier(name.to_string()), span),
232        binding.origin,
233      )
234      .with_path_option(binding.path)
235      .with_mitigation_payload(binding.mitigation_payload);
236      self.merge_body_access_for_path(&mut analysis.body_need, analysis.path.as_deref());
237      return analysis;
238    }
239    if !self.analyzer.options.allow_unknown_variables && !self.schema.has_variable(name) {
240      self
241        .diagnostics
242        .push(Diagnostic::new(format!("unknown variable {name}"), span));
243    }
244    self.validate_variable_phase(name, span);
245    let mut analysis = ExprAnalysis::leaf(
246      VerifiedExpression::new(VerifiedExprKind::Identifier(name.to_string()), span),
247      ObjectOrigin::root(name),
248    )
249    .with_path(vec![name.to_string()]);
250    self.merge_body_access_for_path(&mut analysis.body_need, analysis.path.as_deref());
251    analysis
252  }
253
254  fn analyze_array(
255    &mut self,
256    items: &[AstExpression],
257    span: SourceSpan,
258    depth: usize,
259  ) -> ExprAnalysis {
260    let mut body_need = BodyNeedSummary::default();
261    let mut mitigation_payload = false;
262    let mut nodes = 1;
263    let mut cost = 1;
264    let items = items
265      .iter()
266      .map(|item| {
267        let analysis = self.analyze_expression(item, depth + 1);
268        body_need = body_need
269          .merge(self.body_need_for_consumed_analysis(analysis.body_need, analysis.origin));
270        mitigation_payload |= analysis.mitigation_payload;
271        nodes += analysis.nodes;
272        cost += analysis.cost;
273        analysis.expr
274      })
275      .collect();
276    ExprAnalysis::new(
277      VerifiedExpression::new(VerifiedExprKind::Array(items), span),
278      None,
279      None,
280      body_need,
281      nodes,
282      cost,
283    )
284    .with_mitigation_payload(mitigation_payload)
285  }
286
287  fn analyze_member(
288    &mut self,
289    receiver: &AstExpression,
290    name: &str,
291    span: SourceSpan,
292    depth: usize,
293  ) -> ExprAnalysis {
294    let receiver = self.analyze_expression(receiver, depth + 1);
295    let path = receiver.path.as_ref().map(|path| {
296      let mut path = path.clone();
297      path.push(name.to_string());
298      path
299    });
300    let origin = receiver
301      .origin
302      .and_then(|origin| member_origin(origin, name));
303    let mut body_need = receiver.body_need;
304    self.merge_body_access_for_origin(&mut body_need, receiver.origin, name, span);
305    if let Some(path) = &path
306      && let Some((target, access)) = self.schema.body_access_for_path(path)
307    {
308      body_need.merge_target(target, access);
309    }
310    self.validate_origin_phase(origin, span);
311    let mitigation_payload = receiver.mitigation_payload
312      || origin.is_some_and(ObjectOrigin::is_mitigation_payload_boundary);
313
314    ExprAnalysis::new(
315      VerifiedExpression::new(
316        VerifiedExprKind::Member {
317          receiver: Box::new(receiver.expr),
318          name: name.to_string(),
319        },
320        span,
321      ),
322      origin,
323      path,
324      body_need,
325      receiver.nodes + 1,
326      receiver.cost + 1,
327    )
328    .with_mitigation_payload(mitigation_payload)
329  }
330
331  fn analyze_function_call(
332    &mut self,
333    name: &str,
334    args: &[AstExpression],
335    span: SourceSpan,
336    depth: usize,
337  ) -> ExprAnalysis {
338    if let Some(function) = self
339      .schema
340      .expression_function_for_scope(name, self.current_function_scope())
341    {
342      return self.analyze_expression_function(function, args, span, depth);
343    }
344
345    let capability = self.validate_call(
346      "function",
347      name,
348      args.len(),
349      self.schema.function_accepts(name, args.len()),
350      self.analyzer.options.allow_unknown_functions,
351      span,
352    );
353    let args_analysis = self.analyze_args(args, depth);
354    if let Some(capability) = capability {
355      self.validate_capability(capability, span);
356      self.validate_regex_args(capability, args, span);
357      self.require_capability(capability);
358    }
359    let capability_ticket = capability.map(CapabilityMeta::ticket);
360    let body_need = args_analysis.consumed_body_need;
361    ExprAnalysis::new(
362      verified_with_capability(
363        VerifiedExpression::new(
364          VerifiedExprKind::FunctionCall {
365            name: name.to_string(),
366            args: args_analysis.exprs,
367          },
368          span,
369        ),
370        capability_ticket,
371      ),
372      None,
373      None,
374      body_need,
375      args_analysis.nodes + 1,
376      args_analysis.cost + capability.map_or(1, |capability| capability.cost.static_cost()),
377    )
378    .with_mitigation_payload(args_analysis.mitigation_payload)
379  }
380
381  fn analyze_method_call(
382    &mut self,
383    receiver: &AstExpression,
384    name: &str,
385    args: &[AstExpression],
386    span: SourceSpan,
387    depth: usize,
388  ) -> ExprAnalysis {
389    let receiver = self.analyze_expression(receiver, depth + 1);
390    let capability = self.validate_call(
391      "method",
392      name,
393      args.len(),
394      self.schema.method_accepts(name, args.len()),
395      self.analyzer.options.allow_unknown_methods,
396      span,
397    );
398    let args_analysis = self.analyze_args(args, depth);
399    let receiver_body_need =
400      self.body_need_for_consumed_analysis(receiver.body_need, receiver.origin);
401    let mut body_need = receiver_body_need.merge(args_analysis.consumed_body_need);
402    let mitigation_payload = receiver.mitigation_payload || args_analysis.mitigation_payload;
403    if let Some(capability) = capability {
404      self.validate_capability(capability, span);
405      self.validate_regex_args(capability, args, span);
406      self.merge_body_access_for_method(&mut body_need, receiver.origin, capability);
407      self.require_capability(capability);
408    }
409    let capability_ticket = capability.map(CapabilityMeta::ticket);
410
411    ExprAnalysis::new(
412      verified_with_capability(
413        VerifiedExpression::new(
414          VerifiedExprKind::MethodCall {
415            receiver: Box::new(receiver.expr),
416            name: name.to_string(),
417            args: args_analysis.exprs,
418          },
419          span,
420        ),
421        capability_ticket,
422      ),
423      None,
424      None,
425      body_need,
426      receiver.nodes + args_analysis.nodes + 1,
427      receiver.cost
428        + args_analysis.cost
429        + capability.map_or(1, |capability| capability.cost.static_cost()),
430    )
431    .with_mitigation_payload(mitigation_payload)
432  }
433
434  fn analyze_unary(
435    &mut self,
436    op: UnaryOp,
437    expr: &AstExpression,
438    span: SourceSpan,
439    depth: usize,
440  ) -> ExprAnalysis {
441    let expr = self.analyze_expression(expr, depth + 1);
442    let capability = self
443      .schema
444      .unary_operator_capability(op)
445      .cloned()
446      .unwrap_or_else(|| CapabilityMeta::unary_operator(op));
447    self.validate_capability(&capability, span);
448    self.require_capability(&capability);
449    let ticket = capability.ticket();
450    let body_need = self.body_need_for_consumed_analysis(expr.body_need, expr.origin);
451    ExprAnalysis::new(
452      VerifiedExpression::new(
453        VerifiedExprKind::Unary {
454          op,
455          expr: Box::new(expr.expr),
456        },
457        span,
458      )
459      .with_capability_ticket(ticket),
460      None,
461      None,
462      body_need,
463      expr.nodes + 1,
464      expr.cost + capability.cost.static_cost(),
465    )
466    .with_mitigation_payload(expr.mitigation_payload)
467  }
468
469  fn analyze_binary(
470    &mut self,
471    left: &AstExpression,
472    op: BinaryOp,
473    right: &AstExpression,
474    span: SourceSpan,
475    depth: usize,
476  ) -> ExprAnalysis {
477    let left = self.analyze_expression(left, depth + 1);
478    let right = self.analyze_expression(right, depth + 1);
479    let capability = self
480      .schema
481      .binary_operator_capability(op)
482      .cloned()
483      .unwrap_or_else(|| CapabilityMeta::binary_operator(op));
484    self.validate_capability(&capability, span);
485    self.require_capability(&capability);
486    let ticket = capability.ticket();
487    let left_body_need = self.body_need_for_consumed_analysis(left.body_need, left.origin);
488    let right_body_need = self.body_need_for_consumed_analysis(right.body_need, right.origin);
489    ExprAnalysis::new(
490      VerifiedExpression::new(
491        VerifiedExprKind::Binary {
492          left: Box::new(left.expr),
493          op,
494          right: Box::new(right.expr),
495        },
496        span,
497      )
498      .with_capability_ticket(ticket),
499      None,
500      None,
501      left_body_need.merge(right_body_need),
502      left.nodes + right.nodes + 1,
503      left.cost + right.cost + capability.cost.static_cost(),
504    )
505    .with_mitigation_payload(left.mitigation_payload || right.mitigation_payload)
506  }
507
508  fn analyze_args(&mut self, args: &[AstExpression], depth: usize) -> ArgsAnalysis {
509    let mut body_need = BodyNeedSummary::default();
510    let mut consumed_body_need = BodyNeedSummary::default();
511    let mut mitigation_payload = false;
512    let mut nodes = 0;
513    let mut cost = 0;
514    let exprs = args
515      .iter()
516      .map(|arg| {
517        let analysis = self.analyze_expression(arg, depth + 1);
518        let binding = LocalBinding::from_analysis(&analysis);
519        body_need = body_need.merge(analysis.body_need);
520        consumed_body_need = consumed_body_need
521          .merge(self.body_need_for_consumed_analysis(analysis.body_need, analysis.origin));
522        mitigation_payload |= analysis.mitigation_payload;
523        nodes += analysis.nodes;
524        cost += analysis.cost;
525        (analysis.expr, binding)
526      })
527      .collect::<Vec<_>>();
528    let (exprs, bindings) = exprs.into_iter().unzip();
529    ArgsAnalysis {
530      exprs,
531      bindings,
532      body_need,
533      consumed_body_need,
534      mitigation_payload,
535      nodes,
536      cost,
537    }
538  }
539
540  fn local_binding(&self, name: &str) -> Option<&LocalBinding> {
541    self
542      .local_bindings
543      .iter()
544      .rev()
545      .find_map(|bindings| bindings.get(name))
546  }
547
548  fn validate_call(
549    &mut self,
550    kind: &'static str,
551    name: &str,
552    arity: usize,
553    result: SignatureMatch,
554    allow_unknown: bool,
555    span: SourceSpan,
556  ) -> Option<&'a CapabilityMeta> {
557    match result {
558      SignatureMatch::Matches => {
559        if kind == "function" {
560          self.schema.function_capability(name, arity)
561        } else {
562          self.schema.method_capability(name, arity)
563        }
564      }
565      SignatureMatch::Unknown if allow_unknown => None,
566      SignatureMatch::Unknown => {
567        self
568          .diagnostics
569          .push(Diagnostic::new(format!("unknown {kind} {name}"), span));
570        None
571      }
572      SignatureMatch::ArityMismatch => {
573        self.diagnostics.push(Diagnostic::new(
574          format!("{kind} {name} does not accept {arity} arguments"),
575          span,
576        ));
577        None
578      }
579    }
580  }
581
582  fn validate_regex_args(
583    &mut self,
584    capability: &CapabilityMeta,
585    args: &[AstExpression],
586    span: SourceSpan,
587  ) {
588    for regex_arg in &capability.regex_args {
589      let Some(arg) = args.get(regex_arg.index) else {
590        continue;
591      };
592      match self.analyzer.profile.default_regex_policy {
593        RegexPolicy::Forbid => self.diagnostics.push(Diagnostic::new(
594          "regex arguments are forbidden by profile",
595          span,
596        )),
597        RegexPolicy::LiteralOnlyPrecompiled => {
598          let Some(pattern) = string_literal(arg) else {
599            self.diagnostics.push(Diagnostic::new(
600              "regex argument must be a string literal",
601              arg.span,
602            ));
603            continue;
604          };
605          let literal = RegexLiteral {
606            pattern,
607            flavor: regex_arg.flavor,
608            span: arg.span,
609          };
610          if let Err(error) = self.regex_cache.insert(&literal) {
611            self.diagnostics.push(Diagnostic::new(
612              format!("invalid regex pattern: {error}"),
613              arg.span,
614            ));
615          } else {
616            self.regex_literals.push(literal);
617          }
618        }
619        RegexPolicy::DynamicWithBudget => {
620          if let Some(pattern) = string_literal(arg) {
621            let literal = RegexLiteral {
622              pattern,
623              flavor: regex_arg.flavor,
624              span: arg.span,
625            };
626            if self.regex_cache.insert(&literal).is_ok() {
627              self.regex_literals.push(literal);
628            }
629          }
630        }
631      }
632    }
633  }
634
635  fn validate_capability(&mut self, capability: &CapabilityMeta, span: SourceSpan) {
636    self.validate_capability_phase(capability, span);
637    if matches!(self.analyzer.profile.determinism, Determinism::Required) {
638      if !capability.deterministic {
639        self.diagnostics.push(Diagnostic::new(
640          format!(
641            "{} {} is non-deterministic but profile requires determinism",
642            support::capability_kind_label(capability.kind),
643            capability.name
644          ),
645          span,
646        ));
647      }
648      if !capability.side_effect_free {
649        self.diagnostics.push(Diagnostic::new(
650          format!(
651            "{} {} has side effects but profile requires side-effect-free capabilities",
652            support::capability_kind_label(capability.kind),
653            capability.name
654          ),
655          span,
656        ));
657      }
658    }
659  }
660
661  fn require_capability(&mut self, capability: &CapabilityMeta) {
662    let ticket = capability.ticket();
663    self.required_capabilities.insert(ticket.clone());
664    self
665      .required_capability_metadata
666      .insert(ticket, capability.clone());
667  }
668}
669
670fn verified_with_capability(
671  expression: VerifiedExpression,
672  ticket: Option<CapabilityTicket>,
673) -> VerifiedExpression {
674  if let Some(ticket) = ticket {
675    expression.with_capability_ticket(ticket)
676  } else {
677    expression
678  }
679}