polar-core 0.26.4

Polar core library for oso, an open source policy engine for authorization that’s embedded in your application
Documentation
use std::rc::Rc;

use super::bindings::Binding;
use super::error::{PolarError, PolarResult};
use super::formatting::source_lines;
use super::kb::KnowledgeBase;
use super::partial::simplify_bindings;
use super::terms::*;
use super::traces::*;
use super::vm::*;

impl PolarVirtualMachine {
    pub fn query_summary(&self, query: &Term) -> String {
        let relevant_bindings = self.relevant_bindings(&[query]);
        let bindings_str = relevant_bindings
            .iter()
            .map(|(var, val)| format!("{} = {}", var.0, val))
            .collect::<Vec<_>>()
            .join(", ");
        format!("QUERY: {}, BINDINGS: {{{}}}", query, bindings_str)
    }

    /// If the inner [`Debugger`](struct.Debugger.html) returns a [`Goal`](../vm/enum.Goal.html),
    /// push it onto the goal stack.
    pub fn maybe_break(&mut self, event: DebugEvent) -> PolarResult<bool> {
        self.debugger.maybe_break(event, self).map_or_else(
            || Ok(false),
            |goal| {
                self.push_goal(goal)?;
                Ok(true)
            },
        )
    }
}

/// [`Debugger`](struct.Debugger.html) step granularity.
#[derive(Clone, Debug)]
enum Step {
    /// Pause after evaluating the next [`Goal`](../vm/enum.Goal.html).
    Goal,
    /// Step **over** the current query. Will break on the next query where the trace stack is at the same
    /// level as the current one.
    Over {
        level: usize,
    },
    /// Step **out** of the current query. Will break on the next query where the trace stack is at a lower
    /// level than the current one.
    Out {
        level: usize,
    },
    /// Step **in**. Will break on the next query.
    Into,
    Error,
    Rule,
}

/// VM breakpoints.
///
/// There are currently two breakpoints in the VM, one that fires after every
/// [`Goal`](../vm/enum.Goal.html) and another that fires before every
/// [`Goal::Query`](../vm/enum.Goal.html). When either breakpoint is hit, we check the
/// [`Debugger`](struct.Debugger.html)'s internal [`step`](struct.Debugger.html#structfield.step)
/// field to determine how evaluation should proceed.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum DebugEvent {
    Goal(Rc<Goal>),
    Query,
    Pop,
    Error(PolarError),
    Rule,
}

/// Tracks internal debugger state.
#[derive(Clone, Debug, Default)]
pub struct Debugger {
    /// Next stopping point, as set by the user.
    ///
    /// - `None`: Don't stop.
    /// - `Some(step)`: View the stopping logic in
    ///   [`maybe_break`](struct.Debugger.html#method.maybe_break).
    step: Option<Step>,
    last: Option<String>,
}

impl Debugger {
    /// Retrieve the original source line (and, optionally, additional lines of context) for the
    /// current query.
    fn query_source(&self, query: &Term, num_lines: usize) -> String {
        query.parsed_context().map_or_else(
            || "".to_string(),
            |context| source_lines(&context.source, context.left, num_lines),
        )
    }

