--- a/crates/jrsonnet-evaluator/src/arr/mod.rs +++ b/crates/jrsonnet-evaluator/src/arr/mod.rs @@ -32,6 +32,8 @@ Reverse(Box), /// Returned by `std.map` call Mapped(MappedArray), + /// Returned by `std.repeat` call + Repeated(RepeatedArray), } impl ArrValue { @@ -51,6 +53,10 @@ Self::Eager(EagerArray(values)) } + pub fn repeated(data: ArrValue, repeats: usize) -> Option { + Some(Self::Repeated(RepeatedArray::new(data, repeats)?)) + } + pub fn bytes(bytes: IBytes) -> Self { Self::Bytes(BytesArray(bytes)) } @@ -76,7 +82,11 @@ // TODO: benchmark for an optimal value, currently just a arbitrary choice const ARR_EXTEND_THRESHOLD: usize = 100; - if a.len() + b.len() > ARR_EXTEND_THRESHOLD { + if a.is_empty() { + b + } else if b.is_empty() { + a + } else if a.len() + b.len() > ARR_EXTEND_THRESHOLD { Self::Extended(Cc::new(ExtendedArray::new(a, b))) } else if let (Some(a), Some(b)) = (a.iter_cheap(), b.iter_cheap()) { let mut out = Vec::with_capacity(a.len() + b.len()); @@ -189,10 +199,8 @@ (ArrValue::Lazy(a), ArrValue::Lazy(b)) => Cc::ptr_eq(&a.0, &b.0), (ArrValue::Expr(a), ArrValue::Expr(b)) => Cc::ptr_eq(&a.0, &b.0), (ArrValue::Eager(a), ArrValue::Eager(b)) => Cc::ptr_eq(&a.0, &b.0), - (ArrValue::Extended(a), ArrValue::Extended(b)) => Cc::ptr_eq(&a, &b), + (ArrValue::Extended(a), ArrValue::Extended(b)) => Cc::ptr_eq(a, b), (ArrValue::Range(a), ArrValue::Range(b)) => a == b, - (ArrValue::Slice(_), ArrValue::Slice(_)) => false, - (ArrValue::Reverse(_), ArrValue::Reverse(_)) => false, _ => false, } } @@ -203,6 +211,7 @@ ArrValue::Extended(v) => v.a.is_cheap() && v.b.is_cheap(), ArrValue::Slice(r) => r.inner.is_cheap(), ArrValue::Reverse(i) => i.0.is_cheap(), + ArrValue::Repeated(v) => v.is_cheap(), ArrValue::Expr(_) | ArrValue::Lazy(_) | ArrValue::Mapped(_) => false, } } --- a/crates/jrsonnet-evaluator/src/arr/spec.rs +++ b/crates/jrsonnet-evaluator/src/arr/spec.rs @@ -740,6 +740,96 @@ } // impl MappedArray +#[derive(Trace, Debug)] +pub struct RepeatedArrayInner { + data: ArrValue, + repeats: usize, + total_len: usize, +} +#[derive(Trace, Debug, Clone)] +pub struct RepeatedArray(Cc); +impl RepeatedArray { + pub fn new(data: ArrValue, repeats: usize) -> Option { + let total_len = data.len().checked_mul(repeats)?; + Some(Self(Cc::new(RepeatedArrayInner { + data, + repeats, + total_len, + }))) + } + pub fn is_cheap(&self) -> bool { + self.0.data.is_cheap() + } +} + +type RepeatedArrayIter<'t> = impl DoubleEndedIterator> + ExactSizeIterator + 't; +type RepeatedArrayLazyIter<'t> = + impl DoubleEndedIterator> + ExactSizeIterator + 't; +type RepeatedArrayCheapIter<'t> = impl DoubleEndedIterator + ExactSizeIterator + 't; +impl ArrayLike for RepeatedArray { + type Iter<'t> = RepeatedArrayIter<'t>; + type IterLazy<'t> = RepeatedArrayLazyIter<'t>; + type IterCheap<'t> = RepeatedArrayCheapIter<'t>; + + fn len(&self) -> usize { + self.0.total_len + } + + fn get(&self, index: usize) -> Result> { + if index > self.0.total_len { + return Ok(None); + } + self.0.data.get(index % self.0.data.len()) + } + + fn get_lazy(&self, index: usize) -> Option> { + if index > self.0.total_len { + return None; + } + self.0.data.get_lazy(index % self.0.data.len()) + } + + fn get_cheap(&self, index: usize) -> Option { + if index > self.0.total_len { + return None; + } + self.0.data.get_cheap(index % self.0.data.len()) + } + + fn evaluated(&self) -> Result> { + let mut data = self.0.data.evaluated()?; + let data_range = 0..data.len(); + for _ in 1..self.0.repeats { + data.extend_from_within(data_range.clone()); + } + Ok(data) + } + + fn iter(&self) -> RepeatedArrayIter<'_> { + (0..self.0.total_len) + .map(|i| self.get(i)) + .map(Result::transpose) + .map(Option::unwrap) + } + + fn iter_lazy(&self) -> RepeatedArrayLazyIter<'_> { + (0..self.0.total_len) + .map(|i| self.get_lazy(i)) + .map(Option::unwrap) + } + + fn iter_cheap(&self) -> Option> { + if !self.0.data.is_cheap() { + return None; + } + Some( + (0..self.0.total_len) + .map(|i| self.get_cheap(i)) + .map(Option::unwrap), + ) + } +} + macro_rules! impl_iter_enum { ($n:ident => $v:ident) => { pub enum $n<'t> { @@ -752,6 +842,7 @@ Extended(Box<::$v<'t>>), Reverse(Box<::$v<'t>>), Mapped(Box<::$v<'t>>), + Repeated(Box<::$v<'t>>), } }; } @@ -768,6 +859,7 @@ Self::Extended(e) => e.$m($($ident)*), Self::Reverse(e) => e.$m($($ident)*), Self::Mapped(e) => e.$m($($ident)*), + Self::Repeated(e) => e.$m($($ident)*), } }; } @@ -785,6 +877,7 @@ ArrValue::Extended(e) => $e::Extended(Box::new($($wrap!)?(e.$c()))), ArrValue::Reverse(e) => $e::Reverse(Box::new($($wrap!)?(e.$c()))), ArrValue::Mapped(e) => $e::Mapped(Box::new($($wrap!)?(e.$c()))), + ArrValue::Repeated(e) => $e::Repeated(Box::new($($wrap!)?(e.$c()))), } }; } @@ -827,6 +920,7 @@ } Self::Reverse(e) => e.len(), Self::Mapped(e) => e.len(), + Self::Repeated(e) => e.len(), } } } --- a/crates/jrsonnet-evaluator/src/function/mod.rs +++ b/crates/jrsonnet-evaluator/src/function/mod.rs @@ -12,7 +12,9 @@ native::NativeDesc, parse::{parse_default_function_call, parse_function_call}, }; -use crate::{evaluate, gc::TraceBox, typed::Any, Context, ContextBuilder, Result, Val}; +use crate::{ + evaluate, evaluate_trivial, gc::TraceBox, typed::Any, Context, ContextBuilder, Result, Val, +}; pub mod arglike; pub mod builtin; @@ -80,6 +82,10 @@ ) -> Result { parse_function_call(call_ctx, self.ctx.clone(), &self.params, args, tailstrict) } + + pub fn evaluate_trivial(&self) -> Option { + evaluate_trivial(&self.body) + } } /// Represents a Jsonnet function value, including plain functions and user-provided builtins. @@ -201,4 +207,11 @@ pub const fn identity() -> Self { Self::Id } + + pub fn evaluate_trivial(&self) -> Option { + match self { + FuncVal::Normal(n) => n.evaluate_trivial(), + _ => None, + } + } } --- a/crates/jrsonnet-stdlib/src/arrays.rs +++ b/crates/jrsonnet-stdlib/src/arrays.rs @@ -1,23 +1,41 @@ use jrsonnet_evaluator::{ - error::Result, + error::{ErrorKind::RuntimeError, Result}, function::{builtin, FuncVal}, throw, - typed::{Any, BoundedUsize, Either2, NativeFn, Typed, VecVal}, - val::{equals, ArrValue, IndexableVal}, + typed::{Any, BoundedI32, BoundedUsize, Either2, NativeFn, Typed}, + val::{equals, ArrValue, IndexableVal, StrValue}, Either, IStr, Val, }; use jrsonnet_gcmodule::Cc; #[builtin] -pub fn builtin_make_array(sz: usize, func: NativeFn<((f64,), Any)>) -> Result { - let mut out = Vec::with_capacity(sz); - for i in 0..sz { - out.push(func(i as f64)?.0); +pub fn builtin_make_array(sz: BoundedI32<0, { i32::MAX }>, func: FuncVal) -> Result { + if *sz == 0 { + return Ok(ArrValue::empty()); + } + if let Some(trivial) = func.evaluate_trivial() { + let mut out = Vec::with_capacity(*sz as usize); + for _ in 0..*sz { + out.push(trivial.clone()) + } + Ok(ArrValue::eager(Cc::new(out))) + } else { + Ok(ArrValue::range_exclusive(0, *sz).map(func)) } - Ok(VecVal(Cc::new(out))) } #[builtin] +pub fn builtin_repeat(what: Either![IStr, ArrValue], count: usize) -> Result { + Ok(Any(match what { + Either2::A(s) => Val::Str(StrValue::Flat(s.repeat(count).into())), + Either2::B(arr) => Val::Arr( + ArrValue::repeated(arr, count) + .ok_or_else(|| RuntimeError("repeated length overflow".into()))?, + ), + })) +} + +#[builtin] pub fn builtin_slice( indexable: IndexableVal, index: Option>, --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -63,6 +63,7 @@ ("isFunction", builtin_is_function::INST), // Arrays ("makeArray", builtin_make_array::INST), + ("repeat", builtin_repeat::INST), ("slice", builtin_slice::INST), ("map", builtin_map::INST), ("flatMap", builtin_flatmap::INST), --- a/crates/jrsonnet-stdlib/src/std.jsonnet +++ b/crates/jrsonnet-stdlib/src/std.jsonnet @@ -27,13 +27,6 @@ split(str, c):: std.splitLimit(str, c, -1), - 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)), - mapWithIndex(func, arr):: if !std.isFunction(func) then error ('std.mapWithIndex first param must be function, got ' + std.type(func)) --- /dev/null +++ b/nix/jrsonnet-release.nix @@ -0,0 +1,24 @@ +{ lib, fetchFromGitHub, rustPlatform, runCommand, makeWrapper }: + + +rustPlatform.buildRustPackage rec { + pname = "jrsonnet"; + version = "5f0f8de9f52f961e2ff162e0a3fd4ca20a275f1d"; + + src = fetchFromGitHub { + owner = "CertainLach"; + repo = pname; + rev = version; + hash = lib.fakeHash; + }; + + cargoTestFlags = [ "--package=jrsonnet --features=mimalloc,legacy-this-file" ]; + cargoBuildFlags = [ "--package=jrsonnet --features=mimalloc,legacy-this-file" ]; + + buildInputs = [ makeWrapper ]; + + postInstall = '' + mv $out/bin/jrsonnet $out/bin/jrsonnet-release + wrapProgram $out/bin/jrsonnet-release --add-flags "--max-stack=200000 --os-stack=200000" + ''; +}