difftreelog
fix enforce Val::Num finityness at type level
in: master
13 files changed
crates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -120,7 +120,7 @@
}
fn get_cheap(&self, index: usize) -> Option<Val> {
- self.0.get(index).map(|v| Val::Num(f64::from(*v)))
+ self.0.get(index).map(|v| Val::Num((*v).into()))
}
fn is_cheap(&self) -> bool {
true
@@ -399,7 +399,7 @@
}
fn get_cheap(&self, index: usize) -> Option<Val> {
- self.range().nth(index).map(|i| Val::Num(f64::from(i)))
+ self.range().nth(index).map(|i| Val::Num(i.into()))
}
fn is_cheap(&self) -> bool {
true
@@ -430,12 +430,12 @@
}
#[derive(Trace, Debug, Clone)]
-pub struct MappedArray<const WithIndex: bool> {
+pub struct MappedArray<const WITH_INDEX: bool> {
inner: ArrValue,
cached: Cc<RefCell<Vec<ArrayThunk<()>>>>,
mapper: FuncVal,
}
-impl<const WithIndex: bool> MappedArray<WithIndex> {
+impl<const WITH_INDEX: bool> MappedArray<WITH_INDEX> {
pub fn new(inner: ArrValue, mapper: FuncVal) -> Self {
let len = inner.len();
Self {
@@ -445,14 +445,14 @@
}
}
fn evaluate(&self, index: usize, value: Val) -> Result<Val> {
- if WithIndex {
+ if WITH_INDEX {
self.mapper.evaluate_simple(&(index, value), false)
} else {
self.mapper.evaluate_simple(&(value,), false)
}
}
}
-impl<const WithIndex: bool> ArrayLike for MappedArray<WithIndex> {
+impl<const WITH_INDEX: bool> ArrayLike for MappedArray<WITH_INDEX> {
fn len(&self) -> usize {
self.cached.borrow().len()
}
@@ -493,12 +493,12 @@
}
fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
#[derive(Trace)]
- struct ArrayElement<const WithIndex: bool> {
- arr_thunk: MappedArray<WithIndex>,
+ struct ArrayElement<const WITH_INDEX: bool> {
+ arr_thunk: MappedArray<WITH_INDEX>,
index: usize,
}
- impl<const WithIndex: bool> ThunkValue for ArrayElement<WithIndex> {
+ impl<const WITH_INDEX: bool> ThunkValue for ArrayElement<WITH_INDEX> {
type Output = Val;
fn get(self: Box<Self>) -> Result<Self::Output> {
crates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -1,7 +1,5 @@
use std::{
- cmp::Ordering,
- fmt::{Debug, Display},
- path::PathBuf,
+ cmp::Ordering, convert::Infallible, fmt::{Debug, Display}, path::PathBuf
};
use jrsonnet_gcmodule::Trace;
@@ -14,6 +12,7 @@
function::{builtin::ParamDefault, CallLocation},
stdlib::format::FormatError,
typed::TypeLocError,
+ val::ConvertNumValueError,
ObjValue,
};
@@ -236,6 +235,9 @@
#[error("invalid unicode codepoint: {0}")]
InvalidUnicodeCodepointGot(u32),
+ #[error("convert num value: {0}")]
+ ConvertNumValue(#[from] ConvertNumValueError),
+
#[error("format error: {0}")]
Format(#[from] FormatError),
#[error("type error: {0}")]
@@ -259,6 +261,12 @@
}
}
+impl From<Infallible> for Error {
+ fn from(_value: Infallible) -> Self {
+ unreachable!()
+ }
+}
+
/// Single stack trace frame
#[derive(Clone, Debug, Trace)]
pub struct StackTraceElement {
crates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6 ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7 ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;1011use self::destructure::destruct;12use crate::{13 arr::ArrValue,14 bail,15 destructure::evaluate_dest,16 error::{suggest_object_fields, ErrorKind::*},17 evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18 function::{CallLocation, FuncDesc, FuncVal},19 typed::Typed,20 val::{CachedUnbound, IndexableVal, StrValue, Thunk, ThunkValue},21 Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,22 ResultExt, State, Unbound, Val,23};24pub mod destructure;25pub mod operator;2627pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {28 fn is_trivial(expr: &LocExpr) -> bool {29 match &*expr.0 {30 Expr::Str(_)31 | Expr::Num(_)32 | Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,33 Expr::Arr(a) => a.iter().all(is_trivial),34 Expr::Parened(e) => is_trivial(e),35 _ => false,36 }37 }38 Some(match &*expr.0 {39 Expr::Str(s) => Val::string(s.clone()),40 Expr::Num(n) => Val::Num(*n),41 Expr::Literal(LiteralType::False) => Val::Bool(false),42 Expr::Literal(LiteralType::True) => Val::Bool(true),43 Expr::Literal(LiteralType::Null) => Val::Null,44 Expr::Arr(n) => {45 if n.iter().any(|e| !is_trivial(e)) {46 return None;47 }48 Val::Arr(ArrValue::eager(49 n.iter()50 .map(evaluate_trivial)51 .map(|e| e.expect("checked trivial"))52 .collect(),53 ))54 }55 Expr::Parened(e) => evaluate_trivial(e)?,56 _ => return None,57 })58}5960pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {61 Val::Func(FuncVal::Normal(Cc::new(FuncDesc {62 name,63 ctx,64 params,65 body,66 })))67}6869pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {70 Ok(match field_name {71 FieldName::Fixed(n) => Some(n.clone()),72 FieldName::Dyn(expr) => State::push(73 CallLocation::new(&expr.1),74 || "evaluating field name".to_string(),75 || {76 let value = evaluate(ctx, expr)?;77 if matches!(value, Val::Null) {78 Ok(None)79 } else {80 Ok(Some(IStr::from_untyped(value)?))81 }82 },83 )?,84 })85}8687pub fn evaluate_comp(88 ctx: Context,89 specs: &[CompSpec],90 callback: &mut impl FnMut(Context) -> Result<()>,91) -> Result<()> {92 match specs.first() {93 None => callback(ctx)?,94 Some(CompSpec::IfSpec(IfSpecData(cond))) => {95 if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {96 evaluate_comp(ctx, &specs[1..], callback)?;97 }98 }99 Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {100 Val::Arr(list) => {101 for item in list.iter_lazy() {102 let fctx = Pending::new();103 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());104 destruct(var, item, fctx.clone(), &mut new_bindings)?;105 let ctx = ctx106 .clone()107 .extend(new_bindings, None, None, None)108 .into_future(fctx);109110 evaluate_comp(ctx, &specs[1..], callback)?;111 }112 }113 #[cfg(feature = "exp-object-iteration")]114 Val::Obj(obj) => {115 for field in obj.fields(116 // TODO: Should there be ability to preserve iteration order?117 #[cfg(feature = "exp-preserve-order")]118 false,119 ) {120 #[derive(Trace)]121 struct ObjectFieldThunk {122 obj: ObjValue,123 field: IStr,124 }125 impl ThunkValue for ObjectFieldThunk {126 type Output = Val;127128 fn get(self: Box<Self>) -> Result<Self::Output> {129 self.obj.get(self.field).transpose().expect(130 "field exists, as field name was obtained from object.fields()",131 )132 }133 }134135 let fctx = Pending::new();136 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());137 let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![138 Thunk::evaluated(Val::string(field.clone())),139 Thunk::new(ObjectFieldThunk {140 field: field.clone(),141 obj: obj.clone(),142 }),143 ])));144 destruct(var, value, fctx.clone(), &mut new_bindings)?;145 let ctx = ctx146 .clone()147 .extend(new_bindings, None, None, None)148 .into_future(fctx);149150 evaluate_comp(ctx, &specs[1..], callback)?;151 }152 }153 _ => bail!(InComprehensionCanOnlyIterateOverArray),154 },155 }156 Ok(())157}158159trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}160impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}161162fn evaluate_object_locals(163 fctx: Pending<Context>,164 locals: Rc<Vec<BindSpec>>,165) -> impl CloneableUnbound<Context> {166 #[derive(Trace, Clone)]167 struct UnboundLocals {168 fctx: Pending<Context>,169 locals: Rc<Vec<BindSpec>>,170 }171 impl Unbound for UnboundLocals {172 type Bound = Context;173174 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {175 let fctx = Context::new_future();176 let mut new_bindings =177 GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());178 for b in self.locals.iter() {179 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;180 }181182 let ctx = self.fctx.unwrap();183 let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());184185 let ctx = ctx186 .extend(new_bindings, new_dollar, sup, this)187 .into_future(fctx);188189 Ok(ctx)190 }191 }192193 UnboundLocals { fctx, locals }194}195196pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(197 builder: &mut ObjValueBuilder,198 ctx: Context,199 uctx: B,200 field: &FieldMember,201) -> Result<()> {202 let name = evaluate_field_name(ctx, &field.name)?;203 let Some(name) = name else {204 return Ok(());205 };206207 match field {208 FieldMember {209 plus,210 params: None,211 visibility,212 value,213 ..214 } => {215 #[derive(Trace)]216 struct UnboundValue<B: Trace> {217 uctx: B,218 value: LocExpr,219 name: IStr,220 }221 impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {222 type Bound = Val;223 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {224 evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())225 }226 }227228 builder229 .field(name.clone())230 .with_add(*plus)231 .with_visibility(*visibility)232 .with_location(value.1.clone())233 .bindable(UnboundValue {234 uctx,235 value: value.clone(),236 name,237 })?;238 }239 FieldMember {240 params: Some(params),241 visibility,242 value,243 ..244 } => {245 #[derive(Trace)]246 struct UnboundMethod<B: Trace> {247 uctx: B,248 value: LocExpr,249 params: ParamsDesc,250 name: IStr,251 }252 impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {253 type Bound = Val;254 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {255 Ok(evaluate_method(256 self.uctx.bind(sup, this)?,257 self.name.clone(),258 self.params.clone(),259 self.value.clone(),260 ))261 }262 }263264 builder265 .field(name.clone())266 .with_visibility(*visibility)267 .with_location(value.1.clone())268 .bindable(UnboundMethod {269 uctx,270 value: value.clone(),271 params: params.clone(),272 name,273 })?;274 }275 }276 Ok(())277}278279#[allow(clippy::too_many_lines)]280pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {281 let mut builder = ObjValueBuilder::new();282 let locals = Rc::new(283 members284 .iter()285 .filter_map(|m| match m {286 Member::BindStmt(bind) => Some(bind.clone()),287 _ => None,288 })289 .collect::<Vec<_>>(),290 );291292 let fctx = Context::new_future();293294 // We have single context for all fields, so we can cache binds295 let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));296297 for member in members {298 match member {299 Member::Field(field) => {300 evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;301 }302 Member::AssertStmt(stmt) => {303 #[derive(Trace)]304 struct ObjectAssert<B: Trace> {305 uctx: B,306 assert: AssertStmt,307 }308 impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {309 fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {310 let ctx = self.uctx.bind(sup, this)?;311 evaluate_assert(ctx, &self.assert)312 }313 }314 builder.assert(ObjectAssert {315 uctx: uctx.clone(),316 assert: stmt.clone(),317 });318 }319 Member::BindStmt(_) => {320 // Already handled321 }322 }323 }324 let this = builder.build();325 fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));326 Ok(this)327}328329pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {330 Ok(match object {331 ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,332 ObjBody::ObjComp(obj) => {333 let mut builder = ObjValueBuilder::new();334 let locals = Rc::new(335 obj.pre_locals336 .iter()337 .chain(obj.post_locals.iter())338 .cloned()339 .collect::<Vec<_>>(),340 );341 let mut ctxs = vec![];342 evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {343 let fctx = Context::new_future();344 ctxs.push((ctx.clone(), fctx.clone()));345 let uctx = evaluate_object_locals(fctx, locals.clone());346347 evaluate_field_member(&mut builder, ctx, uctx, &obj.field)348 })?;349350 let this = builder.build();351 for (ctx, fctx) in ctxs {352 let _ctx = ctx353 .extend(GcHashMap::new(), None, None, Some(this.clone()))354 .into_future(fctx);355 }356 this357 }358 })359}360361pub fn evaluate_apply(362 ctx: Context,363 value: &LocExpr,364 args: &ArgsDesc,365 loc: CallLocation<'_>,366 tailstrict: bool,367) -> Result<Val> {368 let value = evaluate(ctx.clone(), value)?;369 Ok(match value {370 Val::Func(f) => {371 let body = || f.evaluate(ctx, loc, args, tailstrict);372 if tailstrict {373 body()?374 } else {375 State::push(loc, || format!("function <{}> call", f.name()), body)?376 }377 }378 v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),379 })380}381382pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {383 let value = &assertion.0;384 let msg = &assertion.1;385 let assertion_result = State::push(386 CallLocation::new(&value.1),387 || "assertion condition".to_owned(),388 || bool::from_untyped(evaluate(ctx.clone(), value)?),389 )?;390 if !assertion_result {391 State::push(392 CallLocation::new(&value.1),393 || "assertion failure".to_owned(),394 || {395 if let Some(msg) = msg {396 bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));397 }398 bail!(AssertionFailed(Val::Null.to_string()?));399 },400 )?;401 }402 Ok(())403}404405pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {406 use Expr::*;407 let LocExpr(raw_expr, _loc) = expr;408 Ok(match &**raw_expr {409 Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),410 _ => evaluate(ctx, expr)?,411 })412}413414#[allow(clippy::too_many_lines)]415pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {416 use Expr::*;417418 if let Some(trivial) = evaluate_trivial(expr) {419 return Ok(trivial);420 }421 let LocExpr(expr, loc) = expr;422 Ok(match &**expr {423 Literal(LiteralType::This) => {424 Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())425 }426 Literal(LiteralType::Super) => Val::Obj(427 ctx.super_obj().ok_or(NoSuperFound)?.with_this(428 ctx.this()429 .expect("if super exists - then this should too")430 .clone(),431 ),432 ),433 Literal(LiteralType::Dollar) => {434 Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())435 }436 Literal(LiteralType::True) => Val::Bool(true),437 Literal(LiteralType::False) => Val::Bool(false),438 Literal(LiteralType::Null) => Val::Null,439 Parened(e) => evaluate(ctx, e)?,440 Str(v) => Val::string(v.clone()),441 Num(v) => Val::new_checked_num(*v)?,442 // I have tried to remove special behavior from super by implementing standalone-super443 // expresion, but looks like this case still needs special treatment.444 //445 // Note that other jsonnet implementations will fail on `if value in (super)` expression,446 // because the standalone super literal is not supported, that is because in other447 // implementations `in super` treated differently from in `smth_else`.448 BinaryOp(field, BinaryOpType::In, e)449 if matches!(&*e.0, Expr::Literal(LiteralType::Super)) =>450 {451 let Some(super_obj) = ctx.super_obj() else {452 return Ok(Val::Bool(false));453 };454 let field = evaluate(ctx.clone(), field)?;455 Val::Bool(super_obj.has_field_ex(field.to_string()?, true))456 }457 BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,458 UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,459 Var(name) => State::push(460 CallLocation::new(loc),461 || format!("variable <{name}> access"),462 || ctx.binding(name.clone())?.evaluate(),463 )?,464 Index { indexable, parts } => {465 let mut parts = parts.iter();466 let mut indexable = match &indexable {467 // Cheaper to execute than creating object with overriden `this`468 LocExpr(v, _) if matches!(&**v, Expr::Literal(LiteralType::Super)) => {469 let part = parts.next().expect("at least part should exist");470 let Some(super_obj) = ctx.super_obj() else {471 #[cfg(feature = "exp-null-coaelse")]472 if part.null_coaelse {473 return Ok(Val::Null);474 }475 bail!(NoSuperFound)476 };477 let name = evaluate(ctx.clone(), &part.value)?;478479 let Val::Str(name) = name else {480 bail!(ValueIndexMustBeTypeGot(481 ValType::Obj,482 ValType::Str,483 name.value_type(),484 ))485 };486487 let this = ctx488 .this()489 .expect("no this found, while super present, should not happen");490 let name = name.into_flat();491 match super_obj492 .get_for(name.clone(), this.clone())493 .with_description_src(&part.value, || format!("field <{name}> access"))?494 {495 Some(v) => v,496 #[cfg(feature = "exp-null-coaelse")]497 None if part.null_coaelse => return Ok(Val::Null),498 None => {499 let suggestions = suggest_object_fields(super_obj, name.clone());500501 bail!(NoSuchField(name, suggestions))502 }503 }504 }505 e => evaluate(ctx.clone(), e)?,506 };507508 for part in parts {509 indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {510 (Val::Obj(v), Val::Str(key)) => match v511 .get(key.clone().into_flat())512 .with_description_src(&part.value, || format!("field <{key}> access"))?513 {514 Some(v) => v,515 #[cfg(feature = "exp-null-coaelse")]516 None if part.null_coaelse => return Ok(Val::Null),517 None => {518 let suggestions = suggest_object_fields(&v, key.clone().into_flat());519520 return Err(Error::from(NoSuchField(521 key.clone().into_flat(),522 suggestions,523 )))524 .with_description_src(&part.value, || format!("field <{key}> access"));525 }526 },527 (Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(528 ValType::Obj,529 ValType::Str,530 n.value_type(),531 )),532 (Val::Arr(v), Val::Num(n)) => {533 if n.fract() > f64::EPSILON {534 bail!(FractionalIndex)535 }536 if n < 0.0 {537 bail!(ArrayBoundsError(n as isize, v.len()));538 }539 v.get(n as usize)?540 .ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?541 }542 (Val::Arr(_), Val::Str(n)) => {543 bail!(AttemptedIndexAnArrayWithString(n.into_flat()))544 }545 (Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(546 ValType::Arr,547 ValType::Num,548 n.value_type(),549 )),550551 (Val::Str(s), Val::Num(n)) => Val::Str({552 let v: IStr = s553 .clone()554 .into_flat()555 .chars()556 .skip(n as usize)557 .take(1)558 .collect::<String>()559 .into();560 if v.is_empty() {561 let size = s.into_flat().chars().count();562 bail!(StringBoundsError(n as usize, size))563 }564 StrValue::Flat(v)565 }),566 (Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(567 ValType::Str,568 ValType::Num,569 n.value_type(),570 )),571 #[cfg(feature = "exp-null-coaelse")]572 (Val::Null, _) if part.null_coaelse => return Ok(Val::Null),573 (v, _) => bail!(CantIndexInto(v.value_type())),574 };575 }576 indexable577 }578 LocalExpr(bindings, returned) => {579 let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =580 GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());581 let fctx = Context::new_future();582 for b in bindings {583 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;584 }585 let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);586 evaluate(ctx, &returned.clone())?587 }588 Arr(items) => {589 if items.is_empty() {590 Val::Arr(ArrValue::empty())591 } else if items.len() == 1 {592 #[derive(Trace)]593 struct ArrayElement {594 ctx: Context,595 item: LocExpr,596 }597 impl ThunkValue for ArrayElement {598 type Output = Val;599 fn get(self: Box<Self>) -> Result<Val> {600 evaluate(self.ctx, &self.item)601 }602 }603 Val::Arr(ArrValue::lazy(vec![Thunk::new(ArrayElement {604 ctx,605 item: items[0].clone(),606 })]))607 } else {608 Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))609 }610 }611 ArrComp(expr, comp_specs) => {612 let mut out = Vec::new();613 evaluate_comp(ctx, comp_specs, &mut |ctx| {614 #[derive(Trace)]615 struct EvaluateThunk {616 ctx: Context,617 expr: LocExpr,618 }619 impl ThunkValue for EvaluateThunk {620 type Output = Val;621 fn get(self: Box<Self>) -> Result<Val> {622 evaluate(self.ctx, &self.expr)623 }624 }625 out.push(Thunk::new(EvaluateThunk {626 ctx,627 expr: expr.clone(),628 }));629 Ok(())630 })?;631 Val::Arr(ArrValue::lazy(out))632 }633 Obj(body) => Val::Obj(evaluate_object(ctx, body)?),634 ObjExtend(a, b) => evaluate_add_op(635 &evaluate(ctx.clone(), a)?,636 &Val::Obj(evaluate_object(ctx, b)?),637 )?,638 Apply(value, args, tailstrict) => {639 evaluate_apply(ctx, value, args, CallLocation::new(loc), *tailstrict)?640 }641 Function(params, body) => {642 evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())643 }644 AssertExpr(assert, returned) => {645 evaluate_assert(ctx.clone(), assert)?;646 evaluate(ctx, returned)?647 }648 ErrorStmt(e) => State::push(649 CallLocation::new(loc),650 || "error statement".to_owned(),651 || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),652 )?,653 IfElse {654 cond,655 cond_then,656 cond_else,657 } => {658 if State::push(659 CallLocation::new(loc),660 || "if condition".to_owned(),661 || bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),662 )? {663 evaluate(ctx, cond_then)?664 } else {665 match cond_else {666 Some(v) => evaluate(ctx, v)?,667 None => Val::Null,668 }669 }670 }671 Slice(value, desc) => {672 fn parse_idx<T: Typed>(673 loc: CallLocation<'_>,674 ctx: &Context,675 expr: Option<&LocExpr>,676 desc: &'static str,677 ) -> Result<Option<T>> {678 if let Some(value) = expr {679 Ok(Some(State::push(680 loc,681 || format!("slice {desc}"),682 || T::from_untyped(evaluate(ctx.clone(), value)?),683 )?))684 } else {685 Ok(None)686 }687 }688689 let indexable = evaluate(ctx.clone(), value)?;690 let loc = CallLocation::new(loc);691692 let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;693 let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;694 let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;695696 IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?697 }698 i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {699 let Expr::Str(path) = &*path.0 else {700 bail!("computed imports are not supported")701 };702 let tmp = loc.clone().0;703 let s = ctx.state();704 let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;705 match i {706 Import(_) => State::push(707 CallLocation::new(loc),708 || format!("import {:?}", path.clone()),709 || s.import_resolved(resolved_path),710 )?,711 ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),712 ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),713 _ => unreachable!(),714 }715 }716 })717}1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6 ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7 ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;1011use self::destructure::destruct;12use crate::{13 arr::ArrValue,14 bail,15 destructure::evaluate_dest,16 error::{suggest_object_fields, ErrorKind::*},17 evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18 function::{CallLocation, FuncDesc, FuncVal},19 typed::Typed,20 val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk, ThunkValue},21 Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,22 ResultExt, State, Unbound, Val,23};24pub mod destructure;25pub mod operator;2627pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {28 fn is_trivial(expr: &LocExpr) -> bool {29 match &*expr.0 {30 Expr::Str(_)31 | Expr::Num(_)32 | Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,33 Expr::Arr(a) => a.iter().all(is_trivial),34 Expr::Parened(e) => is_trivial(e),35 _ => false,36 }37 }38 Some(match &*expr.0 {39 Expr::Str(s) => Val::string(s.clone()),40 Expr::Num(n) => Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values")),41 Expr::Literal(LiteralType::False) => Val::Bool(false),42 Expr::Literal(LiteralType::True) => Val::Bool(true),43 Expr::Literal(LiteralType::Null) => Val::Null,44 Expr::Arr(n) => {45 if n.iter().any(|e| !is_trivial(e)) {46 return None;47 }48 Val::Arr(ArrValue::eager(49 n.iter()50 .map(evaluate_trivial)51 .map(|e| e.expect("checked trivial"))52 .collect(),53 ))54 }55 Expr::Parened(e) => evaluate_trivial(e)?,56 _ => return None,57 })58}5960pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {61 Val::Func(FuncVal::Normal(Cc::new(FuncDesc {62 name,63 ctx,64 params,65 body,66 })))67}6869pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {70 Ok(match field_name {71 FieldName::Fixed(n) => Some(n.clone()),72 FieldName::Dyn(expr) => State::push(73 CallLocation::new(&expr.1),74 || "evaluating field name".to_string(),75 || {76 let value = evaluate(ctx, expr)?;77 if matches!(value, Val::Null) {78 Ok(None)79 } else {80 Ok(Some(IStr::from_untyped(value)?))81 }82 },83 )?,84 })85}8687pub fn evaluate_comp(88 ctx: Context,89 specs: &[CompSpec],90 callback: &mut impl FnMut(Context) -> Result<()>,91) -> Result<()> {92 match specs.first() {93 None => callback(ctx)?,94 Some(CompSpec::IfSpec(IfSpecData(cond))) => {95 if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {96 evaluate_comp(ctx, &specs[1..], callback)?;97 }98 }99 Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {100 Val::Arr(list) => {101 for item in list.iter_lazy() {102 let fctx = Pending::new();103 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());104 destruct(var, item, fctx.clone(), &mut new_bindings)?;105 let ctx = ctx106 .clone()107 .extend(new_bindings, None, None, None)108 .into_future(fctx);109110 evaluate_comp(ctx, &specs[1..], callback)?;111 }112 }113 #[cfg(feature = "exp-object-iteration")]114 Val::Obj(obj) => {115 for field in obj.fields(116 // TODO: Should there be ability to preserve iteration order?117 #[cfg(feature = "exp-preserve-order")]118 false,119 ) {120 #[derive(Trace)]121 struct ObjectFieldThunk {122 obj: ObjValue,123 field: IStr,124 }125 impl ThunkValue for ObjectFieldThunk {126 type Output = Val;127128 fn get(self: Box<Self>) -> Result<Self::Output> {129 self.obj.get(self.field).transpose().expect(130 "field exists, as field name was obtained from object.fields()",131 )132 }133 }134135 let fctx = Pending::new();136 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());137 let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![138 Thunk::evaluated(Val::string(field.clone())),139 Thunk::new(ObjectFieldThunk {140 field: field.clone(),141 obj: obj.clone(),142 }),143 ])));144 destruct(var, value, fctx.clone(), &mut new_bindings)?;145 let ctx = ctx146 .clone()147 .extend(new_bindings, None, None, None)148 .into_future(fctx);149150 evaluate_comp(ctx, &specs[1..], callback)?;151 }152 }153 _ => bail!(InComprehensionCanOnlyIterateOverArray),154 },155 }156 Ok(())157}158159trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}160impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}161162fn evaluate_object_locals(163 fctx: Pending<Context>,164 locals: Rc<Vec<BindSpec>>,165) -> impl CloneableUnbound<Context> {166 #[derive(Trace, Clone)]167 struct UnboundLocals {168 fctx: Pending<Context>,169 locals: Rc<Vec<BindSpec>>,170 }171 impl Unbound for UnboundLocals {172 type Bound = Context;173174 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {175 let fctx = Context::new_future();176 let mut new_bindings =177 GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());178 for b in self.locals.iter() {179 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;180 }181182 let ctx = self.fctx.unwrap();183 let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());184185 let ctx = ctx186 .extend(new_bindings, new_dollar, sup, this)187 .into_future(fctx);188189 Ok(ctx)190 }191 }192193 UnboundLocals { fctx, locals }194}195196pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(197 builder: &mut ObjValueBuilder,198 ctx: Context,199 uctx: B,200 field: &FieldMember,201) -> Result<()> {202 let name = evaluate_field_name(ctx, &field.name)?;203 let Some(name) = name else {204 return Ok(());205 };206207 match field {208 FieldMember {209 plus,210 params: None,211 visibility,212 value,213 ..214 } => {215 #[derive(Trace)]216 struct UnboundValue<B: Trace> {217 uctx: B,218 value: LocExpr,219 name: IStr,220 }221 impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {222 type Bound = Val;223 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {224 evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())225 }226 }227228 builder229 .field(name.clone())230 .with_add(*plus)231 .with_visibility(*visibility)232 .with_location(value.1.clone())233 .bindable(UnboundValue {234 uctx,235 value: value.clone(),236 name,237 })?;238 }239 FieldMember {240 params: Some(params),241 visibility,242 value,243 ..244 } => {245 #[derive(Trace)]246 struct UnboundMethod<B: Trace> {247 uctx: B,248 value: LocExpr,249 params: ParamsDesc,250 name: IStr,251 }252 impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {253 type Bound = Val;254 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {255 Ok(evaluate_method(256 self.uctx.bind(sup, this)?,257 self.name.clone(),258 self.params.clone(),259 self.value.clone(),260 ))261 }262 }263264 builder265 .field(name.clone())266 .with_visibility(*visibility)267 .with_location(value.1.clone())268 .bindable(UnboundMethod {269 uctx,270 value: value.clone(),271 params: params.clone(),272 name,273 })?;274 }275 }276 Ok(())277}278279#[allow(clippy::too_many_lines)]280pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {281 let mut builder = ObjValueBuilder::new();282 let locals = Rc::new(283 members284 .iter()285 .filter_map(|m| match m {286 Member::BindStmt(bind) => Some(bind.clone()),287 _ => None,288 })289 .collect::<Vec<_>>(),290 );291292 let fctx = Context::new_future();293294 // We have single context for all fields, so we can cache binds295 let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));296297 for member in members {298 match member {299 Member::Field(field) => {300 evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;301 }302 Member::AssertStmt(stmt) => {303 #[derive(Trace)]304 struct ObjectAssert<B: Trace> {305 uctx: B,306 assert: AssertStmt,307 }308 impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {309 fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {310 let ctx = self.uctx.bind(sup, this)?;311 evaluate_assert(ctx, &self.assert)312 }313 }314 builder.assert(ObjectAssert {315 uctx: uctx.clone(),316 assert: stmt.clone(),317 });318 }319 Member::BindStmt(_) => {320 // Already handled321 }322 }323 }324 let this = builder.build();325 fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));326 Ok(this)327}328329pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {330 Ok(match object {331 ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,332 ObjBody::ObjComp(obj) => {333 let mut builder = ObjValueBuilder::new();334 let locals = Rc::new(335 obj.pre_locals336 .iter()337 .chain(obj.post_locals.iter())338 .cloned()339 .collect::<Vec<_>>(),340 );341 let mut ctxs = vec![];342 evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {343 let fctx = Context::new_future();344 ctxs.push((ctx.clone(), fctx.clone()));345 let uctx = evaluate_object_locals(fctx, locals.clone());346347 evaluate_field_member(&mut builder, ctx, uctx, &obj.field)348 })?;349350 let this = builder.build();351 for (ctx, fctx) in ctxs {352 let _ctx = ctx353 .extend(GcHashMap::new(), None, None, Some(this.clone()))354 .into_future(fctx);355 }356 this357 }358 })359}360361pub fn evaluate_apply(362 ctx: Context,363 value: &LocExpr,364 args: &ArgsDesc,365 loc: CallLocation<'_>,366 tailstrict: bool,367) -> Result<Val> {368 let value = evaluate(ctx.clone(), value)?;369 Ok(match value {370 Val::Func(f) => {371 let body = || f.evaluate(ctx, loc, args, tailstrict);372 if tailstrict {373 body()?374 } else {375 State::push(loc, || format!("function <{}> call", f.name()), body)?376 }377 }378 v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),379 })380}381382pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {383 let value = &assertion.0;384 let msg = &assertion.1;385 let assertion_result = State::push(386 CallLocation::new(&value.1),387 || "assertion condition".to_owned(),388 || bool::from_untyped(evaluate(ctx.clone(), value)?),389 )?;390 if !assertion_result {391 State::push(392 CallLocation::new(&value.1),393 || "assertion failure".to_owned(),394 || {395 if let Some(msg) = msg {396 bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));397 }398 bail!(AssertionFailed(Val::Null.to_string()?));399 },400 )?;401 }402 Ok(())403}404405pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {406 use Expr::*;407 let LocExpr(raw_expr, _loc) = expr;408 Ok(match &**raw_expr {409 Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),410 _ => evaluate(ctx, expr)?,411 })412}413414#[allow(clippy::too_many_lines)]415pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {416 use Expr::*;417418 if let Some(trivial) = evaluate_trivial(expr) {419 return Ok(trivial);420 }421 let LocExpr(expr, loc) = expr;422 Ok(match &**expr {423 Literal(LiteralType::This) => {424 Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())425 }426 Literal(LiteralType::Super) => Val::Obj(427 ctx.super_obj().ok_or(NoSuperFound)?.with_this(428 ctx.this()429 .expect("if super exists - then this should too")430 .clone(),431 ),432 ),433 Literal(LiteralType::Dollar) => {434 Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())435 }436 Literal(LiteralType::True) => Val::Bool(true),437 Literal(LiteralType::False) => Val::Bool(false),438 Literal(LiteralType::Null) => Val::Null,439 Parened(e) => evaluate(ctx, e)?,440 Str(v) => Val::string(v.clone()),441 Num(v) => Val::try_num(*v)?,442 // I have tried to remove special behavior from super by implementing standalone-super443 // expresion, but looks like this case still needs special treatment.444 //445 // Note that other jsonnet implementations will fail on `if value in (super)` expression,446 // because the standalone super literal is not supported, that is because in other447 // implementations `in super` treated differently from in `smth_else`.448 BinaryOp(field, BinaryOpType::In, e)449 if matches!(&*e.0, Expr::Literal(LiteralType::Super)) =>450 {451 let Some(super_obj) = ctx.super_obj() else {452 return Ok(Val::Bool(false));453 };454 let field = evaluate(ctx.clone(), field)?;455 Val::Bool(super_obj.has_field_ex(field.to_string()?, true))456 }457 BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,458 UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,459 Var(name) => State::push(460 CallLocation::new(loc),461 || format!("variable <{name}> access"),462 || ctx.binding(name.clone())?.evaluate(),463 )?,464 Index { indexable, parts } => {465 let mut parts = parts.iter();466 let mut indexable = match &indexable {467 // Cheaper to execute than creating object with overriden `this`468 LocExpr(v, _) if matches!(&**v, Expr::Literal(LiteralType::Super)) => {469 let part = parts.next().expect("at least part should exist");470 let Some(super_obj) = ctx.super_obj() else {471 #[cfg(feature = "exp-null-coaelse")]472 if part.null_coaelse {473 return Ok(Val::Null);474 }475 bail!(NoSuperFound)476 };477 let name = evaluate(ctx.clone(), &part.value)?;478479 let Val::Str(name) = name else {480 bail!(ValueIndexMustBeTypeGot(481 ValType::Obj,482 ValType::Str,483 name.value_type(),484 ))485 };486487 let this = ctx488 .this()489 .expect("no this found, while super present, should not happen");490 let name = name.into_flat();491 match super_obj492 .get_for(name.clone(), this.clone())493 .with_description_src(&part.value, || format!("field <{name}> access"))?494 {495 Some(v) => v,496 #[cfg(feature = "exp-null-coaelse")]497 None if part.null_coaelse => return Ok(Val::Null),498 None => {499 let suggestions = suggest_object_fields(super_obj, name.clone());500501 bail!(NoSuchField(name, suggestions))502 }503 }504 }505 e => evaluate(ctx.clone(), e)?,506 };507508 for part in parts {509 indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {510 (Val::Obj(v), Val::Str(key)) => match v511 .get(key.clone().into_flat())512 .with_description_src(&part.value, || format!("field <{key}> access"))?513 {514 Some(v) => v,515 #[cfg(feature = "exp-null-coaelse")]516 None if part.null_coaelse => return Ok(Val::Null),517 None => {518 let suggestions = suggest_object_fields(&v, key.clone().into_flat());519520 return Err(Error::from(NoSuchField(521 key.clone().into_flat(),522 suggestions,523 )))524 .with_description_src(&part.value, || format!("field <{key}> access"));525 }526 },527 (Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(528 ValType::Obj,529 ValType::Str,530 n.value_type(),531 )),532 (Val::Arr(v), Val::Num(n)) => {533 let n = n.get();534 if n.fract() > f64::EPSILON {535 bail!(FractionalIndex)536 }537 if n < 0.0 {538 bail!(ArrayBoundsError(n as isize, v.len()));539 }540 v.get(n as usize)?541 .ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?542 }543 (Val::Arr(_), Val::Str(n)) => {544 bail!(AttemptedIndexAnArrayWithString(n.into_flat()))545 }546 (Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(547 ValType::Arr,548 ValType::Num,549 n.value_type(),550 )),551552 (Val::Str(s), Val::Num(n)) => Val::Str({553 let v: IStr = s554 .clone()555 .into_flat()556 .chars()557 .skip(n.get() as usize)558 .take(1)559 .collect::<String>()560 .into();561 if v.is_empty() {562 let size = s.into_flat().chars().count();563 bail!(StringBoundsError(n.get() as usize, size))564 }565 StrValue::Flat(v)566 }),567 (Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(568 ValType::Str,569 ValType::Num,570 n.value_type(),571 )),572 #[cfg(feature = "exp-null-coaelse")]573 (Val::Null, _) if part.null_coaelse => return Ok(Val::Null),574 (v, _) => bail!(CantIndexInto(v.value_type())),575 };576 }577 indexable578 }579 LocalExpr(bindings, returned) => {580 let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =581 GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());582 let fctx = Context::new_future();583 for b in bindings {584 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;585 }586 let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);587 evaluate(ctx, &returned.clone())?588 }589 Arr(items) => {590 if items.is_empty() {591 Val::Arr(ArrValue::empty())592 } else if items.len() == 1 {593 #[derive(Trace)]594 struct ArrayElement {595 ctx: Context,596 item: LocExpr,597 }598 impl ThunkValue for ArrayElement {599 type Output = Val;600 fn get(self: Box<Self>) -> Result<Val> {601 evaluate(self.ctx, &self.item)602 }603 }604 Val::Arr(ArrValue::lazy(vec![Thunk::new(ArrayElement {605 ctx,606 item: items[0].clone(),607 })]))608 } else {609 Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))610 }611 }612 ArrComp(expr, comp_specs) => {613 let mut out = Vec::new();614 evaluate_comp(ctx, comp_specs, &mut |ctx| {615 #[derive(Trace)]616 struct EvaluateThunk {617 ctx: Context,618 expr: LocExpr,619 }620 impl ThunkValue for EvaluateThunk {621 type Output = Val;622 fn get(self: Box<Self>) -> Result<Val> {623 evaluate(self.ctx, &self.expr)624 }625 }626 out.push(Thunk::new(EvaluateThunk {627 ctx,628 expr: expr.clone(),629 }));630 Ok(())631 })?;632 Val::Arr(ArrValue::lazy(out))633 }634 Obj(body) => Val::Obj(evaluate_object(ctx, body)?),635 ObjExtend(a, b) => evaluate_add_op(636 &evaluate(ctx.clone(), a)?,637 &Val::Obj(evaluate_object(ctx, b)?),638 )?,639 Apply(value, args, tailstrict) => {640 evaluate_apply(ctx, value, args, CallLocation::new(loc), *tailstrict)?641 }642 Function(params, body) => {643 evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())644 }645 AssertExpr(assert, returned) => {646 evaluate_assert(ctx.clone(), assert)?;647 evaluate(ctx, returned)?648 }649 ErrorStmt(e) => State::push(650 CallLocation::new(loc),651 || "error statement".to_owned(),652 || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),653 )?,654 IfElse {655 cond,656 cond_then,657 cond_else,658 } => {659 if State::push(660 CallLocation::new(loc),661 || "if condition".to_owned(),662 || bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),663 )? {664 evaluate(ctx, cond_then)?665 } else {666 match cond_else {667 Some(v) => evaluate(ctx, v)?,668 None => Val::Null,669 }670 }671 }672 Slice(value, desc) => {673 fn parse_idx<T: Typed>(674 loc: CallLocation<'_>,675 ctx: &Context,676 expr: Option<&LocExpr>,677 desc: &'static str,678 ) -> Result<Option<T>> {679 if let Some(value) = expr {680 Ok(Some(State::push(681 loc,682 || format!("slice {desc}"),683 || T::from_untyped(evaluate(ctx.clone(), value)?),684 )?))685 } else {686 Ok(None)687 }688 }689690 let indexable = evaluate(ctx.clone(), value)?;691 let loc = CallLocation::new(loc);692693 let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;694 let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;695 let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;696697 IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?698 }699 i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {700 let Expr::Str(path) = &*path.0 else {701 bail!("computed imports are not supported")702 };703 let tmp = loc.clone().0;704 let s = ctx.state();705 let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;706 match i {707 Import(_) => State::push(708 CallLocation::new(loc),709 || format!("import {:?}", path.clone()),710 || s.import_resolved(resolved_path),711 )?,712 ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),713 ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),714 _ => unreachable!(),715 }716 }717 })718}crates/jrsonnet-evaluator/src/evaluate/operator.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs
@@ -17,10 +17,10 @@
use UnaryOpType::*;
use Val::*;
Ok(match (op, b) {
- (Plus, Num(n)) => Num(*n),
- (Minus, Num(n)) => Num(-*n),
+ (Plus, Num(n)) => Val::Num(*n),
+ (Minus, Num(n)) => Val::try_num(-n.get())?,
(Not, Bool(v)) => Bool(!v),
- (BitNot, Num(n)) => Num(!(*n as i64) as f64),
+ (BitNot, Num(n)) => Val::try_num(!(n.get() as i64) as f64)?,
(op, o) => bail!(UnaryOperatorDoesNotOperateOnType(op, o.value_type())),
})
}
@@ -40,7 +40,7 @@
(Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())),
(Arr(a), Arr(b)) => Val::Arr(ArrValue::extended(a.clone(), b.clone())),
- (Num(v1), Num(v2)) => Val::new_checked_num(v1 + v2)?,
+ (Num(v1), Num(v2)) => Val::try_num(v1.get() + v2.get())?,
#[cfg(feature = "exp-bigint")]
(BigInt(a), BigInt(b)) => BigInt(Box::new(&**a + &**b)),
_ => bail!(BinaryOperatorDoesNotOperateOnValues(
@@ -55,10 +55,10 @@
use Val::*;
match (a, b) {
(Num(a), Num(b)) => {
- if *b == 0.0 {
+ if b.get() == 0.0 {
bail!(DivisionByZero)
}
- Ok(Num(a % b))
+ Ok(Val::try_num(a.get() % b.get())?)
}
(Str(str), vals) => {
String::into_untyped(std_format(&str.clone().into_flat(), vals.clone())?)
@@ -143,39 +143,39 @@
(Str(a), In, Obj(obj)) => Bool(obj.has_field_ex(a.clone().into_flat(), true)),
(a, Mod, b) => evaluate_mod_op(a, b)?,
- (Str(v1), Mul, Num(v2)) => Val::string(v1.to_string().repeat(*v2 as usize)),
+ (Str(v1), Mul, Num(v2)) => Val::string(v1.to_string().repeat(v2.get() as usize)),
// Bool X Bool
(Bool(a), And, Bool(b)) => Bool(*a && *b),
(Bool(a), Or, Bool(b)) => Bool(*a || *b),
// Num X Num
- (Num(v1), Mul, Num(v2)) => Val::new_checked_num(v1 * v2)?,
+ (Num(v1), Mul, Num(v2)) => Val::try_num(v1.get() * v2.get())?,
(Num(v1), Div, Num(v2)) => {
- if *v2 == 0.0 {
+ if v2.get() == 0.0 {
bail!(DivisionByZero)
}
- Val::new_checked_num(v1 / v2)?
+ Val::try_num(v1.get() / v2.get())?
}
- (Num(v1), Sub, Num(v2)) => Val::new_checked_num(v1 - v2)?,
+ (Num(v1), Sub, Num(v2)) => Val::try_num(v1.get() - v2.get())?,
- (Num(v1), BitAnd, Num(v2)) => Num((*v1 as i64 & *v2 as i64) as f64),
- (Num(v1), BitOr, Num(v2)) => Num((*v1 as i64 | *v2 as i64) as f64),
- (Num(v1), BitXor, Num(v2)) => Num((*v1 as i64 ^ *v2 as i64) as f64),
+ (Num(v1), BitAnd, Num(v2)) => Val::try_num((v1.get() as i64 & v2.get() as i64) as f64)?,
+ (Num(v1), BitOr, Num(v2)) => Val::try_num((v1.get() as i64 | v2.get() as i64) as f64)?,
+ (Num(v1), BitXor, Num(v2)) => Val::try_num((v1.get() as i64 ^ v2.get() as i64) as f64)?,
(Num(v1), Lhs, Num(v2)) => {
- if *v2 < 0.0 {
+ if v2.get() < 0.0 {
bail!("shift by negative exponent")
}
- let exp = ((*v2 as i64) & 63) as u32;
- Num((*v1 as i64).wrapping_shl(exp) as f64)
+ let exp = ((v2.get() as i64) & 63) as u32;
+ Val::try_num((v1.get() as i64).wrapping_shl(exp) as f64)?
}
(Num(v1), Rhs, Num(v2)) => {
- if *v2 < 0.0 {
+ if v2.get() < 0.0 {
bail!("shift by negative exponent")
}
- let exp = ((*v2 as i64) & 63) as u32;
- Num((*v1 as i64).wrapping_shr(exp) as f64)
+ let exp = ((v2.get() as i64) & 63) as u32;
+ Val::try_num((v1.get() as i64).wrapping_shr(exp) as f64)?
}
// Bigint X Bigint
crates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -2,7 +2,7 @@
use jrsonnet_interner::IStr;
use serde::{
- de::Visitor,
+ de::{self, Visitor},
ser::{
Error, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
SerializeTupleStruct, SerializeTupleVariant,
@@ -11,7 +11,8 @@
};
use crate::{
- arr::ArrValue, runtime_error, Error as JrError, ObjValue, ObjValueBuilder, Result, State, Val,
+ arr::ArrValue, runtime_error, val::NumValue, Error as JrError, ObjValue, ObjValueBuilder,
+ Result, State, Val,
};
impl<'de> Deserialize<'de> for Val {
@@ -37,22 +38,21 @@
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
Ok(Val::Bool(v))
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
- if !v.is_finite() {
- return Err(E::custom("only finite numbers are supported"));
- }
- Ok(Val::Num(v))
+ Ok(Val::Num(NumValue::new(v).ok_or_else(|| {
+ E::custom("only finite numbers are supported")
+ })?))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
Ok(Val::string(v))
}
@@ -67,27 +67,27 @@
// }
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
- Ok(Val::Num(v as f64))
+ Ok(Val::Num(NumValue::new(v as f64).expect("no overflow")))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
- Ok(Val::Num(v as f64))
+ Ok(Val::Num(NumValue::new(v as f64).expect("no overflow")))
}
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
Ok(Val::Arr(ArrValue::bytes(v.into())))
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
Ok(Val::Null)
}
@@ -100,7 +100,7 @@
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
- E: serde::de::Error,
+ E: de::Error,
{
Ok(Val::Null)
}
@@ -114,7 +114,7 @@
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
- A: serde::de::SeqAccess<'de>,
+ A: de::SeqAccess<'de>,
{
let mut out = seq.size_hint().map_or_else(Vec::new, Vec::with_capacity);
@@ -127,7 +127,7 @@
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
- A: serde::de::MapAccess<'de>,
+ A: de::MapAccess<'de>,
{
let mut out = map
.size_hint()
@@ -159,11 +159,12 @@
Self::Null => serializer.serialize_none(),
Self::Str(s) => serializer.serialize_str(&s.clone().into_flat()),
Self::Num(n) => {
+ let n = n.get();
if n.fract() == 0.0 {
- let n = *n as i64;
+ let n = n as i64;
serializer.serialize_i64(n)
} else {
- serializer.serialize_f64(*n)
+ serializer.serialize_f64(n)
}
}
#[cfg(feature = "exp-bigint")]
@@ -449,15 +450,15 @@
}
fn serialize_i8(self, v: i8) -> Result<Val> {
- Ok(Val::Num(f64::from(v)))
+ Ok(Val::Num(v.into()))
}
fn serialize_i16(self, v: i16) -> Result<Val> {
- Ok(Val::Num(f64::from(v)))
+ Ok(Val::Num(v.into()))
}
fn serialize_i32(self, v: i32) -> Result<Val> {
- Ok(Val::Num(f64::from(v)))
+ Ok(Val::Num(v.into()))
}
fn serialize_i64(self, v: i64) -> Result<Val> {
@@ -465,15 +466,15 @@
}
fn serialize_u8(self, v: u8) -> Result<Val> {
- Ok(Val::Num(f64::from(v)))
+ Ok(Val::Num(v.into()))
}
fn serialize_u16(self, v: u16) -> Result<Val> {
- Ok(Val::Num(f64::from(v)))
+ Ok(Val::Num(v.into()))
}
fn serialize_u32(self, v: u32) -> Result<Val> {
- Ok(Val::Num(f64::from(v)))
+ Ok(Val::Num(v.into()))
}
fn serialize_u64(self, v: u64) -> Result<Val> {
@@ -481,11 +482,11 @@
}
fn serialize_f32(self, v: f32) -> Result<Val> {
- Ok(Val::Num(f64::from(v)))
+ Ok(Val::try_num(f64::from(v))?)
}
fn serialize_f64(self, v: f64) -> Result<Val> {
- Ok(Val::Num(v))
+ Ok(Val::try_num(v)?)
}
fn serialize_char(self, v: char) -> Result<Val> {
crates/jrsonnet-evaluator/src/stdlib/format.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/stdlib/format.rs
+++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs
@@ -604,10 +604,13 @@
}
}
ConvTypeV::Char => match value.clone() {
- Val::Num(n) => tmp_out.push(
- std::char::from_u32(n as u32)
- .ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,
- ),
+ Val::Num(n) => {
+ let n = n.get();
+ tmp_out.push(
+ std::char::from_u32(n as u32)
+ .ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,
+ )
+ }
Val::Str(s) => {
let s = s.into_flat();
if s.chars().count() != 1 {
@@ -786,6 +789,7 @@
#[cfg(test)]
pub mod test_format {
use super::*;
+ use crate::val::NumValue;
#[test]
fn parse() {
@@ -799,17 +803,21 @@
);
}
+ fn num(v: f64) -> Val {
+ Val::Num(NumValue::new(v).expect("finite"))
+ }
+
#[test]
fn octals() {
- assert_eq!(format_arr("%#o", &[Val::Num(8.0)]).unwrap(), "010");
- assert_eq!(format_arr("%#4o", &[Val::Num(8.0)]).unwrap(), " 010");
- assert_eq!(format_arr("%4o", &[Val::Num(8.0)]).unwrap(), " 10");
- assert_eq!(format_arr("%04o", &[Val::Num(8.0)]).unwrap(), "0010");
- assert_eq!(format_arr("%+4o", &[Val::Num(8.0)]).unwrap(), " +10");
- assert_eq!(format_arr("%+04o", &[Val::Num(8.0)]).unwrap(), "+010");
- assert_eq!(format_arr("%-4o", &[Val::Num(8.0)]).unwrap(), "10 ");
- assert_eq!(format_arr("%+-4o", &[Val::Num(8.0)]).unwrap(), "+10 ");
- assert_eq!(format_arr("%+-04o", &[Val::Num(8.0)]).unwrap(), "+10 ");
+ assert_eq!(format_arr("%#o", &[num(8.0)]).unwrap(), "010");
+ assert_eq!(format_arr("%#4o", &[num(8.0)]).unwrap(), " 010");
+ assert_eq!(format_arr("%4o", &[num(8.0)]).unwrap(), " 10");
+ assert_eq!(format_arr("%04o", &[num(8.0)]).unwrap(), "0010");
+ assert_eq!(format_arr("%+4o", &[num(8.0)]).unwrap(), " +10");
+ assert_eq!(format_arr("%+04o", &[num(8.0)]).unwrap(), "+010");
+ assert_eq!(format_arr("%-4o", &[num(8.0)]).unwrap(), "10 ");
+ assert_eq!(format_arr("%+-4o", &[num(8.0)]).unwrap(), "+10 ");
+ assert_eq!(format_arr("%+-04o", &[num(8.0)]).unwrap(), "+10 ");
}
#[test]
@@ -817,7 +825,7 @@
assert_eq!(
format_arr(
"How much error budget is left looking at our %.3f%% availability gurantees?",
- &[Val::Num(4.0)]
+ &[num(4.0)]
)
.unwrap(),
"How much error budget is left looking at our 4.000% availability gurantees?"
crates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -10,7 +10,7 @@
bail,
function::{native::NativeDesc, FuncDesc, FuncVal},
typed::CheckType,
- val::{IndexableVal, StrValue, ThunkMapper},
+ val::{IndexableVal, NumValue, StrValue, ThunkMapper},
ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val,
};
@@ -120,7 +120,8 @@
}
}
-const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS + 1)) - 1) as f64;
+pub const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS + 1)) - 1) as f64;
+pub const MIN_SAFE_INTEGER: f64 = -MAX_SAFE_INTEGER;
macro_rules! impl_int {
($($ty:ty)*) => {$(
@@ -131,6 +132,7 @@
<Self as Typed>::TYPE.check(&value)?;
match value {
Val::Num(n) => {
+ let n = n.get();
#[allow(clippy::float_cmp)]
if n.trunc() != n {
bail!(
@@ -143,9 +145,8 @@
_ => unreachable!(),
}
}
- #[allow(clippy::cast_lossless)]
fn into_untyped(value: Self) -> Result<Val> {
- Ok(Val::Num(value as f64))
+ Ok(Val::Num(value.into()))
}
}
)*};
@@ -187,6 +188,7 @@
<Self as Typed>::TYPE.check(&value)?;
match value {
Val::Num(n) => {
+ let n = n.get();
#[allow(clippy::float_cmp)]
if n.trunc() != n {
bail!(
@@ -202,7 +204,7 @@
#[allow(clippy::cast_lossless)]
fn into_untyped(value: Self) -> Result<Val> {
- Ok(Val::Num(value.0 as f64))
+ Ok(Val::try_num(value.0)?)
}
}
)*};
@@ -220,13 +222,13 @@
const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);
fn into_untyped(value: Self) -> Result<Val> {
- Ok(Val::Num(value))
+ Ok(Val::try_num(value)?)
}
fn from_untyped(value: Val) -> Result<Self> {
<Self as Typed>::TYPE.check(&value)?;
match value {
- Val::Num(n) => Ok(n),
+ Val::Num(n) => Ok(n.get()),
_ => unreachable!(),
}
}
@@ -237,13 +239,13 @@
const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(0.0), None);
fn into_untyped(value: Self) -> Result<Val> {
- Ok(Val::Num(value.0))
+ Ok(Val::try_num(value.0)?)
}
fn from_untyped(value: Val) -> Result<Self> {
<Self as Typed>::TYPE.check(&value)?;
match value {
- Val::Num(n) => Ok(Self(n)),
+ Val::Num(n) => Ok(Self(n.get())),
_ => unreachable!(),
}
}
@@ -253,16 +255,14 @@
&ComplexValType::BoundedNumber(Some(0.0), Some(MAX_SAFE_INTEGER));
fn into_untyped(value: Self) -> Result<Val> {
- if value > MAX_SAFE_INTEGER as Self {
- bail!("number is too large")
- }
- Ok(Val::Num(value as f64))
+ Ok(Val::try_num(value)?)
}
fn from_untyped(value: Val) -> Result<Self> {
<Self as Typed>::TYPE.check(&value)?;
match value {
Val::Num(n) => {
+ let n = n.get();
#[allow(clippy::float_cmp)]
if n.trunc() != n {
bail!("cannot convert number with fractional part to usize")
@@ -479,7 +479,7 @@
const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(-1.0), Some(-1.0));
fn into_untyped(_: Self) -> Result<Val> {
- Ok(Val::Num(-1.0))
+ Ok(Val::Num(NumValue::new(-1.0).expect("finite")))
}
fn from_untyped(value: Val) -> Result<Self> {
@@ -679,3 +679,19 @@
))
}
}
+
+impl Typed for NumValue {
+ const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);
+
+ fn into_untyped(typed: Self) -> Result<Val> {
+ Ok(Val::Num(typed))
+ }
+
+ fn from_untyped(untyped: Val) -> Result<Self> {
+ Self::TYPE.check(&untyped)?;
+ match untyped {
+ Val::Num(v) => Ok(v),
+ _ => unreachable!(),
+ }
+ }
+}
crates/jrsonnet-evaluator/src/typed/mod.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/typed/mod.rs
+++ b/crates/jrsonnet-evaluator/src/typed/mod.rs
@@ -1,6 +1,6 @@
use std::{fmt::Display, rc::Rc};
-mod conversions;
+pub(crate) mod conversions;
pub use conversions::*;
use jrsonnet_gcmodule::Trace;
pub use jrsonnet_types::{ComplexValType, ValType};
@@ -155,10 +155,11 @@
},
Self::BoundedNumber(from, to) => {
if let Val::Num(n) = value {
- if from.map(|from| from > *n).unwrap_or(false)
- || to.map(|to| to < *n).unwrap_or(false)
+ let n = n.get();
+ if from.map(|from| from > n).unwrap_or(false)
+ || to.map(|to| to < n).unwrap_or(false)
{
- return Err(TypeError::BoundsFailed(*n, *from, *to).into());
+ return Err(TypeError::BoundsFailed(n, *from, *to).into());
}
Ok(())
} else {
crates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -1,14 +1,18 @@
use std::{
cell::RefCell,
+ cmp::Ordering,
fmt::{self, Debug, Display},
mem::replace,
num::NonZeroU32,
+ ops::Deref,
rc::Rc,
};
+use derivative::Derivative;
use jrsonnet_gcmodule::{Cc, Trace};
use jrsonnet_interner::IStr;
use jrsonnet_types::ValType;
+use thiserror::Error;
pub use crate::arr::{ArrValue, ArrayLike};
use crate::{
@@ -379,18 +383,127 @@
}
impl Eq for StrValue {}
impl PartialOrd for StrValue {
- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StrValue {
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ fn cmp(&self, other: &Self) -> Ordering {
let a = self.clone().into_flat();
let b = other.clone().into_flat();
a.cmp(&b)
}
}
+/// Represents jsonnet number
+/// Jsonnet numbers are finite f64, with NaNs disallowed
+#[derive(Trace, Clone, Copy, Derivative)]
+#[derivative(Debug = "transparent")]
+#[repr(transparent)]
+pub struct NumValue(f64);
+impl NumValue {
+ /// Creates a [`NumValue`], if value is finite and not NaN
+ pub fn new(v: f64) -> Option<Self> {
+ if !v.is_finite() {
+ return None;
+ }
+ Some(Self(v))
+ }
+ pub const fn get(&self) -> f64 {
+ self.0
+ }
+}
+impl PartialEq for NumValue {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
+impl Eq for NumValue {}
+impl Ord for NumValue {
+ fn cmp(&self, other: &Self) -> Ordering {
+ // Can't use `total_cmp`: its behavior for `-0` and `0`
+ // is not following wanted.
+ self.0.partial_cmp(&other.0).expect("NaNs are disallowed")
+ }
+}
+impl PartialOrd for NumValue {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+impl Display for NumValue {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ Display::fmt(&self.0, f)
+ }
+}
+impl Deref for NumValue {
+ type Target = f64;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+macro_rules! impl_num {
+ ($($ty:ty),+) => {$(
+ impl From<$ty> for NumValue {
+ fn from(value: $ty) -> Self {
+ Self(value.into())
+ }
+ }
+ )+};
+}
+impl_num!(i8, u8, i16, u16, i32, u32);
+
+#[derive(Clone, Copy, Debug, Error, Trace)]
+pub enum ConvertNumValueError {
+ #[error("overflow")]
+ Overflow,
+ #[error("underflow")]
+ Underflow,
+ #[error("non-finite")]
+ NonFinite,
+}
+impl From<ConvertNumValueError> for Error {
+ fn from(e: ConvertNumValueError) -> Self {
+ Self::new(e.into())
+ }
+}
+
+macro_rules! impl_try_num {
+ ($($ty:ty),+) => {$(
+ impl TryFrom<$ty> for NumValue {
+ type Error = ConvertNumValueError;
+ fn try_from(value: $ty) -> Result<Self, ConvertNumValueError> {
+ use crate::typed::conversions::{MIN_SAFE_INTEGER, MAX_SAFE_INTEGER};
+ let value = value as f64;
+ if value < MIN_SAFE_INTEGER {
+ return Err(ConvertNumValueError::Underflow)
+ } else if value > MAX_SAFE_INTEGER {
+ return Err(ConvertNumValueError::Overflow)
+ }
+ // Number is finite.
+ Ok(Self(value))
+ }
+ }
+ )+};
+}
+impl_try_num!(usize, isize, i64, u64);
+
+impl TryFrom<f64> for NumValue {
+ type Error = ConvertNumValueError;
+
+ fn try_from(value: f64) -> Result<Self, Self::Error> {
+ Self::new(value).ok_or(ConvertNumValueError::NonFinite)
+ }
+}
+impl TryFrom<f32> for NumValue {
+ type Error = ConvertNumValueError;
+
+ fn try_from(value: f32) -> Result<Self, Self::Error> {
+ Self::new(f64::from(value)).ok_or(ConvertNumValueError::NonFinite)
+ }
+}
+
/// Represents any valid Jsonnet value.
#[derive(Debug, Clone, Trace, Default)]
pub enum Val {
@@ -404,7 +517,7 @@
/// Represents a Jsonnet number.
/// Should be finite, and not NaN
/// This restriction isn't enforced by enum, as enum field can't be marked as private
- Num(f64),
+ Num(NumValue),
/// Experimental bigint
#[cfg(feature = "exp-bigint")]
BigInt(#[trace(skip)] Box<num_bigint::BigInt>),
@@ -449,7 +562,7 @@
}
pub const fn as_num(&self) -> Option<f64> {
match self {
- Self::Num(n) => Some(*n),
+ Self::Num(n) => Some(n.get()),
_ => None,
}
}
@@ -472,16 +585,6 @@
}
}
- /// Creates `Val::Num` after checking for numeric overflow.
- /// As numbers are `f64`, we can just check for their finity.
- pub fn new_checked_num(num: f64) -> Result<Self> {
- if num.is_finite() {
- Ok(Self::Num(num))
- } else {
- bail!("overflow")
- }
- }
-
pub const fn value_type(&self) -> ValType {
match self {
Self::Str(..) => ValType::Str,
@@ -527,6 +630,15 @@
pub fn string(string: impl Into<StrValue>) -> Self {
Self::Str(string.into())
}
+ pub fn num(num: impl Into<NumValue>) -> Self {
+ Self::Num(num.into())
+ }
+ pub fn try_num<V, E>(num: V) -> Result<Self, E>
+ where
+ NumValue: TryFrom<V, Error = E>,
+ {
+ Ok(Self::Num(num.try_into()?))
+ }
}
impl From<IStr> for Val {
@@ -560,7 +672,7 @@
(Val::Bool(a), Val::Bool(b)) => a == b,
(Val::Null, Val::Null) => true,
(Val::Str(a), Val::Str(b)) => a == b,
- (Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,
+ (Val::Num(a), Val::Num(b)) => (a.get() - b.get()).abs() <= f64::EPSILON,
#[cfg(feature = "exp-bigint")]
(Val::BigInt(a), Val::BigInt(b)) => a == b,
(Val::Arr(_), Val::Arr(_)) => {
crates/jrsonnet-stdlib/src/arrays.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/arrays.rs
+++ b/crates/jrsonnet-stdlib/src/arrays.rs
@@ -275,7 +275,7 @@
if arr.is_empty() {
return eval_on_empty(onEmpty);
}
- Ok(Val::Num(arr.iter().sum::<f64>() / (arr.len() as f64)))
+ Ok(Val::try_num(arr.iter().sum::<f64>() / (arr.len() as f64))?)
}
#[builtin]
crates/jrsonnet-stdlib/src/operator.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/operator.rs
+++ b/crates/jrsonnet-stdlib/src/operator.rs
@@ -6,12 +6,12 @@
operator::evaluate_mod_op,
stdlib::std_format,
typed::{Either, Either2},
- val::{equals, primitive_equals},
+ val::{equals, primitive_equals, NumValue},
IStr, Result, Val,
};
#[builtin]
-pub fn builtin_mod(a: Either![f64, IStr], b: Val) -> Result<Val> {
+pub fn builtin_mod(a: Either![NumValue, IStr], b: Val) -> Result<Val> {
use Either2::*;
evaluate_mod_op(
&match a {
crates/jrsonnet-stdlib/src/sort.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/sort.rs
+++ b/crates/jrsonnet-stdlib/src/sort.rs
@@ -20,20 +20,6 @@
Unknown,
}
-#[derive(PartialEq)]
-struct NonNaNf64(f64);
-impl PartialOrd for NonNaNf64 {
- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.cmp(other))
- }
-}
-impl Eq for NonNaNf64 {}
-impl Ord for NonNaNf64 {
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
- self.0.partial_cmp(&other.0).expect("non nan")
- }
-}
-
fn get_sort_type<T>(values: &[T], key_getter: impl Fn(&T) -> &Val) -> Result<SortKeyType> {
let mut sort_type = SortKeyType::Unknown;
for i in values {
@@ -56,7 +42,7 @@
let sort_type = get_sort_type(&values, |k| k)?;
match sort_type {
SortKeyType::Number => values.sort_unstable_by_key(|v| match v {
- Val::Num(n) => NonNaNf64(*n),
+ Val::Num(n) => *n,
_ => unreachable!(),
}),
SortKeyType::String => values.sort_unstable_by_key(|v| match v {
@@ -95,7 +81,7 @@
let sort_type = get_sort_type(&vk, |v| &v.1)?;
match sort_type {
SortKeyType::Number => vk.sort_by_key(|v| match v.1 {
- Val::Num(n) => NonNaNf64(n),
+ Val::Num(n) => n,
_ => unreachable!(),
}),
SortKeyType::String => vk.sort_by_key(|v| match &v.1 {
crates/jrsonnet-stdlib/src/strings.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/strings.rs
+++ b/crates/jrsonnet-stdlib/src/strings.rs
@@ -116,7 +116,9 @@
.enumerate()
{
if &strb[i..i + pat.len()] == pat {
- out.push(Val::Num(ch_idx as f64));
+ out.push(Val::Num(
+ ch_idx.try_into().expect("unrealisticly long string"),
+ ));
}
}
out.into()