From c137fa77fb64d024ca078c05934ab87995ce71d1 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sun, 24 Apr 2022 11:30:49 +0000 Subject: [PATCH] refactor: move modules from root --- --- a/bindings/jsonnet/src/native.rs +++ b/bindings/jsonnet/src/native.rs @@ -8,9 +8,8 @@ use gcmodule::Cc; use jrsonnet_evaluator::{ error::{Error, LocError}, - function::BuiltinParam, + function::builtin::{BuiltinParam, NativeCallback, NativeCallbackHandler}, gc::TraceBox, - native::{NativeCallback, NativeCallbackHandler}, typed::Typed, IStr, State, Val, }; --- a/crates/jrsonnet-evaluator/src/builtin/format.rs +++ /dev/null @@ -1,796 +0,0 @@ -//! faster std.format impl -#![allow(clippy::too_many_arguments)] - -use gcmodule::Trace; -use jrsonnet_interner::IStr; -use jrsonnet_types::ValType; -use thiserror::Error; - -use crate::{error::Error::*, throw, typed::Typed, LocError, ObjValue, Result, State, Val}; - -#[derive(Debug, Clone, Error, Trace)] -pub enum FormatError { - #[error("truncated format code")] - TruncatedFormatCode, - #[error("unrecognized conversion type: {0}")] - UnrecognizedConversionType(char), - - #[error("not enough values")] - NotEnoughValues, - - #[error("cannot use * width with object")] - CannotUseStarWidthWithObject, - #[error("mapping keys required")] - MappingKeysRequired, - #[error("no such format field: {0}")] - NoSuchFormatField(IStr), -} - -impl From for LocError { - fn from(e: FormatError) -> Self { - Self::new(Format(e)) - } -} - -use FormatError::*; - -type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>; - -pub fn try_parse_mapping_key(str: &str) -> ParseResult<&str> { - if str.is_empty() { - return Err(TruncatedFormatCode); - } - let bytes = str.as_bytes(); - if bytes[0] == b'(' { - let mut i = 1; - while i < bytes.len() { - if bytes[i] == b')' { - return Ok((&str[1..i as usize], &str[i as usize + 1..])); - } - i += 1; - } - Err(TruncatedFormatCode) - } else { - Ok(("", str)) - } -} - -#[cfg(test)] -pub mod tests_key { - use super::*; - - #[test] - fn parse_key() { - assert_eq!( - try_parse_mapping_key("(hello ) world").unwrap(), - ("hello ", " world") - ); - assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world")); - assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world")); - assert_eq!( - try_parse_mapping_key(" () world").unwrap(), - ("", " () world") - ); - } - - #[test] - #[should_panic] - fn parse_key_missing_start() { - try_parse_mapping_key("").unwrap(); - } - - #[test] - #[should_panic] - fn parse_key_missing_end() { - try_parse_mapping_key("( ").unwrap(); - } -} - -#[allow(clippy::struct_excessive_bools)] -#[derive(Default, Debug)] -pub struct CFlags { - pub alt: bool, - pub zero: bool, - pub left: bool, - pub blank: bool, - pub sign: bool, -} - -pub fn try_parse_cflags(str: &str) -> ParseResult { - if str.is_empty() { - return Err(TruncatedFormatCode); - } - let bytes = str.as_bytes(); - let mut i = 0; - let mut out = CFlags::default(); - loop { - if bytes.len() == i { - return Err(TruncatedFormatCode); - } - match bytes[i] { - b'#' => out.alt = true, - b'0' => out.zero = true, - b'-' => out.left = true, - b' ' => out.blank = true, - b'+' => out.sign = true, - _ => break, - } - i += 1; - } - Ok((out, &str[i..])) -} - -#[derive(Debug, PartialEq)] -pub enum Width { - Star, - Fixed(usize), -} -pub fn try_parse_field_width(str: &str) -> ParseResult { - if str.is_empty() { - return Err(TruncatedFormatCode); - } - let bytes = str.as_bytes(); - if bytes[0] == b'*' { - return Ok((Width::Star, &str[1..])); - } - let mut out: usize = 0; - let mut digits = 0; - while let Some(digit) = (bytes[digits] as char).to_digit(10) { - out *= 10; - out += digit as usize; - digits += 1; - if digits == bytes.len() { - return Err(TruncatedFormatCode); - } - } - Ok((Width::Fixed(out), &str[digits..])) -} - -pub fn try_parse_precision(str: &str) -> ParseResult> { - if str.is_empty() { - return Err(TruncatedFormatCode); - } - let bytes = str.as_bytes(); - if bytes[0] == b'.' { - try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s)) - } else { - Ok((None, str)) - } -} - -// Only skips -pub fn try_parse_length_modifier(str: &str) -> ParseResult<()> { - if str.is_empty() { - return Err(TruncatedFormatCode); - } - let bytes = str.as_bytes(); - let mut idx = 0; - while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' { - idx += 1; - if bytes.len() == idx { - return Err(TruncatedFormatCode); - } - } - Ok(((), &str[idx..])) -} - -#[derive(Debug, PartialEq)] -pub enum ConvTypeV { - Decimal, - Octal, - Hexadecimal, - Scientific, - Float, - Shorter, - Char, - String, - Percent, -} -pub struct ConvType { - v: ConvTypeV, - caps: bool, -} - -pub fn parse_conversion_type(str: &str) -> ParseResult { - if str.is_empty() { - return Err(TruncatedFormatCode); - } - - let code = str.as_bytes()[0]; - let v: (ConvTypeV, bool) = match code { - b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false), - b'o' => (ConvTypeV::Octal, false), - b'x' => (ConvTypeV::Hexadecimal, false), - b'X' => (ConvTypeV::Hexadecimal, true), - b'e' => (ConvTypeV::Scientific, false), - b'E' => (ConvTypeV::Scientific, true), - b'f' => (ConvTypeV::Float, false), - b'F' => (ConvTypeV::Float, true), - b'g' => (ConvTypeV::Shorter, false), - b'G' => (ConvTypeV::Shorter, true), - b'c' => (ConvTypeV::Char, false), - b's' => (ConvTypeV::String, false), - b'%' => (ConvTypeV::Percent, false), - c => return Err(UnrecognizedConversionType(c as char)), - }; - - Ok((ConvType { v: v.0, caps: v.1 }, &str[1..])) -} - -#[derive(Debug)] -pub struct Code<'s> { - mkey: &'s str, - cflags: CFlags, - width: Width, - precision: Option, - convtype: ConvTypeV, - caps: bool, -} -pub fn parse_code(str: &str) -> ParseResult { - if str.is_empty() { - return Err(TruncatedFormatCode); - } - let (mkey, str) = try_parse_mapping_key(str)?; - let (cflags, str) = try_parse_cflags(str)?; - let (width, str) = try_parse_field_width(str)?; - let (precision, str) = try_parse_precision(str)?; - let (_, str) = try_parse_length_modifier(str)?; - let (convtype, str) = parse_conversion_type(str)?; - - Ok(( - Code { - mkey, - cflags, - width, - precision, - convtype: convtype.v, - caps: convtype.caps, - }, - str, - )) -} - -#[derive(Debug)] -pub enum Element<'s> { - String(&'s str), - Code(Code<'s>), -} -pub fn parse_codes(mut str: &str) -> Result> { - let mut bytes = str.as_bytes(); - let mut out = vec![]; - let mut offset = 0; - - loop { - while offset != bytes.len() && bytes[offset] != b'%' { - offset += 1; - } - if offset != 0 { - out.push(Element::String(&str[0..offset])); - } - if offset == bytes.len() { - return Ok(out); - } - str = &str[offset + 1..]; - let code; - (code, str) = parse_code(str)?; - bytes = str.as_bytes(); - offset = 0; - - out.push(Element::Code(code)); - } -} - -const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz"; - -#[inline] -pub fn render_integer( - out: &mut String, - iv: i64, - padding: usize, - precision: usize, - blank: bool, - sign: bool, - radix: i64, - prefix: &str, - caps: bool, -) { - // Digit char indexes in reverse order, i.e - // for radix = 16 and n = 12f: [15, 2, 1] - let digits = if iv == 0 { - vec![0u8] - } else { - let mut v = iv.abs(); - let mut nums = Vec::with_capacity(1); - while v > 0 { - nums.push((v % radix) as u8); - v /= radix; - } - nums - }; - let neg = iv < 0; - let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 }); - let zp2 = zp - .max(precision) - .saturating_sub(prefix.len() + digits.len()); - - if neg { - out.push('-'); - } else if sign { - out.push('+'); - } else if blank { - out.push(' '); - } - - out.reserve(zp2); - for _ in 0..zp2 { - out.push('0'); - } - out.push_str(prefix); - - for digit in digits.into_iter().rev() { - let ch = NUMBERS[digit as usize] as char; - out.push(if caps { ch.to_ascii_uppercase() } else { ch }); - } -} - -pub fn render_decimal( - out: &mut String, - iv: i64, - padding: usize, - precision: usize, - blank: bool, - sign: bool, -) { - render_integer(out, iv, padding, precision, blank, sign, 10, "", false); -} -pub fn render_octal( - out: &mut String, - iv: i64, - padding: usize, - precision: usize, - alt: bool, - blank: bool, - sign: bool, -) { - render_integer( - out, - iv, - padding, - precision, - blank, - sign, - 8, - if alt && iv != 0 { "0" } else { "" }, - false, - ); -} - -#[allow(clippy::fn_params_excessive_bools)] -pub fn render_hexadecimal( - out: &mut String, - iv: i64, - padding: usize, - precision: usize, - alt: bool, - blank: bool, - sign: bool, - caps: bool, -) { - render_integer( - out, - iv, - padding, - precision, - blank, - sign, - 16, - match (alt, caps) { - (true, true) => "0X", - (true, false) => "0x", - (false, _) => "", - }, - caps, - ); -} - -#[allow(clippy::fn_params_excessive_bools)] -pub fn render_float( - out: &mut String, - n: f64, - mut padding: usize, - precision: usize, - blank: bool, - sign: bool, - ensure_pt: bool, - trailing: bool, -) { - let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 }; - padding = padding.saturating_sub(dot_size + precision); - render_decimal(out, n.floor() as i64, padding, 0, blank, sign); - if precision == 0 { - if ensure_pt { - out.push('.'); - } - return; - } - let frac = n - .fract() - .mul_add(10.0_f64.powf(precision as f64), 0.5) - .floor(); - if trailing || frac > 0.0 { - out.push('.'); - let mut frac_str = String::new(); - render_decimal(&mut frac_str, frac as i64, precision, 0, false, false); - let mut trim = frac_str.len(); - if !trailing { - for b in frac_str.as_bytes().iter().rev() { - if *b == b'0' { - trim -= 1; - } - } - } - out.push_str(&frac_str[..trim]); - } else if ensure_pt { - out.push('.'); - } -} - -#[allow(clippy::fn_params_excessive_bools)] -pub fn render_float_sci( - out: &mut String, - n: f64, - mut padding: usize, - precision: usize, - blank: bool, - sign: bool, - ensure_pt: bool, - trailing: bool, - caps: bool, -) { - let exponent = n.log10().floor(); - let mantissa = if exponent as i16 == -324 { - n * 10.0 / 10.0_f64.powf(exponent + 1.0) - } else { - n / 10.0_f64.powf(exponent) - }; - let mut exponent_str = String::new(); - render_decimal(&mut exponent_str, exponent as i64, 3, 0, false, true); - - // +1 for e - padding = padding.saturating_sub(exponent_str.len() + 1); - - render_float( - out, mantissa, padding, precision, blank, sign, ensure_pt, trailing, - ); - out.push(if caps { 'E' } else { 'e' }); - out.push_str(&exponent_str); -} - -#[allow(clippy::too_many_lines)] -pub fn format_code( - s: State, - out: &mut String, - value: &Val, - code: &Code, - width: usize, - precision: Option, -) -> Result<()> { - let clfags = &code.cflags; - let (fpprec, iprec) = match precision { - Some(v) => (v, v), - None => (6, 0), - }; - let padding = if clfags.zero && !clfags.left { - width - } else { - 0 - }; - - // TODO: If left padded, can optimize by writing directly to out - let mut tmp_out = String::new(); - - match code.convtype { - ConvTypeV::String => tmp_out.push_str(&value.clone().to_string(s)?), - ConvTypeV::Decimal => { - let value = f64::from_untyped(value.clone(), s)?; - render_decimal( - &mut tmp_out, - value as i64, - padding, - iprec, - clfags.blank, - clfags.sign, - ); - } - ConvTypeV::Octal => { - let value = f64::from_untyped(value.clone(), s)?; - render_octal( - &mut tmp_out, - value as i64, - padding, - iprec, - clfags.alt, - clfags.blank, - clfags.sign, - ); - } - ConvTypeV::Hexadecimal => { - let value = f64::from_untyped(value.clone(), s)?; - render_hexadecimal( - &mut tmp_out, - value as i64, - padding, - iprec, - clfags.alt, - clfags.blank, - clfags.sign, - code.caps, - ); - } - ConvTypeV::Scientific => { - let value = f64::from_untyped(value.clone(), s)?; - render_float_sci( - &mut tmp_out, - value, - padding, - fpprec, - clfags.blank, - clfags.sign, - clfags.alt, - true, - code.caps, - ); - } - ConvTypeV::Float => { - let value = f64::from_untyped(value.clone(), s)?; - render_float( - &mut tmp_out, - value, - padding, - fpprec, - clfags.blank, - clfags.sign, - clfags.alt, - true, - ); - } - ConvTypeV::Shorter => { - let value = f64::from_untyped(value.clone(), s)?; - let exponent = value.log10().floor(); - if exponent < -4.0 || exponent >= fpprec as f64 { - render_float_sci( - &mut tmp_out, - value, - padding, - fpprec - 1, - clfags.blank, - clfags.sign, - clfags.alt, - clfags.alt, - code.caps, - ); - } else { - let digits_before_pt = 1.max(exponent as usize + 1); - render_float( - &mut tmp_out, - value, - padding, - fpprec - digits_before_pt, - clfags.blank, - clfags.sign, - clfags.alt, - clfags.alt, - ); - } - } - ConvTypeV::Char => match value.clone() { - Val::Num(n) => tmp_out - .push(std::char::from_u32(n as u32).ok_or(InvalidUnicodeCodepointGot(n as u32))?), - Val::Str(s) => { - if s.chars().count() != 1 { - throw!(RuntimeError( - format!("%c expected 1 char string, got {}", s.chars().count()).into(), - )); - } - tmp_out.push_str(&s); - } - _ => { - throw!(TypeMismatch( - "%c requires number/string", - vec![ValType::Num, ValType::Str], - value.value_type(), - )); - } - }, - ConvTypeV::Percent => tmp_out.push('%'), - }; - - let padding = width.saturating_sub(tmp_out.len()); - - if !clfags.left { - for _ in 0..padding { - out.push(' '); - } - } - out.push_str(&tmp_out); - if clfags.left { - for _ in 0..padding { - out.push(' '); - } - } - - Ok(()) -} - -pub fn format_arr(s: State, str: &str, mut values: &[Val]) -> Result { - let codes = parse_codes(str)?; - let mut out = String::new(); - - for code in codes { - match code { - Element::String(s) => { - out.push_str(s); - } - Element::Code(c) => { - let width = match c.width { - Width::Star => { - if values.is_empty() { - throw!(NotEnoughValues); - } - let value = &values[0]; - values = &values[1..]; - usize::from_untyped(value.clone(), s.clone())? - } - Width::Fixed(n) => n, - }; - let precision = match c.precision { - Some(Width::Star) => { - if values.is_empty() { - throw!(NotEnoughValues); - } - let value = &values[0]; - values = &values[1..]; - Some(usize::from_untyped(value.clone(), s.clone())?) - } - Some(Width::Fixed(n)) => Some(n), - None => None, - }; - - // %% should not consume a value - let value = if c.convtype == ConvTypeV::Percent { - &Val::Null - } else { - if values.is_empty() { - throw!(NotEnoughValues); - } - let value = &values[0]; - values = &values[1..]; - value - }; - - format_code(s.clone(), &mut out, value, &c, width, precision)?; - } - } - } - - Ok(out) -} - -pub fn format_obj(s: State, str: &str, values: &ObjValue) -> Result { - let codes = parse_codes(str)?; - let mut out = String::new(); - - for code in codes { - match code { - Element::String(s) => { - out.push_str(s); - } - Element::Code(c) => { - // TODO: Operate on ref - let f: IStr = c.mkey.into(); - let width = match c.width { - Width::Star => { - throw!(CannotUseStarWidthWithObject); - } - Width::Fixed(n) => n, - }; - let precision = match c.precision { - Some(Width::Star) => { - throw!(CannotUseStarWidthWithObject); - } - Some(Width::Fixed(n)) => Some(n), - None => None, - }; - - let value = if c.convtype == ConvTypeV::Percent { - Val::Null - } else { - if f.is_empty() { - throw!(MappingKeysRequired); - } - if let Some(v) = values.get(s.clone(), f.clone())? { - v - } else { - throw!(NoSuchFormatField(f)); - } - }; - - format_code(s.clone(), &mut out, &value, &c, width, precision)?; - } - } - } - - Ok(out) -} - -#[cfg(test)] -pub mod test_format { - use super::*; - - #[test] - fn parse() { - assert_eq!( - parse_codes( - "How much error budget is left looking at our %.3f%% availability gurantees?" - ) - .unwrap() - .len(), - 4 - ); - } - - #[test] - fn octals() { - let s = State::default(); - assert_eq!( - format_arr(s.clone(), "%#o", &[Val::Num(8.0)]).unwrap(), - "010" - ); - assert_eq!( - format_arr(s.clone(), "%#4o", &[Val::Num(8.0)]).unwrap(), - " 010" - ); - assert_eq!( - format_arr(s.clone(), "%4o", &[Val::Num(8.0)]).unwrap(), - " 10" - ); - assert_eq!( - format_arr(s.clone(), "%04o", &[Val::Num(8.0)]).unwrap(), - "0010" - ); - assert_eq!( - format_arr(s.clone(), "%+4o", &[Val::Num(8.0)]).unwrap(), - " +10" - ); - assert_eq!( - format_arr(s.clone(), "%+04o", &[Val::Num(8.0)]).unwrap(), - "+010" - ); - assert_eq!( - format_arr(s.clone(), "%-4o", &[Val::Num(8.0)]).unwrap(), - "10 " - ); - assert_eq!( - format_arr(s.clone(), "%+-4o", &[Val::Num(8.0)]).unwrap(), - "+10 " - ); - assert_eq!( - format_arr(s.clone(), "%+-04o", &[Val::Num(8.0)]).unwrap(), - "+10 " - ); - } - - #[test] - fn percent_doesnt_consumes_values() { - let s = State::default(); - assert_eq!( - format_arr( - s, - "How much error budget is left looking at our %.3f%% availability gurantees?", - &[Val::Num(4.0)] - ) - .unwrap(), - "How much error budget is left looking at our 4.000% availability gurantees?" - ); - } -} --- a/crates/jrsonnet-evaluator/src/builtin/manifest.rs +++ /dev/null @@ -1,347 +0,0 @@ -use crate::{ - error::{Error::*, Result}, - throw, State, Val, -}; - -#[derive(PartialEq, Clone, Copy)] -pub enum ManifestType { - // Applied in manifestification - Manifest, - /// Used for std.manifestJson - /// Empty array/objects extends to "[\n\n]" instead of "[ ]" as in manifest - Std, - /// No line breaks, used in `obj+''` - ToString, - /// Minified json - Minify, -} - -pub struct ManifestJsonOptions<'s> { - pub padding: &'s str, - pub mtype: ManifestType, - pub newline: &'s str, - pub key_val_sep: &'s str, - #[cfg(feature = "exp-preserve-order")] - pub preserve_order: bool, -} - -pub fn manifest_json_ex(s: State, val: &Val, options: &ManifestJsonOptions<'_>) -> Result { - let mut out = String::new(); - manifest_json_ex_buf(s, val, &mut out, &mut String::new(), options)?; - Ok(out) -} -fn manifest_json_ex_buf( - s: State, - val: &Val, - buf: &mut String, - cur_padding: &mut String, - options: &ManifestJsonOptions<'_>, -) -> Result<()> { - use std::fmt::Write; - let mtype = options.mtype; - match val { - Val::Bool(v) => { - if *v { - buf.push_str("true"); - } else { - buf.push_str("false"); - } - } - Val::Null => buf.push_str("null"), - Val::Str(s) => escape_string_json_buf(s, buf), - Val::Num(n) => write!(buf, "{}", n).unwrap(), - Val::Arr(items) => { - buf.push('['); - if !items.is_empty() { - if mtype != ManifestType::ToString && mtype != ManifestType::Minify { - buf.push_str(options.newline); - } - - let old_len = cur_padding.len(); - cur_padding.push_str(options.padding); - for (i, item) in items.iter(s.clone()).enumerate() { - if i != 0 { - buf.push(','); - if mtype == ManifestType::ToString { - buf.push(' '); - } else if mtype != ManifestType::Minify { - buf.push_str(options.newline); - } - } - buf.push_str(cur_padding); - manifest_json_ex_buf(s.clone(), &item?, buf, cur_padding, options)?; - } - cur_padding.truncate(old_len); - - if mtype != ManifestType::ToString && mtype != ManifestType::Minify { - buf.push_str(options.newline); - buf.push_str(cur_padding); - } - } else if mtype == ManifestType::Std { - buf.push_str("\n\n"); - buf.push_str(cur_padding); - } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest { - buf.push(' '); - } - buf.push(']'); - } - Val::Obj(obj) => { - obj.run_assertions(s.clone())?; - buf.push('{'); - let fields = obj.fields( - #[cfg(feature = "exp-preserve-order")] - options.preserve_order, - ); - if !fields.is_empty() { - if mtype != ManifestType::ToString && mtype != ManifestType::Minify { - buf.push_str(options.newline); - } - - let old_len = cur_padding.len(); - cur_padding.push_str(options.padding); - for (i, field) in fields.into_iter().enumerate() { - if i != 0 { - buf.push(','); - if mtype == ManifestType::ToString { - buf.push(' '); - } else if mtype != ManifestType::Minify { - buf.push_str(options.newline); - } - } - buf.push_str(cur_padding); - escape_string_json_buf(&field, buf); - buf.push_str(options.key_val_sep); - s.push_description( - || format!("field <{}> manifestification", field.clone()), - || { - let value = obj.get(s.clone(), field.clone())?.unwrap(); - manifest_json_ex_buf(s.clone(), &value, buf, cur_padding, options)?; - Ok(Val::Null) - }, - )?; - } - cur_padding.truncate(old_len); - - if mtype != ManifestType::ToString && mtype != ManifestType::Minify { - buf.push_str(options.newline); - buf.push_str(cur_padding); - } - } else if mtype == ManifestType::Std { - buf.push_str("\n\n"); - buf.push_str(cur_padding); - } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest { - buf.push(' '); - } - buf.push('}'); - } - Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())), - }; - Ok(()) -} - -pub fn escape_string_json(s: &str) -> String { - let mut buf = String::new(); - escape_string_json_buf(s, &mut buf); - buf -} - -fn escape_string_json_buf(s: &str, buf: &mut String) { - use std::fmt::Write; - buf.push('"'); - for c in s.chars() { - match c { - '"' => buf.push_str("\\\""), - '\\' => buf.push_str("\\\\"), - '\u{0008}' => buf.push_str("\\b"), - '\u{000c}' => buf.push_str("\\f"), - '\n' => buf.push_str("\\n"), - '\r' => buf.push_str("\\r"), - '\t' => buf.push_str("\\t"), - c if c < 32 as char || (c >= 127 as char && c <= 159 as char) => { - write!(buf, "\\u{:04x}", c as u32).unwrap(); - } - c => buf.push(c), - } - } - buf.push('"'); -} - -pub struct ManifestYamlOptions<'s> { - /// Padding before fields, i.e - /// ```yaml - /// a: - /// b: - /// ## <- this - /// ``` - pub padding: &'s str, - /// Padding before array elements in objects - /// ```yaml - /// a: - /// - 1 - /// ## <- this - /// ``` - pub arr_element_padding: &'s str, - /// Should yaml keys appear unescaped, when possible - /// ```yaml - /// "safe_key": 1 - /// # vs - /// safe_key: 1 - /// ``` - pub quote_keys: bool, - /// If true - then order of fields is preserved as written, - /// instead of sorting alphabetically - #[cfg(feature = "exp-preserve-order")] - pub preserve_order: bool, -} - -/// From -/// With added date check -fn yaml_needs_quotes(string: &str) -> bool { - fn need_quotes_spaces(string: &str) -> bool { - string.starts_with(' ') || string.ends_with(' ') - } - - string.is_empty() - || need_quotes_spaces(string) - || string.starts_with(|c| matches!(c, '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@')) - || string.contains(|c| matches!(c, ':' | '{' | '}' | '[' | ']' | ',' | '#' | '`' | '\"' | '\'' | '\\' | '\0'..='\x06' | '\t' | '\n' | '\r' | '\x0e'..='\x1a' | '\x1c'..='\x1f')) - || [ - // http://yaml.org/type/bool.html - // Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse - // them as string, not booleans, although it is violating the YAML 1.1 specification. - // See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088. - "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", "false", - "on", "On", "ON", "off", "Off", "OFF", // http://yaml.org/type/null.html - "null", "Null", "NULL", "~", - ].contains(&string) - || (string.chars().all(|c| matches!(c, '0'..='9' | '-')) - && string.chars().filter(|c| *c == '-').count() == 2) - || string.starts_with('.') - || string.starts_with("0x") - || string.parse::().is_ok() - || string.parse::().is_ok() -} - -pub fn manifest_yaml_ex(s: State, val: &Val, options: &ManifestYamlOptions<'_>) -> Result { - let mut out = String::new(); - manifest_yaml_ex_buf(s, val, &mut out, &mut String::new(), options)?; - Ok(out) -} - -#[allow(clippy::too_many_lines)] -fn manifest_yaml_ex_buf( - s: State, - val: &Val, - buf: &mut String, - cur_padding: &mut String, - options: &ManifestYamlOptions<'_>, -) -> Result<()> { - use std::fmt::Write; - match val { - Val::Bool(v) => { - if *v { - buf.push_str("true"); - } else { - buf.push_str("false"); - } - } - Val::Null => buf.push_str("null"), - Val::Str(s) => { - if s.is_empty() { - buf.push_str("\"\""); - } else if let Some(s) = s.strip_suffix('\n') { - buf.push('|'); - for line in s.split('\n') { - buf.push('\n'); - buf.push_str(cur_padding); - buf.push_str(options.padding); - buf.push_str(line); - } - } else if !options.quote_keys && !yaml_needs_quotes(s) { - buf.push_str(s); - } else { - escape_string_json_buf(s, buf); - } - } - Val::Num(n) => write!(buf, "{}", *n).unwrap(), - Val::Arr(a) => { - if a.is_empty() { - buf.push_str("[]"); - } else { - for (i, item) in a.iter(s.clone()).enumerate() { - if i != 0 { - buf.push('\n'); - buf.push_str(cur_padding); - } - let item = item?; - buf.push('-'); - match &item { - Val::Arr(a) if !a.is_empty() => { - buf.push('\n'); - buf.push_str(cur_padding); - buf.push_str(options.padding); - } - _ => buf.push(' '), - } - let extra_padding = match &item { - Val::Arr(a) => !a.is_empty(), - Val::Obj(o) => !o.is_empty(), - _ => false, - }; - let prev_len = cur_padding.len(); - if extra_padding { - cur_padding.push_str(options.padding); - } - manifest_yaml_ex_buf(s.clone(), &item, buf, cur_padding, options)?; - cur_padding.truncate(prev_len); - } - } - } - Val::Obj(o) => { - if o.is_empty() { - buf.push_str("{}"); - } else { - for (i, key) in o - .fields( - #[cfg(feature = "exp-preserve-order")] - options.preserve_order, - ) - .iter() - .enumerate() - { - if i != 0 { - buf.push('\n'); - buf.push_str(cur_padding); - } - if !options.quote_keys && !yaml_needs_quotes(key) { - buf.push_str(key); - } else { - escape_string_json_buf(key, buf); - } - buf.push(':'); - let prev_len = cur_padding.len(); - let item = o.get(s.clone(), key.clone())?.expect("field exists"); - match &item { - Val::Arr(a) if !a.is_empty() => { - buf.push('\n'); - buf.push_str(cur_padding); - buf.push_str(options.arr_element_padding); - cur_padding.push_str(options.arr_element_padding); - } - Val::Obj(o) if !o.is_empty() => { - buf.push('\n'); - buf.push_str(cur_padding); - buf.push_str(options.padding); - cur_padding.push_str(options.padding); - } - _ => buf.push(' '), - } - manifest_yaml_ex_buf(s.clone(), &item, buf, cur_padding, options)?; - cur_padding.truncate(prev_len); - } - } - } - Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())), - } - Ok(()) -} --- a/crates/jrsonnet-evaluator/src/builtin/mod.rs +++ /dev/null @@ -1,749 +0,0 @@ -// All builtins should return results -#![allow(clippy::unnecessary_wraps)] - -use std::collections::HashMap; - -use format::{format_arr, format_obj}; -use gcmodule::Cc; -use jrsonnet_interner::IStr; -use serde::Deserialize; -use serde_yaml::DeserializingQuirks; - -use crate::{ - builtin::manifest::{manifest_yaml_ex, ManifestYamlOptions}, - error::{Error::*, Result}, - function::{CallLocation, StaticBuiltin}, - operator::evaluate_mod_op, - throw, - typed::{Any, BoundedUsize, Bytes, Either2, Either4, PositiveF64, Typed, VecVal, M1}, - val::{equals, primitive_equals, ArrValue, FuncVal, IndexableVal, Slice}, - Either, ObjValue, State, Val, -}; - -pub mod stdlib; -pub use stdlib::*; - -use self::manifest::{escape_string_json, manifest_json_ex, ManifestJsonOptions, ManifestType}; - -pub mod format; -pub mod manifest; -pub mod sort; - -pub fn std_format(s: State, str: IStr, vals: Val) -> Result { - s.push( - CallLocation::native(), - || format!("std.format of {}", str), - || { - Ok(match vals { - Val::Arr(vals) => format_arr(s.clone(), &str, &vals.evaluated(s.clone())?)?, - Val::Obj(obj) => format_obj(s.clone(), &str, &obj)?, - o => format_arr(s.clone(), &str, &[o])?, - }) - }, - ) -} - -pub fn std_slice( - indexable: IndexableVal, - index: Option>, - end: Option>, - step: Option>, -) -> Result { - match &indexable { - IndexableVal::Str(s) => { - let index = index.as_deref().copied().unwrap_or(0); - let end = end.as_deref().copied().unwrap_or(usize::MAX); - let step = step.as_deref().copied().unwrap_or(1); - - if index >= end { - return Ok(Val::Str("".into())); - } - - Ok(Val::Str( - (s.chars() - .skip(index) - .take(end - index) - .step_by(step) - .collect::()) - .into(), - )) - } - IndexableVal::Arr(arr) => { - let index = index.as_deref().copied().unwrap_or(0); - let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len()); - let step = step.as_deref().copied().unwrap_or(1); - - if index >= end { - return Ok(Val::Arr(ArrValue::new_eager())); - } - - Ok(Val::Arr(ArrValue::Slice(Box::new(Slice { - inner: arr.clone(), - from: index as u32, - to: end as u32, - step: step as u32, - })))) - } - } -} - -type BuiltinsType = HashMap; - -thread_local! { - pub static BUILTINS: BuiltinsType = { - [ - ("length".into(), builtin_length::INST), - ("type".into(), builtin_type::INST), - ("makeArray".into(), builtin_make_array::INST), - ("codepoint".into(), builtin_codepoint::INST), - ("objectFieldsEx".into(), builtin_object_fields_ex::INST), - ("objectHasEx".into(), builtin_object_has_ex::INST), - ("slice".into(), builtin_slice::INST), - ("substr".into(), builtin_substr::INST), - ("primitiveEquals".into(), builtin_primitive_equals::INST), - ("equals".into(), builtin_equals::INST), - ("modulo".into(), builtin_modulo::INST), - ("mod".into(), builtin_mod::INST), - ("floor".into(), builtin_floor::INST), - ("ceil".into(), builtin_ceil::INST), - ("log".into(), builtin_log::INST), - ("pow".into(), builtin_pow::INST), - ("sqrt".into(), builtin_sqrt::INST), - ("sin".into(), builtin_sin::INST), - ("cos".into(), builtin_cos::INST), - ("tan".into(), builtin_tan::INST), - ("asin".into(), builtin_asin::INST), - ("acos".into(), builtin_acos::INST), - ("atan".into(), builtin_atan::INST), - ("exp".into(), builtin_exp::INST), - ("mantissa".into(), builtin_mantissa::INST), - ("exponent".into(), builtin_exponent::INST), - ("extVar".into(), builtin_ext_var::INST), - ("native".into(), builtin_native::INST), - ("filter".into(), builtin_filter::INST), - ("map".into(), builtin_map::INST), - ("flatMap".into(), builtin_flatmap::INST), - ("foldl".into(), builtin_foldl::INST), - ("foldr".into(), builtin_foldr::INST), - ("sort".into(), builtin_sort::INST), - ("format".into(), builtin_format::INST), - ("range".into(), builtin_range::INST), - ("char".into(), builtin_char::INST), - ("encodeUTF8".into(), builtin_encode_utf8::INST), - ("decodeUTF8".into(), builtin_decode_utf8::INST), - ("md5".into(), builtin_md5::INST), - ("base64".into(), builtin_base64::INST), - ("base64DecodeBytes".into(), builtin_base64_decode_bytes::INST), - ("base64Decode".into(), builtin_base64_decode::INST), - ("trace".into(), builtin_trace::INST), - ("join".into(), builtin_join::INST), - ("escapeStringJson".into(), builtin_escape_string_json::INST), - ("manifestJsonEx".into(), builtin_manifest_json_ex::INST), - ("manifestYamlDoc".into(), builtin_manifest_yaml_doc::INST), - ("reverse".into(), builtin_reverse::INST), - ("id".into(), builtin_id::INST), - ("strReplace".into(), builtin_str_replace::INST), - ("splitLimit".into(), builtin_splitlimit::INST), - ("parseJson".into(), builtin_parse_json::INST), - ("parseYaml".into(), builtin_parse_yaml::INST), - ("asciiUpper".into(), builtin_ascii_upper::INST), - ("asciiLower".into(), builtin_ascii_lower::INST), - ("member".into(), builtin_member::INST), - ("count".into(), builtin_count::INST), - ("any".into(), builtin_any::INST), - ("all".into(), builtin_all::INST), - ].iter().cloned().collect() - }; -} - -#[jrsonnet_macros::builtin] -fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> Result { - use Either4::*; - Ok(match x { - A(x) => x.chars().count(), - B(x) => x.len(), - C(x) => x.len(), - D(f) => f.args_len(), - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_type(x: Any) -> Result { - Ok(x.0.value_type().name().into()) -} - -#[jrsonnet_macros::builtin] -fn builtin_make_array(s: State, sz: usize, func: FuncVal) -> Result { - let mut out = Vec::with_capacity(sz); - for i in 0..sz { - out.push(func.evaluate_simple(s.clone(), &[i as f64].as_slice())?); - } - Ok(VecVal(Cc::new(out))) -} - -#[jrsonnet_macros::builtin] -const fn builtin_codepoint(str: char) -> Result { - Ok(str as u32) -} - -#[jrsonnet_macros::builtin] -fn builtin_object_fields_ex( - obj: ObjValue, - inc_hidden: bool, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, -) -> Result { - #[cfg(feature = "exp-preserve-order")] - let preserve_order = preserve_order.unwrap_or(false); - let out = obj.fields_ex( - inc_hidden, - #[cfg(feature = "exp-preserve-order")] - preserve_order, - ); - Ok(VecVal(Cc::new( - out.into_iter().map(Val::Str).collect::>(), - ))) -} - -#[jrsonnet_macros::builtin] -fn builtin_object_has_ex(obj: ObjValue, f: IStr, inc_hidden: bool) -> Result { - Ok(obj.has_field_ex(f, inc_hidden)) -} - -#[jrsonnet_macros::builtin] -fn builtin_parse_json(st: State, s: IStr) -> Result { - use serde_json::Value; - let value: Value = serde_json::from_str(&s) - .map_err(|e| RuntimeError(format!("failed to parse json: {}", e).into()))?; - Ok(Any(Value::into_untyped(value, st)?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_parse_yaml(st: State, s: IStr) -> Result { - use serde_json::Value; - let value = serde_yaml::Deserializer::from_str_with_quirks( - &s, - DeserializingQuirks { old_octals: true }, - ); - let mut out = vec![]; - for item in value { - let value = Value::deserialize(item) - .map_err(|e| RuntimeError(format!("failed to parse yaml: {}", e).into()))?; - let val = Value::into_untyped(value, st.clone())?; - out.push(val); - } - Ok(Any(if out.is_empty() { - Val::Null - } else if out.len() == 1 { - out.into_iter().next().unwrap() - } else { - Val::Arr(out.into()) - })) -} - -#[jrsonnet_macros::builtin] -fn builtin_slice( - indexable: IndexableVal, - index: Option>, - end: Option>, - step: Option>, -) -> Result { - std_slice(indexable, index, end, step).map(Any) -} - -#[jrsonnet_macros::builtin] -fn builtin_substr(str: IStr, from: usize, len: usize) -> Result { - Ok(str.chars().skip(from as usize).take(len as usize).collect()) -} - -#[jrsonnet_macros::builtin] -fn builtin_primitive_equals(a: Any, b: Any) -> Result { - primitive_equals(&a.0, &b.0) -} - -#[jrsonnet_macros::builtin] -fn builtin_equals(s: State, a: Any, b: Any) -> Result { - equals(s, &a.0, &b.0) -} - -#[jrsonnet_macros::builtin] -fn builtin_modulo(a: f64, b: f64) -> Result { - Ok(a % b) -} - -#[jrsonnet_macros::builtin] -fn builtin_mod(s: State, a: Either![f64, IStr], b: Any) -> Result { - use Either2::*; - Ok(Any(evaluate_mod_op( - s, - &match a { - A(v) => Val::Num(v), - B(s) => Val::Str(s), - }, - &b.0, - )?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_floor(x: f64) -> Result { - Ok(x.floor()) -} - -#[jrsonnet_macros::builtin] -fn builtin_ceil(x: f64) -> Result { - Ok(x.ceil()) -} - -#[jrsonnet_macros::builtin] -fn builtin_log(n: f64) -> Result { - Ok(n.ln()) -} - -#[jrsonnet_macros::builtin] -fn builtin_pow(x: f64, n: f64) -> Result { - Ok(x.powf(n)) -} - -#[jrsonnet_macros::builtin] -fn builtin_sqrt(x: PositiveF64) -> Result { - Ok(x.0.sqrt()) -} - -#[jrsonnet_macros::builtin] -fn builtin_sin(x: f64) -> Result { - Ok(x.sin()) -} - -#[jrsonnet_macros::builtin] -fn builtin_cos(x: f64) -> Result { - Ok(x.cos()) -} - -#[jrsonnet_macros::builtin] -fn builtin_tan(x: f64) -> Result { - Ok(x.tan()) -} - -#[jrsonnet_macros::builtin] -fn builtin_asin(x: f64) -> Result { - Ok(x.asin()) -} - -#[jrsonnet_macros::builtin] -fn builtin_acos(x: f64) -> Result { - Ok(x.acos()) -} - -#[jrsonnet_macros::builtin] -fn builtin_atan(x: f64) -> Result { - Ok(x.atan()) -} - -#[jrsonnet_macros::builtin] -fn builtin_exp(x: f64) -> Result { - Ok(x.exp()) -} - -fn frexp(s: f64) -> (f64, i16) { - if 0.0 == s { - (s, 0) - } else { - let lg = s.abs().log2(); - let x = (lg - lg.floor() - 1.0).exp2(); - let exp = lg.floor() + 1.0; - (s.signum() * x, exp as i16) - } -} - -#[jrsonnet_macros::builtin] -fn builtin_mantissa(x: f64) -> Result { - Ok(frexp(x).0) -} - -#[jrsonnet_macros::builtin] -fn builtin_exponent(x: f64) -> Result { - Ok(frexp(x).1) -} - -#[jrsonnet_macros::builtin] -fn builtin_ext_var(s: State, x: IStr) -> Result { - Ok(Any(s - .settings() - .ext_vars - .get(&x) - .cloned() - .ok_or(UndefinedExternalVariable(x))?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_native(s: State, name: IStr) -> Result { - Ok(Any(s - .settings() - .ext_natives - .get(&name) - .cloned() - .map_or(Val::Null, |v| { - Val::Func(FuncVal::Builtin(v.clone())) - }))) -} - -#[jrsonnet_macros::builtin] -fn builtin_filter(s: State, func: FuncVal, arr: ArrValue) -> Result { - arr.filter(s.clone(), |val| { - bool::from_untyped( - func.evaluate_simple(s.clone(), &[Any(val.clone())].as_slice())?, - s.clone(), - ) - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_map(s: State, func: FuncVal, arr: ArrValue) -> Result { - arr.map(s.clone(), |val| { - func.evaluate_simple(s.clone(), &[Any(val)].as_slice()) - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_flatmap(s: State, func: FuncVal, arr: IndexableVal) -> Result { - match arr { - IndexableVal::Str(str) => { - let mut out = String::new(); - for c in str.chars() { - match func.evaluate_simple(s.clone(), &[c.to_string()].as_slice())? { - Val::Str(o) => out.push_str(&o), - Val::Null => continue, - _ => throw!(RuntimeError( - "in std.join all items should be strings".into() - )), - }; - } - Ok(IndexableVal::Str(out.into())) - } - IndexableVal::Arr(a) => { - let mut out = Vec::new(); - for el in a.iter(s.clone()) { - let el = el?; - match func.evaluate_simple(s.clone(), &[Any(el)].as_slice())? { - Val::Arr(o) => { - for oe in o.iter(s.clone()) { - out.push(oe?); - } - } - Val::Null => continue, - _ => throw!(RuntimeError( - "in std.join all items should be arrays".into() - )), - }; - } - Ok(IndexableVal::Arr(out.into())) - } - } -} - -#[jrsonnet_macros::builtin] -fn builtin_foldl(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { - let mut acc = init.0; - for i in arr.iter(s.clone()) { - acc = func.evaluate_simple(s.clone(), &[Any(acc), Any(i?)].as_slice())?; - } - Ok(Any(acc)) -} - -#[jrsonnet_macros::builtin] -fn builtin_foldr(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { - let mut acc = init.0; - for i in arr.iter(s.clone()).rev() { - acc = func.evaluate_simple(s.clone(), &[Any(i?), Any(acc)].as_slice())?; - } - Ok(Any(acc)) -} - -#[jrsonnet_macros::builtin] -#[allow(non_snake_case)] -fn builtin_sort(s: State, arr: ArrValue, keyF: Option) -> Result { - if arr.len() <= 1 { - return Ok(arr); - } - Ok(ArrValue::Eager(sort::sort( - s.clone(), - arr.evaluated(s)?, - keyF.as_ref(), - )?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_format(s: State, str: IStr, vals: Any) -> Result { - std_format(s, str, vals.0) -} - -#[jrsonnet_macros::builtin] -fn builtin_range(from: i32, to: i32) -> Result { - if to < from { - return Ok(ArrValue::new_eager()); - } - Ok(ArrValue::new_range(from, to)) -} - -#[jrsonnet_macros::builtin] -fn builtin_char(n: u32) -> Result { - Ok(std::char::from_u32(n as u32).ok_or(InvalidUnicodeCodepointGot(n as u32))?) -} - -#[jrsonnet_macros::builtin] -fn builtin_encode_utf8(str: IStr) -> Result { - Ok(Bytes(str.bytes().collect::>().into())) -} - -#[jrsonnet_macros::builtin] -fn builtin_decode_utf8(arr: Bytes) -> Result { - Ok(std::str::from_utf8(&arr.0) - .map_err(|_| RuntimeError("bad utf8".into()))? - .into()) -} - -#[jrsonnet_macros::builtin] -fn builtin_md5(str: IStr) -> Result { - Ok(format!("{:x}", md5::compute(&str.as_bytes()))) -} - -#[jrsonnet_macros::builtin] -fn builtin_trace(s: State, loc: CallLocation, str: IStr, rest: Any) -> Result { - eprint!("TRACE:"); - if let Some(loc) = loc.0 { - let locs = s.map_source_locations(&loc.0, &[loc.1]); - eprint!( - " {}:{}", - loc.0.file_name().unwrap().to_str().unwrap(), - locs[0].line - ); - } - eprintln!(" {}", str); - Ok(rest) as Result -} - -#[jrsonnet_macros::builtin] -fn builtin_base64(input: Either![Bytes, IStr]) -> Result { - use Either2::*; - Ok(match input { - A(a) => base64::encode(a.0), - B(l) => base64::encode(l.bytes().collect::>()), - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_base64_decode_bytes(input: IStr) -> Result { - Ok(Bytes( - base64::decode(&input.as_bytes()) - .map_err(|_| RuntimeError("bad base64".into()))? - .into(), - )) -} - -#[jrsonnet_macros::builtin] -fn builtin_base64_decode(input: IStr) -> Result { - let bytes = base64::decode(&input.as_bytes()).map_err(|_| RuntimeError("bad base64".into()))?; - Ok(String::from_utf8(bytes).map_err(|_| RuntimeError("bad utf8".into()))?) -} - -#[jrsonnet_macros::builtin] -fn builtin_join(s: State, sep: IndexableVal, arr: ArrValue) -> Result { - Ok(match sep { - IndexableVal::Arr(joiner_items) => { - let mut out = Vec::new(); - - let mut first = true; - for item in arr.iter(s.clone()) { - let item = item?.clone(); - if let Val::Arr(items) = item { - if !first { - out.reserve(joiner_items.len()); - // TODO: extend - for item in joiner_items.iter(s.clone()) { - out.push(item?); - } - } - first = false; - out.reserve(items.len()); - for item in items.iter(s.clone()) { - out.push(item?); - } - } else if matches!(item, Val::Null) { - continue; - } else { - throw!(RuntimeError( - "in std.join all items should be arrays".into() - )); - } - } - - IndexableVal::Arr(out.into()) - } - IndexableVal::Str(sep) => { - let mut out = String::new(); - - let mut first = true; - for item in arr.iter(s) { - let item = item?.clone(); - if let Val::Str(item) = item { - if !first { - out += &sep; - } - first = false; - out += &item; - } else if matches!(item, Val::Null) { - continue; - } else { - throw!(RuntimeError( - "in std.join all items should be strings".into() - )); - } - } - - IndexableVal::Str(out.into()) - } - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_escape_string_json(str_: IStr) -> Result { - Ok(escape_string_json(&str_)) -} - -#[jrsonnet_macros::builtin] -fn builtin_manifest_json_ex( - s: State, - value: Any, - indent: IStr, - newline: Option, - key_val_sep: Option, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, -) -> Result { - let newline = newline.as_deref().unwrap_or("\n"); - let key_val_sep = key_val_sep.as_deref().unwrap_or(": "); - manifest_json_ex( - s, - &value.0, - &ManifestJsonOptions { - padding: &indent, - mtype: ManifestType::Std, - newline, - key_val_sep, - #[cfg(feature = "exp-preserve-order")] - preserve_order: preserve_order.unwrap_or(false), - }, - ) -} - -#[jrsonnet_macros::builtin] -fn builtin_manifest_yaml_doc( - s: State, - value: Any, - indent_array_in_object: Option, - quote_keys: Option, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, -) -> Result { - manifest_yaml_ex( - s, - &value.0, - &ManifestYamlOptions { - padding: " ", - arr_element_padding: if indent_array_in_object.unwrap_or(false) { - " " - } else { - "" - }, - quote_keys: quote_keys.unwrap_or(true), - #[cfg(feature = "exp-preserve-order")] - preserve_order: preserve_order.unwrap_or(false), - }, - ) -} - -#[jrsonnet_macros::builtin] -fn builtin_reverse(value: ArrValue) -> Result { - Ok(value.reversed()) -} - -#[jrsonnet_macros::builtin] -const fn builtin_id(v: Any) -> Result { - Ok(v) -} - -#[jrsonnet_macros::builtin] -fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result { - Ok(str.replace(&from as &str, &to as &str)) -} - -#[jrsonnet_macros::builtin] -fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> Result { - use Either2::*; - Ok(VecVal(Cc::new(match maxsplits { - A(n) => str - .splitn(n + 1, &c as &str) - .map(|s| Val::Str(s.into())) - .collect(), - B(_) => str.split(&c as &str).map(|s| Val::Str(s.into())).collect(), - }))) -} - -#[jrsonnet_macros::builtin] -fn builtin_ascii_upper(str: IStr) -> Result { - Ok(str.to_ascii_uppercase()) -} - -#[jrsonnet_macros::builtin] -fn builtin_ascii_lower(str: IStr) -> Result { - Ok(str.to_ascii_lowercase()) -} - -#[jrsonnet_macros::builtin] -fn builtin_member(s: State, arr: IndexableVal, x: Any) -> Result { - match arr { - IndexableVal::Str(str) => { - let x: IStr = IStr::from_untyped(x.0, s)?; - Ok(!x.is_empty() && str.contains(&*x)) - } - IndexableVal::Arr(a) => { - for item in a.iter(s.clone()) { - let item = item?; - if equals(s.clone(), &item, &x.0)? { - return Ok(true); - } - } - Ok(false) - } - } -} - -#[jrsonnet_macros::builtin] -fn builtin_count(s: State, arr: Vec, v: Any) -> Result { - let mut count = 0; - for item in &arr { - if equals(s.clone(), &item.0, &v.0)? { - count += 1; - } - } - Ok(count) -} - -#[jrsonnet_macros::builtin] -fn builtin_any(s: State, arr: ArrValue) -> Result { - for v in arr.iter(s.clone()) { - let v = bool::from_untyped(v?, s.clone())?; - if v { - return Ok(true); - } - } - Ok(false) -} - -#[jrsonnet_macros::builtin] -fn builtin_all(s: State, arr: ArrValue) -> Result { - for v in arr.iter(s.clone()) { - let v = bool::from_untyped(v?, s.clone())?; - if !v { - return Ok(false); - } - } - Ok(true) -} --- a/crates/jrsonnet-evaluator/src/builtin/sort.rs +++ /dev/null @@ -1,109 +0,0 @@ -use gcmodule::{Cc, Trace}; - -use crate::{ - error::{Error, LocError, Result}, - throw, - typed::Any, - val::FuncVal, - State, Val, -}; - -#[derive(Debug, Clone, thiserror::Error, Trace)] -pub enum SortError { - #[error("sort key should be string or number")] - SortKeyShouldBeStringOrNumber, - #[error("sort elements should have equal types")] - SortElementsShouldHaveEqualType, -} - -impl From for LocError { - fn from(s: SortError) -> Self { - Self::new(Error::Sort(s)) - } -} - -#[derive(Copy, Clone)] -enum SortKeyType { - Number, - String, - Unknown, -} - -#[derive(PartialEq)] -struct NonNaNf64(f64); -impl PartialOrd for NonNaNf64 { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } -} -impl Eq for NonNaNf64 {} -impl Ord for NonNaNf64 { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other).expect("non nan") - } -} - -fn get_sort_type( - values: &mut Vec, - key_getter: impl Fn(&mut T) -> &mut Val, -) -> Result { - let mut sort_type = SortKeyType::Unknown; - for i in values.iter_mut() { - let i = key_getter(i); - match (i, sort_type) { - (Val::Str(_), SortKeyType::Unknown) => sort_type = SortKeyType::String, - (Val::Num(_), SortKeyType::Unknown) => sort_type = SortKeyType::Number, - (Val::Str(_), SortKeyType::String) | (Val::Num(_), SortKeyType::Number) => {} - (Val::Str(_) | Val::Num(_), _) => { - throw!(SortError::SortElementsShouldHaveEqualType) - } - _ => throw!(SortError::SortKeyShouldBeStringOrNumber), - } - } - Ok(sort_type) -} - -pub fn sort(s: State, values: Cc>, key_getter: Option<&FuncVal>) -> Result>> { - if values.len() <= 1 { - return Ok(values); - } - if let Some(key_getter) = key_getter { - // Slow path, user provided key getter - let mut vk = Vec::with_capacity(values.len()); - for value in values.iter() { - vk.push(( - value.clone(), - key_getter.evaluate_simple(s.clone(), &[Any(value.clone())].as_slice())?, - )); - } - let sort_type = get_sort_type(&mut vk, |v| &mut v.1)?; - match sort_type { - SortKeyType::Number => vk.sort_by_key(|v| match v.1 { - Val::Num(n) => NonNaNf64(n), - _ => unreachable!(), - }), - SortKeyType::String => vk.sort_by_key(|v| match &v.1 { - Val::Str(s) => s.clone(), - _ => unreachable!(), - }), - SortKeyType::Unknown => unreachable!(), - }; - Ok(Cc::new(vk.into_iter().map(|v| v.0).collect())) - } else { - // Fast path, identity key getter - let mut values = (*values).clone(); - let sort_type = get_sort_type(&mut values, |k| k)?; - match sort_type { - SortKeyType::Number => values.sort_unstable_by_key(|v| match v { - Val::Num(n) => NonNaNf64(*n), - _ => unreachable!(), - }), - SortKeyType::String => values.sort_unstable_by_key(|v| match v { - Val::Str(s) => s.clone(), - _ => unreachable!(), - }), - SortKeyType::Unknown => unreachable!(), - }; - Ok(Cc::new(values)) - } -} --- a/crates/jrsonnet-evaluator/src/builtin/stdlib.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::path::PathBuf; - -use jrsonnet_parser::{LocExpr, ParserSettings}; - -thread_local! { - /// To avoid parsing again when issued from the same thread - #[allow(unreachable_code)] - static PARSED_STDLIB: LocExpr = { - #[cfg(feature = "serialized-stdlib")] - { - // Should not panic, stdlib.bincode is generated in build.rs - return bincode::deserialize(include_bytes!(concat!(env!("OUT_DIR"), "/stdlib.bincode"))) - .unwrap(); - } - - jrsonnet_parser::parse( - jrsonnet_stdlib::STDLIB_STR, - &ParserSettings { - file_name: PathBuf::from("std.jsonnet").into(), - }, - ) - .unwrap() - } -} - -pub fn get_parsed_stdlib() -> LocExpr { - PARSED_STDLIB.with(Clone::clone) -} --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -11,7 +11,7 @@ use thiserror::Error; use crate::{ - builtin::{format::FormatError, sort::SortError}, + stdlib::{format::FormatError, sort::SortError}, typed::TypeLocError, }; --- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs @@ -7,14 +7,14 @@ use jrsonnet_types::ValType; use crate::{ - builtin::{std_slice, BUILTINS}, error::Error::*, evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op}, - function::CallLocation, + function::{CallLocation, FuncDesc, FuncVal}, gc::TraceBox, + stdlib::{std_slice, BUILTINS}, throw, typed::Typed, - val::{ArrValue, FuncDesc, FuncVal, LazyValValue}, + val::{ArrValue, LazyValValue}, Bindable, Context, ContextCreator, FutureWrapper, GcHashMap, LazyBinding, LazyVal, ObjValue, ObjValueBuilder, ObjectAssertion, Result, State, Val, }; @@ -676,6 +676,7 @@ .ok_or_else(|| IntrinsicNotFound(name.clone()))?, )), IntrinsicThisFile => return Err(MagicThisFileUsed.into()), + IntrinsicId => Val::Func(FuncVal::identity()), AssertExpr(assert, returned) => { evaluate_assert(s.clone(), ctx.clone(), assert)?; evaluate(s, ctx, returned)? --- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs @@ -3,7 +3,7 @@ use jrsonnet_parser::{BinaryOpType, LocExpr, UnaryOpType}; use crate::{ - builtin::std_format, error::Error::*, evaluate, throw, typed::Typed, val::equals, Context, + error::Error::*, evaluate, stdlib::std_format, throw, typed::Typed, val::equals, Context, Result, State, Val, }; --- a/crates/jrsonnet-evaluator/src/function.rs +++ /dev/null @@ -1,549 +0,0 @@ -use std::{borrow::Cow, collections::HashMap}; - -use gcmodule::Trace; -use jrsonnet_interner::IStr; -pub use jrsonnet_macros::builtin; -use jrsonnet_parser::{ArgsDesc, ExprLocation, LocExpr, ParamsDesc}; - -use crate::{ - error::Error::*, evaluate, evaluate_named, gc::TraceBox, throw, typed::Typed, - val::LazyValValue, Context, FutureWrapper, GcHashMap, LazyVal, Result, State, Val, -}; - -#[derive(Clone, Copy)] -pub struct CallLocation<'l>(pub Option<&'l ExprLocation>); -impl<'l> CallLocation<'l> { - pub const fn new(loc: &'l ExprLocation) -> Self { - Self(Some(loc)) - } -} -impl CallLocation<'static> { - pub const fn native() -> Self { - Self(None) - } -} - -#[derive(Trace)] -struct EvaluateLazyVal { - ctx: Context, - expr: LocExpr, -} -impl LazyValValue for EvaluateLazyVal { - fn get(self: Box, s: State) -> Result { - evaluate(s, self.ctx, &self.expr) - } -} - -#[derive(Trace)] -struct EvaluateNamedLazyVal { - ctx: FutureWrapper, - name: IStr, - value: LocExpr, -} -impl LazyValValue for EvaluateNamedLazyVal { - fn get(self: Box, s: State) -> Result { - evaluate_named(s, self.ctx.unwrap(), &self.value, self.name) - } -} -pub trait ArgLike { - fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result; -} -impl ArgLike for &LocExpr { - fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result { - Ok(if tailstrict { - LazyVal::new_resolved(evaluate(s, ctx, self)?) - } else { - LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { - ctx, - expr: (*self).clone(), - }))) - }) - } -} -impl ArgLike for T -where - T: Typed + Clone, -{ - fn evaluate_arg(&self, s: State, _ctx: Context, _tailstrict: bool) -> Result { - let val = T::into_untyped(self.clone(), s)?; - Ok(LazyVal::new_resolved(val)) - } -} -pub enum TlaArg { - String(IStr), - Code(LocExpr), - Val(Val), -} -impl ArgLike for TlaArg { - fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result { - match self { - TlaArg::String(s) => Ok(LazyVal::new_resolved(Val::Str(s.clone()))), - TlaArg::Code(code) => Ok(if tailstrict { - LazyVal::new_resolved(evaluate(s, ctx, code)?) - } else { - LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { - ctx, - expr: code.clone(), - }))) - }), - TlaArg::Val(val) => Ok(LazyVal::new_resolved(val.clone())), - } - } -} - -pub trait ArgsLike { - fn unnamed_len(&self) -> usize; - fn unnamed_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, - ) -> Result<()>; - fn named_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, - ) -> Result<()>; - fn named_names(&self, handler: &mut dyn FnMut(&IStr)); -} - -impl ArgsLike for ArgsDesc { - fn unnamed_len(&self) -> usize { - self.unnamed.len() - } - - fn unnamed_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, - ) -> Result<()> { - for (id, arg) in self.unnamed.iter().enumerate() { - handler( - id, - if tailstrict { - LazyVal::new_resolved(evaluate(s.clone(), ctx.clone(), arg)?) - } else { - LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { - ctx: ctx.clone(), - expr: arg.clone(), - }))) - }, - )?; - } - Ok(()) - } - - fn named_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, - ) -> Result<()> { - for (name, arg) in &self.named { - handler( - name, - if tailstrict { - LazyVal::new_resolved(evaluate(s.clone(), ctx.clone(), arg)?) - } else { - LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { - ctx: ctx.clone(), - expr: arg.clone(), - }))) - }, - )?; - } - Ok(()) - } - - fn named_names(&self, handler: &mut dyn FnMut(&IStr)) { - for (name, _) in &self.named { - handler(name); - } - } -} - -impl ArgsLike for [(); 0] { - fn unnamed_len(&self) -> usize { - 0 - } - - fn unnamed_iter( - &self, - _s: State, - _ctx: Context, - _tailstrict: bool, - _handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, - ) -> Result<()> { - Ok(()) - } - - fn named_iter( - &self, - _s: State, - _ctx: Context, - _tailstrict: bool, - _handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, - ) -> Result<()> { - Ok(()) - } - - fn named_names(&self, _handler: &mut dyn FnMut(&IStr)) {} -} - -impl ArgsLike for [(IStr, A)] { - fn unnamed_len(&self) -> usize { - 0 - } - - fn unnamed_iter( - &self, - _s: State, - _ctx: Context, - _tailstrict: bool, - _handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, - ) -> Result<()> { - Ok(()) - } - - fn named_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, - ) -> Result<()> { - for (name, val) in self.iter() { - handler(name, val.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?)?; - } - Ok(()) - } - - fn named_names(&self, handler: &mut dyn FnMut(&IStr)) { - for (name, _) in self.iter() { - handler(name); - } - } -} - -impl ArgsLike for HashMap { - fn unnamed_len(&self) -> usize { - 0 - } - - fn unnamed_iter( - &self, - _s: State, - _ctx: Context, - _tailstrict: bool, - _handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, - ) -> Result<()> { - Ok(()) - } - - fn named_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, - ) -> Result<()> { - for (name, value) in self.iter() { - handler( - name, - value.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?, - )?; - } - Ok(()) - } - - fn named_names(&self, handler: &mut dyn FnMut(&IStr)) { - for (name, _) in self.iter() { - handler(name); - } - } -} - -impl ArgsLike for [A] { - fn unnamed_len(&self) -> usize { - self.len() - } - - fn unnamed_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, - ) -> Result<()> { - for (i, arg) in self.iter().enumerate() { - handler(i, arg.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?)?; - } - Ok(()) - } - - fn named_iter( - &self, - _s: State, - _ctx: Context, - _tailstrict: bool, - _handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, - ) -> Result<()> { - Ok(()) - } - - fn named_names(&self, _handler: &mut dyn FnMut(&IStr)) {} -} -impl ArgsLike for &[A] { - fn unnamed_len(&self) -> usize { - (*self).unnamed_len() - } - - fn unnamed_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, - ) -> Result<()> { - (*self).unnamed_iter(s, ctx, tailstrict, handler) - } - - fn named_iter( - &self, - s: State, - ctx: Context, - tailstrict: bool, - handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, - ) -> Result<()> { - (*self).named_iter(s, ctx, tailstrict, handler) - } - - fn named_names(&self, handler: &mut dyn FnMut(&IStr)) { - (*self).named_names(handler); - } -} - -/// Creates correct [context](Context) for function body evaluation returning error on invalid call. -/// -/// ## Parameters -/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set) -/// * `body_ctx`: used for default parameter values' execution and for body execution (if set) -/// * `params`: function parameters' definition -/// * `args`: passed function arguments -/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily -pub fn parse_function_call( - s: State, - ctx: Context, - body_ctx: Context, - params: &ParamsDesc, - args: &dyn ArgsLike, - tailstrict: bool, -) -> Result { - let mut passed_args = GcHashMap::with_capacity(params.len()); - if args.unnamed_len() > params.len() { - throw!(TooManyArgsFunctionHas(params.len())) - } - - let mut filled_args = 0; - - args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| { - let name = params[id].0.clone(); - passed_args.insert(name, arg); - filled_args += 1; - Ok(()) - })?; - - args.named_iter(s, ctx, tailstrict, &mut |name, value| { - // FIXME: O(n) for arg existence check - if !params.iter().any(|p| &p.0 == name) { - throw!(UnknownFunctionParameter((name as &str).to_owned())); - } - if passed_args.insert(name.clone(), value).is_some() { - throw!(BindingParameterASecondTime(name.clone())); - } - filled_args += 1; - Ok(()) - })?; - - if filled_args < params.len() { - // Some args are unset, but maybe we have defaults for them - // Default values should be created in newly created context - let fctx = Context::new_future(); - let mut defaults = GcHashMap::with_capacity(params.len() - filled_args); - - for param in params.iter().filter(|p| p.1.is_some()) { - if passed_args.contains_key(¶m.0.clone()) { - continue; - } - - defaults.insert( - param.0.clone(), - LazyVal::new(TraceBox(Box::new(EvaluateNamedLazyVal { - ctx: fctx.clone(), - name: param.0.clone(), - value: param.1.clone().expect("default exists"), - }))), - ); - filled_args += 1; - } - - // Some args still wasn't filled - if filled_args != params.len() { - for param in params.iter().skip(args.unnamed_len()) { - let mut found = false; - args.named_names(&mut |name| { - if name == ¶m.0 { - found = true; - } - }); - if !found { - throw!(FunctionParameterNotBoundInCall(param.0.clone())); - } - } - unreachable!(); - } - - Ok(body_ctx - .extend(passed_args, None, None, None) - .extend_bound(defaults) - .into_future(fctx)) - } else { - let body_ctx = body_ctx.extend(passed_args, None, None, None); - Ok(body_ctx) - } -} - -type BuiltinParamName = Cow<'static, str>; - -#[derive(Clone, Trace)] -pub struct BuiltinParam { - pub name: BuiltinParamName, - pub has_default: bool, -} - -/// Do not implement it directly, instead use #[builtin] macro -pub trait Builtin: Trace { - fn name(&self) -> &str; - fn params(&self) -> &[BuiltinParam]; - fn call(&self, s: State, ctx: Context, loc: CallLocation, args: &dyn ArgsLike) -> Result; -} - -pub trait StaticBuiltin: Builtin + Send + Sync -where - Self: 'static, -{ - // In impl, to make it object safe: - // const INST: &'static Self; -} - -/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead -/// -/// ## Parameters -/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set) -/// * `params`: function parameters' definition -/// * `args`: passed function arguments -/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily -pub fn parse_builtin_call( - s: State, - ctx: Context, - params: &[BuiltinParam], - args: &dyn ArgsLike, - tailstrict: bool, -) -> Result> { - let mut passed_args = GcHashMap::with_capacity(params.len()); - if args.unnamed_len() > params.len() { - throw!(TooManyArgsFunctionHas(params.len())) - } - - let mut filled_args = 0; - - args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| { - let name = params[id].name.clone(); - passed_args.insert(name, arg); - filled_args += 1; - Ok(()) - })?; - - args.named_iter(s, ctx, tailstrict, &mut |name, arg| { - // FIXME: O(n) for arg existence check - let p = params - .iter() - .find(|p| p.name == name as &str) - .ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?; - if passed_args.insert(p.name.clone(), arg).is_some() { - throw!(BindingParameterASecondTime(name.clone())); - } - filled_args += 1; - Ok(()) - })?; - - if filled_args < params.len() { - for param in params.iter().filter(|p| p.has_default) { - if passed_args.contains_key(¶m.name) { - continue; - } - filled_args += 1; - } - - // Some args still wasn't filled - if filled_args != params.len() { - for param in params.iter().skip(args.unnamed_len()) { - let mut found = false; - args.named_names(&mut |name| { - if name as &str == ¶m.name as &str { - found = true; - } - }); - if !found { - throw!(FunctionParameterNotBoundInCall(param.name.clone().into())); - } - } - unreachable!(); - } - } - Ok(passed_args) -} - -/// Creates Context, which has all argument default values applied -/// and with unbound values causing error to be returned -pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Context { - #[derive(Trace)] - struct DependsOnUnbound(IStr); - impl LazyValValue for DependsOnUnbound { - fn get(self: Box, _: State) -> Result { - Err(FunctionParameterNotBoundInCall(self.0.clone()).into()) - } - } - - let fctx = Context::new_future(); - - let mut bindings = GcHashMap::new(); - - for param in params.iter() { - if let Some(v) = ¶m.1 { - bindings.insert( - param.0.clone(), - LazyVal::new(TraceBox(Box::new(EvaluateNamedLazyVal { - ctx: fctx.clone(), - name: param.0.clone(), - value: v.clone(), - }))), - ); - } else { - bindings.insert( - param.0.clone(), - LazyVal::new(TraceBox(Box::new(DependsOnUnbound(param.0.clone())))), - ); - } - } - - body_ctx - .extend(bindings, None, None, None) - .into_future(fctx) -} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/function/arglike.rs @@ -0,0 +1,304 @@ +use std::collections::HashMap; + +use gcmodule::Trace; +use jrsonnet_interner::IStr; +use jrsonnet_parser::{ArgsDesc, LocExpr}; + +use crate::{ + error::Result, evaluate, gc::TraceBox, typed::Typed, val::LazyValValue, Context, LazyVal, + State, Val, +}; + +#[derive(Trace)] +struct EvaluateLazyVal { + ctx: Context, + expr: LocExpr, +} +impl LazyValValue for EvaluateLazyVal { + fn get(self: Box, s: State) -> Result { + evaluate(s, self.ctx, &self.expr) + } +} + +pub trait ArgLike { + fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result; +} + +impl ArgLike for &LocExpr { + fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result { + Ok(if tailstrict { + LazyVal::new_resolved(evaluate(s, ctx, self)?) + } else { + LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { + ctx, + expr: (*self).clone(), + }))) + }) + } +} + +impl ArgLike for T +where + T: Typed + Clone, +{ + fn evaluate_arg(&self, s: State, _ctx: Context, _tailstrict: bool) -> Result { + let val = T::into_untyped(self.clone(), s)?; + Ok(LazyVal::new_resolved(val)) + } +} + +pub enum TlaArg { + String(IStr), + Code(LocExpr), + Val(Val), +} +impl ArgLike for TlaArg { + fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result { + match self { + TlaArg::String(s) => Ok(LazyVal::new_resolved(Val::Str(s.clone()))), + TlaArg::Code(code) => Ok(if tailstrict { + LazyVal::new_resolved(evaluate(s, ctx, code)?) + } else { + LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { + ctx, + expr: code.clone(), + }))) + }), + TlaArg::Val(val) => Ok(LazyVal::new_resolved(val.clone())), + } + } +} + +mod sealed { + /// Implemented for `ArgsLike`, where only unnamed arguments present + pub trait Unnamed {} + /// Implemented for `ArgsLike`, where only named arguments present + pub trait Named {} +} + +pub trait ArgsLike { + fn unnamed_len(&self) -> usize; + fn unnamed_iter( + &self, + s: State, + ctx: Context, + tailstrict: bool, + handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, + ) -> Result<()>; + fn named_iter( + &self, + s: State, + ctx: Context, + tailstrict: bool, + handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, + ) -> Result<()>; + fn named_names(&self, handler: &mut dyn FnMut(&IStr)); +} + +impl ArgsLike for ArgsDesc { + fn unnamed_len(&self) -> usize { + self.unnamed.len() + } + + fn unnamed_iter( + &self, + s: State, + ctx: Context, + tailstrict: bool, + handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, + ) -> Result<()> { + for (id, arg) in self.unnamed.iter().enumerate() { + handler( + id, + if tailstrict { + LazyVal::new_resolved(evaluate(s.clone(), ctx.clone(), arg)?) + } else { + LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { + ctx: ctx.clone(), + expr: arg.clone(), + }))) + }, + )?; + } + Ok(()) + } + + fn named_iter( + &self, + s: State, + ctx: Context, + tailstrict: bool, + handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, + ) -> Result<()> { + for (name, arg) in &self.named { + handler( + name, + if tailstrict { + LazyVal::new_resolved(evaluate(s.clone(), ctx.clone(), arg)?) + } else { + LazyVal::new(TraceBox(Box::new(EvaluateLazyVal { + ctx: ctx.clone(), + expr: arg.clone(), + }))) + }, + )?; + } + Ok(()) + } + + fn named_names(&self, handler: &mut dyn FnMut(&IStr)) { + for (name, _) in &self.named { + handler(name); + } + } +} + +impl sealed::Named for HashMap {} +impl ArgsLike for HashMap { + fn unnamed_len(&self) -> usize { + 0 + } + + fn unnamed_iter( + &self, + _s: State, + _ctx: Context, + _tailstrict: bool, + _handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, + ) -> Result<()> { + Ok(()) + } + + fn named_iter( + &self, + s: State, + ctx: Context, + tailstrict: bool, + handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, + ) -> Result<()> { + for (name, value) in self.iter() { + handler( + name, + value.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?, + )?; + } + Ok(()) + } + + fn named_names(&self, handler: &mut dyn FnMut(&IStr)) { + for (name, _) in self.iter() { + handler(name); + } + } +} + +macro_rules! impl_args_like { + ($count:expr; $($gen:ident)*) => { + impl<$($gen: ArgLike,)*> sealed::Unnamed for ($($gen,)*) {} + impl<$($gen: ArgLike,)*> ArgsLike for ($($gen,)*) { + fn unnamed_len(&self) -> usize { + $count + } + #[allow(non_snake_case, unused_assignments)] + fn unnamed_iter( + &self, + s: State, + ctx: Context, + tailstrict: bool, + handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, + ) -> Result<()> { + let mut i = 0usize; + let ($($gen,)*) = self; + $( + handler(i, $gen.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?)?; + i+=1; + )* + Ok(()) + } + fn named_iter( + &self, + _s: State, + _ctx: Context, + _tailstrict: bool, + _handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, + ) -> Result<()> { + Ok(()) + } + fn named_names(&self, _handler: &mut dyn FnMut(&IStr)) {} + } + impl<$($gen: ArgLike,)*> sealed::Named for ($((IStr, $gen),)*) {} + impl<$($gen: ArgLike,)*> ArgsLike for ($((IStr, $gen),)*) { + fn unnamed_len(&self) -> usize { + 0 + } + fn unnamed_iter( + &self, + _s: State, + _ctx: Context, + _tailstrict: bool, + _handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, + ) -> Result<()> { + Ok(()) + } + #[allow(non_snake_case)] + fn named_iter( + &self, + s: State, + ctx: Context, + tailstrict: bool, + handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, + ) -> Result<()> { + let ($($gen,)*) = self; + $( + handler(&$gen.0, $gen.1.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?)?; + )* + Ok(()) + } + #[allow(non_snake_case)] + fn named_names(&self, handler: &mut dyn FnMut(&IStr)) { + let ($($gen,)*) = self; + $( + handler(&$gen.0); + )* + } + } + }; + ($count:expr; $($cur:ident)* @ $c:ident $($rest:ident)*) => { + impl_args_like!($count; $($cur)*); + impl_args_like!($count + 1usize; $($cur)* $c @ $($rest)*); + }; + ($count:expr; $($cur:ident)* @) => { + impl_args_like!($count; $($cur)*); + } +} +impl_args_like! { + 0usize; A @ B C D E F G H I J K L +} + +impl ArgsLike for () { + fn unnamed_len(&self) -> usize { + 0 + } + + fn unnamed_iter( + &self, + _s: State, + _ctx: Context, + _tailstrict: bool, + _handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>, + ) -> Result<()> { + Ok(()) + } + + fn named_iter( + &self, + _s: State, + _ctx: Context, + _tailstrict: bool, + _handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>, + ) -> Result<()> { + Ok(()) + } + + fn named_names(&self, _handler: &mut dyn FnMut(&IStr)) {} +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/function/builtin.rs @@ -0,0 +1,66 @@ +use std::{borrow::Cow, path::Path, rc::Rc}; + +use gcmodule::Trace; + +use super::{arglike::ArgsLike, parse::parse_builtin_call, CallLocation}; +use crate::{error::Result, gc::TraceBox, Context, State, Val}; + +pub type BuiltinParamName = Cow<'static, str>; + +#[derive(Clone, Trace)] +pub struct BuiltinParam { + pub name: BuiltinParamName, + pub has_default: bool, +} + +/// Do not implement it directly, instead use #[builtin] macro +pub trait Builtin: Trace { + fn name(&self) -> &str; + fn params(&self) -> &[BuiltinParam]; + fn call(&self, s: State, ctx: Context, loc: CallLocation, args: &dyn ArgsLike) -> Result; +} + +pub trait StaticBuiltin: Builtin + Send + Sync +where + Self: 'static, +{ + // In impl, to make it object safe: + // const INST: &'static Self; +} + +#[derive(Trace)] +pub struct NativeCallback { + pub(crate) params: Vec, + handler: TraceBox, +} +impl NativeCallback { + #[deprecated = "prefer using builtins directly, use this interface only for bindings"] + pub fn new(params: Vec, handler: TraceBox) -> Self { + Self { params, handler } + } +} + +impl Builtin for NativeCallback { + fn name(&self) -> &str { + // TODO: standard natives gets their names from definition + // But builitins should already have them + "" + } + + fn params(&self) -> &[BuiltinParam] { + &self.params + } + + fn call(&self, s: State, ctx: Context, loc: CallLocation, args: &dyn ArgsLike) -> Result { + let args = parse_builtin_call(s.clone(), ctx, &self.params, args, true)?; + let mut out_args = Vec::with_capacity(self.params.len()); + for p in &self.params { + out_args.push(args[&p.name].evaluate(s.clone())?); + } + self.handler.call(s, loc.0.map(|l| l.0.clone()), &out_args) + } +} + +pub trait NativeCallbackHandler: Trace { + fn call(&self, s: State, from: Option>, args: &[Val]) -> Result; +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/function/mod.rs @@ -0,0 +1,168 @@ +use std::fmt::Debug; + +pub use arglike::{ArgLike, ArgsLike, TlaArg}; +use gcmodule::{Cc, Trace}; +use jrsonnet_interner::IStr; +pub use jrsonnet_macros::builtin; +use jrsonnet_parser::{ExprLocation, LocExpr, ParamsDesc}; + +use self::{ + builtin::{Builtin, StaticBuiltin}, + native::NativeDesc, + parse::{parse_default_function_call, parse_function_call}, +}; +use crate::{evaluate, gc::TraceBox, typed::Any, Context, Result, State, Val}; + +pub mod arglike; +pub mod builtin; +pub mod native; +pub mod parse; + +#[derive(Clone, Copy)] +pub struct CallLocation<'l>(pub Option<&'l ExprLocation>); +impl<'l> CallLocation<'l> { + pub const fn new(loc: &'l ExprLocation) -> Self { + Self(Some(loc)) + } +} +impl CallLocation<'static> { + pub const fn native() -> Self { + Self(None) + } +} + +/// Function implemented in jsonnet +#[derive(Debug, PartialEq, Trace)] +pub struct FuncDesc { + /// In expressions like + /// ```jsonnet + /// local a = function() ... + /// local a() ... + /// { a: function() ... } + /// { a() = ... } + /// ``` + /// + /// Deducted to `a`, unspecified otherwise + pub name: IStr, + /// Context, in which this function was evaluated + /// + /// I.e in + /// ```jsonnet + /// local a = 2; + /// function() ... + /// ``` + /// context will contain `a` + pub ctx: Context, + + pub params: ParamsDesc, + pub body: LocExpr, +} +impl FuncDesc { + /// Create body context, but fill arguments without defaults with lazy error + pub fn default_body_context(&self) -> Context { + parse_default_function_call(self.ctx.clone(), &self.params) + } + + /// Create context, with which body code will run + pub fn call_body_context( + &self, + s: State, + call_ctx: Context, + args: &dyn ArgsLike, + tailstrict: bool, + ) -> Result { + parse_function_call( + s, + call_ctx, + self.ctx.clone(), + &self.params, + args, + tailstrict, + ) + } +} + +/// Any possible function value, including plain functions and user-provided builtins +#[allow(clippy::module_name_repetitions)] +#[derive(Trace, Clone)] +pub enum FuncVal { + /// std.id + Id, + /// Plain function implemented in jsonnet + Normal(Cc), + /// Standard library function + StaticBuiltin(#[skip_trace] &'static dyn StaticBuiltin), + /// User-provided function + Builtin(Cc>), +} + +impl Debug for FuncVal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Id => f.debug_tuple("Id").finish(), + Self::Normal(arg0) => f.debug_tuple("Normal").field(arg0).finish(), + Self::StaticBuiltin(arg0) => { + f.debug_tuple("StaticBuiltin").field(&arg0.name()).finish() + } + Self::Builtin(arg0) => f.debug_tuple("Builtin").field(&arg0.name()).finish(), + } + } +} + +impl FuncVal { + pub fn into_native(self) -> D::Value { + D::into_native(self) + } + pub fn params_len(&self) -> usize { + match self { + Self::Id => 1, + Self::Normal(n) => n.params.iter().filter(|p| p.1.is_none()).count(), + Self::StaticBuiltin(i) => i.params().iter().filter(|p| !p.has_default).count(), + Self::Builtin(i) => i.params().iter().filter(|p| !p.has_default).count(), + } + } + pub fn name(&self) -> IStr { + match self { + Self::Id => "id".into(), + Self::Normal(normal) => normal.name.clone(), + Self::StaticBuiltin(builtin) => builtin.name().into(), + Self::Builtin(builtin) => builtin.name().into(), + } + } + pub fn evaluate( + &self, + s: State, + call_ctx: Context, + loc: CallLocation, + args: &dyn ArgsLike, + tailstrict: bool, + ) -> Result { + match self { + Self::Id => { + #[builtin] + fn builtin_id(v: Any) -> Result { + Ok(v) + } + static ID: &'static builtin_id = &builtin_id {}; + + ID.call(s, call_ctx, loc, args) + } + Self::Normal(func) => { + let body_ctx = func.call_body_context(s.clone(), call_ctx, args, tailstrict)?; + evaluate(s, body_ctx, &func.body) + } + Self::StaticBuiltin(b) => b.call(s, call_ctx, loc, args), + Self::Builtin(b) => b.call(s, call_ctx, loc, args), + } + } + pub fn evaluate_simple(&self, s: State, args: &dyn ArgsLike) -> Result { + self.evaluate(s, Context::default(), CallLocation::native(), args, true) + } + + pub fn is_identity(&self) -> bool { + matches!(self, Self::Id) + } + pub fn identity() -> Self { + Self::Id + } +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/function/native.rs @@ -0,0 +1,43 @@ +use super::{arglike::ArgLike, CallLocation, FuncVal}; +use crate::{error::Result, typed::Typed, State}; + +pub trait NativeDesc { + type Value; + fn into_native(val: FuncVal) -> Self::Value; +} +macro_rules! impl_native_desc { + ($($gen:ident)*) => { + impl<$($gen,)* O> NativeDesc for (($($gen,)*), O) + where + $($gen: ArgLike,)* + O: Typed, + { + type Value = Box Result>; + + #[allow(non_snake_case)] + fn into_native(val: FuncVal) -> Self::Value { + Box::new(move |s: State, $($gen),*| { + let val = val.evaluate( + s.clone(), + s.create_default_context(), + CallLocation::native(), + &($($gen,)*), + true + )?; + O::from_untyped(val, s.clone()) + }) + } + } + }; + ($($cur:ident)* @ $c:ident $($rest:ident)*) => { + impl_native_desc!($($cur)*); + impl_native_desc!($($cur)* $c @ $($rest)*); + }; + ($($cur:ident)* @) => { + impl_native_desc!($($cur)*); + } +} + +impl_native_desc! { + @ A B C D E F G H I J K L +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/function/parse.rs @@ -0,0 +1,225 @@ +use gcmodule::Trace; +use jrsonnet_interner::IStr; +use jrsonnet_parser::{LocExpr, ParamsDesc}; + +use super::{ + arglike::ArgsLike, + builtin::{BuiltinParam, BuiltinParamName}, +}; +use crate::{ + error::{Error::*, Result}, + evaluate_named, + gc::{GcHashMap, TraceBox}, + throw, + val::LazyValValue, + Context, FutureWrapper, LazyVal, State, Val, +}; + +#[derive(Trace)] +struct EvaluateNamedLazyVal { + ctx: FutureWrapper, + name: IStr, + value: LocExpr, +} + +impl LazyValValue for EvaluateNamedLazyVal { + fn get(self: Box, s: State) -> Result { + evaluate_named(s, self.ctx.unwrap(), &self.value, self.name) + } +} + +/// Creates correct [context](Context) for function body evaluation returning error on invalid call. +/// +/// ## Parameters +/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set) +/// * `body_ctx`: used for default parameter values' execution and for body execution (if set) +/// * `params`: function parameters' definition +/// * `args`: passed function arguments +/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily +pub fn parse_function_call( + s: State, + ctx: Context, + body_ctx: Context, + params: &ParamsDesc, + args: &dyn ArgsLike, + tailstrict: bool, +) -> Result { + let mut passed_args = GcHashMap::with_capacity(params.len()); + if args.unnamed_len() > params.len() { + throw!(TooManyArgsFunctionHas(params.len())) + } + + let mut filled_args = 0; + + args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| { + let name = params[id].0.clone(); + passed_args.insert(name, arg); + filled_args += 1; + Ok(()) + })?; + + args.named_iter(s, ctx, tailstrict, &mut |name, value| { + // FIXME: O(n) for arg existence check + if !params.iter().any(|p| &p.0 == name) { + throw!(UnknownFunctionParameter((name as &str).to_owned())); + } + if passed_args.insert(name.clone(), value).is_some() { + throw!(BindingParameterASecondTime(name.clone())); + } + filled_args += 1; + Ok(()) + })?; + + if filled_args < params.len() { + // Some args are unset, but maybe we have defaults for them + // Default values should be created in newly created context + let fctx = Context::new_future(); + let mut defaults = GcHashMap::with_capacity(params.len() - filled_args); + + for param in params.iter().filter(|p| p.1.is_some()) { + if passed_args.contains_key(¶m.0.clone()) { + continue; + } + + defaults.insert( + param.0.clone(), + LazyVal::new(TraceBox(Box::new(EvaluateNamedLazyVal { + ctx: fctx.clone(), + name: param.0.clone(), + value: param.1.clone().expect("default exists"), + }))), + ); + filled_args += 1; + } + + // Some args still wasn't filled + if filled_args != params.len() { + for param in params.iter().skip(args.unnamed_len()) { + let mut found = false; + args.named_names(&mut |name| { + if name == ¶m.0 { + found = true; + } + }); + if !found { + throw!(FunctionParameterNotBoundInCall(param.0.clone())); + } + } + unreachable!(); + } + + Ok(body_ctx + .extend(passed_args, None, None, None) + .extend_bound(defaults) + .into_future(fctx)) + } else { + let body_ctx = body_ctx.extend(passed_args, None, None, None); + Ok(body_ctx) + } +} + +/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead +/// +/// ## Parameters +/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set) +/// * `params`: function parameters' definition +/// * `args`: passed function arguments +/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily +pub fn parse_builtin_call( + s: State, + ctx: Context, + params: &[BuiltinParam], + args: &dyn ArgsLike, + tailstrict: bool, +) -> Result> { + let mut passed_args = GcHashMap::with_capacity(params.len()); + if args.unnamed_len() > params.len() { + throw!(TooManyArgsFunctionHas(params.len())) + } + + let mut filled_args = 0; + + args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| { + let name = params[id].name.clone(); + passed_args.insert(name, arg); + filled_args += 1; + Ok(()) + })?; + + args.named_iter(s, ctx, tailstrict, &mut |name, arg| { + // FIXME: O(n) for arg existence check + let p = params + .iter() + .find(|p| p.name == name as &str) + .ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?; + if passed_args.insert(p.name.clone(), arg).is_some() { + throw!(BindingParameterASecondTime(name.clone())); + } + filled_args += 1; + Ok(()) + })?; + + if filled_args < params.len() { + for param in params.iter().filter(|p| p.has_default) { + if passed_args.contains_key(¶m.name) { + continue; + } + filled_args += 1; + } + + // Some args still wasn't filled + if filled_args != params.len() { + for param in params.iter().skip(args.unnamed_len()) { + let mut found = false; + args.named_names(&mut |name| { + if name as &str == ¶m.name as &str { + found = true; + } + }); + if !found { + throw!(FunctionParameterNotBoundInCall(param.name.clone().into())); + } + } + unreachable!(); + } + } + Ok(passed_args) +} + +/// Creates Context, which has all argument default values applied +/// and with unbound values causing error to be returned +pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Context { + #[derive(Trace)] + struct DependsOnUnbound(IStr); + impl LazyValValue for DependsOnUnbound { + fn get(self: Box, _: State) -> Result { + Err(FunctionParameterNotBoundInCall(self.0.clone()).into()) + } + } + + let fctx = Context::new_future(); + + let mut bindings = GcHashMap::new(); + + for param in params.iter() { + if let Some(v) = ¶m.1 { + bindings.insert( + param.0.clone(), + LazyVal::new(TraceBox(Box::new(EvaluateNamedLazyVal { + ctx: fctx.clone(), + name: param.0.clone(), + value: v.clone(), + }))), + ); + } else { + bindings.insert( + param.0.clone(), + LazyVal::new(TraceBox(Box::new(DependsOnUnbound(param.0.clone())))), + ); + } + } + + body_ctx + .extend(bindings, None, None, None) + .into_future(fctx) +} --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -22,7 +22,6 @@ // For jrsonnet-macros extern crate self as jrsonnet_evaluator; -mod builtin; mod ctx; mod dynamic; pub mod error; @@ -32,8 +31,8 @@ mod import; mod integrations; mod map; -pub mod native; mod obj; +mod stdlib; pub mod trace; pub mod typed; pub mod val; @@ -41,7 +40,7 @@ use std::{ cell::{Ref, RefCell, RefMut}, collections::HashMap, - fmt::Debug, + fmt::{self, Debug}, path::{Path, PathBuf}, rc::Rc, }; @@ -50,7 +49,7 @@ pub use dynamic::*; use error::{Error::*, LocError, Result, StackTraceElement}; pub use evaluate::*; -use function::{Builtin, CallLocation, TlaArg}; +use function::{builtin::Builtin, CallLocation, TlaArg}; use gc::{GcHashMap, TraceBox}; use gcmodule::{Cc, Trace, Weak}; pub use import::*; @@ -77,7 +76,7 @@ } impl Debug for LazyBinding { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LazyBinding") } } @@ -329,7 +328,7 @@ self.add_parsed_file( std_path.clone(), STDLIB_STR.to_owned().into(), - builtin::get_parsed_stdlib(), + stdlib::get_parsed_stdlib(), ) .expect("stdlib is correct"); let val = self --- a/crates/jrsonnet-evaluator/src/native.rs +++ /dev/null @@ -1,49 +0,0 @@ -#![allow(clippy::type_complexity)] - -use std::{path::Path, rc::Rc}; - -use gcmodule::Trace; - -use crate::{ - error::Result, - function::{parse_builtin_call, ArgsLike, Builtin, BuiltinParam, CallLocation}, - gc::TraceBox, - Context, State, Val, -}; - -#[derive(Trace)] -pub struct NativeCallback { - pub(crate) params: Vec, - handler: TraceBox, -} -impl NativeCallback { - #[deprecated = "prefer using builtins directly, use this interface only for bindings"] - pub fn new(params: Vec, handler: TraceBox) -> Self { - Self { params, handler } - } -} - -impl Builtin for NativeCallback { - fn name(&self) -> &str { - // TODO: standard natives gets their names from definition - // But builitins should already have them - "" - } - - fn params(&self) -> &[BuiltinParam] { - &self.params - } - - fn call(&self, s: State, ctx: Context, loc: CallLocation, args: &dyn ArgsLike) -> Result { - let args = parse_builtin_call(s.clone(), ctx, &self.params, args, true)?; - let mut out_args = Vec::with_capacity(self.params.len()); - for p in &self.params { - out_args.push(args[&p.name].evaluate(s.clone())?); - } - self.handler.call(s, loc.0.map(|l| l.0.clone()), &out_args) - } -} - -pub trait NativeCallbackHandler: Trace { - fn call(&self, s: State, from: Option>, args: &[Val]) -> Result; -} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/stdlib/expr.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; + +use jrsonnet_parser::{LocExpr, ParserSettings}; + +thread_local! { + /// To avoid parsing again when issued from the same thread + #[allow(unreachable_code)] + static PARSED_STDLIB: LocExpr = { + #[cfg(feature = "serialized-stdlib")] + { + // Should not panic, stdlib.bincode is generated in build.rs + return bincode::deserialize(include_bytes!(concat!(env!("OUT_DIR"), "/stdlib.bincode"))) + .unwrap(); + } + + jrsonnet_parser::parse( + jrsonnet_stdlib::STDLIB_STR, + &ParserSettings { + file_name: PathBuf::from("std.jsonnet").into(), + }, + ) + .unwrap() + } +} + +pub fn get_parsed_stdlib() -> LocExpr { + PARSED_STDLIB.with(Clone::clone) +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs @@ -0,0 +1,796 @@ +//! faster std.format impl +#![allow(clippy::too_many_arguments)] + +use gcmodule::Trace; +use jrsonnet_interner::IStr; +use jrsonnet_types::ValType; +use thiserror::Error; + +use crate::{error::Error::*, throw, typed::Typed, LocError, ObjValue, Result, State, Val}; + +#[derive(Debug, Clone, Error, Trace)] +pub enum FormatError { + #[error("truncated format code")] + TruncatedFormatCode, + #[error("unrecognized conversion type: {0}")] + UnrecognizedConversionType(char), + + #[error("not enough values")] + NotEnoughValues, + + #[error("cannot use * width with object")] + CannotUseStarWidthWithObject, + #[error("mapping keys required")] + MappingKeysRequired, + #[error("no such format field: {0}")] + NoSuchFormatField(IStr), +} + +impl From for LocError { + fn from(e: FormatError) -> Self { + Self::new(Format(e)) + } +} + +use FormatError::*; + +type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>; + +pub fn try_parse_mapping_key(str: &str) -> ParseResult<&str> { + if str.is_empty() { + return Err(TruncatedFormatCode); + } + let bytes = str.as_bytes(); + if bytes[0] == b'(' { + let mut i = 1; + while i < bytes.len() { + if bytes[i] == b')' { + return Ok((&str[1..i as usize], &str[i as usize + 1..])); + } + i += 1; + } + Err(TruncatedFormatCode) + } else { + Ok(("", str)) + } +} + +#[cfg(test)] +pub mod tests_key { + use super::*; + + #[test] + fn parse_key() { + assert_eq!( + try_parse_mapping_key("(hello ) world").unwrap(), + ("hello ", " world") + ); + assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world")); + assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world")); + assert_eq!( + try_parse_mapping_key(" () world").unwrap(), + ("", " () world") + ); + } + + #[test] + #[should_panic] + fn parse_key_missing_start() { + try_parse_mapping_key("").unwrap(); + } + + #[test] + #[should_panic] + fn parse_key_missing_end() { + try_parse_mapping_key("( ").unwrap(); + } +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(Default, Debug)] +pub struct CFlags { + pub alt: bool, + pub zero: bool, + pub left: bool, + pub blank: bool, + pub sign: bool, +} + +pub fn try_parse_cflags(str: &str) -> ParseResult { + if str.is_empty() { + return Err(TruncatedFormatCode); + } + let bytes = str.as_bytes(); + let mut i = 0; + let mut out = CFlags::default(); + loop { + if bytes.len() == i { + return Err(TruncatedFormatCode); + } + match bytes[i] { + b'#' => out.alt = true, + b'0' => out.zero = true, + b'-' => out.left = true, + b' ' => out.blank = true, + b'+' => out.sign = true, + _ => break, + } + i += 1; + } + Ok((out, &str[i..])) +} + +#[derive(Debug, PartialEq)] +pub enum Width { + Star, + Fixed(usize), +} +pub fn try_parse_field_width(str: &str) -> ParseResult { + if str.is_empty() { + return Err(TruncatedFormatCode); + } + let bytes = str.as_bytes(); + if bytes[0] == b'*' { + return Ok((Width::Star, &str[1..])); + } + let mut out: usize = 0; + let mut digits = 0; + while let Some(digit) = (bytes[digits] as char).to_digit(10) { + out *= 10; + out += digit as usize; + digits += 1; + if digits == bytes.len() { + return Err(TruncatedFormatCode); + } + } + Ok((Width::Fixed(out), &str[digits..])) +} + +pub fn try_parse_precision(str: &str) -> ParseResult> { + if str.is_empty() { + return Err(TruncatedFormatCode); + } + let bytes = str.as_bytes(); + if bytes[0] == b'.' { + try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s)) + } else { + Ok((None, str)) + } +} + +// Only skips +pub fn try_parse_length_modifier(str: &str) -> ParseResult<()> { + if str.is_empty() { + return Err(TruncatedFormatCode); + } + let bytes = str.as_bytes(); + let mut idx = 0; + while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' { + idx += 1; + if bytes.len() == idx { + return Err(TruncatedFormatCode); + } + } + Ok(((), &str[idx..])) +} + +#[derive(Debug, PartialEq)] +pub enum ConvTypeV { + Decimal, + Octal, + Hexadecimal, + Scientific, + Float, + Shorter, + Char, + String, + Percent, +} +pub struct ConvType { + v: ConvTypeV, + caps: bool, +} + +pub fn parse_conversion_type(str: &str) -> ParseResult { + if str.is_empty() { + return Err(TruncatedFormatCode); + } + + let code = str.as_bytes()[0]; + let v: (ConvTypeV, bool) = match code { + b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false), + b'o' => (ConvTypeV::Octal, false), + b'x' => (ConvTypeV::Hexadecimal, false), + b'X' => (ConvTypeV::Hexadecimal, true), + b'e' => (ConvTypeV::Scientific, false), + b'E' => (ConvTypeV::Scientific, true), + b'f' => (ConvTypeV::Float, false), + b'F' => (ConvTypeV::Float, true), + b'g' => (ConvTypeV::Shorter, false), + b'G' => (ConvTypeV::Shorter, true), + b'c' => (ConvTypeV::Char, false), + b's' => (ConvTypeV::String, false), + b'%' => (ConvTypeV::Percent, false), + c => return Err(UnrecognizedConversionType(c as char)), + }; + + Ok((ConvType { v: v.0, caps: v.1 }, &str[1..])) +} + +#[derive(Debug)] +pub struct Code<'s> { + mkey: &'s str, + cflags: CFlags, + width: Width, + precision: Option, + convtype: ConvTypeV, + caps: bool, +} +pub fn parse_code(str: &str) -> ParseResult { + if str.is_empty() { + return Err(TruncatedFormatCode); + } + let (mkey, str) = try_parse_mapping_key(str)?; + let (cflags, str) = try_parse_cflags(str)?; + let (width, str) = try_parse_field_width(str)?; + let (precision, str) = try_parse_precision(str)?; + let (_, str) = try_parse_length_modifier(str)?; + let (convtype, str) = parse_conversion_type(str)?; + + Ok(( + Code { + mkey, + cflags, + width, + precision, + convtype: convtype.v, + caps: convtype.caps, + }, + str, + )) +} + +#[derive(Debug)] +pub enum Element<'s> { + String(&'s str), + Code(Code<'s>), +} +pub fn parse_codes(mut str: &str) -> Result> { + let mut bytes = str.as_bytes(); + let mut out = vec![]; + let mut offset = 0; + + loop { + while offset != bytes.len() && bytes[offset] != b'%' { + offset += 1; + } + if offset != 0 { + out.push(Element::String(&str[0..offset])); + } + if offset == bytes.len() { + return Ok(out); + } + str = &str[offset + 1..]; + let code; + (code, str) = parse_code(str)?; + bytes = str.as_bytes(); + offset = 0; + + out.push(Element::Code(code)); + } +} + +const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz"; + +#[inline] +pub fn render_integer( + out: &mut String, + iv: i64, + padding: usize, + precision: usize, + blank: bool, + sign: bool, + radix: i64, + prefix: &str, + caps: bool, +) { + // Digit char indexes in reverse order, i.e + // for radix = 16 and n = 12f: [15, 2, 1] + let digits = if iv == 0 { + vec![0u8] + } else { + let mut v = iv.abs(); + let mut nums = Vec::with_capacity(1); + while v > 0 { + nums.push((v % radix) as u8); + v /= radix; + } + nums + }; + let neg = iv < 0; + let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 }); + let zp2 = zp + .max(precision) + .saturating_sub(prefix.len() + digits.len()); + + if neg { + out.push('-'); + } else if sign { + out.push('+'); + } else if blank { + out.push(' '); + } + + out.reserve(zp2); + for _ in 0..zp2 { + out.push('0'); + } + out.push_str(prefix); + + for digit in digits.into_iter().rev() { + let ch = NUMBERS[digit as usize] as char; + out.push(if caps { ch.to_ascii_uppercase() } else { ch }); + } +} + +pub fn render_decimal( + out: &mut String, + iv: i64, + padding: usize, + precision: usize, + blank: bool, + sign: bool, +) { + render_integer(out, iv, padding, precision, blank, sign, 10, "", false); +} +pub fn render_octal( + out: &mut String, + iv: i64, + padding: usize, + precision: usize, + alt: bool, + blank: bool, + sign: bool, +) { + render_integer( + out, + iv, + padding, + precision, + blank, + sign, + 8, + if alt && iv != 0 { "0" } else { "" }, + false, + ); +} + +#[allow(clippy::fn_params_excessive_bools)] +pub fn render_hexadecimal( + out: &mut String, + iv: i64, + padding: usize, + precision: usize, + alt: bool, + blank: bool, + sign: bool, + caps: bool, +) { + render_integer( + out, + iv, + padding, + precision, + blank, + sign, + 16, + match (alt, caps) { + (true, true) => "0X", + (true, false) => "0x", + (false, _) => "", + }, + caps, + ); +} + +#[allow(clippy::fn_params_excessive_bools)] +pub fn render_float( + out: &mut String, + n: f64, + mut padding: usize, + precision: usize, + blank: bool, + sign: bool, + ensure_pt: bool, + trailing: bool, +) { + let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 }; + padding = padding.saturating_sub(dot_size + precision); + render_decimal(out, n.floor() as i64, padding, 0, blank, sign); + if precision == 0 { + if ensure_pt { + out.push('.'); + } + return; + } + let frac = n + .fract() + .mul_add(10.0_f64.powf(precision as f64), 0.5) + .floor(); + if trailing || frac > 0.0 { + out.push('.'); + let mut frac_str = String::new(); + render_decimal(&mut frac_str, frac as i64, precision, 0, false, false); + let mut trim = frac_str.len(); + if !trailing { + for b in frac_str.as_bytes().iter().rev() { + if *b == b'0' { + trim -= 1; + } + } + } + out.push_str(&frac_str[..trim]); + } else if ensure_pt { + out.push('.'); + } +} + +#[allow(clippy::fn_params_excessive_bools)] +pub fn render_float_sci( + out: &mut String, + n: f64, + mut padding: usize, + precision: usize, + blank: bool, + sign: bool, + ensure_pt: bool, + trailing: bool, + caps: bool, +) { + let exponent = n.log10().floor(); + let mantissa = if exponent as i16 == -324 { + n * 10.0 / 10.0_f64.powf(exponent + 1.0) + } else { + n / 10.0_f64.powf(exponent) + }; + let mut exponent_str = String::new(); + render_decimal(&mut exponent_str, exponent as i64, 3, 0, false, true); + + // +1 for e + padding = padding.saturating_sub(exponent_str.len() + 1); + + render_float( + out, mantissa, padding, precision, blank, sign, ensure_pt, trailing, + ); + out.push(if caps { 'E' } else { 'e' }); + out.push_str(&exponent_str); +} + +#[allow(clippy::too_many_lines)] +pub fn format_code( + s: State, + out: &mut String, + value: &Val, + code: &Code, + width: usize, + precision: Option, +) -> Result<()> { + let clfags = &code.cflags; + let (fpprec, iprec) = match precision { + Some(v) => (v, v), + None => (6, 0), + }; + let padding = if clfags.zero && !clfags.left { + width + } else { + 0 + }; + + // TODO: If left padded, can optimize by writing directly to out + let mut tmp_out = String::new(); + + match code.convtype { + ConvTypeV::String => tmp_out.push_str(&value.clone().to_string(s)?), + ConvTypeV::Decimal => { + let value = f64::from_untyped(value.clone(), s)?; + render_decimal( + &mut tmp_out, + value as i64, + padding, + iprec, + clfags.blank, + clfags.sign, + ); + } + ConvTypeV::Octal => { + let value = f64::from_untyped(value.clone(), s)?; + render_octal( + &mut tmp_out, + value as i64, + padding, + iprec, + clfags.alt, + clfags.blank, + clfags.sign, + ); + } + ConvTypeV::Hexadecimal => { + let value = f64::from_untyped(value.clone(), s)?; + render_hexadecimal( + &mut tmp_out, + value as i64, + padding, + iprec, + clfags.alt, + clfags.blank, + clfags.sign, + code.caps, + ); + } + ConvTypeV::Scientific => { + let value = f64::from_untyped(value.clone(), s)?; + render_float_sci( + &mut tmp_out, + value, + padding, + fpprec, + clfags.blank, + clfags.sign, + clfags.alt, + true, + code.caps, + ); + } + ConvTypeV::Float => { + let value = f64::from_untyped(value.clone(), s)?; + render_float( + &mut tmp_out, + value, + padding, + fpprec, + clfags.blank, + clfags.sign, + clfags.alt, + true, + ); + } + ConvTypeV::Shorter => { + let value = f64::from_untyped(value.clone(), s)?; + let exponent = value.log10().floor(); + if exponent < -4.0 || exponent >= fpprec as f64 { + render_float_sci( + &mut tmp_out, + value, + padding, + fpprec - 1, + clfags.blank, + clfags.sign, + clfags.alt, + clfags.alt, + code.caps, + ); + } else { + let digits_before_pt = 1.max(exponent as usize + 1); + render_float( + &mut tmp_out, + value, + padding, + fpprec - digits_before_pt, + clfags.blank, + clfags.sign, + clfags.alt, + clfags.alt, + ); + } + } + ConvTypeV::Char => match value.clone() { + Val::Num(n) => tmp_out + .push(std::char::from_u32(n as u32).ok_or(InvalidUnicodeCodepointGot(n as u32))?), + Val::Str(s) => { + if s.chars().count() != 1 { + throw!(RuntimeError( + format!("%c expected 1 char string, got {}", s.chars().count()).into(), + )); + } + tmp_out.push_str(&s); + } + _ => { + throw!(TypeMismatch( + "%c requires number/string", + vec![ValType::Num, ValType::Str], + value.value_type(), + )); + } + }, + ConvTypeV::Percent => tmp_out.push('%'), + }; + + let padding = width.saturating_sub(tmp_out.len()); + + if !clfags.left { + for _ in 0..padding { + out.push(' '); + } + } + out.push_str(&tmp_out); + if clfags.left { + for _ in 0..padding { + out.push(' '); + } + } + + Ok(()) +} + +pub fn format_arr(s: State, str: &str, mut values: &[Val]) -> Result { + let codes = parse_codes(str)?; + let mut out = String::new(); + + for code in codes { + match code { + Element::String(s) => { + out.push_str(s); + } + Element::Code(c) => { + let width = match c.width { + Width::Star => { + if values.is_empty() { + throw!(NotEnoughValues); + } + let value = &values[0]; + values = &values[1..]; + usize::from_untyped(value.clone(), s.clone())? + } + Width::Fixed(n) => n, + }; + let precision = match c.precision { + Some(Width::Star) => { + if values.is_empty() { + throw!(NotEnoughValues); + } + let value = &values[0]; + values = &values[1..]; + Some(usize::from_untyped(value.clone(), s.clone())?) + } + Some(Width::Fixed(n)) => Some(n), + None => None, + }; + + // %% should not consume a value + let value = if c.convtype == ConvTypeV::Percent { + &Val::Null + } else { + if values.is_empty() { + throw!(NotEnoughValues); + } + let value = &values[0]; + values = &values[1..]; + value + }; + + format_code(s.clone(), &mut out, value, &c, width, precision)?; + } + } + } + + Ok(out) +} + +pub fn format_obj(s: State, str: &str, values: &ObjValue) -> Result { + let codes = parse_codes(str)?; + let mut out = String::new(); + + for code in codes { + match code { + Element::String(s) => { + out.push_str(s); + } + Element::Code(c) => { + // TODO: Operate on ref + let f: IStr = c.mkey.into(); + let width = match c.width { + Width::Star => { + throw!(CannotUseStarWidthWithObject); + } + Width::Fixed(n) => n, + }; + let precision = match c.precision { + Some(Width::Star) => { + throw!(CannotUseStarWidthWithObject); + } + Some(Width::Fixed(n)) => Some(n), + None => None, + }; + + let value = if c.convtype == ConvTypeV::Percent { + Val::Null + } else { + if f.is_empty() { + throw!(MappingKeysRequired); + } + if let Some(v) = values.get(s.clone(), f.clone())? { + v + } else { + throw!(NoSuchFormatField(f)); + } + }; + + format_code(s.clone(), &mut out, &value, &c, width, precision)?; + } + } + } + + Ok(out) +} + +#[cfg(test)] +pub mod test_format { + use super::*; + + #[test] + fn parse() { + assert_eq!( + parse_codes( + "How much error budget is left looking at our %.3f%% availability gurantees?" + ) + .unwrap() + .len(), + 4 + ); + } + + #[test] + fn octals() { + let s = State::default(); + assert_eq!( + format_arr(s.clone(), "%#o", &[Val::Num(8.0)]).unwrap(), + "010" + ); + assert_eq!( + format_arr(s.clone(), "%#4o", &[Val::Num(8.0)]).unwrap(), + " 010" + ); + assert_eq!( + format_arr(s.clone(), "%4o", &[Val::Num(8.0)]).unwrap(), + " 10" + ); + assert_eq!( + format_arr(s.clone(), "%04o", &[Val::Num(8.0)]).unwrap(), + "0010" + ); + assert_eq!( + format_arr(s.clone(), "%+4o", &[Val::Num(8.0)]).unwrap(), + " +10" + ); + assert_eq!( + format_arr(s.clone(), "%+04o", &[Val::Num(8.0)]).unwrap(), + "+010" + ); + assert_eq!( + format_arr(s.clone(), "%-4o", &[Val::Num(8.0)]).unwrap(), + "10 " + ); + assert_eq!( + format_arr(s.clone(), "%+-4o", &[Val::Num(8.0)]).unwrap(), + "+10 " + ); + assert_eq!( + format_arr(s.clone(), "%+-04o", &[Val::Num(8.0)]).unwrap(), + "+10 " + ); + } + + #[test] + fn percent_doesnt_consumes_values() { + let s = State::default(); + assert_eq!( + format_arr( + s, + "How much error budget is left looking at our %.3f%% availability gurantees?", + &[Val::Num(4.0)] + ) + .unwrap(), + "How much error budget is left looking at our 4.000% availability gurantees?" + ); + } +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/stdlib/manifest.rs @@ -0,0 +1,347 @@ +use crate::{ + error::{Error::*, Result}, + throw, State, Val, +}; + +#[derive(PartialEq, Clone, Copy)] +pub enum ManifestType { + // Applied in manifestification + Manifest, + /// Used for std.manifestJson + /// Empty array/objects extends to "[\n\n]" instead of "[ ]" as in manifest + Std, + /// No line breaks, used in `obj+''` + ToString, + /// Minified json + Minify, +} + +pub struct ManifestJsonOptions<'s> { + pub padding: &'s str, + pub mtype: ManifestType, + pub newline: &'s str, + pub key_val_sep: &'s str, + #[cfg(feature = "exp-preserve-order")] + pub preserve_order: bool, +} + +pub fn manifest_json_ex(s: State, val: &Val, options: &ManifestJsonOptions<'_>) -> Result { + let mut out = String::new(); + manifest_json_ex_buf(s, val, &mut out, &mut String::new(), options)?; + Ok(out) +} +fn manifest_json_ex_buf( + s: State, + val: &Val, + buf: &mut String, + cur_padding: &mut String, + options: &ManifestJsonOptions<'_>, +) -> Result<()> { + use std::fmt::Write; + let mtype = options.mtype; + match val { + Val::Bool(v) => { + if *v { + buf.push_str("true"); + } else { + buf.push_str("false"); + } + } + Val::Null => buf.push_str("null"), + Val::Str(s) => escape_string_json_buf(s, buf), + Val::Num(n) => write!(buf, "{}", n).unwrap(), + Val::Arr(items) => { + buf.push('['); + if !items.is_empty() { + if mtype != ManifestType::ToString && mtype != ManifestType::Minify { + buf.push_str(options.newline); + } + + let old_len = cur_padding.len(); + cur_padding.push_str(options.padding); + for (i, item) in items.iter(s.clone()).enumerate() { + if i != 0 { + buf.push(','); + if mtype == ManifestType::ToString { + buf.push(' '); + } else if mtype != ManifestType::Minify { + buf.push_str(options.newline); + } + } + buf.push_str(cur_padding); + manifest_json_ex_buf(s.clone(), &item?, buf, cur_padding, options)?; + } + cur_padding.truncate(old_len); + + if mtype != ManifestType::ToString && mtype != ManifestType::Minify { + buf.push_str(options.newline); + buf.push_str(cur_padding); + } + } else if mtype == ManifestType::Std { + buf.push_str("\n\n"); + buf.push_str(cur_padding); + } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest { + buf.push(' '); + } + buf.push(']'); + } + Val::Obj(obj) => { + obj.run_assertions(s.clone())?; + buf.push('{'); + let fields = obj.fields( + #[cfg(feature = "exp-preserve-order")] + options.preserve_order, + ); + if !fields.is_empty() { + if mtype != ManifestType::ToString && mtype != ManifestType::Minify { + buf.push_str(options.newline); + } + + let old_len = cur_padding.len(); + cur_padding.push_str(options.padding); + for (i, field) in fields.into_iter().enumerate() { + if i != 0 { + buf.push(','); + if mtype == ManifestType::ToString { + buf.push(' '); + } else if mtype != ManifestType::Minify { + buf.push_str(options.newline); + } + } + buf.push_str(cur_padding); + escape_string_json_buf(&field, buf); + buf.push_str(options.key_val_sep); + s.push_description( + || format!("field <{}> manifestification", field.clone()), + || { + let value = obj.get(s.clone(), field.clone())?.unwrap(); + manifest_json_ex_buf(s.clone(), &value, buf, cur_padding, options)?; + Ok(Val::Null) + }, + )?; + } + cur_padding.truncate(old_len); + + if mtype != ManifestType::ToString && mtype != ManifestType::Minify { + buf.push_str(options.newline); + buf.push_str(cur_padding); + } + } else if mtype == ManifestType::Std { + buf.push_str("\n\n"); + buf.push_str(cur_padding); + } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest { + buf.push(' '); + } + buf.push('}'); + } + Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())), + }; + Ok(()) +} + +pub fn escape_string_json(s: &str) -> String { + let mut buf = String::new(); + escape_string_json_buf(s, &mut buf); + buf +} + +fn escape_string_json_buf(s: &str, buf: &mut String) { + use std::fmt::Write; + buf.push('"'); + for c in s.chars() { + match c { + '"' => buf.push_str("\\\""), + '\\' => buf.push_str("\\\\"), + '\u{0008}' => buf.push_str("\\b"), + '\u{000c}' => buf.push_str("\\f"), + '\n' => buf.push_str("\\n"), + '\r' => buf.push_str("\\r"), + '\t' => buf.push_str("\\t"), + c if c < 32 as char || (c >= 127 as char && c <= 159 as char) => { + write!(buf, "\\u{:04x}", c as u32).unwrap(); + } + c => buf.push(c), + } + } + buf.push('"'); +} + +pub struct ManifestYamlOptions<'s> { + /// Padding before fields, i.e + /// ```yaml + /// a: + /// b: + /// ## <- this + /// ``` + pub padding: &'s str, + /// Padding before array elements in objects + /// ```yaml + /// a: + /// - 1 + /// ## <- this + /// ``` + pub arr_element_padding: &'s str, + /// Should yaml keys appear unescaped, when possible + /// ```yaml + /// "safe_key": 1 + /// # vs + /// safe_key: 1 + /// ``` + pub quote_keys: bool, + /// If true - then order of fields is preserved as written, + /// instead of sorting alphabetically + #[cfg(feature = "exp-preserve-order")] + pub preserve_order: bool, +} + +/// From +/// With added date check +fn yaml_needs_quotes(string: &str) -> bool { + fn need_quotes_spaces(string: &str) -> bool { + string.starts_with(' ') || string.ends_with(' ') + } + + string.is_empty() + || need_quotes_spaces(string) + || string.starts_with(|c| matches!(c, '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@')) + || string.contains(|c| matches!(c, ':' | '{' | '}' | '[' | ']' | ',' | '#' | '`' | '\"' | '\'' | '\\' | '\0'..='\x06' | '\t' | '\n' | '\r' | '\x0e'..='\x1a' | '\x1c'..='\x1f')) + || [ + // http://yaml.org/type/bool.html + // Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse + // them as string, not booleans, although it is violating the YAML 1.1 specification. + // See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088. + "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", "false", + "on", "On", "ON", "off", "Off", "OFF", // http://yaml.org/type/null.html + "null", "Null", "NULL", "~", + ].contains(&string) + || (string.chars().all(|c| matches!(c, '0'..='9' | '-')) + && string.chars().filter(|c| *c == '-').count() == 2) + || string.starts_with('.') + || string.starts_with("0x") + || string.parse::().is_ok() + || string.parse::().is_ok() +} + +pub fn manifest_yaml_ex(s: State, val: &Val, options: &ManifestYamlOptions<'_>) -> Result { + let mut out = String::new(); + manifest_yaml_ex_buf(s, val, &mut out, &mut String::new(), options)?; + Ok(out) +} + +#[allow(clippy::too_many_lines)] +fn manifest_yaml_ex_buf( + s: State, + val: &Val, + buf: &mut String, + cur_padding: &mut String, + options: &ManifestYamlOptions<'_>, +) -> Result<()> { + use std::fmt::Write; + match val { + Val::Bool(v) => { + if *v { + buf.push_str("true"); + } else { + buf.push_str("false"); + } + } + Val::Null => buf.push_str("null"), + Val::Str(s) => { + if s.is_empty() { + buf.push_str("\"\""); + } else if let Some(s) = s.strip_suffix('\n') { + buf.push('|'); + for line in s.split('\n') { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(options.padding); + buf.push_str(line); + } + } else if !options.quote_keys && !yaml_needs_quotes(s) { + buf.push_str(s); + } else { + escape_string_json_buf(s, buf); + } + } + Val::Num(n) => write!(buf, "{}", *n).unwrap(), + Val::Arr(a) => { + if a.is_empty() { + buf.push_str("[]"); + } else { + for (i, item) in a.iter(s.clone()).enumerate() { + if i != 0 { + buf.push('\n'); + buf.push_str(cur_padding); + } + let item = item?; + buf.push('-'); + match &item { + Val::Arr(a) if !a.is_empty() => { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(options.padding); + } + _ => buf.push(' '), + } + let extra_padding = match &item { + Val::Arr(a) => !a.is_empty(), + Val::Obj(o) => !o.is_empty(), + _ => false, + }; + let prev_len = cur_padding.len(); + if extra_padding { + cur_padding.push_str(options.padding); + } + manifest_yaml_ex_buf(s.clone(), &item, buf, cur_padding, options)?; + cur_padding.truncate(prev_len); + } + } + } + Val::Obj(o) => { + if o.is_empty() { + buf.push_str("{}"); + } else { + for (i, key) in o + .fields( + #[cfg(feature = "exp-preserve-order")] + options.preserve_order, + ) + .iter() + .enumerate() + { + if i != 0 { + buf.push('\n'); + buf.push_str(cur_padding); + } + if !options.quote_keys && !yaml_needs_quotes(key) { + buf.push_str(key); + } else { + escape_string_json_buf(key, buf); + } + buf.push(':'); + let prev_len = cur_padding.len(); + let item = o.get(s.clone(), key.clone())?.expect("field exists"); + match &item { + Val::Arr(a) if !a.is_empty() => { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(options.arr_element_padding); + cur_padding.push_str(options.arr_element_padding); + } + Val::Obj(o) if !o.is_empty() => { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(options.padding); + cur_padding.push_str(options.padding); + } + _ => buf.push(' '), + } + manifest_yaml_ex_buf(s.clone(), &item, buf, cur_padding, options)?; + cur_padding.truncate(prev_len); + } + } + } + Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())), + } + Ok(()) +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/stdlib/mod.rs @@ -0,0 +1,743 @@ +// All builtins should return results +#![allow(clippy::unnecessary_wraps)] + +use std::collections::HashMap; + +use format::{format_arr, format_obj}; +use gcmodule::Cc; +use jrsonnet_interner::IStr; +use serde::Deserialize; +use serde_yaml::DeserializingQuirks; + +use crate::{ + error::{Error::*, Result}, + function::{builtin::StaticBuiltin, CallLocation, FuncVal}, + operator::evaluate_mod_op, + stdlib::manifest::{manifest_yaml_ex, ManifestYamlOptions}, + throw, + typed::{Any, BoundedUsize, Bytes, Either2, Either4, PositiveF64, Typed, VecVal, M1}, + val::{equals, primitive_equals, ArrValue, IndexableVal, Slice}, + Either, ObjValue, State, Val, +}; + +pub mod expr; +pub use expr::*; + +use self::manifest::{escape_string_json, manifest_json_ex, ManifestJsonOptions, ManifestType}; + +pub mod format; +pub mod manifest; +pub mod sort; + +pub fn std_format(s: State, str: IStr, vals: Val) -> Result { + s.push( + CallLocation::native(), + || format!("std.format of {}", str), + || { + Ok(match vals { + Val::Arr(vals) => format_arr(s.clone(), &str, &vals.evaluated(s.clone())?)?, + Val::Obj(obj) => format_obj(s.clone(), &str, &obj)?, + o => format_arr(s.clone(), &str, &[o])?, + }) + }, + ) +} + +pub fn std_slice( + indexable: IndexableVal, + index: Option>, + end: Option>, + step: Option>, +) -> Result { + match &indexable { + IndexableVal::Str(s) => { + let index = index.as_deref().copied().unwrap_or(0); + let end = end.as_deref().copied().unwrap_or(usize::MAX); + let step = step.as_deref().copied().unwrap_or(1); + + if index >= end { + return Ok(Val::Str("".into())); + } + + Ok(Val::Str( + (s.chars() + .skip(index) + .take(end - index) + .step_by(step) + .collect::()) + .into(), + )) + } + IndexableVal::Arr(arr) => { + let index = index.as_deref().copied().unwrap_or(0); + let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len()); + let step = step.as_deref().copied().unwrap_or(1); + + if index >= end { + return Ok(Val::Arr(ArrValue::new_eager())); + } + + Ok(Val::Arr(ArrValue::Slice(Box::new(Slice { + inner: arr.clone(), + from: index as u32, + to: end as u32, + step: step as u32, + })))) + } + } +} + +type BuiltinsType = HashMap; + +thread_local! { + pub static BUILTINS: BuiltinsType = { + [ + ("length".into(), builtin_length::INST), + ("type".into(), builtin_type::INST), + ("makeArray".into(), builtin_make_array::INST), + ("codepoint".into(), builtin_codepoint::INST), + ("objectFieldsEx".into(), builtin_object_fields_ex::INST), + ("objectHasEx".into(), builtin_object_has_ex::INST), + ("slice".into(), builtin_slice::INST), + ("substr".into(), builtin_substr::INST), + ("primitiveEquals".into(), builtin_primitive_equals::INST), + ("equals".into(), builtin_equals::INST), + ("modulo".into(), builtin_modulo::INST), + ("mod".into(), builtin_mod::INST), + ("floor".into(), builtin_floor::INST), + ("ceil".into(), builtin_ceil::INST), + ("log".into(), builtin_log::INST), + ("pow".into(), builtin_pow::INST), + ("sqrt".into(), builtin_sqrt::INST), + ("sin".into(), builtin_sin::INST), + ("cos".into(), builtin_cos::INST), + ("tan".into(), builtin_tan::INST), + ("asin".into(), builtin_asin::INST), + ("acos".into(), builtin_acos::INST), + ("atan".into(), builtin_atan::INST), + ("exp".into(), builtin_exp::INST), + ("mantissa".into(), builtin_mantissa::INST), + ("exponent".into(), builtin_exponent::INST), + ("extVar".into(), builtin_ext_var::INST), + ("native".into(), builtin_native::INST), + ("filter".into(), builtin_filter::INST), + ("map".into(), builtin_map::INST), + ("flatMap".into(), builtin_flatmap::INST), + ("foldl".into(), builtin_foldl::INST), + ("foldr".into(), builtin_foldr::INST), + ("sort".into(), builtin_sort::INST), + ("format".into(), builtin_format::INST), + ("range".into(), builtin_range::INST), + ("char".into(), builtin_char::INST), + ("encodeUTF8".into(), builtin_encode_utf8::INST), + ("decodeUTF8".into(), builtin_decode_utf8::INST), + ("md5".into(), builtin_md5::INST), + ("base64".into(), builtin_base64::INST), + ("base64DecodeBytes".into(), builtin_base64_decode_bytes::INST), + ("base64Decode".into(), builtin_base64_decode::INST), + ("trace".into(), builtin_trace::INST), + ("join".into(), builtin_join::INST), + ("escapeStringJson".into(), builtin_escape_string_json::INST), + ("manifestJsonEx".into(), builtin_manifest_json_ex::INST), + ("manifestYamlDoc".into(), builtin_manifest_yaml_doc::INST), + ("reverse".into(), builtin_reverse::INST), + ("strReplace".into(), builtin_str_replace::INST), + ("splitLimit".into(), builtin_splitlimit::INST), + ("parseJson".into(), builtin_parse_json::INST), + ("parseYaml".into(), builtin_parse_yaml::INST), + ("asciiUpper".into(), builtin_ascii_upper::INST), + ("asciiLower".into(), builtin_ascii_lower::INST), + ("member".into(), builtin_member::INST), + ("count".into(), builtin_count::INST), + ("any".into(), builtin_any::INST), + ("all".into(), builtin_all::INST), + ].iter().cloned().collect() + }; +} + +#[jrsonnet_macros::builtin] +fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> Result { + use Either4::*; + Ok(match x { + A(x) => x.chars().count(), + B(x) => x.len(), + C(x) => x.len(), + D(f) => f.params_len(), + }) +} + +#[jrsonnet_macros::builtin] +fn builtin_type(x: Any) -> Result { + Ok(x.0.value_type().name().into()) +} + +#[jrsonnet_macros::builtin] +fn builtin_make_array(s: State, sz: usize, func: FuncVal) -> Result { + let mut out = Vec::with_capacity(sz); + for i in 0..sz { + out.push(func.evaluate_simple(s.clone(), &(i as f64,))?); + } + Ok(VecVal(Cc::new(out))) +} + +#[jrsonnet_macros::builtin] +const fn builtin_codepoint(str: char) -> Result { + Ok(str as u32) +} + +#[jrsonnet_macros::builtin] +fn builtin_object_fields_ex( + obj: ObjValue, + inc_hidden: bool, + #[cfg(feature = "exp-preserve-order")] preserve_order: Option, +) -> Result { + #[cfg(feature = "exp-preserve-order")] + let preserve_order = preserve_order.unwrap_or(false); + let out = obj.fields_ex( + inc_hidden, + #[cfg(feature = "exp-preserve-order")] + preserve_order, + ); + Ok(VecVal(Cc::new( + out.into_iter().map(Val::Str).collect::>(), + ))) +} + +#[jrsonnet_macros::builtin] +fn builtin_object_has_ex(obj: ObjValue, f: IStr, inc_hidden: bool) -> Result { + Ok(obj.has_field_ex(f, inc_hidden)) +} + +#[jrsonnet_macros::builtin] +fn builtin_parse_json(st: State, s: IStr) -> Result { + use serde_json::Value; + let value: Value = serde_json::from_str(&s) + .map_err(|e| RuntimeError(format!("failed to parse json: {}", e).into()))?; + Ok(Any(Value::into_untyped(value, st)?)) +} + +#[jrsonnet_macros::builtin] +fn builtin_parse_yaml(st: State, s: IStr) -> Result { + use serde_json::Value; + let value = serde_yaml::Deserializer::from_str_with_quirks( + &s, + DeserializingQuirks { old_octals: true }, + ); + let mut out = vec![]; + for item in value { + let value = Value::deserialize(item) + .map_err(|e| RuntimeError(format!("failed to parse yaml: {}", e).into()))?; + let val = Value::into_untyped(value, st.clone())?; + out.push(val); + } + Ok(Any(if out.is_empty() { + Val::Null + } else if out.len() == 1 { + out.into_iter().next().unwrap() + } else { + Val::Arr(out.into()) + })) +} + +#[jrsonnet_macros::builtin] +fn builtin_slice( + indexable: IndexableVal, + index: Option>, + end: Option>, + step: Option>, +) -> Result { + std_slice(indexable, index, end, step).map(Any) +} + +#[jrsonnet_macros::builtin] +fn builtin_substr(str: IStr, from: usize, len: usize) -> Result { + Ok(str.chars().skip(from as usize).take(len as usize).collect()) +} + +#[jrsonnet_macros::builtin] +fn builtin_primitive_equals(a: Any, b: Any) -> Result { + primitive_equals(&a.0, &b.0) +} + +#[jrsonnet_macros::builtin] +fn builtin_equals(s: State, a: Any, b: Any) -> Result { + equals(s, &a.0, &b.0) +} + +#[jrsonnet_macros::builtin] +fn builtin_modulo(a: f64, b: f64) -> Result { + Ok(a % b) +} + +#[jrsonnet_macros::builtin] +fn builtin_mod(s: State, a: Either![f64, IStr], b: Any) -> Result { + use Either2::*; + Ok(Any(evaluate_mod_op( + s, + &match a { + A(v) => Val::Num(v), + B(s) => Val::Str(s), + }, + &b.0, + )?)) +} + +#[jrsonnet_macros::builtin] +fn builtin_floor(x: f64) -> Result { + Ok(x.floor()) +} + +#[jrsonnet_macros::builtin] +fn builtin_ceil(x: f64) -> Result { + Ok(x.ceil()) +} + +#[jrsonnet_macros::builtin] +fn builtin_log(n: f64) -> Result { + Ok(n.ln()) +} + +#[jrsonnet_macros::builtin] +fn builtin_pow(x: f64, n: f64) -> Result { + Ok(x.powf(n)) +} + +#[jrsonnet_macros::builtin] +fn builtin_sqrt(x: PositiveF64) -> Result { + Ok(x.0.sqrt()) +} + +#[jrsonnet_macros::builtin] +fn builtin_sin(x: f64) -> Result { + Ok(x.sin()) +} + +#[jrsonnet_macros::builtin] +fn builtin_cos(x: f64) -> Result { + Ok(x.cos()) +} + +#[jrsonnet_macros::builtin] +fn builtin_tan(x: f64) -> Result { + Ok(x.tan()) +} + +#[jrsonnet_macros::builtin] +fn builtin_asin(x: f64) -> Result { + Ok(x.asin()) +} + +#[jrsonnet_macros::builtin] +fn builtin_acos(x: f64) -> Result { + Ok(x.acos()) +} + +#[jrsonnet_macros::builtin] +fn builtin_atan(x: f64) -> Result { + Ok(x.atan()) +} + +#[jrsonnet_macros::builtin] +fn builtin_exp(x: f64) -> Result { + Ok(x.exp()) +} + +fn frexp(s: f64) -> (f64, i16) { + if 0.0 == s { + (s, 0) + } else { + let lg = s.abs().log2(); + let x = (lg - lg.floor() - 1.0).exp2(); + let exp = lg.floor() + 1.0; + (s.signum() * x, exp as i16) + } +} + +#[jrsonnet_macros::builtin] +fn builtin_mantissa(x: f64) -> Result { + Ok(frexp(x).0) +} + +#[jrsonnet_macros::builtin] +fn builtin_exponent(x: f64) -> Result { + Ok(frexp(x).1) +} + +#[jrsonnet_macros::builtin] +fn builtin_ext_var(s: State, x: IStr) -> Result { + Ok(Any(s + .settings() + .ext_vars + .get(&x) + .cloned() + .ok_or(UndefinedExternalVariable(x))?)) +} + +#[jrsonnet_macros::builtin] +fn builtin_native(s: State, name: IStr) -> Result { + Ok(Any(s + .settings() + .ext_natives + .get(&name) + .cloned() + .map_or(Val::Null, |v| { + Val::Func(FuncVal::Builtin(v.clone())) + }))) +} + +#[jrsonnet_macros::builtin] +fn builtin_filter(s: State, func: FuncVal, arr: ArrValue) -> Result { + arr.filter(s.clone(), |val| { + bool::from_untyped( + func.evaluate_simple(s.clone(), &(Any(val.clone()),))?, + s.clone(), + ) + }) +} + +#[jrsonnet_macros::builtin] +fn builtin_map(s: State, func: FuncVal, arr: ArrValue) -> Result { + arr.map(s.clone(), |val| { + func.evaluate_simple(s.clone(), &(Any(val),)) + }) +} + +#[jrsonnet_macros::builtin] +fn builtin_flatmap(s: State, func: FuncVal, arr: IndexableVal) -> Result { + match arr { + IndexableVal::Str(str) => { + let mut out = String::new(); + for c in str.chars() { + match func.evaluate_simple(s.clone(), &(c.to_string(),))? { + Val::Str(o) => out.push_str(&o), + Val::Null => continue, + _ => throw!(RuntimeError( + "in std.join all items should be strings".into() + )), + }; + } + Ok(IndexableVal::Str(out.into())) + } + IndexableVal::Arr(a) => { + let mut out = Vec::new(); + for el in a.iter(s.clone()) { + let el = el?; + match func.evaluate_simple(s.clone(), &(Any(el),))? { + Val::Arr(o) => { + for oe in o.iter(s.clone()) { + out.push(oe?); + } + } + Val::Null => continue, + _ => throw!(RuntimeError( + "in std.join all items should be arrays".into() + )), + }; + } + Ok(IndexableVal::Arr(out.into())) + } + } +} + +#[jrsonnet_macros::builtin] +fn builtin_foldl(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { + let mut acc = init.0; + for i in arr.iter(s.clone()) { + acc = func.evaluate_simple(s.clone(), &(Any(acc), Any(i?)))?; + } + Ok(Any(acc)) +} + +#[jrsonnet_macros::builtin] +fn builtin_foldr(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { + let mut acc = init.0; + for i in arr.iter(s.clone()).rev() { + acc = func.evaluate_simple(s.clone(), &(Any(i?), Any(acc)))?; + } + Ok(Any(acc)) +} + +#[jrsonnet_macros::builtin] +#[allow(non_snake_case)] +fn builtin_sort(s: State, arr: ArrValue, keyF: Option) -> Result { + if arr.len() <= 1 { + return Ok(arr); + } + Ok(ArrValue::Eager(sort::sort( + s.clone(), + arr.evaluated(s)?, + keyF.unwrap_or(FuncVal::identity()), + )?)) +} + +#[jrsonnet_macros::builtin] +fn builtin_format(s: State, str: IStr, vals: Any) -> Result { + std_format(s, str, vals.0) +} + +#[jrsonnet_macros::builtin] +fn builtin_range(from: i32, to: i32) -> Result { + if to < from { + return Ok(ArrValue::new_eager()); + } + Ok(ArrValue::new_range(from, to)) +} + +#[jrsonnet_macros::builtin] +fn builtin_char(n: u32) -> Result { + Ok(std::char::from_u32(n as u32).ok_or(InvalidUnicodeCodepointGot(n as u32))?) +} + +#[jrsonnet_macros::builtin] +fn builtin_encode_utf8(str: IStr) -> Result { + Ok(Bytes(str.bytes().collect::>().into())) +} + +#[jrsonnet_macros::builtin] +fn builtin_decode_utf8(arr: Bytes) -> Result { + Ok(std::str::from_utf8(&arr.0) + .map_err(|_| RuntimeError("bad utf8".into()))? + .into()) +} + +#[jrsonnet_macros::builtin] +fn builtin_md5(str: IStr) -> Result { + Ok(format!("{:x}", md5::compute(&str.as_bytes()))) +} + +#[jrsonnet_macros::builtin] +fn builtin_trace(s: State, loc: CallLocation, str: IStr, rest: Any) -> Result { + eprint!("TRACE:"); + if let Some(loc) = loc.0 { + let locs = s.map_source_locations(&loc.0, &[loc.1]); + eprint!( + " {}:{}", + loc.0.file_name().unwrap().to_str().unwrap(), + locs[0].line + ); + } + eprintln!(" {}", str); + Ok(rest) as Result +} + +#[jrsonnet_macros::builtin] +fn builtin_base64(input: Either![Bytes, IStr]) -> Result { + use Either2::*; + Ok(match input { + A(a) => base64::encode(a.0), + B(l) => base64::encode(l.bytes().collect::>()), + }) +} + +#[jrsonnet_macros::builtin] +fn builtin_base64_decode_bytes(input: IStr) -> Result { + Ok(Bytes( + base64::decode(&input.as_bytes()) + .map_err(|_| RuntimeError("bad base64".into()))? + .into(), + )) +} + +#[jrsonnet_macros::builtin] +fn builtin_base64_decode(input: IStr) -> Result { + let bytes = base64::decode(&input.as_bytes()).map_err(|_| RuntimeError("bad base64".into()))?; + Ok(String::from_utf8(bytes).map_err(|_| RuntimeError("bad utf8".into()))?) +} + +#[jrsonnet_macros::builtin] +fn builtin_join(s: State, sep: IndexableVal, arr: ArrValue) -> Result { + Ok(match sep { + IndexableVal::Arr(joiner_items) => { + let mut out = Vec::new(); + + let mut first = true; + for item in arr.iter(s.clone()) { + let item = item?.clone(); + if let Val::Arr(items) = item { + if !first { + out.reserve(joiner_items.len()); + // TODO: extend + for item in joiner_items.iter(s.clone()) { + out.push(item?); + } + } + first = false; + out.reserve(items.len()); + for item in items.iter(s.clone()) { + out.push(item?); + } + } else if matches!(item, Val::Null) { + continue; + } else { + throw!(RuntimeError( + "in std.join all items should be arrays".into() + )); + } + } + + IndexableVal::Arr(out.into()) + } + IndexableVal::Str(sep) => { + let mut out = String::new(); + + let mut first = true; + for item in arr.iter(s) { + let item = item?.clone(); + if let Val::Str(item) = item { + if !first { + out += &sep; + } + first = false; + out += &item; + } else if matches!(item, Val::Null) { + continue; + } else { + throw!(RuntimeError( + "in std.join all items should be strings".into() + )); + } + } + + IndexableVal::Str(out.into()) + } + }) +} + +#[jrsonnet_macros::builtin] +fn builtin_escape_string_json(str_: IStr) -> Result { + Ok(escape_string_json(&str_)) +} + +#[jrsonnet_macros::builtin] +fn builtin_manifest_json_ex( + s: State, + value: Any, + indent: IStr, + newline: Option, + key_val_sep: Option, + #[cfg(feature = "exp-preserve-order")] preserve_order: Option, +) -> Result { + let newline = newline.as_deref().unwrap_or("\n"); + let key_val_sep = key_val_sep.as_deref().unwrap_or(": "); + manifest_json_ex( + s, + &value.0, + &ManifestJsonOptions { + padding: &indent, + mtype: ManifestType::Std, + newline, + key_val_sep, + #[cfg(feature = "exp-preserve-order")] + preserve_order: preserve_order.unwrap_or(false), + }, + ) +} + +#[jrsonnet_macros::builtin] +fn builtin_manifest_yaml_doc( + s: State, + value: Any, + indent_array_in_object: Option, + quote_keys: Option, + #[cfg(feature = "exp-preserve-order")] preserve_order: Option, +) -> Result { + manifest_yaml_ex( + s, + &value.0, + &ManifestYamlOptions { + padding: " ", + arr_element_padding: if indent_array_in_object.unwrap_or(false) { + " " + } else { + "" + }, + quote_keys: quote_keys.unwrap_or(true), + #[cfg(feature = "exp-preserve-order")] + preserve_order: preserve_order.unwrap_or(false), + }, + ) +} + +#[jrsonnet_macros::builtin] +fn builtin_reverse(value: ArrValue) -> Result { + Ok(value.reversed()) +} + +#[jrsonnet_macros::builtin] +fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result { + Ok(str.replace(&from as &str, &to as &str)) +} + +#[jrsonnet_macros::builtin] +fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> Result { + use Either2::*; + Ok(VecVal(Cc::new(match maxsplits { + A(n) => str + .splitn(n + 1, &c as &str) + .map(|s| Val::Str(s.into())) + .collect(), + B(_) => str.split(&c as &str).map(|s| Val::Str(s.into())).collect(), + }))) +} + +#[jrsonnet_macros::builtin] +fn builtin_ascii_upper(str: IStr) -> Result { + Ok(str.to_ascii_uppercase()) +} + +#[jrsonnet_macros::builtin] +fn builtin_ascii_lower(str: IStr) -> Result { + Ok(str.to_ascii_lowercase()) +} + +#[jrsonnet_macros::builtin] +fn builtin_member(s: State, arr: IndexableVal, x: Any) -> Result { + match arr { + IndexableVal::Str(str) => { + let x: IStr = IStr::from_untyped(x.0, s)?; + Ok(!x.is_empty() && str.contains(&*x)) + } + IndexableVal::Arr(a) => { + for item in a.iter(s.clone()) { + let item = item?; + if equals(s.clone(), &item, &x.0)? { + return Ok(true); + } + } + Ok(false) + } + } +} + +#[jrsonnet_macros::builtin] +fn builtin_count(s: State, arr: Vec, v: Any) -> Result { + let mut count = 0; + for item in &arr { + if equals(s.clone(), &item.0, &v.0)? { + count += 1; + } + } + Ok(count) +} + +#[jrsonnet_macros::builtin] +fn builtin_any(s: State, arr: ArrValue) -> Result { + for v in arr.iter(s.clone()) { + let v = bool::from_untyped(v?, s.clone())?; + if v { + return Ok(true); + } + } + Ok(false) +} + +#[jrsonnet_macros::builtin] +fn builtin_all(s: State, arr: ArrValue) -> Result { + for v in arr.iter(s.clone()) { + let v = bool::from_untyped(v?, s.clone())?; + if !v { + return Ok(false); + } + } + Ok(true) +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/stdlib/sort.rs @@ -0,0 +1,110 @@ +use gcmodule::{Cc, Trace}; + +use crate::{ + error::{Error, LocError, Result}, + function::FuncVal, + throw, + typed::Any, + State, Val, +}; + +#[derive(Debug, Clone, thiserror::Error, Trace)] +pub enum SortError { + #[error("sort key should be string or number")] + SortKeyShouldBeStringOrNumber, + #[error("sort elements should have equal types")] + SortElementsShouldHaveEqualType, +} + +impl From for LocError { + fn from(s: SortError) -> Self { + Self::new(Error::Sort(s)) + } +} + +#[derive(Copy, Clone)] +enum SortKeyType { + Number, + String, + Unknown, +} + +#[derive(PartialEq)] +struct NonNaNf64(f64); +impl PartialOrd for NonNaNf64 { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} +impl Eq for NonNaNf64 {} +impl Ord for NonNaNf64 { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).expect("non nan") + } +} + +fn get_sort_type( + values: &mut Vec, + key_getter: impl Fn(&mut T) -> &mut Val, +) -> Result { + let mut sort_type = SortKeyType::Unknown; + for i in values.iter_mut() { + let i = key_getter(i); + match (i, sort_type) { + (Val::Str(_), SortKeyType::Unknown) => sort_type = SortKeyType::String, + (Val::Num(_), SortKeyType::Unknown) => sort_type = SortKeyType::Number, + (Val::Str(_), SortKeyType::String) | (Val::Num(_), SortKeyType::Number) => {} + (Val::Str(_) | Val::Num(_), _) => { + throw!(SortError::SortElementsShouldHaveEqualType) + } + _ => throw!(SortError::SortKeyShouldBeStringOrNumber), + } + } + Ok(sort_type) +} + +/// * `key_getter` - None, if identity sort required +pub fn sort(s: State, values: Cc>, key_getter: FuncVal) -> Result>> { + if values.len() <= 1 { + return Ok(values); + } + if !key_getter.is_identity() { + // Slow path, user provided key getter + let mut vk = Vec::with_capacity(values.len()); + for value in values.iter() { + vk.push(( + value.clone(), + key_getter.evaluate_simple(s.clone(), &(Any(value.clone()),))?, + )); + } + let sort_type = get_sort_type(&mut vk, |v| &mut v.1)?; + match sort_type { + SortKeyType::Number => vk.sort_by_key(|v| match v.1 { + Val::Num(n) => NonNaNf64(n), + _ => unreachable!(), + }), + SortKeyType::String => vk.sort_by_key(|v| match &v.1 { + Val::Str(s) => s.clone(), + _ => unreachable!(), + }), + SortKeyType::Unknown => unreachable!(), + }; + Ok(Cc::new(vk.into_iter().map(|v| v.0).collect())) + } else { + // Fast path, identity key getter + let mut values = (*values).clone(); + let sort_type = get_sort_type(&mut values, |k| k)?; + match sort_type { + SortKeyType::Number => values.sort_unstable_by_key(|v| match v { + Val::Num(n) => NonNaNf64(*n), + _ => unreachable!(), + }), + SortKeyType::String => values.sort_unstable_by_key(|v| match v { + Val::Str(s) => s.clone(), + _ => unreachable!(), + }), + SortKeyType::Unknown => unreachable!(), + }; + Ok(Cc::new(values)) + } +} --- a/crates/jrsonnet-evaluator/src/typed/conversions.rs +++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs @@ -7,9 +7,10 @@ use crate::{ error::{Error::*, Result}, + function::{FuncDesc, FuncVal}, throw, typed::CheckType, - val::{ArrValue, FuncDesc, FuncVal, IndexableVal}, + val::{ArrValue, IndexableVal}, ObjValue, ObjValueBuilder, State, Val, }; --- a/crates/jrsonnet-evaluator/src/val.rs +++ b/crates/jrsonnet-evaluator/src/val.rs @@ -2,22 +2,17 @@ use gcmodule::{Cc, Trace}; use jrsonnet_interner::IStr; -use jrsonnet_parser::{LocExpr, ParamsDesc}; use jrsonnet_types::ValType; use crate::{ - builtin::manifest::{ - manifest_json_ex, manifest_yaml_ex, ManifestJsonOptions, ManifestType, ManifestYamlOptions, - }, cc_ptr_eq, error::{Error::*, LocError}, - evaluate, - function::{ - parse_default_function_call, parse_function_call, ArgsLike, Builtin, CallLocation, - StaticBuiltin, + function::FuncVal, + gc::TraceBox, + stdlib::manifest::{ + manifest_json_ex, manifest_yaml_ex, ManifestJsonOptions, ManifestType, ManifestYamlOptions, }, - gc::TraceBox, - throw, Context, ObjValue, Result, State, + throw, ObjValue, Result, State, }; pub trait LazyValValue: Trace { @@ -80,98 +75,6 @@ impl PartialEq for LazyVal { fn eq(&self, other: &Self) -> bool { cc_ptr_eq(&self.0, &other.0) - } -} - -#[derive(Debug, PartialEq, Trace)] -pub struct FuncDesc { - pub name: IStr, - pub ctx: Context, - pub params: ParamsDesc, - pub body: LocExpr, -} -impl FuncDesc { - /// Create body context, but fill arguments without defaults with lazy error - pub fn default_body_context(&self) -> Context { - parse_default_function_call(self.ctx.clone(), &self.params) - } - - /// Create context, with which body code will run - pub fn call_body_context( - &self, - s: State, - call_ctx: Context, - args: &dyn ArgsLike, - tailstrict: bool, - ) -> Result { - parse_function_call( - s, - call_ctx, - self.ctx.clone(), - &self.params, - args, - tailstrict, - ) - } -} - -#[allow(clippy::module_name_repetitions)] -#[derive(Trace, Clone)] -pub enum FuncVal { - /// Plain function implemented in jsonnet - Normal(Cc), - /// Standard library function - StaticBuiltin(#[skip_trace] &'static dyn StaticBuiltin), - /// User-provided function - Builtin(Cc>), -} - -impl Debug for FuncVal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Normal(arg0) => f.debug_tuple("Normal").field(arg0).finish(), - Self::StaticBuiltin(arg0) => { - f.debug_tuple("StaticBuiltin").field(&arg0.name()).finish() - } - Self::Builtin(arg0) => f.debug_tuple("Builtin").field(&arg0.name()).finish(), - } - } -} - -impl FuncVal { - pub fn args_len(&self) -> usize { - match self { - Self::Normal(n) => n.params.iter().filter(|p| p.1.is_none()).count(), - Self::StaticBuiltin(i) => i.params().iter().filter(|p| !p.has_default).count(), - Self::Builtin(i) => i.params().iter().filter(|p| !p.has_default).count(), - } - } - pub fn name(&self) -> IStr { - match self { - Self::Normal(normal) => normal.name.clone(), - Self::StaticBuiltin(builtin) => builtin.name().into(), - Self::Builtin(builtin) => builtin.name().into(), - } - } - pub fn evaluate( - &self, - s: State, - call_ctx: Context, - loc: CallLocation, - args: &dyn ArgsLike, - tailstrict: bool, - ) -> Result { - match self { - Self::Normal(func) => { - let body_ctx = func.call_body_context(s.clone(), call_ctx, args, tailstrict)?; - evaluate(s, body_ctx, &func.body) - } - Self::StaticBuiltin(b) => b.call(s, call_ctx, loc, args), - Self::Builtin(b) => b.call(s, call_ctx, loc, args), - } - } - pub fn evaluate_simple(&self, s: State, args: &dyn ArgsLike) -> Result { - self.evaluate(s, Context::default(), CallLocation::native(), args, true) } } --- /dev/null +++ b/crates/jrsonnet-evaluator/tests/as_native.rs @@ -0,0 +1,21 @@ +use std::path::PathBuf; + +use jrsonnet_evaluator::{error::Result, State}; + +mod common; + +#[test] +fn as_native() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + + let val = s.evaluate_snippet_raw(PathBuf::new().into(), r#"function(a, b) a + b"#.into())?; + let func = val.as_func().expect("this is function"); + + let native = func.into_native::<((u32, u32), u32)>(); + + ensure_eq!(native(s.clone(), 1, 2)?, 3); + ensure_eq!(native(s, 3, 4)?, 7); + + Ok(()) +} --- a/crates/jrsonnet-evaluator/tests/builtin.rs +++ b/crates/jrsonnet-evaluator/tests/builtin.rs @@ -5,10 +5,9 @@ use gcmodule::Cc; use jrsonnet_evaluator::{ error::Result, - function::{builtin, Builtin, CallLocation}, + function::{builtin, builtin::Builtin, CallLocation, FuncVal}, gc::TraceBox, typed::Typed, - val::FuncVal, State, Val, }; @@ -26,7 +25,7 @@ s.clone(), s.create_default_context(), CallLocation::native(), - &[], + &(), )?, s.clone(), )?; --- a/crates/jrsonnet-evaluator/tests/common.rs +++ b/crates/jrsonnet-evaluator/tests/common.rs @@ -1,17 +1,16 @@ use jrsonnet_evaluator::{ - error::Result, function::builtin, throw_runtime, val::FuncVal, LazyVal, ObjValueBuilder, State, - Val, + error::Result, + function::{builtin, FuncVal}, + throw_runtime, LazyVal, ObjValueBuilder, State, Val, }; #[macro_export] macro_rules! ensure_eq { ($a:expr, $b:expr $(,)?) => {{ - if $a != $b { - ::jrsonnet_evaluator::throw_runtime!( - "assertion failed: a != b\na={:#?}\nb={:#?}", - $a, - $b, - ) + let a = &$a; + let b = &$b; + if a != b { + ::jrsonnet_evaluator::throw_runtime!("assertion failed: a != b\na={:#?}\nb={:#?}", a, b) } }}; } --- a/crates/jrsonnet-macros/src/lib.rs +++ b/crates/jrsonnet-macros/src/lib.rs @@ -349,7 +349,7 @@ const _: () = { use ::jrsonnet_evaluator::{ State, Val, - function::{Builtin, CallLocation, StaticBuiltin, BuiltinParam, ArgsLike, parse_builtin_call}, + function::{builtin::{Builtin, StaticBuiltin, BuiltinParam}, CallLocation, ArgsLike, parse::parse_builtin_call}, error::Result, Context, typed::Typed, parser::ExprLocation, }; --- a/crates/jrsonnet-parser/src/expr.rs +++ b/crates/jrsonnet-parser/src/expr.rs @@ -298,6 +298,8 @@ Function(ParamsDesc, LocExpr), /// std.thisFile IntrinsicThisFile, + /// std.id, + IntrinsicId, /// std.primitiveEquals Intrinsic(IStr), /// if true == false then 1 else 2 --- a/crates/jrsonnet-parser/src/lib.rs +++ b/crates/jrsonnet-parser/src/lib.rs @@ -211,6 +211,7 @@ = literal(s) / quiet!{"$intrinsicThisFile" {Expr::IntrinsicThisFile}} + / quiet!{"$intrinsicId" {Expr::IntrinsicId}} / quiet!{"$intrinsic(" name:$(id()) ")" {Expr::Intrinsic(name.into())}} / string_expr(s) / number_expr(s) --- a/crates/jrsonnet-stdlib/src/std.jsonnet +++ b/crates/jrsonnet-stdlib/src/std.jsonnet @@ -4,6 +4,7 @@ # Magic legacy field thisFile:: $intrinsicThisFile, + id:: $intrinsicId, # Those functions aren't normally located in stdlib length:: $intrinsic(length), @@ -24,7 +25,6 @@ decodeUTF8:: $intrinsic(decodeUTF8), md5:: $intrinsic(md5), trace:: $intrinsic(trace), - id:: $intrinsic(id), parseJson:: $intrinsic(parseJson), parseYaml:: $intrinsic(parseYaml), -- gitstuff