Skip to content

Commit

Permalink
Improved calculation checking.
Browse files Browse the repository at this point in the history
  • Loading branch information
kaj committed Sep 30, 2023
1 parent 50b20ae commit ecd38b5
Show file tree
Hide file tree
Showing 15 changed files with 117 additions and 51 deletions.
6 changes: 2 additions & 4 deletions rsass/src/css/binop.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{is_function_name, InvalidCss, Value};
use crate::output::{Format, Formatted};
use crate::value::{CssDimension, Numeric, Operator};
use crate::value::{CssDimensionSet, Numeric, Operator};
use std::fmt::{self, Display, Write};

/// A binary operation.
Expand Down Expand Up @@ -59,9 +59,7 @@ impl BinOp {
if self.a.is_calculation() || self.b.is_calculation() {
Err(InvalidCss::UndefOp(self.into()))
} else {
fn cmp_dim(
x: &Numeric,
) -> Option<Vec<(CssDimension, i8)>> {
fn cmp_dim(x: &Numeric) -> Option<CssDimensionSet> {
let u = &x.unit;
if u.is_known() && !u.is_percent() {
Some(u.css_dimension())
Expand Down
3 changes: 3 additions & 0 deletions rsass/src/parser/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,9 @@ fn literal_or_color(s: SassString) -> Value {
if val == "infinity" {
return Value::scalar(f64::INFINITY);
}
if val == "NaN" {
return Value::scalar(f64::NAN);
}
if let Some(rgba) = Rgba::from_name(val) {
return Value::Color(rgba, Some(val.to_string()));
}
Expand Down
12 changes: 11 additions & 1 deletion rsass/src/sass/functions/call_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::super::{ArgsError, Name};
use crate::css::BadSelector;
use crate::css::{BadSelector, Value};
use crate::input::SourcePos;
use crate::output::Format;
use crate::{Error, Invalid, ScopeError};
use std::fmt;

Expand All @@ -26,6 +27,15 @@ impl CallError {
CallError::Invalid(Invalid::AtError(msg.to_string()))
}

/// The values were expected to be compatible, but wasn't.
pub fn incompatible_values(a: &Value, b: &Value) -> Self {
Self::msg(format!(
"{} and {} are incompatible.",
a.format(Format::introspect()),
b.format(Format::introspect()),
))
}

/// Map this error to a [`crate::Error`].
pub fn called_from(self, call_pos: &SourcePos, name: &str) -> Error {
match self {
Expand Down
42 changes: 34 additions & 8 deletions rsass/src/sass/functions/math.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{
check, expected_to, is_not, is_special, CallError, CheckedArg,
check, css_dim, expected_to, is_not, is_special, CallError, CheckedArg,
FunctionMap, ResolvedArgs, Scope,
};
use crate::css::{BinOp, CallArgs, CssString, Value};
Expand Down Expand Up @@ -323,16 +323,21 @@ pub fn expose(m: &Scope, global: &mut FunctionMap) {
def!(global, mod(y, x), |s| {
fn real_mod(s: &ResolvedArgs) -> Result<Value, CallError> {
let y: Numeric = s.get(name!(y))?;
let x = s.get_map(name!(x), |v| {
let v = Numeric::try_from(v)?;
v.as_unitset(&y.unit)
.ok_or_else(|| diff_units_msg(&v, &y, name!(y)))
})?;
let x: Numeric = s.get(name!(x))?;
let x = x.as_unitset(&y.unit)
.ok_or_else(|| CallError::msg(diff_units_msg2(&y, &x)))?;
let unit = y.unit;
let y = f64::from(y.value);
let m = f64::from(x);
let mut y = y.rem(m);
if dbg!(dbg!(dbg!(y) * m.signum()) < 0.) { y += m; }
if (y * m.signum()).is_sign_negative() {
if m.is_finite() {
y += m;
y = y.rem(m);
} else {
y = f64::NAN;
}
}
let y = y.abs() * m.signum();
Ok(number(y, unit))
}
Expand Down Expand Up @@ -419,7 +424,15 @@ fn fallback2a(
a2: Name,
) -> Result<Value, ()> {
let (a1, a2) = match (get_expr_a(s, a1), get_expr_a(s, a2)) {
(Ok(a1), Ok(a2)) => (a1, a2),
(Ok(a1), Ok(a2)) => {
let dim1 = css_dim(&a1);
let dim2 = css_dim(&a2);
if (dim1 == dim2) || dim1.is_none() || dim2.is_none() {
(a1, a2)
} else {
return Err(());
}
}
_ => return Err(()),
};
Ok(Value::Call(name.into(), CallArgs::from_list(vec![a1, a2])))
Expand Down Expand Up @@ -578,6 +591,19 @@ fn diff_units_msg(
)
}

fn diff_units_msg2(one: &Numeric, other: &Numeric) -> String {
format!(
"{} and {} are incompatible{}.",
one.format(Format::introspect()),
other.format(Format::introspect()),
if one.is_no_unit() || other.is_no_unit() {
" (one has units and the other doesn't)"
} else {
""
}
)
}

pub(crate) fn clamp_fn(s: &ResolvedArgs) -> Result<Value, CallError> {
let min_v = s.get::<Numeric>(name!(min))?;
let check_numeric_compat_unit = |v: Value| -> Result<Numeric, String> {
Expand Down
24 changes: 20 additions & 4 deletions rsass/src/sass/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::{Call, Closure, FormalArgs, Name};
use crate::css::{self, is_not, BinOp, CallArgs, CssString, Value};
use crate::input::SourcePos;
use crate::output::{Format, Formatted};
use crate::value::{CssDimension, Operator, Quotes};
use crate::value::{CssDimensionSet, Operator, Quotes};
use crate::{Scope, ScopeRef};
use lazy_static::lazy_static;
use std::collections::BTreeMap;
Expand Down Expand Up @@ -289,7 +289,10 @@ lazy_static! {
} else {
let arg = match css_fn_arg(v)? {
Value::Paren(arg)
if !matches!(arg.as_ref(), Value::Call(..)) =>
if !matches!(
arg.as_ref(),
Value::Call(..) | Value::Literal(_)
) =>
{
*arg
}
Expand All @@ -300,13 +303,26 @@ lazy_static! {
});
def!(f, clamp(min, number = b"null", max = b"null"), |s| {
self::math::clamp_fn(s).or_else(|_| {
let mut args = vec![s.get(name!(min))?];
let mut args = vec![s.get::<Value>(name!(min))?];
if let Some(b) = s.get_opt(name!(number))? {
args.push(b);
}
if let Some(c) = s.get_opt(name!(max))? {
args.push(c);
}
if let Some((a, rest)) = args.split_first() {
if let Some(adim) = css_dim(a) {
for b in rest {
if let Some(bdim) = css_dim(b) {
if adim != bdim {
return Err(
CallError::incompatible_values(a, b),
);
}
}
}
}
}
Ok(css::Value::Call(
"clamp".into(),
css::CallArgs::from_list(args),
Expand Down Expand Up @@ -368,7 +384,7 @@ fn css_fn_arg(v: Value) -> Result<Value, CallError> {
}

// Note: None here is for unknown, e.g. the dimension of something that is not a number.
fn css_dim(v: &Value) -> Option<Vec<(CssDimension, i8)>> {
fn css_dim(v: &Value) -> Option<CssDimensionSet> {
match v {
// TODO: Handle BinOp recursively (again) (or let in_calc return (Value, CssDimension)?)
Value::Numeric(num, _) => {
Expand Down
2 changes: 1 addition & 1 deletion rsass/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ pub use self::numeric::Numeric;
pub use self::operator::{BadOp, Operator};
pub use self::quotes::Quotes;
pub use self::unit::{CssDimension, Dimension, Unit};
pub use self::unitset::UnitSet;
pub use self::unitset::{CssDimensionSet, UnitSet};
pub(crate) use range::{RangeError, ValueRange};
44 changes: 35 additions & 9 deletions rsass/src/value/unitset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ impl UnitSet {
.collect::<Vec<_>>()
}

pub(crate) fn css_dimension(&self) -> Vec<(CssDimension, i8)> {
pub(crate) fn css_dimension(&self) -> CssDimensionSet {
use std::collections::BTreeMap;
self.units
let dim = self
.units
.iter()
.fold(BTreeMap::new(), |mut map, (unit, power)| {
let dim = CssDimension::from(unit.dimension());
Expand All @@ -71,15 +72,12 @@ impl UnitSet {
})
.into_iter()
.filter(|(_d, power)| *power != 0)
.collect::<Vec<_>>()
.collect::<Vec<_>>();
CssDimensionSet { dim }
}

pub(crate) fn valid_in_css(&self) -> bool {
let dim = self.css_dimension();
match &dim[..] {
[] => true,
[(_d, p)] => *p == 1,
_ => false,
}
self.css_dimension().valid_in_css()
}

/// Get a scaling factor to convert this unit to another unit.
Expand Down Expand Up @@ -250,3 +248,31 @@ impl fmt::Debug for UnitSet {
out.debug_list().entries(&self.units).finish()
}
}

/// The dimension of a numeric value.
///
/// May be e.g. lenght, or something complex like length^17*angle*time^-3.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct CssDimensionSet {
dim: Vec<(CssDimension, i8)>,
}
impl CssDimensionSet {
/// Return true for the empty dimension, i.e. the dimension of a unitless number.
pub fn is_empty(&self) -> bool {
self.dim.is_empty()
}
/// Return true if any unknown unit is part of the dimension.
pub fn is_unknown(&self) -> bool {
self.dim
.iter()
.any(|(dim, _)| matches!(dim, CssDimension::Unknown(_)))
}

pub(crate) fn valid_in_css(&self) -> bool {
match &self.dim[..] {
[] => true,
[(_d, p)] => *p == 1,
_ => false,
}
}
}
4 changes: 2 additions & 2 deletions rsass/tests/spec/values/calculation/atan2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ mod error {
);
}
#[test]
#[ignore] // missing error
#[ignore] // wrong error
fn known_incompatible() {
assert_eq!(
runner().err("a {b: atan2(1deg, 1px)}\n"),
Expand All @@ -109,7 +109,7 @@ mod error {
);
}
#[test]
#[ignore] // missing error
#[ignore] // wrong error
fn unitless_and_real() {
assert_eq!(
runner().err("a {b: atan2(1, 1px)}\n"),
Expand Down
2 changes: 1 addition & 1 deletion rsass/tests/spec/values/calculation/calc/no_operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ mod interpolation {
use super::runner;

#[test]
#[ignore] // wrong result
fn nested() {
assert_eq!(
runner().ok("a {b: calc(calc(#{c}))}\n"),
Expand All @@ -213,7 +214,6 @@ mod interpolation {
);
}
#[test]
#[ignore] // wrong result
fn parens() {
assert_eq!(
runner().ok(
Expand Down
5 changes: 1 addition & 4 deletions rsass/tests/spec/values/calculation/calc/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ mod precedence {
use super::runner;

#[test]
#[ignore] // wrong result
fn asterisk() {
assert_eq!(
runner().ok("a {b: calc(calc(#{\"c*\"}))}\n"),
Expand All @@ -188,6 +187,7 @@ mod precedence {
);
}
#[test]
#[ignore] // wrong result
fn plain() {
assert_eq!(
runner().ok("a {b: calc(calc(#{c}))}\n"),
Expand All @@ -197,7 +197,6 @@ mod precedence {
);
}
#[test]
#[ignore] // wrong result
fn slash() {
assert_eq!(
runner().ok("a {b: calc(calc(#{\"c/\"}))}\n"),
Expand All @@ -207,7 +206,6 @@ mod precedence {
);
}
#[test]
#[ignore] // wrong result
fn whitespace() {
assert_eq!(
runner().ok("a {b: calc(calc(#{\"c \"}))}\n"),
Expand All @@ -218,7 +216,6 @@ mod precedence {
}
}
#[test]
#[ignore] // wrong result
fn parens() {
assert_eq!(
runner().ok("a {b: calc((#{c}))}\n"),
Expand Down
3 changes: 0 additions & 3 deletions rsass/tests/spec/values/calculation/calc/parens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ fn double_preserved() {
);
}
#[test]
#[ignore] // wrong result
fn identifier() {
assert_eq!(
runner().ok("a {b: calc((d))}\n"),
Expand All @@ -35,7 +34,6 @@ fn identifier() {
);
}
#[test]
#[ignore] // wrong result
fn interpolation() {
assert_eq!(
runner().ok("a {b: calc((#{\"1 + 2\"}))}\n"),
Expand Down Expand Up @@ -87,7 +85,6 @@ mod var {
}
}
#[test]
#[ignore] // wrong result
fn variable() {
assert_eq!(
runner().ok("$c: unquote(\"1 + 2\");\
Expand Down
6 changes: 3 additions & 3 deletions rsass/tests/spec/values/calculation/clamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ mod error {
use super::runner;

#[test]
#[ignore] // missing error
#[ignore] // wrong error
fn first() {
assert_eq!(
runner().err("a {b: clamp(1s, 2px, 3px)}\n"),
Expand All @@ -50,7 +50,7 @@ mod error {
);
}
#[test]
#[ignore] // missing error
#[ignore] // wrong error
fn second() {
assert_eq!(
runner().err("a {b: clamp(1px, 2s, 3px)}\n"),
Expand All @@ -64,7 +64,7 @@ mod error {
);
}
#[test]
#[ignore] // missing error
#[ignore] // wrong error
fn third() {
assert_eq!(
runner().err("a {b: clamp(1px, 2px, 3s)}\n"),
Expand Down
4 changes: 2 additions & 2 deletions rsass/tests/spec/values/calculation/rem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ mod error {
);
}
#[test]
#[ignore] // missing error
#[ignore] // wrong error
fn incompatible() {
assert_eq!(
runner().err("a {b: rem(16px, 5deg)}\n"),
Expand All @@ -144,7 +144,7 @@ mod error {
);
}
#[test]
#[ignore] // missing error
#[ignore] // wrong error
fn real_and_unitless() {
assert_eq!(
runner().err("a {b: rem(16px, 5)}\n"),
Expand Down
Loading

0 comments on commit ecd38b5

Please sign in to comment.