--- a/crates/jrsonnet-stdlib/Cargo.toml +++ b/crates/jrsonnet-stdlib/Cargo.toml @@ -7,5 +7,46 @@ edition = "2021" [features] +default = [] +# Serializes standard library AST, and deserialize on start, instead of parsing it every run from text +serialized-stdlib = ["bincode", "jrsonnet-parser/serde"] +# Enables legacy `std.thisFile` support, at the cost of worse caching +legacy-this-file = [] +# Add order preservation flag to some functions +exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"] +# Preserve order for files parsed via `std.parseJson` +# Shame it isn't possible to enable per parse call, instead of globally +exp-serde-preserve-order = [ + "serde_json/preserve_order", + "jrsonnet-evaluator/exp-serde-preserve-order", +] [dependencies] +jrsonnet-evaluator = { path = "../jrsonnet-evaluator", features = [ + # std.parseJson parses file via serde, then converts Value to evaluator Val + "serde_json", +], version = "0.4.2" } +jrsonnet-macros = { path = "../jrsonnet-macros", version = "0.4.2" } +jrsonnet-parser = { path = "../jrsonnet-parser", version = "0.4.2" } +jrsonnet-gcmodule = "0.3.4" + +# Used for stdlib AST serialization +bincode = { version = "1.3", optional = true } +# Used both for stdlib AST serialization and std.parseJson/std.parseYaml +serde = "1.0" + +# std.md5 +md5 = "0.7.0" +# std.base64 +base64 = "0.13.0" +# std.parseJson +serde_json = "1.0" +# std.parseYaml, custom library fork is used for C++/golang compatibility +serde_yaml_with_quirks = "0.8.24" + +[build-dependencies] +jrsonnet-parser = { path = "../jrsonnet-parser", version = "0.4.2", features = [ + "serde", +] } +serde = "1.0" +bincode = "1.3" --- a/crates/jrsonnet-stdlib/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# jrsonnet-stdlib - -Jsonnet standard library packaged as crate --- /dev/null +++ b/crates/jrsonnet-stdlib/build.rs @@ -0,0 +1,21 @@ +use std::{borrow::Cow, env, fs::File, io::Write, path::Path}; + +use bincode::serialize; +use jrsonnet_parser::{parse, ParserSettings, Source}; + +fn main() { + let parsed = parse( + include_str!("./src/std.jsonnet"), + &ParserSettings { + file_name: Source::new_virtual(Cow::Borrowed("")), + }, + ) + .expect("parse"); + + { + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("stdlib.bincode"); + let mut f = File::create(&dest_path).unwrap(); + f.write_all(&serialize(&parsed).unwrap()).unwrap(); + } +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/arrays.rs @@ -0,0 +1,216 @@ +use jrsonnet_evaluator::{ + error::Result, + function::{builtin, FuncVal}, + throw_runtime, + typed::{Any, BoundedUsize, Typed, VecVal}, + val::{equals, ArrValue, IndexableVal}, + IStr, State, Val, +}; +use jrsonnet_gcmodule::Cc; + +#[builtin] +pub 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))) +} + +#[builtin] +pub fn builtin_slice( + indexable: IndexableVal, + index: Option>, + end: Option>, + step: Option>, +) -> Result { + indexable.slice(index, end, step).map(Val::from).map(Any) +} + +#[builtin] +pub fn builtin_map(s: State, func: FuncVal, arr: ArrValue) -> Result { + arr.map(s.clone(), |val| { + func.evaluate_simple(s.clone(), &(Any(val),)) + }) +} + +#[builtin] +pub 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_runtime!("in std.join all items should be strings"), + }; + } + 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_runtime!("in std.join all items should be arrays"), + }; + } + Ok(IndexableVal::Arr(out.into())) + } + } +} + +#[builtin] +pub 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(), + ) + }) +} + +#[builtin] +pub 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)) +} + +#[builtin] +pub 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)) +} + +#[builtin] +pub fn builtin_range(from: i32, to: i32) -> Result { + if to < from { + return Ok(ArrValue::new_eager()); + } + Ok(ArrValue::new_range(from, to)) +} + +#[builtin] +pub 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_runtime!("in std.join all items should be arrays"); + } + } + + 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_runtime!("in std.join all items should be strings"); + } + } + + IndexableVal::Str(out.into()) + } + }) +} + +#[builtin] +pub fn builtin_reverse(value: ArrValue) -> Result { + Ok(value.reversed()) +} + +#[builtin] +pub 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) +} + +#[builtin] +pub 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) +} + +#[builtin] +pub 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) + } + } +} + +#[builtin] +pub 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) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/encoding.rs @@ -0,0 +1,41 @@ +use jrsonnet_evaluator::{ + error::{Error::RuntimeError, Result}, + function::builtin, + typed::{Either, Either2}, + IBytes, IStr, +}; + +#[builtin] +pub fn builtin_encode_utf8(str: IStr) -> Result { + Ok(str.cast_bytes()) +} + +#[builtin] +pub fn builtin_decode_utf8(arr: IBytes) -> Result { + Ok(arr + .cast_str() + .ok_or_else(|| RuntimeError("bad utf8".into()))?) +} + +#[builtin] +pub fn builtin_base64(input: Either![IBytes, IStr]) -> Result { + use Either2::*; + Ok(match input { + A(a) => base64::encode(a.as_slice()), + B(l) => base64::encode(l.bytes().collect::>()), + }) +} + +#[builtin] +pub fn builtin_base64_decode_bytes(input: IStr) -> Result { + Ok(base64::decode(&input.as_bytes()) + .map_err(|_| RuntimeError("bad base64".into()))? + .as_slice() + .into()) +} + +#[builtin] +pub 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()))?) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/expr.rs @@ -0,0 +1,22 @@ +use std::borrow::Cow; + +use jrsonnet_parser::{LocExpr, ParserSettings, Source}; + +pub const STDLIB_STR: &str = include_str!("./std.jsonnet"); + +pub fn stdlib_expr() -> 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( + STDLIB_STR, + &ParserSettings { + file_name: Source::new_virtual(Cow::Borrowed("")), + }, + ) + .unwrap() +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/hash.rs @@ -0,0 +1,6 @@ +use jrsonnet_evaluator::{error::Result, function::builtin, IStr}; + +#[builtin] +pub fn builtin_md5(str: IStr) -> Result { + Ok(format!("{:x}", md5::compute(&str.as_bytes()))) +} --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -1 +1,420 @@ -pub const STDLIB_STR: &str = include_str!("./std.jsonnet"); +use std::{ + borrow::Cow, + cell::{Ref, RefCell, RefMut}, + collections::HashMap, + rc::Rc, +}; + +use jrsonnet_evaluator::{ + error::{Error::*, Result}, + function::{builtin::Builtin, ArgLike, CallLocation, FuncVal, TlaArg}, + gc::TraceBox, + tb, + typed::{Any, Either, Either2, Either4, VecVal, M1}, + val::ArrValue, + Context, ContextBuilder, IStr, ObjValue, ObjValueBuilder, State, Thunk, Val, +}; +use jrsonnet_gcmodule::Cc; +use jrsonnet_macros::builtin; +use jrsonnet_parser::Source; + +mod expr; +mod types; +pub use types::*; +mod arrays; +pub use arrays::*; +mod math; +pub use math::*; +mod operator; +pub use operator::*; +mod sort; +pub use sort::*; +mod hash; +pub use hash::*; +mod encoding; +pub use encoding::*; +mod objects; +pub use objects::*; +mod manifest; +pub use manifest::*; +mod parse; +pub use parse::*; + +pub fn stdlib_uncached(s: State, settings: Rc>) -> ObjValue { + let mut builder = ObjValueBuilder::new(); + + let expr = expr::stdlib_expr(); + let eval = jrsonnet_evaluator::evaluate(s.clone(), Context::default(), &expr) + .expect("stdlib.jsonnet should have no errors") + .as_obj() + .expect("stdlib.jsonnet should evaluate to object"); + + builder.with_super(eval); + + for (name, builtin) in [ + ("length".into(), builtin_length::INST), + // Types + ("type".into(), builtin_type::INST), + ("isString".into(), builtin_is_string::INST), + ("isNumber".into(), builtin_is_number::INST), + ("isBoolean".into(), builtin_is_boolean::INST), + ("isObject".into(), builtin_is_object::INST), + ("isArray".into(), builtin_is_array::INST), + ("isFunction".into(), builtin_is_function::INST), + // Arrays + ("makeArray".into(), builtin_make_array::INST), + ("slice".into(), builtin_slice::INST), + ("map".into(), builtin_map::INST), + ("flatMap".into(), builtin_flatmap::INST), + ("filter".into(), builtin_filter::INST), + ("foldl".into(), builtin_foldl::INST), + ("foldr".into(), builtin_foldr::INST), + ("range".into(), builtin_range::INST), + ("join".into(), builtin_join::INST), + ("reverse".into(), builtin_reverse::INST), + ("any".into(), builtin_any::INST), + ("all".into(), builtin_all::INST), + ("member".into(), builtin_member::INST), + ("count".into(), builtin_count::INST), + // Math + ("modulo".into(), builtin_modulo::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), + // Operator + ("mod".into(), builtin_mod::INST), + ("primitiveEquals".into(), builtin_primitive_equals::INST), + ("equals".into(), builtin_equals::INST), + ("format".into(), builtin_format::INST), + // Sort + ("sort".into(), builtin_sort::INST), + // Hash + ("md5".into(), builtin_md5::INST), + // Encoding + ("encodeUTF8".into(), builtin_encode_utf8::INST), + ("decodeUTF8".into(), builtin_decode_utf8::INST), + ("base64".into(), builtin_base64::INST), + ("base64Decode".into(), builtin_base64_decode::INST), + ( + "base64DecodeBytes".into(), + builtin_base64_decode_bytes::INST, + ), + // Objects + ("objectFieldsEx".into(), builtin_object_fields_ex::INST), + ("objectHasEx".into(), builtin_object_has_ex::INST), + // Manifest + ("escapeStringJson".into(), builtin_escape_string_json::INST), + ("manifestJsonEx".into(), builtin_manifest_json_ex::INST), + ("manifestYamlDoc".into(), builtin_manifest_yaml_doc::INST), + // Parsing + ("parseJson".into(), builtin_parse_json::INST), + ("parseYaml".into(), builtin_parse_yaml::INST), + // Misc + ("codepoint".into(), builtin_codepoint::INST), + ("substr".into(), builtin_substr::INST), + ("char".into(), builtin_char::INST), + ("strReplace".into(), builtin_str_replace::INST), + ("splitLimit".into(), builtin_splitlimit::INST), + ("asciiUpper".into(), builtin_ascii_upper::INST), + ("asciiLower".into(), builtin_ascii_lower::INST), + ] + .iter() + .cloned() + { + builder + .member(name) + .hide() + .value(s.clone(), Val::Func(FuncVal::StaticBuiltin(builtin))) + .expect("no conflict"); + } + + builder + .member("extVar".into()) + .hide() + .value( + s.clone(), + Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_ext_var { + settings: settings.clone() + })))), + ) + .expect("no conflict"); + builder + .member("native".into()) + .hide() + .value( + s.clone(), + Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_native { + settings: settings.clone() + })))), + ) + .expect("no conflict"); + builder + .member("trace".into()) + .hide() + .value( + s.clone(), + Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_trace { settings })))), + ) + .expect("no conflict"); + + builder + .member("id".into()) + .hide() + .value(s, Val::Func(FuncVal::Id)) + .expect("no conflict"); + + builder.build() +} + +pub trait TracePrinter { + fn print_trace(&self, s: State, loc: CallLocation, value: IStr); +} + +pub struct StdTracePrinter; +impl TracePrinter for StdTracePrinter { + fn print_trace(&self, s: State, loc: CallLocation, value: IStr) { + eprint!("TRACE:"); + if let Some(loc) = loc.0 { + let locs = s.map_source_locations(loc.0.clone(), &[loc.1]); + eprint!(" {}:{}", loc.0.short_display(), locs[0].line); + } + eprintln!(" {}", value); + } +} + +pub struct Settings { + /// Used for `std.extVar` + pub ext_vars: HashMap, + /// Used for `std.native` + pub ext_natives: HashMap>>, + /// Used for `std.trace` + pub trace_printer: Box, +} + +impl Default for Settings { + fn default() -> Self { + Self { + ext_vars: Default::default(), + ext_natives: Default::default(), + trace_printer: Box::new(StdTracePrinter), + } + } +} + +pub fn extvar_source(name: &str) -> Source { + let source_name = format!("", name); + Source::new_virtual(Cow::Owned(source_name)) +} + +pub struct ContextInitializer { + // When we don't need to support legacy-this-file, we can reuse same context for all files + #[cfg(not(feature = "legacy-this-file"))] + context: Context, + // Otherwise, we can only keep first stdlib layer, and then stack thisFile on top of it + #[cfg(feature = "legacy-this-file")] + stdlib_obj: ObjValue, + settings: Rc>, +} +impl ContextInitializer { + pub fn new(s: State) -> Self { + let settings = Rc::new(RefCell::new(Settings::default())); + Self { + #[cfg(not(feature = "legacy-this-file"))] + context: { + let mut context = ContextBuilder::with_capacity(1); + context.bind( + "std".into(), + Thunk::evaluated(Val::Obj(stdlib_uncached(s, settings.clone()))), + ); + context.build() + }, + #[cfg(feature = "legacy-this-file")] + stdlib_obj: stdlib_uncached(s, settings.clone()), + settings, + } + } + pub fn settings(&self) -> Ref { + self.settings.borrow() + } + pub fn settings_mut(&self) -> RefMut { + self.settings.borrow_mut() + } + pub fn add_ext_var(&self, name: IStr, value: Val) { + self.settings_mut() + .ext_vars + .insert(name, TlaArg::Val(value)); + } + pub fn add_ext_str(&self, name: IStr, value: IStr) { + self.settings_mut() + .ext_vars + .insert(name, TlaArg::String(value)); + } + pub fn add_ext_code(&self, name: &str, code: String) -> Result<()> { + let source = extvar_source(name); + let parsed = jrsonnet_parser::parse( + &code, + &jrsonnet_parser::ParserSettings { + file_name: source.clone(), + }, + ) + .map_err(|e| ImportSyntaxError { + path: source, + source_code: code.clone().into(), + error: Box::new(e), + })?; + // self.data_mut().volatile_files.insert(source_name, code); + self.settings_mut() + .ext_vars + .insert(name.into(), TlaArg::Code(parsed)); + Ok(()) + } + pub fn add_native(&self, name: IStr, cb: Cc>) { + self.settings_mut().ext_natives.insert(name, cb); + } +} +impl jrsonnet_evaluator::ContextInitializer for ContextInitializer { + #[cfg(not(feature = "legacy-this-file"))] + fn initialize(&self, _s: State, _source: Source) -> jrsonnet_evaluator::Context { + self.context.clone() + } + #[cfg(feature = "legacy-this-file")] + fn initialize(&self, s: State, source: Source) -> jrsonnet_evaluator::Context { + let mut builder = ObjValueBuilder::new(); + builder.with_super(self.stdlib_obj.clone()); + builder + .member("thisFile".into()) + .hide() + .value( + s, + Val::Str(match source.repr() { + Ok(p) => p.display().to_string().into(), + // Virtual files end up as empty strings in std.thisFile + Err(_e) => "".into(), + }), + ) + .expect("this object builder is empty"); + let stdlib_with_this_file = builder.build(); + + let mut context = ContextBuilder::with_capacity(1); + context.bind( + "std".into(), + Thunk::evaluated(Val::Obj(stdlib_with_this_file)), + ); + context.build() + } + unsafe fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[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(), + }) +} + +#[builtin] +const fn builtin_codepoint(str: char) -> Result { + Ok(str as u32) +} + +#[builtin] +fn builtin_substr(str: IStr, from: usize, len: usize) -> Result { + Ok(str.chars().skip(from as usize).take(len as usize).collect()) +} + +#[builtin(fields( + settings: Rc>, +))] +fn builtin_ext_var(this: &builtin_ext_var, s: State, x: IStr) -> Result { + let ctx = s.create_default_context(extvar_source(&x)); + Ok(Any(this + .settings + .borrow() + .ext_vars + .get(&x) + .cloned() + .ok_or(UndefinedExternalVariable(x))? + .evaluate_arg(s.clone(), ctx, true)? + .evaluate(s)?)) +} + +#[builtin(fields( + settings: Rc>, +))] +fn builtin_native(this: &builtin_native, name: IStr) -> Result { + Ok(Any(this + .settings + .borrow() + .ext_natives + .get(&name) + .cloned() + .map_or(Val::Null, |v| { + Val::Func(FuncVal::Builtin(v.clone())) + }))) +} + +#[builtin] +fn builtin_char(n: u32) -> Result { + Ok(std::char::from_u32(n as u32).ok_or(InvalidUnicodeCodepointGot(n as u32))?) +} + +#[builtin(fields( + settings: Rc>, +))] +fn builtin_trace( + this: &builtin_trace, + s: State, + loc: CallLocation, + str: IStr, + rest: Any, +) -> Result { + this.settings + .borrow() + .trace_printer + .print_trace(s, loc, str); + Ok(rest) as Result +} + +#[builtin] +fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result { + Ok(str.replace(&from as &str, &to as &str)) +} + +#[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(), + }))) +} + +#[builtin] +fn builtin_ascii_upper(str: IStr) -> Result { + Ok(str.to_ascii_uppercase()) +} + +#[builtin] +fn builtin_ascii_lower(str: IStr) -> Result { + Ok(str.to_ascii_lowercase()) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/manifest.rs @@ -0,0 +1,65 @@ +use jrsonnet_evaluator::{ + error::Result, + function::builtin, + stdlib::manifest::{ + escape_string_json, manifest_json_ex, manifest_yaml_ex, ManifestJsonOptions, ManifestType, + ManifestYamlOptions, + }, + typed::Any, + IStr, State, +}; + +#[builtin] +pub fn builtin_escape_string_json(str_: IStr) -> Result { + Ok(escape_string_json(&str_)) +} + +#[builtin] +pub 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), + }, + ) +} + +#[builtin] +pub 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), + }, + ) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/math.rs @@ -0,0 +1,87 @@ +use jrsonnet_evaluator::{error::Result, function::builtin, typed::PositiveF64}; + +#[builtin] +pub fn builtin_modulo(a: f64, b: f64) -> Result { + Ok(a % b) +} + +#[builtin] +pub fn builtin_floor(x: f64) -> Result { + Ok(x.floor()) +} + +#[builtin] +pub fn builtin_ceil(x: f64) -> Result { + Ok(x.ceil()) +} + +#[builtin] +pub fn builtin_log(n: f64) -> Result { + Ok(n.ln()) +} + +#[builtin] +pub fn builtin_pow(x: f64, n: f64) -> Result { + Ok(x.powf(n)) +} + +#[builtin] +pub fn builtin_sqrt(x: PositiveF64) -> Result { + Ok(x.0.sqrt()) +} + +#[builtin] +pub fn builtin_sin(x: f64) -> Result { + Ok(x.sin()) +} + +#[builtin] +pub fn builtin_cos(x: f64) -> Result { + Ok(x.cos()) +} + +#[builtin] +pub fn builtin_tan(x: f64) -> Result { + Ok(x.tan()) +} + +#[builtin] +pub fn builtin_asin(x: f64) -> Result { + Ok(x.asin()) +} + +#[builtin] +pub fn builtin_acos(x: f64) -> Result { + Ok(x.acos()) +} + +#[builtin] +pub fn builtin_atan(x: f64) -> Result { + Ok(x.atan()) +} + +#[builtin] +pub 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) + } +} + +#[builtin] +pub fn builtin_mantissa(x: f64) -> Result { + Ok(frexp(x).0) +} + +#[builtin] +pub fn builtin_exponent(x: f64) -> Result { + Ok(frexp(x).1) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/objects.rs @@ -0,0 +1,27 @@ +use jrsonnet_evaluator::{ + error::Result, function::builtin, typed::VecVal, val::Val, IStr, ObjValue, +}; +use jrsonnet_gcmodule::Cc; + +#[builtin] +pub 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::>(), + ))) +} + +#[builtin] +pub fn builtin_object_has_ex(obj: ObjValue, f: IStr, inc_hidden: bool) -> Result { + Ok(obj.has_field_ex(f, inc_hidden)) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/operator.rs @@ -0,0 +1,40 @@ +//! Some jsonnet operations are desugared to stdlib functions... +//! However, in our case we instead implement them in native, and implement native functions on top of core for backwards compatibility + +use jrsonnet_evaluator::{ + error::Result, + function::builtin, + operator::evaluate_mod_op, + stdlib::std_format, + typed::{Any, Either, Either2}, + val::{equals, primitive_equals}, + IStr, State, Val, +}; + +#[builtin] +pub 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, + )?)) +} + +#[builtin] +pub fn builtin_primitive_equals(a: Any, b: Any) -> Result { + primitive_equals(&a.0, &b.0) +} + +#[builtin] +pub fn builtin_equals(s: State, a: Any, b: Any) -> Result { + equals(s, &a.0, &b.0) +} + +#[builtin] +pub fn builtin_format(s: State, str: IStr, vals: Any) -> Result { + std_format(s, str, vals.0) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/parse.rs @@ -0,0 +1,39 @@ +use jrsonnet_evaluator::{ + error::{Error::RuntimeError, Result}, + function::builtin, + typed::{Any, Typed}, + IStr, State, Val, +}; +use serde::Deserialize; + +#[builtin] +pub 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)?)) +} + +#[builtin] +pub fn builtin_parse_yaml(st: State, s: IStr) -> Result { + use serde_json::Value; + use serde_yaml_with_quirks::DeserializingQuirks; + let value = serde_yaml_with_quirks::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()) + })) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/sort.rs @@ -0,0 +1,109 @@ +use jrsonnet_evaluator::{ + error::Result, + function::{builtin, FuncVal}, + throw_runtime, + typed::Any, + val::ArrValue, + State, Val, +}; +use jrsonnet_gcmodule::Cc; + +#[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 [T], + 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_runtime!("sort elements should have same types") + } + _ => throw_runtime!("sort key should be string or number"), + } + } + 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() { + // 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)) + } else { + // 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())) + } +} + +#[builtin] +#[allow(non_snake_case)] +pub fn builtin_sort(s: State, arr: ArrValue, keyF: Option) -> Result { + if arr.len() <= 1 { + return Ok(arr); + } + Ok(ArrValue::Eager(super::sort::sort( + s.clone(), + arr.evaluated(s)?, + keyF.unwrap_or_else(FuncVal::identity), + )?)) +} --- a/crates/jrsonnet-stdlib/src/std.jsonnet +++ b/crates/jrsonnet-stdlib/src/std.jsonnet @@ -2,62 +2,11 @@ local std = self, local id = std.id, - # Magic legacy field - thisFile:: $intrinsicThisFile, - id:: $intrinsicId, - - # Those functions aren't normally located in stdlib - length:: $intrinsic(length), - type:: $intrinsic(type), - makeArray:: $intrinsic(makeArray), - codepoint:: $intrinsic(codepoint), - objectFieldsEx:: $intrinsic(objectFieldsEx), - objectHasEx:: $intrinsic(objectHasEx), - primitiveEquals:: $intrinsic(primitiveEquals), - modulo:: $intrinsic(modulo), - floor:: $intrinsic(floor), - ceil:: $intrinsic(ceil), - extVar:: $intrinsic(extVar), - native:: $intrinsic(native), - filter:: $intrinsic(filter), - char:: $intrinsic(char), - encodeUTF8:: $intrinsic(encodeUTF8), - decodeUTF8:: $intrinsic(decodeUTF8), - md5:: $intrinsic(md5), - trace:: $intrinsic(trace), - parseJson:: $intrinsic(parseJson), - parseYaml:: $intrinsic(parseYaml), - - log:: $intrinsic(log), - pow:: $intrinsic(pow), - sqrt:: $intrinsic(sqrt), - - sin:: $intrinsic(sin), - cos:: $intrinsic(cos), - tan:: $intrinsic(tan), - asin:: $intrinsic(asin), - acos:: $intrinsic(acos), - atan:: $intrinsic(atan), - - exp:: $intrinsic(exp), - mantissa:: $intrinsic(mantissa), - exponent:: $intrinsic(exponent), + thisFile:: error 'std.thisFile is deprecated, to enable its support in jrsonnet - recompile it with "legacy-this-file" support. This will slow down stdlib caching a bit, though', - any:: $intrinsic(any), - all:: $intrinsic(all), - - isString(v):: std.type(v) == 'string', - isNumber(v):: std.type(v) == 'number', - isBoolean(v):: std.type(v) == 'boolean', - isObject(v):: std.type(v) == 'object', - isArray(v):: std.type(v) == 'array', - isFunction(v):: std.type(v) == 'function', - toString(a):: if std.type(a) == 'string' then a else '' + a, - substr:: $intrinsic(substr), - startsWith(a, b):: if std.length(a) < std.length(b) then false @@ -127,33 +76,13 @@ split(str, c):: std.splitLimit(str, c, -1), - splitLimit:: $intrinsic(splitLimit), - - strReplace:: $intrinsic(strReplace), - - asciiUpper:: $intrinsic(asciiUpper), - - asciiLower:: $intrinsic(asciiLower), - - range:: $intrinsic(range), - repeat(what, count):: local joiner = if std.isString(what) then '' else if std.isArray(what) then [] else error 'std.repeat first argument must be an array or a string'; std.join(joiner, std.makeArray(count, function(i) what)), - - slice:: $intrinsic(slice), - member:: $intrinsic(member), - - count:: $intrinsic(count), - - mod:: $intrinsic(mod), - - map:: $intrinsic(map), - mapWithIndex(func, arr):: if !std.isFunction(func) then error ('std.mapWithIndex first param must be function, got ' + std.type(func)) @@ -169,11 +98,7 @@ error ('std.mapWithKey second param must be object, got ' + std.type(obj)) else { [k]: func(k, obj[k]) for k in std.objectFields(obj) }, - - flatMap:: $intrinsic(flatMap), - join:: $intrinsic(join), - lines(arr):: std.join('\n', arr + ['']), @@ -184,14 +109,7 @@ std.join('', [std.deepJoin(x) for x in arr]) else error 'Expected string or array, got %s' % std.type(arr), - - format:: $intrinsic(format), - - foldr:: $intrinsic(foldr), - - foldl:: $intrinsic(foldl), - filterMap(filter_func, map_func, arr):: if !std.isFunction(filter_func) then error ('std.filterMap first param must be function, got ' + std.type(filter_func)) @@ -350,8 +268,6 @@ else error 'TOML body must be an object. Got ' + std.type(value), - escapeStringJson:: $intrinsic(escapeStringJson), - escapeStringPython(str):: std.escapeStringJson(str), @@ -376,11 +292,7 @@ manifestJson(value):: std.manifestJsonEx(value, ' ') tailstrict, manifestJsonMinified(value):: std.manifestJsonEx(value, '', '', ':'), - - manifestJsonEx:: $intrinsic(manifestJsonEx), - manifestYamlDoc:: $intrinsic(manifestYamlDoc), - manifestYamlStream(value, indent_array_in_object=false, c_document_end=true):: if !std.isArray(value) then error 'manifestYamlStream only takes arrays, got ' + std.type(value) @@ -433,20 +345,7 @@ std.deepJoin(['<', tag, attrs_str, '>', [aux(x) for x in children], '']); aux(value), - - local base64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', - local base64_inv = { [base64_table[i]]: i for i in std.range(0, 63) }, - - base64:: $intrinsic(base64), - - base64DecodeBytes:: $intrinsic(base64DecodeBytes), - - base64Decode:: $intrinsic(base64Decode), - - reverse:: $intrinsic(reverse), - sort:: $intrinsic(sort), - uniq(arr, keyF=id):: local f(a, b) = if std.length(a) == 0 then @@ -534,7 +433,7 @@ else patch, - get(o, f, default = null, inc_hidden = true):: + get(o, f, default=null, inc_hidden=true):: if std.objectHasEx(o, f, inc_hidden) then o[f] else default, objectFields(o):: @@ -554,8 +453,6 @@ objectValuesAll(o):: [o[k] for k in std.objectFieldsAll(o)], - - equals:: $intrinsic(equals), resolvePath(f, r):: local arr = std.split(f, '/'); --- /dev/null +++ b/crates/jrsonnet-stdlib/src/types.rs @@ -0,0 +1,31 @@ +use jrsonnet_evaluator::{error::Result, function::builtin, typed::Any, IStr, Val}; + +#[builtin] +pub fn builtin_type(x: Any) -> Result { + Ok(x.0.value_type().name().into()) +} + +#[builtin] +pub fn builtin_is_string(x: Any) -> Result { + Ok(matches!(x.0, Val::Str(_))) +} +#[builtin] +pub fn builtin_is_number(x: Any) -> Result { + Ok(matches!(x.0, Val::Num(_))) +} +#[builtin] +pub fn builtin_is_boolean(x: Any) -> Result { + Ok(matches!(x.0, Val::Bool(_))) +} +#[builtin] +pub fn builtin_is_object(x: Any) -> Result { + Ok(matches!(x.0, Val::Obj(_))) +} +#[builtin] +pub fn builtin_is_array(x: Any) -> Result { + Ok(matches!(x.0, Val::Arr(_))) +} +#[builtin] +pub fn builtin_is_function(x: Any) -> Result { + Ok(matches!(x.0, Val::Func(_))) +}