diff --git a/src/eval/array.rs b/src/eval/array.rs index 5cd8df821..2da1a5f47 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -66,6 +66,11 @@ impl Array { Arc::make_mut(&mut self.0).push(value); } + /// Whether the array contains a specific value. + pub fn contains(&self, value: &Value) -> bool { + self.0.contains(value) + } + /// Clear the array. pub fn clear(&mut self) { if Arc::strong_count(&self.0) == 1 { diff --git a/src/eval/dict.rs b/src/eval/dict.rs index 03871fa08..9127b2eb9 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -61,6 +61,11 @@ impl Dict { Arc::make_mut(&mut self.0).insert(key, value); } + /// Whether the dictionary contains a specific key. + pub fn contains_key(&self, key: &str) -> bool { + self.0.contains_key(key) + } + /// Clear the dictionary. pub fn clear(&mut self) { if Arc::strong_count(&self.0) == 1 { diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 4ccf377b2..2c864036d 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -344,6 +344,8 @@ impl Eval for BinaryExpr { BinOp::Leq => self.apply(ctx, scp, ops::leq), BinOp::Gt => self.apply(ctx, scp, ops::gt), BinOp::Geq => self.apply(ctx, scp, ops::geq), + BinOp::In => self.apply(ctx, scp, ops::in_), + BinOp::NotIn => self.apply(ctx, scp, ops::not_in), BinOp::Assign => self.assign(ctx, scp, |_, b| Ok(b)), BinOp::AddAssign => self.assign(ctx, scp, ops::add), BinOp::SubAssign => self.assign(ctx, scp, ops::sub), diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 04a13fd11..6a8f5284b 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -335,3 +335,31 @@ pub fn compare(lhs: &Value, rhs: &Value) -> Option { _ => Option::None, } } + +/// Test whether one value is "in" another one. +pub fn in_(lhs: Value, rhs: Value) -> StrResult { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Bool(b)) + } else { + mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) + } +} + +/// Test whether one value is "not in" another one. +pub fn not_in(lhs: Value, rhs: Value) -> StrResult { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Bool(!b)) + } else { + mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) + } +} + +/// Test for containment. +pub fn contains(lhs: &Value, rhs: &Value) -> Option { + Some(match (lhs, rhs) { + (Value::Str(a), Value::Str(b)) => b.contains(a.as_str()), + (Value::Str(a), Value::Dict(b)) => b.contains_key(a), + (a, Value::Array(b)) => b.contains(a), + _ => return Option::None, + }) +} diff --git a/src/library/mod.rs b/src/library/mod.rs index 505f1a1d1..087ff7eaa 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -79,7 +79,7 @@ pub fn new() -> Scope { std.def_fn("max", utility::max); std.def_fn("even", utility::even); std.def_fn("odd", utility::odd); - std.def_fn("mod", utility::modulo); + std.def_fn("mod", utility::mod_); std.def_fn("range", utility::range); std.def_fn("rgb", utility::rgb); std.def_fn("cmyk", utility::cmyk); diff --git a/src/library/utility/math.rs b/src/library/utility/math.rs index 9389b4b95..e48af4268 100644 --- a/src/library/utility/math.rs +++ b/src/library/utility/math.rs @@ -59,7 +59,7 @@ pub fn odd(_: &mut Context, args: &mut Args) -> TypResult { } /// The modulo of two numbers. -pub fn modulo(_: &mut Context, args: &mut Args) -> TypResult { +pub fn mod_(_: &mut Context, args: &mut Args) -> TypResult { let Spanned { v: v1, span: span1 } = args.expect("integer or float")?; let Spanned { v: v2, span: span2 } = args.expect("integer or float")?; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index f07fefce3..5eaba8b04 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -376,9 +376,18 @@ fn expr_prec(p: &mut Parser, atomic: bool, min_prec: usize) -> ParseResult { with_expr(p, marker)?; } - let op = match p.peek().and_then(BinOp::from_token) { - Some(binop) => binop, - None => break, + let op = if p.eat_if(&NodeKind::Not) { + if p.at(&NodeKind::In) { + BinOp::NotIn + } else { + p.expected("keyword `in`"); + return Err(ParseError); + } + } else { + match p.peek().and_then(BinOp::from_token) { + Some(binop) => binop, + None => break, + } }; let mut prec = op.precedence(); diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 928680e57..b87805908 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -551,9 +551,17 @@ node! { impl BinaryExpr { /// The binary operator: `+`. pub fn op(&self) -> BinOp { + let mut not = false; self.0 .children() - .find_map(|node| BinOp::from_token(node.kind())) + .find_map(|node| match node.kind() { + NodeKind::Not => { + not = true; + None + } + NodeKind::In if not => Some(BinOp::NotIn), + _ => BinOp::from_token(node.kind()), + }) .expect("binary expression is missing operator") } @@ -601,6 +609,10 @@ pub enum BinOp { Geq, /// The assignment operator: `=`. Assign, + /// The containment operator: `in`. + In, + /// The inversed containment operator: `not in`. + NotIn, /// The add-assign operator: `+=`. AddAssign, /// The subtract-assign oeprator: `-=`. @@ -628,6 +640,7 @@ impl BinOp { NodeKind::Gt => Self::Gt, NodeKind::GtEq => Self::Geq, NodeKind::Eq => Self::Assign, + NodeKind::In => Self::In, NodeKind::PlusEq => Self::AddAssign, NodeKind::HyphEq => Self::SubAssign, NodeKind::StarEq => Self::MulAssign, @@ -649,6 +662,8 @@ impl BinOp { Self::Leq => 4, Self::Gt => 4, Self::Geq => 4, + Self::In => 4, + Self::NotIn => 4, Self::And => 3, Self::Or => 2, Self::Assign => 1, @@ -674,6 +689,8 @@ impl BinOp { Self::Leq => Associativity::Left, Self::Gt => Associativity::Left, Self::Geq => Associativity::Left, + Self::In => Associativity::Left, + Self::NotIn => Associativity::Left, Self::Assign => Associativity::Right, Self::AddAssign => Associativity::Right, Self::SubAssign => Associativity::Right, @@ -697,6 +714,8 @@ impl BinOp { Self::Leq => "<=", Self::Gt => ">", Self::Geq => ">=", + Self::In => "in", + Self::NotIn => "not in", Self::Assign => "=", Self::AddAssign => "+=", Self::SubAssign => "-=", diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ index b81fc8412..899ee71c1 100644 --- a/tests/typ/code/ops.typ +++ b/tests/typ/code/ops.typ @@ -165,7 +165,26 @@ { x += "thing" } #test(x, "something") --- -// Test with operator. +// Test `in` operator. +#test("hi" in "worship", true) +#test("hi" in ("we", "hi", "bye"), true) +#test("Hey" in "abHeyCd", true) +#test("Hey" in "abheyCd", false) +#test(5 in range(10), true) +#test(12 in range(10), false) +#test("" in (), false) +#test("key" in (key: "value"), true) +#test("value" in (key: "value"), false) +#test("Hey" not in "abheyCd", true) +#test("a" not +/* fun comment? */ in "abc", false) + +--- +// Error: 9 expected keyword `in` +{"a" not} + +--- +// Test `with` operator. // Apply positional arguments. #let add(x, y) = x + y