    /// When the [`VM`](../vm/struct.PolarVirtualMachine.html) hits a breakpoint, check if
    /// evaluation should pause.
    ///
    /// The check is a comparison of the [`Debugger`](struct.Debugger.html)'s
    /// [`step`](struct.Debugger.html#structfield.step) field with the passed-in
    /// [`DebugEvent`](enum.DebugEvent.html). If [`step`](struct.Debugger.html#structfield.step) is
    /// set to `None`, evaluation continues. For details about how the `Some()` values of
    /// [`step`](struct.Debugger.html#structfield.step) are handled, see the explanations in the
    /// [`Step`](enum.Step.html) documentation.
    ///
    /// ## Returns
    ///
    /// - `Some(Goal::Debug { message })` -> Pause evaluation.
    /// - `None` -> Continue evaluation.
    fn maybe_break(&self, event: DebugEvent, vm: &PolarVirtualMachine) -> Option<Goal> {
        self.step.as_ref().and_then(|step| match (step, event) {
            (Step::Goal, DebugEvent::Goal(goal)) => Some(Goal::Debug {
                message: goal.to_string(),
            }),
            (Step::Into, DebugEvent::Query) => self.break_query(vm),
            (Step::Out { level }, DebugEvent::Query)
                if vm.trace_stack.is_empty() || vm.trace_stack.len() < *level =>
            {
                self.break_query(vm)
            }
            (Step::Over { level }, DebugEvent::Query) if vm.trace_stack.len() == *level => {
                self.break_query(vm)
            }
            (Step::Error, DebugEvent::Error(error)) => {
                let context = error
                    .get_context()
                    .map_or_else(|| "".into(), |c| c.source_position());
                self.break_msg(vm).map(|message| Goal::Debug {
                    message: format!("{}\nERROR: {}{}\n", message, error.0, context),
                })
            }
            (Step::Rule, DebugEvent::Rule) => self.break_query(vm),
            _ => None,
        })
    }

    pub fn break_msg(&self, vm: &PolarVirtualMachine) -> Option<String> {
        vm.trace.last().and_then(|trace| match trace.node {
            Node::Term(ref q) => match q.value() {
                Value::Expression(Operation {
                    operator: Operator::And,
                    args,
                }) if args.len() == 1 => None,
                _ => {
                    let source = self.query_source(q, 3);
                    Some(format!("{}\n\n{}\n", vm.query_summary(q), source))
                }
            },
            Node::Rule(ref r) => Some(r.to_string()),
        })
    }

    /// Produce the `Goal::Debug` for breaking on a Query (as opposed to breaking on a Goal).
    /// This is used to implement the `step`, `over`, and `out` debug commands.
    fn break_query(&self, vm: &PolarVirtualMachine) -> Option<Goal> {
        self.break_msg(vm).map(|message| Goal::Debug { message })
    }

