diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ec4b4b9b..1dc0a5a7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,6 +18,8 @@ jobs: - uses: actions/checkout@v3 - name: Add musl target run: rustup target add x86_64-unknown-linux-musl + - name: Add no_std target + run: rustup target add thumbv7m-none-eabi - name: Install musl-gcc run: sudo apt update && sudo apt install -y musl-tools - name: Format Check @@ -26,6 +28,12 @@ jobs: run: cargo build -r --all-features --verbose - name: Build run: cargo build -r --verbose + - name: Build no_std + run: cd tests/ensure_no_std && cargo build -r --target thumbv7m-none-eabi + - name: Test no_std + run: cargo test -r --no-default-features + - name: Build only std + run: cargo build -r --example regorus --no-default-features --features "std" - name: Doc Tests run: cargo test -r --doc - name: Run tests diff --git a/Cargo.toml b/Cargo.toml index 7d753bbf..6ee7d065 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "bindings/wasm", "bindings/java", "bindings/ruby/ext/regorusrb", + "tests/ensure_no_std", ] [package] @@ -15,7 +16,7 @@ version = "0.1.5" edition = "2021" license-file = "LICENSE" repository = "https://github.com/microsoft/regorus" -keywords = ["interpreter", "opa", "policy-as-code", "rego"] +keywords = ["interpreter", "no_std", "opa", "policy-as-code", "rego"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -33,14 +34,15 @@ crypto = ["dep:constant_time_eq", "dep:hmac", "dep:hex", "dep:md-5", "dep:sha1", deprecated = [] hex = ["dep:data-encoding"] http = [] -jwt = ["dep:jsonwebtoken", "dep:data-encoding"] glob = ["dep:wax"] graph = [] jsonschema = ["dep:jsonschema"] +jwt = ["dep:jsonwebtoken", "dep:data-encoding", "dep:itertools"] +no_std = ["lazy_static/spin_no_std"] opa-runtime = [] regex = ["dep:regex"] semver = ["dep:semver"] -std = ["serde_json/std"] +std = ["rand/std", "rand/std_rng", "serde_json/std"] time = ["dep:chrono", "dep:chrono-tz"] uuid = ["dep:uuid"] urlquery = ["dep:url"] @@ -67,41 +69,62 @@ full-opa = [ "yaml" ] +# Features that can be used in no_std environments. +# Note that: the spin_no_std feature in lazy_static must be specified. +opa-no-std = [ + "arc", + "base64", + "base64url", + "coverage", + "crypto", + "deprecated", + "graph", + "hex", + "no_std", + "opa-runtime", + "regex", + "semver", + # Configure lazy_static to use spinlocks. + "lazy_static/spin_no_std" +] + # This feature enables some testing utils for OPA tests. opa-testutil = [] +rand = ["dep:rand"] [dependencies] -anyhow = { version = "1.0.45", default-features=false } +anyhow = { version = "1.0.45", default-features = false } serde = {version = "1.0.150", default-features = false, features = ["derive", "rc"] } -serde_json = { version = "1.0.89", default-features=false, features = ["alloc"] } -serde_yaml = {version = "0.9.16", optional = true } -lazy_static = "1.4.0" -rand = "0.8.5" -num = "0.4.1" +serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] } +lazy_static = { version = "1.4.0", default-features = false } # Crypto -constant_time_eq = {version = "0.3.0", optional = true} -hmac = {version = "0.12.1", optional = true} -sha2 = {version= "0.10.8", optional = true} -hex = {version = "0.4.3", optional = true} -sha1 = {version = "0.10.6", optional = true} -md-5 = {version = "0.10.6", optional = true} - -data-encoding = { version = "2.4.0", optional = true } +constant_time_eq = {version = "0.3.0", optional = true, default-features = false } +hmac = {version = "0.12.1", optional = true, default-features = false} +sha2 = {version= "0.10.8", optional = true, default-features = false } +hex = {version = "0.4.3", optional = true, default-features = false, features = ["alloc"] } +sha1 = {version = "0.10.6", optional = true, default-features = false } +md-5 = {version = "0.10.6", optional = true, default-features = false } + +data-encoding = { version = "2.4.0", optional = true, default-features=false, features = ["alloc"] } scientific = { version = "0.5.2" } -regex = {version = "1.10.2", optional = true} -semver = {version = "1.0.20", optional = true} +regex = {version = "1.10.2", optional = true, default-features = false } +semver = {version = "1.0.20", optional = true, default-features = false } wax = { version = "0.6.0", features = [], default-features = false, optional = true } url = { version = "2.5.0", optional = true } -uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true } +uuid = { version = "1.6.1", default-features = false, 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 } jsonwebtoken = { version = "9.2.0", optional = true } -itertools = "0.12.1" +itertools = { version = "0.12.1", default-features = false, optional = true } + +serde_yaml = {version = "0.9.16", default-features = false, optional = true } +rand = { version = "0.8.5", default-features = false, optional = true } [dev-dependencies] +anyhow = "1.0.45" cfg-if = "1.0.0" clap = { version = "4.4.7", features = ["derive"] } prettydiff = { version = "0.6.4", default-features = false } @@ -131,6 +154,12 @@ name="kata" harness=false test=false +[[example]] +name="regorus" +harness=false +test=false +doctest=false + [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps diff --git a/README.md b/README.md index 66752b7f..be9adb51 100644 --- a/README.md +++ b/README.md @@ -88,14 +88,14 @@ features. By default all features are enabled. The default build of regorus example program is 6.4M: ```bash $ cargo build -r --example regorus; strip target/release/examples/regorus; ls -lh target/release/examples/regorus --rwxr-xr-x 1 anand staff 6.4M Jan 19 11:23 target/release/examples/regorus* +-rwxr-xr-x 1 anand staff 6.3M May 11 22:03 target/release/examples/regorus* ``` -When all features except for `yaml` are disabled, the binary size drops down to 2.9M. +When all default features are disabled, the binary size drops down to 1.9M. ```bash -$ cargo build -r --example regorus --features "yaml" --no-default-features; strip target/release/examples/regorus; ls -lh target/release/examples/regorus --rwxr-xr-x 1 anand staff 2.9M Jan 19 11:26 target/release/examples/regorus* +$ cargo build -r --example regorus --no-default-features; strip target/release/examples/regorus; ls -lh target/release/examples/regorus +-rwxr-xr-x 1 anand staff 1.9M May 11 22:04 target/release/examples/regorus* ``` Regorus passes the [OPA v0.64.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few diff --git a/examples/regorus.rs b/examples/regorus.rs index 22118da3..4d4b437e 100644 --- a/examples/regorus.rs +++ b/examples/regorus.rs @@ -1,7 +1,37 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; + +#[allow(dead_code)] +fn read_file(path: &String) -> Result { + std::fs::read_to_string(path).map_err(|_| anyhow!("could not read {path}")) +} + +#[allow(unused_variables)] +fn read_value_from_yaml_file(path: &String) -> Result { + #[cfg(feature = "yaml")] + return regorus::Value::from_yaml_file(path); + + #[cfg(not(feature = "yaml"))] + bail!("regorus has not been built with yaml support"); +} + +fn read_value_from_json_file(path: &String) -> Result { + #[cfg(feature = "std")] + return regorus::Value::from_json_file(path); + + #[cfg(not(feature = "std"))] + regorus::Value::from_json_str(&read_file(path)?) +} + +fn add_policy_from_file(engine: &mut regorus::Engine, path: String) -> Result<()> { + #[cfg(feature = "std")] + return engine.add_policy_from_file(path); + + #[cfg(not(feature = "std"))] + engine.add_policy(path.clone(), read_file(&path)?) +} fn rego_eval( bundles: &[String], @@ -35,7 +65,7 @@ fn rego_eval( _ => continue, } - engine.add_policy_from_file(entry.path())?; + add_policy_from_file(&mut engine, entry.path().display().to_string())?; } } @@ -43,15 +73,15 @@ fn rego_eval( for file in files.iter() { if file.ends_with(".rego") { // Read policy file. - engine.add_policy_from_file(file)?; + add_policy_from_file(&mut engine, file.clone())?; } else { // Read data file. let data = if file.ends_with(".json") { - regorus::Value::from_json_file(file)? + read_value_from_json_file(file)? } else if file.ends_with(".yaml") { - regorus::Value::from_yaml_file(file)? + read_value_from_yaml_file(file)? } else { - bail!("Unsupported data file `{file}`. Must be rego, json or yaml.") + bail!("Unsupported data file `{file}`. Must be rego, json or yaml."); }; // Merge given data. @@ -61,9 +91,9 @@ fn rego_eval( if let Some(file) = input { let input = if file.ends_with(".json") { - regorus::Value::from_json_file(&file)? + read_value_from_json_file(&file)? } else if file.ends_with(".yaml") { - regorus::Value::from_yaml_file(&file)? + read_value_from_yaml_file(&file)? } else { bail!("Unsupported input file `{file}`. Must be json or yaml.") }; @@ -95,8 +125,12 @@ fn rego_lex(file: String, verbose: bool) -> Result<()> { use regorus::unstable::*; // Create source. + #[cfg(feature = "std")] let source = Source::from_file(file)?; + #[cfg(not(feature = "std"))] + let source = Source::from_contents(file.clone(), read_file(&file)?)?; + // Create lexer. let mut lexer = Lexer::new(&source); @@ -122,8 +156,12 @@ fn rego_parse(file: String) -> Result<()> { use regorus::unstable::*; // Create source. + #[cfg(feature = "std")] let source = Source::from_file(file)?; + #[cfg(not(feature = "std"))] + let source = Source::from_contents(file.clone(), read_file(&file)?)?; + // Create a parser and parse the source. let mut parser = Parser::new(&source)?; let ast = parser.parse()?; diff --git a/scripts/pre-push b/scripts/pre-push index 7a2bec87..d57f1309 100755 --- a/scripts/pre-push +++ b/scripts/pre-push @@ -5,17 +5,27 @@ set -eo pipefail if [ -f Cargo.toml ]; then - # Run precommit checks + # Run precommit checks. dir=$(dirname "${BASH_SOURCE[0]}") "$dir/pre-commit" - # Ensure that the public API works + # Ensure that the public API works. cargo test -r --doc - # Ensure that we can build with all features + # Ensure that no_std build succeeds. + # Build for a target that has no std available. + if command -v rustup > /dev/null; then + rustup target add thumbv7m-none-eabi + (cd tests/ensure_no_std; cargo build -r --target thumbv7m-none-eabi) + fi + + # Ensure that we can build with only std. + cargo build -r --example regorus --no-default-features --features std + + # Ensure that we can build with all features. cargo build -r --all-features - # Ensure that all tests pass + # Ensure that all tests pass. cargo test -r cargo test -r --test aci cargo test -r --test kata diff --git a/src/builtins/debugging.rs b/src/builtins/debugging.rs deleted file mode 100644 index 003e1383..00000000 --- a/src/builtins/debugging.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::ast::{Expr, Ref}; -use crate::builtins; -use crate::lexer::Span; -use crate::value::Value; -use crate::*; - -use anyhow::{bail, Result}; - -// TODO: Should we avoid this limit? -const MAX_ARGS: u8 = core::u8::MAX; - -pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) { - m.insert("print", (print, MAX_ARGS)); -} - -pub fn print_to_string( - span: &Span, - _params: &[Ref], - args: &[Value], - _strict: bool, -) -> Result { - if args.len() > MAX_ARGS as usize { - bail!(span.error("print supports up to 100 arguments")); - } - - let mut msg = String::default(); - for a in args { - match a { - Value::Undefined => msg += " ", - Value::String(s) => msg += &format!(" {s}"), - _ => msg += &format!(" {a}"), - }; - } - - Ok(msg) -} - -// Symbol analyzer must ensure that vars used by print are defined before -// the print statement. Scheduler must ensure the above constraint. -// Additionally interpreter must allow undefined inputs to print. -fn print(span: &Span, params: &[Ref], args: &[Value], strict: bool) -> Result { - let msg = print_to_string(span, params, args, strict)?; - - #[cfg(feature = "std")] - if !msg.is_empty() { - std::eprintln!("{}", &msg[1..]); - } - - Ok(Value::Bool(true)) -} diff --git a/src/builtins/encoding.rs b/src/builtins/encoding.rs index 6c05a056..0c89ab8a 100644 --- a/src/builtins/encoding.rs +++ b/src/builtins/encoding.rs @@ -63,7 +63,13 @@ fn base64_decode( ensure_args_count(span, name, params, args, 1)?; let encoded_str = ensure_string(name, ¶ms[0], &args[0])?; - let decoded_bytes = data_encoding::BASE64.decode(encoded_str.as_bytes())?; + let decoded_bytes = data_encoding::BASE64 + .decode(encoded_str.as_bytes()) + .map_err(|e| { + params[0] + .span() + .error(&format!("decode failed\nCaused by\n{e}")) + })?; Ok(Value::String( String::from_utf8_lossy(&decoded_bytes).into(), )) @@ -173,7 +179,13 @@ fn hex_decode(span: &Span, params: &[Ref], args: &[Value], _strict: bool) ensure_args_count(span, name, params, args, 1)?; let encoded_str = ensure_string(name, ¶ms[0], &args[0])?; - let decoded_bytes = data_encoding::HEXLOWER_PERMISSIVE.decode(encoded_str.as_bytes())?; + let decoded_bytes = data_encoding::HEXLOWER_PERMISSIVE + .decode(encoded_str.as_bytes()) + .map_err(|e| { + params[0] + .span() + .error(&format!("decode failure\nCaused by\n{e}")) + })?; Ok(Value::String( String::from_utf8_lossy(&decoded_bytes).into(), )) @@ -361,11 +373,9 @@ fn json_is_valid( fn json_marshal(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { let name = "json.marshal"; ensure_args_count(span, name, params, args, 1)?; - Ok(Value::String( - serde_json::to_string(&args[0]) - .with_context(|| span.error("could not serialize to json"))? - .into(), - )) + Ok(Value::from(serde_json::to_string(&args[0]).map_err( + |e| span.error(&format!("could not serialize to json\nCaused by\n{e}")), + )?)) } fn json_marshal_with_options( @@ -406,15 +416,13 @@ fn json_marshal_with_options( } if !pretty || options.is_empty() { - return Ok(Value::String( - serde_json::to_string(&args[0]) - .with_context(|| span.error("could not serialize to json"))? - .into(), - )); + return Ok(Value::from(serde_json::to_string(&args[0]).map_err( + |e| span.error(&format!("could not serialize to json\nCaused by\n{e}")), + )?)); } let lines: Vec = serde_json::to_string_pretty(&args[0]) - .with_context(|| span.error("could not serialize to json"))? + .map_err(|e| span.error(&format!("could not serialize to json\nCaused by\n{e}")))? .split('\n') .map(|line| { let mut line = line.to_string(); diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 9abfd589..adf6c89c 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -9,7 +9,6 @@ mod conversions; #[cfg(feature = "crypto")] mod crypto; -mod debugging; #[cfg(feature = "deprecated")] pub mod deprecated; mod encoding; @@ -54,8 +53,6 @@ use lazy_static::lazy_static; pub type BuiltinFcn = (fn(&Span, &[Ref], &[Value], bool) -> Result, u8); -pub use debugging::print_to_string; - #[cfg(feature = "deprecated")] pub use deprecated::DEPRECATED; @@ -104,7 +101,6 @@ lazy_static! { //rego::register(&mut m); #[cfg(feature = "opa-runtime")] opa::register(&mut m); - debugging::register(&mut m); tracing::register(&mut m); units::register(&mut m); diff --git a/src/builtins/numbers.rs b/src/builtins/numbers.rs index a3bf55cc..87675386 100644 --- a/src/builtins/numbers.rs +++ b/src/builtins/numbers.rs @@ -3,13 +3,15 @@ use crate::ast::{ArithOp, Expr, Ref}; use crate::builtins; -use crate::builtins::utils::{ensure_args_count, ensure_numeric, ensure_string}; +use crate::builtins::utils::{ensure_args_count, ensure_numeric}; use crate::lexer::Span; use crate::number::Number; use crate::value::Value; use crate::*; use anyhow::{bail, Result}; + +#[cfg(feature = "std")] use rand::{thread_rng, Rng}; pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) { @@ -18,6 +20,7 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn m.insert("floor", (floor, 1)); m.insert("numbers.range", (range, 2)); m.insert("numbers.range_step", (range_step, 3)); + #[cfg(feature = "std")] m.insert("rand.intn", (intn, 2)); m.insert("round", (round, 1)); } @@ -155,10 +158,11 @@ fn round(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re )) } +#[cfg(feature = "std")] fn intn(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { let fcn = "rand.intn"; ensure_args_count(span, fcn, params, args, 2)?; - let _ = ensure_string(fcn, ¶ms[0], &args[0])?; + let _ = crate::builtins::utils::ensure_string(fcn, ¶ms[0], &args[0])?; let n = ensure_numeric(fcn, ¶ms[0], &args[1])?; Ok(match n.as_u64() { diff --git a/src/builtins/opa.rs b/src/builtins/opa.rs index fc388650..b427b779 100644 --- a/src/builtins/opa.rs +++ b/src/builtins/opa.rs @@ -38,6 +38,7 @@ fn opa_runtime(span: &Span, params: &[Ref], args: &[Value], _strict: bool) ); // Emitting environment variables could lead to confidential data being leaked. + #[cfg(feature = "std")] if false { obj.insert( Value::String("env".into()), diff --git a/src/builtins/semver.rs b/src/builtins/semver.rs index 8154fd38..6f4c916d 100644 --- a/src/builtins/semver.rs +++ b/src/builtins/semver.rs @@ -24,8 +24,8 @@ fn compare(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> let v1 = ensure_string(name, ¶ms[0], &args[0])?; let v2 = ensure_string(name, ¶ms[1], &args[1])?; - let version1 = Version::parse(&v1)?; - let version2 = Version::parse(&v2)?; + let version1 = Version::parse(&v1).map_err(|_| params[0].span().error("invalid semver"))?; + let version2 = Version::parse(&v2).map_err(|_| params[0].span().error("invalid semver"))?; let result = match version1.cmp_precedence(&version2) { Ordering::Less => -1, Ordering::Equal => 0, diff --git a/src/builtins/test.rs b/src/builtins/test.rs index 0fe42d06..7a6d5264 100644 --- a/src/builtins/test.rs +++ b/src/builtins/test.rs @@ -7,6 +7,7 @@ use crate::builtins::time; use crate::builtins::utils::{ensure_args_count, ensure_string}; use crate::lexer::Span; use crate::value::Value; +use crate::*; use std::thread; @@ -21,7 +22,8 @@ fn sleep(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re ensure_args_count(span, name, params, args, 1)?; let val = ensure_string(name, ¶ms[0], &args[0])?; - let dur = time::compat::parse_duration(val.as_ref())?; + let dur = time::compat::parse_duration(val.as_ref()) + .map_err(|e| params[0].span().error(&format!("{e}")))?; thread::sleep(dur.to_std()?); diff --git a/src/builtins/time.rs b/src/builtins/time.rs index 12cf4743..8bc2ffa6 100644 --- a/src/builtins/time.rs +++ b/src/builtins/time.rs @@ -147,7 +147,7 @@ fn parse_duration_ns( ensure_args_count(span, name, params, args, 1)?; let value = ensure_string(name, ¶ms[0], &args[0])?; - let dur = compat::parse_duration(value.as_ref())?; + let dur = compat::parse_duration(value.as_ref()).map_err(anyhow::Error::msg)?; safe_timestamp_nanos(span, strict, dur.num_nanoseconds()) } diff --git a/src/builtins/time/compat.rs b/src/builtins/time/compat.rs index e2e0b440..0d491d04 100644 --- a/src/builtins/time/compat.rs +++ b/src/builtins/time/compat.rs @@ -34,7 +34,6 @@ use crate::*; use core::fmt; use core::iter; -use std::error::Error; use chrono::TimeZone; use chrono::{ @@ -72,8 +71,6 @@ impl fmt::Display for ParseDurationError { } } -impl Error for ParseDurationError {} - // Parses a duration string in the form of `10h12m45s`. // // Adapted from Go's `time.ParseDuration`: diff --git a/src/interpreter.rs b/src/interpreter.rs index caa0a299..a6a4233c 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -61,6 +61,7 @@ pub struct Interpreter { builtins_cache: BTreeMap<(&'static str, Vec), Value>, no_rules_lookup: bool, traces: Option>>, + #[cfg(feature = "deprecated")] allow_deprecated: bool, strict_builtin_errors: bool, imports: BTreeMap>, @@ -186,6 +187,7 @@ impl Interpreter { builtins_cache: BTreeMap::new(), no_rules_lookup: false, traces: None, + #[cfg(feature = "deprecated")] allow_deprecated: true, strict_builtin_errors: true, imports: BTreeMap::default(), @@ -1208,7 +1210,7 @@ impl Interpreter { let mut target = path.join("."); let mut target_is_function = self.lookup_function_by_name(&target).is_some() - || matches!(self.lookup_builtin(wm.refr.span(), &target), Ok(Some(_))); + || self.is_builtin(wm.refr.span(), &target); if !target_is_function && !target.starts_with("data.") @@ -1217,7 +1219,7 @@ impl Interpreter { { // target must be a function. if self.lookup_function_by_name(&target).is_none() - && !matches!(self.lookup_builtin(wm.refr.span(), &target), Ok(Some(_))) + && !self.is_builtin(wm.refr.span(), &target) { // Prefix target with current module path. target = self.current_module_path.clone() + "." + ⌖ @@ -1244,10 +1246,7 @@ impl Interpreter { // Lookup without current module path prefixed. function_path = get_path_string(&wm.r#as, None)?; if self.lookup_function_by_name(&function_path).is_none() - && !matches!( - self.lookup_builtin(wm.r#as.span(), &function_path), - Ok(Some(_)) - ) + && !self.is_builtin(wm.r#as.span(), &function_path) { // bail!(wm.r#as.span().error("could not evaluate expression")); skip_exec = true; @@ -1743,9 +1742,9 @@ impl Interpreter { span.col, format!( "value for key `{}` generated multiple times: `{}` and `{}`", - serde_json::to_string_pretty(&key)?, - serde_json::to_string_pretty(&pv)?, - serde_json::to_string_pretty(&value)?, + serde_json::to_string_pretty(&key).map_err(anyhow::Error::msg)?, + serde_json::to_string_pretty(&pv).map_err(anyhow::Error::msg)?, + serde_json::to_string_pretty(&value).map_err(anyhow::Error::msg)?, ) .as_str(), )); @@ -2121,27 +2120,11 @@ impl Interpreter { name: &str, builtin: builtins::BuiltinFcn, params: &[ExprRef], + args: Vec, ) -> Result { - let mut args = vec![]; - let is_print = name == "print"; // TODO: with modifier - let allow_undefined = is_print; - for p in params { - match self.eval_expr(p)? { - // If any argument is undefined, then the call is undefined. - Value::Undefined if !allow_undefined => return Ok(Value::Undefined), - p => args.push(p), - } - } - - if is_print && self.gather_prints { - // Do not print to stderr. Instead, gather. - let msg = - builtins::print_to_string(span, params, &args[..], self.strict_builtin_errors)?; - - // Prefix location information. - self.prints - .push(format!("{}:{}: {msg}", span.source.file(), span.line)); - return Ok(Value::Bool(true)); + // If any argument is undefined, then the call is undefined. + if args.iter().any(|a| a == &Value::Undefined) { + return Ok(Value::Undefined); } let cache = builtins::must_cache(name); @@ -2173,6 +2156,7 @@ impl Interpreter { Ok(v) } + #[allow(unused_variables)] fn lookup_builtin(&self, span: &Span, path: &str) -> Result> { if let Some(builtin) = builtins::BUILTINS.get(path) { return Ok(Some(builtin)); @@ -2187,12 +2171,92 @@ impl Interpreter { return Ok(Some(builtin)); } - // Mark as used when deprecated feature is not enabled. - core::convert::identity((span, self.allow_deprecated)); - Ok(None) } + fn is_builtin(&self, span: &Span, path: &str) -> bool { + path == "print" || matches!(self.lookup_builtin(span, path), Ok(Some(_))) + } + + fn to_printable(v: &Value, s: &mut String) { + match v { + Value::Array(array) => { + s.push('['); + for (idx, e) in array.iter().enumerate() { + if idx > 0 { + s.push_str(", "); + } + Self::to_printable(e, s); + } + s.push(']'); + } + Value::Set(set) => { + s.push('{'); + for (idx, e) in set.iter().enumerate() { + if idx > 0 { + s.push_str(", "); + } + Self::to_printable(e, s); + } + s.push('}'); + } + Value::Object(map) => { + s.push('{'); + for (idx, (k, v)) in map.iter().enumerate() { + if idx > 0 { + s.push_str(", "); + } + Self::to_printable(k, s); + s.push_str(": "); + Self::to_printable(v, s); + } + s.push('}'); + } + v => s.push_str(&format!("{v}")), + } + } + + fn eval_print(&mut self, span: &Span, params: &[ExprRef], args: Vec) -> Result { + const MAX_ARGS: u8 = 100; + if args.len() > MAX_ARGS as usize { + bail!(span.error(&format!("print supports upto {MAX_ARGS} arguments"))); + } + + // If not compiling for std target, return early if gathering is not + // requested. + #[cfg(not(feature = "std"))] + if !self.gather_prints { + return Ok(Value::Bool(true)); + } + + let mut msg = String::default(); + for (i, p) in params.iter().enumerate() { + if i > 0 { + msg.push(' '); + } + match self.eval_expr(p)? { + Value::Undefined => msg.push_str(""), + // Do not print quotes for string values. + Value::String(s) => msg.push_str(&format!("{s}")), + a => Self::to_printable(&a, &mut msg), + } + } + + if self.gather_prints { + // Prefix location information. + self.prints + .push(format!("{}:{}: {msg}", span.source.file(), span.line)); + } + + // Print to stderr only if not gathering. + #[cfg(feature = "std")] + if !self.gather_prints { + std::eprintln!("{msg}"); + } + + Ok(Value::Bool(true)) + } + fn eval_call_impl( &mut self, span: &Span, @@ -2261,10 +2325,18 @@ impl Interpreter { else if let Some(ext) = self.extensions.get_mut(&fcn_path) { extension = Some(ext); (&empty, None) + } else if fcn_path == "print" { + return self.eval_print(span, params, param_values); } // Look up builtin function. else if let Some(builtin) = self.lookup_builtin(span, &fcn_path)? { - let r = self.eval_builtin_call(span, &fcn_path.clone(), *builtin, params); + let r = self.eval_builtin_call( + span, + &fcn_path.clone(), + *builtin, + params, + param_values, + ); if let Some(with_functions) = with_functions_saved { self.with_functions = with_functions; } diff --git a/src/lib.rs b/src/lib.rs index f30546c1..a63befbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -404,30 +404,29 @@ pub mod coverage { /// pub fn to_colored_string(&self) -> anyhow::Result { - use std::io::Write; - let mut s = Vec::new(); - writeln!(&mut s, "COVERAGE REPORT:")?; + let mut s = String::default(); + s.push_str("COVERAGE REPORT:\n"); for file in self.files.iter() { if file.not_covered.is_empty() { - writeln!(&mut s, "{} has full coverage", file.path)?; + s.push_str(&format!("{} has full coverage\n", file.path)); continue; } - writeln!(&mut s, "{}:", file.path)?; + s.push_str(&format!("{}:", file.path)); for (line, code) in file.code.split('\n').enumerate() { let line = line as u32 + 1; if file.not_covered.contains(&line) { - writeln!(&mut s, "\x1b[31m {line:4} {code}\x1b[0m")?; + s.push_str(&format!("\x1b[31m {line:4} {code}\x1b[0m\n")); } else if file.covered.contains(&line) { - writeln!(&mut s, "\x1b[32m {line:4} {code}\x1b[0m")?; + s.push_str(&format!("\x1b[32m {line:4} {code}\x1b[0m\n")); } else { - writeln!(&mut s, " {line:4} {code}")?; + s.push_str(&format!(" {line:4} {code}\n")); } } } - writeln!(&mut s)?; - Ok(core::str::from_utf8(&s)?.to_string()) + s.push('\n'); + Ok(s) } } } diff --git a/src/parser.rs b/src/parser.rs index 3f34eab0..45d45b8a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -70,16 +70,19 @@ impl<'source> Parser<'source> { } pub fn warn_future_keyword(&self) { - let kw = self.token_text(); - let msg = format!( - "`{kw}` will be treated as identifier due to missing `import future.keywords.{kw}`" - ); #[cfg(feature = "std")] - std::println!( - "{}", - self.source - .message(self.tok.1.line, self.tok.1.col, "warning", &msg) - ); + { + let kw = self.token_text(); + let msg = format!( + "`{kw}` will be treated as identifier due to missing `import future.keywords.{kw}`" + ); + + std::println!( + "{}", + self.source + .message(self.tok.1.line, self.tok.1.col, "warning", &msg) + ); + } } pub fn set_future_keyword(&mut self, kw: &str, span: &Span) -> Result<()> { diff --git a/src/scheduler.rs b/src/scheduler.rs index 98f96c52..3645996a 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -47,7 +47,6 @@ pub fn schedule( empty: &Str, ) -> Result { let num_statements = infos.len(); - let orig_infos: Vec<&StmtInfo> = infos.iter().collect(); // Mapping from each var to the list of statements that define it. let mut defining_stmts: BTreeMap> = BTreeMap::new(); @@ -198,7 +197,7 @@ pub fn schedule( if order.len() != num_statements { #[cfg(feature = "std")] - std::eprintln!("could not schedule all statements {order:?} {orig_infos:?}"); + std::eprintln!("could not schedule all statements {order:?}"); return Ok(SortResult::Order( (0..num_statements).map(|i| i as u16).collect(), )); @@ -633,8 +632,8 @@ impl Analyzer { ) -> Result<(Vec, Vec>)> { let mut used_vars = vec![]; let mut comprs = vec![]; + #[cfg(feature = "deprecated")] let full_expr = expr; - core::convert::identity(&full_expr); traverse(expr, &mut |e| match e.as_ref() { Var(v) if !matches!(v.0.text(), "_" | "input" | "data") => { let name = v.0.source_str(); diff --git a/src/tests/interpreter/mod.rs b/src/tests/interpreter/mod.rs index a68701ef..0613f413 100644 --- a/src/tests/interpreter/mod.rs +++ b/src/tests/interpreter/mod.rs @@ -277,6 +277,35 @@ fn yaml_test_impl(file: &str) -> Result<()> { let yaml_str = std::fs::read_to_string(file)?; let test: YamlTest = serde_yaml::from_str(&yaml_str)?; + #[cfg(not(feature = "std"))] + { + // Skip tests that depend on bultins that need std feature. + let skip = [ + "intn.yaml", + "is_valid.yaml", + "add_date.yaml", + "date.yaml", + "clock.yaml", + "compare.yaml", + "diff.yaml", + "format.yaml", + "now_ns.yaml", + "parse_duration_ns.yaml", + "parse_ns.yaml", + "parse_rfc3339_ns.yaml", + "weekday.yaml", + "generate.yaml", + "parse.yaml", + "tests.yaml", + ]; + for s in skip { + if file.contains(s) { + std::println!("skipped {file} in no_std mode."); + return Ok(()); + } + } + } + std::println!("running {file}"); for case in test.cases { diff --git a/src/value.rs b/src/value.rs index 212aee38..758368ca 100644 --- a/src/value.rs +++ b/src/value.rs @@ -406,6 +406,7 @@ impl Value { /// Deserialize a value from a file containing YAML. /// Note: Deserialization from YAML does not support arbitrary precision numbers. + #[cfg(feature = "std")] #[cfg(feature = "yaml")] pub fn from_yaml_file(path: &String) -> Result { match std::fs::read_to_string(path) { diff --git a/tests/arc.rs b/tests/arc.rs index 528671b7..47d3bdb4 100644 --- a/tests/arc.rs +++ b/tests/arc.rs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#![allow(unused)] use lazy_static::lazy_static; use std::sync::Mutex; use regorus::*; +#[cfg(feature = "arc")] // Ensure that types can be s lazy_static! { static ref VALUE: Value = Value::Null; @@ -14,6 +16,7 @@ lazy_static! { } #[test] +#[cfg(feature = "arc")] fn shared_engine() -> anyhow::Result<()> { let e_guard = ENGINE.lock(); let mut engine = e_guard.expect("failed to lock engine"); diff --git a/tests/ensure_no_std/Cargo.toml b/tests/ensure_no_std/Cargo.toml new file mode 100644 index 00000000..bcf7745f --- /dev/null +++ b/tests/ensure_no_std/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ensure_no_std" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { version = "1.0.83", default-features = false } +regorus = { path = "../..", default-features = false, features = ["opa-no-std"] } diff --git a/tests/ensure_no_std/src/main.rs b/tests/ensure_no_std/src/main.rs new file mode 100644 index 00000000..d4aff8a4 --- /dev/null +++ b/tests/ensure_no_std/src/main.rs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + loop {} +} diff --git a/tests/lexer/mod.rs b/tests/lexer/mod.rs index 909709f9..ec8f8c64 100644 --- a/tests/lexer/mod.rs +++ b/tests/lexer/mod.rs @@ -225,6 +225,7 @@ fn invalid_line() -> Result<()> { } #[test] +#[cfg(feature = "std")] fn file_more_than_64_kb_size() -> Result<()> { let source = Source::from_file("tests/kata/data/large.rego")?; let mut lexer = Lexer::new(&source); diff --git a/tests/mod.rs b/tests/mod.rs index b7ed10cb..e0a04c4e 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -8,6 +8,3 @@ mod engine; mod lexer; mod parser; mod value; - -#[cfg(feature = "arc")] -mod arc;