From 83a6619c44c7891aa8e291d4620dd9407c9b72a5 Mon Sep 17 00:00:00 2001
From: Anand Krishnamoorthi <anakrish@microsoft.com>
Date: Thu, 9 May 2024 20:44:48 -0700
Subject: [PATCH] no_std support

- Disable default features in dependencies
- Use anyhow::Error::msg to map errors. Note: anyhow will itself be removed later.
- lazy_static/spin_no_std used in no_std environments
- ensure_no_std binary is built to target  thumbv7m-none-eabi to ensure that
  there are no std dependencies.  thumbv7m-none-eabi target has no std support.
- The opa-no-std feature enables only those Regorus features that work with no_std.
- Enable tests with no_std
- Update sizes of regorus binary in  README.md
- Ensure that regorus example can be built with only std
- Ensure that regorus example can be built with no_std

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
---
 .github/workflows/pr.yml        |   8 ++
 Cargo.toml                      |  71 +++++++++++-----
 README.md                       |   8 +-
 examples/regorus.rs             |  54 +++++++++++--
 scripts/pre-push                |  18 ++++-
 src/builtins/debugging.rs       |  53 ------------
 src/builtins/encoding.rs        |  34 +++++---
 src/builtins/mod.rs             |   4 -
 src/builtins/numbers.rs         |   8 +-
 src/builtins/opa.rs             |   1 +
 src/builtins/semver.rs          |   4 +-
 src/builtins/test.rs            |   4 +-
 src/builtins/time.rs            |   2 +-
 src/builtins/time/compat.rs     |   3 -
 src/interpreter.rs              | 138 ++++++++++++++++++++++++--------
 src/lib.rs                      |  19 +++--
 src/parser.rs                   |  21 ++---
 src/scheduler.rs                |   5 +-
 src/tests/interpreter/mod.rs    |  29 +++++++
 src/value.rs                    |   1 +
 tests/arc.rs                    |   3 +
 tests/ensure_no_std/Cargo.toml  |  10 +++
 tests/ensure_no_std/src/main.rs |  16 ++++
 tests/lexer/mod.rs              |   1 +
 tests/mod.rs                    |   3 -
 25 files changed, 344 insertions(+), 174 deletions(-)
 delete mode 100644 src/builtins/debugging.rs
 create mode 100644 tests/ensure_no_std/Cargo.toml
 create mode 100644 tests/ensure_no_std/src/main.rs

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<String> {
+    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<regorus::Value> {
+    #[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<regorus::Value> {
+    #[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<Expr>],
-    args: &[Value],
-    _strict: bool,
-) -> Result<String> {
-    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 += " <undefined>",
-            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<Expr>], args: &[Value], strict: bool) -> Result<Value> {
-    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, &params[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<Expr>], args: &[Value], _strict: bool)
     ensure_args_count(span, name, params, args, 1)?;
 
     let encoded_str = ensure_string(name, &params[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<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
     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<String> = 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<Expr>], &[Value], bool) -> Result<Value>, 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<Expr>], args: &[Value], _strict: bool) -> Re
     ))
 }
 
+#[cfg(feature = "std")]
 fn intn(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
     let fcn = "rand.intn";
     ensure_args_count(span, fcn, params, args, 2)?;
-    let _ = ensure_string(fcn, &params[0], &args[0])?;
+    let _ = crate::builtins::utils::ensure_string(fcn, &params[0], &args[0])?;
     let n = ensure_numeric(fcn, &params[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<Expr>], 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<Expr>], args: &[Value], _strict: bool) ->
 
     let v1 = ensure_string(name, &params[0], &args[0])?;
     let v2 = ensure_string(name, &params[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<Expr>], args: &[Value], _strict: bool) -> Re
     ensure_args_count(span, name, params, args, 1)?;
 
     let val = ensure_string(name, &params[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, &params[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>), Value>,
     no_rules_lookup: bool,
     traces: Option<Vec<Rc<str>>>,
+    #[cfg(feature = "deprecated")]
     allow_deprecated: bool,
     strict_builtin_errors: bool,
     imports: BTreeMap<String, Ref<Expr>>,
@@ -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() + "." + &target;
@@ -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<Value>,
     ) -> Result<Value> {
-        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<Option<&BuiltinFcn>> {
         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<Value>) -> Result<Value> {
+        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("<undefined>"),
+                // 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 {
         /// <img src="https://github.com/microsoft/regorus/blob/main/docs/coverage.png?raw=true">
 
         pub fn to_colored_string(&self) -> anyhow::Result<String> {
-            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<Str: Clone + cmp::Ord + fmt::Debug>(
     empty: &Str,
 ) -> Result<SortResult> {
     let num_statements = infos.len();
-    let orig_infos: Vec<&StmtInfo<Str>> = infos.iter().collect();
 
     // Mapping from each var to the list of statements that define it.
     let mut defining_stmts: BTreeMap<Str, Vec<usize>> = BTreeMap::new();
@@ -198,7 +197,7 @@ pub fn schedule<Str: Clone + cmp::Ord + fmt::Debug>(
 
     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<SourceStr>, Vec<Ref<Expr>>)> {
         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<Value> {
         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;