    /// Process debugging commands from the user.
    ///
    /// For informational commands (`"bindings"`, `"goals"`, `"line"`, `"queries"`, and `"var"`),
    /// look up relevant data via the passed-in
    /// [`PolarVirtualMachine`](../vm/struct.PolarVirtualMachine.html), format it, and return a
    /// [`Goal::Debug`](../vm/enum.Goal.html) containing the formatted string that will be
    /// displayed to the user.
    ///
    /// For movement commands (`"continue"`, `"over"`, `"out"`, `"step"`), set the internal state
    /// of the [`Debugger`](struct.Debugger.html) to the appropriate
    /// [`Option<Step>`](struct.Debugger.html#structfield.step).
    pub fn debug_command(&mut self, command: &str, vm: &PolarVirtualMachine) -> Option<Goal> {
        fn show<T>(stack: &[T]) -> Goal
        where
            T: std::fmt::Display,
        {
            Goal::Debug {
                message: stack
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join("\n"),
            }
        }
        let parts: Vec<&str> = command.split_whitespace().collect();
        let default_command = match self.last.take() {
            Some(s) => s,
            _ => "help".to_owned(),
        };
        let command = *parts.first().unwrap_or(&&default_command[..]);
        self.last = Some(String::from(command));
        match command {
            "c" | "continue" | "q" | "quit" => self.step = None,

            "n" | "next" | "over" => {
                self.step = Some(Step::Over{ level: vm.trace_stack.len() })
            }
            "s" | "step" | "into" => {
                self.step = Some(Step::Into)
            }
            "o" | "out" => {
                self.step = Some(Step::Out{ level: vm.trace_stack.len() })
            }
            "g" | "goal" => {
                self.step = Some(Step::Goal)
            }
            "e" | "error" => {
                self.step = Some(Step::Error)
            }
            "r" | "rule" => {
                self.step = Some(Step::Rule)
            }
            "l" | "line" => {
                let lines = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
                return Some(Goal::Debug {
                    message: vm.queries.last().map_or_else(
                        || "".to_string(),
                        |query| self.query_source(query, lines),
                    ),
                });
            }
            "query" => {
                let mut level = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
                let mut trace_stack = vm.trace_stack.clone();

                // Walk up the trace stack to get the query at the requested level.
                let mut term = vm.trace.last().and_then(|t| t.term());
                while level > 0 {
                    if let Some(trace) = trace_stack.pop().map(|ts| ts.as_ref().clone()) {
                        if let Some(t) = trace.last() {
                            if let Trace{node: Node::Term(t), ..} = &**t {
                                term = Some(t.clone());
                                level-=1;
                            }
                        }
                    } else {
                        return Some(Goal::Debug {
                            message: "Error: level is out of range".to_owned()
                        })
                    }
                }

                if let Some(query) = term {
                    return Some(Goal::Debug {
                        message: vm.query_summary(&query)});
                } else {
                    return Some(Goal::Debug {
                        message: "".to_owned()
                    })
                }
            }
            "stack" | "trace" => {
                return Some(Goal::Debug {
                    message: vm.stack_trace()
                })
            }
            "goals" => return Some(show(&vm.goals)),
            "bindings" => {
                return Some(show(vm.bindings_debug().as_slice()))
            }
            "var" => {
                if parts.len() > 1 {
                    let vars: Vec<Binding> = parts[1..]
                        .iter()
                        .map(|name| {
                            get_binding_for_var(name, vm)
                        })
                        .collect();
                    return Some(show(&vars));
                } else {
                    let mut vars = vm
                        .bindings(true)
                        .keys()
                        .map(|k| k.0.as_ref())
                        .collect::<Vec<_>>()
                        .join(", ");
                    if vars.is_empty() {
                        vars = "No variables in scope.".to_string();
                    }
                    return Some(Goal::Debug { message: vars });
                }
            }
            _ => {
                return Some(Goal::Debug {
                    message: "Debugger Commands
  h[elp]                  Print this help documentation.
  c[ontinue]              Continue evaluation.
  s[tep] | into           Step to the next query (will step into rules).
  n[ext] | over           Step to the next query at the same level of the query stack (will not step into rules).
  o[ut]                   Step out of the current query stack level to the next query in the level above.
  g[oal]                  Step to the next goal of the Polar VM.
  e[rror]                 Step to the next error.
  r[ule]                  Step to the next rule.
  l[ine] [<n>]            Print the current line and <n> lines of context.
  query [<i>]             Print the current query or the query at level <i> in the query stack.
  stack | trace           Print the current query stack.
  goals                   Print the current goal stack.
  bindings                Print all bindings
  var [<name> ...]        Print available variables. If one or more arguments
                          are provided, print the value of those variables.
  q[uit]                  Alias for 'continue'."
                        .to_string(),
                })
            }
        }
        None
    }
}

/// *** variable name mapping ***
/// if the requested variable is bound, then we return that binding.
/// otherwise, we look for the matching bound temp variable with the
/// highest numeric component in its name, and return that binding
/// if we find it. otherwise, show that the variable is unbound.
pub fn get_binding_for_var(name: &str, vm: &PolarVirtualMachine) -> Binding {
    let var = Symbol::new(name);
    let bindings = simplify_bindings(vm.bindings(true)).unwrap();
    bindings.get(&var).cloned().map_or_else(
        || {
            let prefix = KnowledgeBase::temp_prefix(name);
            bindings
                .keys()
                .filter_map(|k| {
                    k.0.strip_prefix(&prefix)
                        .and_then(|i| i.parse::<i64>().map_or(None, |i| Some((k, i))))
                })
                .max_by(|a, b| a.1.cmp(&b.1))
                .map_or_else(
                    || Binding(sym!(name), Term::from(sym!("<unbound>"))),
                    |b| {
                        Binding(
                            sym!(format!("{}@{}", name, b.0 .0).as_str()),
                            bindings.get(b.0).unwrap().clone(),
                        )
                    },
                )
        },
        |val| Binding(var, val),
    )
}