Compare commits

...

4 Commits

Author SHA1 Message Date
Anand Krishnamoorthi
7fde3382f6 chore: release (#210)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-22 15:09:01 -07:00
Anand Krishnamoorthi
316f3a7692 early return (#189)
If a rule is written to produce a constant value, then not all iterations of loops
within it need to be executed. Execution can stop via early return once the first iteration
that produces a value has been executed.

This brings forth the question : What if one of the subsequent iterations would have resulted
in an error?
e.g:
x {
  [1, "hello"][_] + 1
}

Such errors are not raised; consistent with OPA.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-04-22 07:41:35 -07:00
Anand Krishnamoorthi
de56cce7cb Fix anyhow dependency issues (#208)
- Do not require backtrace feature
- Starting version 1.0.77, anyhow gathers backtrace is std feature (enabled by default)
  is specified even if backtrace feature is not enabled.
  Therefore specify default features as false.
- Specify version 1.0.45 since that is the minimul version required to successfully
  compile regorus

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-04-20 09:57:52 -07:00
Anand Krishnamoorthi
72ced23366 build: remove unused compact-rc dependency (#207)
Remove unused compact-rc dependency, to avoid a build error:

error[E0658]: use of unstable library feature 'ptr_addr_eq'
<...>/registry/src/index.crates.io-6f17d22bba15001f/compact-rc-0.5.4/src/base.rs:319:9
    |
319 |         std::ptr::addr_eq(Self::as_ptr(this), Self::as_ptr(other))

Signed-off-by: Dan Mihai <dmihai@microsoft.com>
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
Co-authored-by: Dan Mihai <dmihai@microsoft.com>
2024-04-20 06:47:18 -07:00
12 changed files with 199 additions and 21 deletions

View File

@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.4](https://github.com/microsoft/regorus/compare/regorus-v0.1.3...regorus-v0.1.4) - 2024-04-22
### Other
- early return ([#189](https://github.com/microsoft/regorus/pull/189))
- Fix anyhow dependency issues ([#208](https://github.com/microsoft/regorus/pull/208))
- remove unused compact-rc dependency ([#207](https://github.com/microsoft/regorus/pull/207))
## [0.1.3](https://github.com/microsoft/regorus/compare/regorus-v0.1.2...regorus-v0.1.3) - 2024-04-11
### Other

View File

@@ -11,7 +11,7 @@ members = [
[package]
name = "regorus"
description = "A fast, lightweight Rego (OPA policy language) interpreter"
version = "0.1.3"
version = "0.1.4"
edition = "2021"
license-file = "LICENSE"
repository = "https://github.com/microsoft/regorus"
@@ -69,7 +69,7 @@ full-opa = [
opa-testutil = []
[dependencies]
anyhow = {version = "1.0.66", features = ["backtrace"] }
anyhow = { version = "1.0.45", default-features=false }
serde = {version = "1.0.150", features = ["derive", "rc"] }
serde_json = "1.0.89"
serde_yaml = {version = "0.9.16", optional = true }
@@ -96,20 +96,19 @@ uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.17.1", default-features = false, optional = true }
chrono = { version = "0.4.31", optional = true }
chrono-tz = { version = "0.8.5", optional = true }
compact-rc = "0.5.2"
jsonwebtoken = { version = "9.2.0", optional = true }
itertools = "0.12.1"
[dev-dependencies]
cfg-if = "1.0.0"
clap = { version = "4.4.7", features = ["derive"] }
colored-diff = "0.2.3"
prettydiff = { version = "0.6.4", default-features = false }
serde_yaml = "0.9.16"
test-generator = "0.3.1"
walkdir = "2.3.2"
[build-dependencies]
anyhow = "1.0.66"
anyhow = "1.0"
[profile.release]
debug = true

View File

@@ -8,7 +8,7 @@ edition = "2021"
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.79"
anyhow = "1.0"
regorus = { path = "../.." }
serde_json = "1.0.113"

View File

@@ -11,7 +11,7 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.79"
anyhow = "1.0"
serde_json = "1.0.112"
jni = "0.21.1"
regorus = { path = "../.." }

View File

@@ -12,7 +12,7 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.79"
anyhow = "1.0"
ordered-float = "4.2.0"
pyo3 = {version = "0.21.0", features = ["anyhow", "extension-module"] }
regorus = { path = "../.." }

View File

@@ -12,7 +12,7 @@ use semver::Version;
use std::cmp::Ordering;
use std::collections::HashMap;
use anyhow::{Ok, Result};
use anyhow::Result;
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
m.insert("semver.compare", (compare, 2));

View File

@@ -9,7 +9,7 @@ use crate::value::Value;
use std::collections::{BTreeMap, HashMap};
use anyhow::{Ok, Result};
use anyhow::Result;
use uuid::{Timestamp, Uuid};
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {

View File

@@ -94,6 +94,8 @@ struct Context {
rule_value: Value,
is_set: bool,
is_old_style_set: bool,
output_constness_determined: bool,
early_return: bool,
}
impl Default for Context {
@@ -109,6 +111,8 @@ impl Default for Context {
rule_value: Value::new_object(),
is_set: false,
is_old_style_set: false,
output_constness_determined: false,
early_return: false,
}
}
}
@@ -1428,6 +1432,9 @@ impl Interpreter {
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result = query_result.clone();
if ctx.early_return {
break;
}
}
}
@@ -1452,6 +1459,9 @@ impl Interpreter {
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result = query_result.clone();
if ctx.early_return {
break;
}
}
}
self.loop_var_values.remove(&loop_expr.expr());
@@ -1474,6 +1484,9 @@ impl Interpreter {
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result = query_result.clone();
if ctx.early_return {
break;
}
}
}
self.loop_var_values.remove(&loop_expr.expr());
@@ -1586,30 +1599,95 @@ impl Interpreter {
Ok(())
}
// A ref is a constant ref, if it does not contain any local variables.
// For now, we restrict constant refs to those that contain only simple literals.
fn is_constant_ref(&self, mut expr: &Ref<Expr>) -> Result<bool> {
loop {
match expr.as_ref() {
Expr::Var(_) => break,
Expr::RefDot { refr, .. } => expr = refr,
Expr::RefBrack { refr, index, .. } if self.is_simple_literal(index)? => expr = refr,
_ => return Ok(false),
}
}
Ok(true)
}
fn is_simple_literal(&self, expr: &Ref<Expr>) -> Result<bool> {
Ok(matches!(
expr.as_ref(),
Expr::String(_)
| Expr::RawString(_)
| Expr::True(_)
| Expr::False(_)
| Expr::Null(_)
| Expr::Number(_)
))
}
// A rule's output expression is constant if it does not contain local variables.
// For now, we restrict output expressions to those that contain only simple literals.
fn is_constant_output(
&self,
key_expr: &Option<Ref<Expr>>,
output_expr: &Ref<Expr>,
) -> Result<bool> {
let mut is_const = true;
if let Some(key_expr) = key_expr {
is_const = self.is_simple_literal(key_expr)?;
}
Ok(is_const && self.is_simple_literal(output_expr)?)
}
fn eval_output_expr_in_loop(&mut self, loops: &[LoopExpr]) -> Result<bool> {
if loops.is_empty() {
let (key_expr, output_expr) = self.get_exprs_from_context()?;
let ctx = self.get_current_context()?;
let (is_set, is_old_style_set) = (ctx.is_set, ctx.is_old_style_set);
let (is_set, is_old_style_set, is_rule, constness_determined) = (
ctx.is_set,
ctx.is_old_style_set,
!ctx.is_compr,
ctx.output_constness_determined,
);
if let Some(rule_ref) = ctx.rule_ref.clone() {
let mut is_const_rule = if is_rule && !constness_determined {
self.is_constant_ref(&rule_ref)?
} else {
// Constness has already been determined or is not a rule.
// Treat the expression as not constant.
false
};
let mut comps = self.eval_rule_ref(&rule_ref)?;
if let Some(ke) = &key_expr {
comps.push(self.eval_expr(ke)?);
}
let output = if let Some(oe) = &output_expr {
// Rule is constant only if its ref, key and output are constant.
is_const_rule = is_const_rule && self.is_constant_output(&key_expr, oe)?;
self.eval_expr(oe)?
} else if is_old_style_set && !comps.is_empty() {
// Rule's constness is determined only by its ref.
let output = comps[comps.len() - 1].clone();
comps.pop();
output
} else {
// Rule's constness is determined only by its ref.
Value::Bool(true)
};
let comps_defined = comps.iter().all(|v| v != &Value::Undefined);
let ctx = self.contexts.last_mut().expect("no current context");
if is_const_rule {
ctx.early_return = true;
}
if is_rule {
ctx.output_constness_determined = true;
}
if output == Value::Undefined || !comps_defined {
return Ok(false);
}

View File

@@ -114,7 +114,17 @@ impl<'source> Parser<'source> {
}
Expr::Var(v) => comps.push(v.0.clone()),
Expr::String(s) => comps.push(s.0.clone()),
_ => bail!("internal error: not a simple ref"),
Expr::True(s) | Expr::False(s) | Expr::Null(s) => comps.push(s.clone()),
Expr::Number(s) => {
// Ensure that the span will be the serialized representation.
if *s.0.text() == s.1.to_json_str()? {
comps.push(s.0.clone());
} else {
bail!(refr.span().error("not a valid ref"));
}
}
_ => bail!(refr.span().error("not a valid ref")),
}
Ok(())
}

View File

@@ -73,10 +73,10 @@ fn match_values(computed: &Value, expected: &Value) -> Result<()> {
if computed != expected {
panic!(
"{}",
colored_diff::PrettyDifference {
expected: &serde_yaml::to_string(&expected)?,
actual: &serde_yaml::to_string(&computed)?
}
prettydiff::diff_chars(
&serde_yaml::to_string(&expected)?,
&serde_yaml::to_string(&computed)?
)
);
}
Ok(())
@@ -347,7 +347,7 @@ fn yaml_test(file: &str) -> Result<()> {
Err(e) => {
// If Err is returned, it doesn't always get printed by cargo test.
// Therefore, panic with the error.
panic!("{}", e);
panic!("{e}");
}
}
}

View File

@@ -92,10 +92,10 @@ fn run_aci_tests(dir: &Path) -> Result<()> {
Ok(actual) => {
println!(
"DIFF {}",
colored_diff::PrettyDifference {
expected: &serde_yaml::to_string(&case.want_result)?,
actual: &serde_yaml::to_string(&actual)?
}
prettydiff::diff_chars(
&serde_yaml::to_string(&case.want_result)?,
&serde_yaml::to_string(&actual)?
)
);
nfailures += 1;

View File

@@ -21,3 +21,87 @@ cases:
x1: [[1, 0], [2, 1], [3, 2], [4, 3]]
x2: [[1, 1], [2, 2], [3, 3], [4, 4]]
x3: [["q", "p"], ["s", "r"]]
- note: early return
data: {}
modules:
- |
package test
import future.keywords
a = [1, "hello"]
# Implicit value
b1 {
a[_] + 1
}
# Literals
b2 := true { a[_] + 1 }
b3 := false { a[_] + 1 }
b4 := 1 { a[_] + 1 }
b5 := null { a[_] + 1 }
b6 := "hello" { a[_] + 1 }
b7 := `world` { a[_] + 1 }
# constant refs
c[null] := true { a[_] + 1 }
c["hello"] := false { a[_] + 1 }
c[`world`] := 1 { a[_] + 1 }
c[true] := null { a[_] + 1 }
c[false] := "hello" { a[_] + 1 }
c[7] := `world` { a[_] + 1 }
# Old style set must should also be considered for early return.
old.style { a[_] + 1 }
# Multi part constant refactor
multi[1]["hello"] := 5 { a[_] +1 }
# Two elements must be produced
d = [1 | [1,2][_] ]
# Non simple ref must not result in early return.
f[p] = 5 {
p := a[_]
}
f1[p] = 5 {
a[p]
}
# Contains syntax
g contains p if {
p := a[_]
}
query: data.test
want_result:
a: [1, "hello"]
b1: true
b2: true
b3: false
b4: 1
b5: null
b6: "hello"
b7: "world"
c:
null: true
"hello": false
"world": 1
true: null
false: "hello"
7: "world"
d: [1, 1]
f:
1: 5
"hello": 5
f1:
0: 5
1: 5
g:
set!: [1, "hello"]
multi:
1:
"hello": 5
old:
set!: ["style"]