no_std support (#232)

- 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>
This commit is contained in:
Anand Krishnamoorthi
2024-05-13 09:42:35 -04:00
committed by GitHub
parent 01fc234a33
commit e86b590f91
25 changed files with 343 additions and 173 deletions

View File

@@ -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))
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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()),

View File

@@ -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,

View File

@@ -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()?);

View File

@@ -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())
}

View File

@@ -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`:

View File

@@ -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;
}

View File

@@ -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)
}
}
}

View File

@@ -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<()> {

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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) {