Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for the ..= RangeInclusive syntax #6972

Merged
6 changes: 3 additions & 3 deletions corelib/src/ops.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub mod index;
pub use index::{Index, IndexView};

mod range;
// `RangeOp` is used internally by the compiler.
pub use range::{Range, RangeInclusive, RangeInclusiveIterator, RangeIterator, RangeTrait};
// `RangeOp` and `RangeInclusiveOp` are used internally by the compiler.
#[allow(unused_imports)]
use range::RangeOp;
pub use range::{Range, RangeIterator, RangeTrait};
use range::{RangeInclusiveOp, RangeOp};
82 changes: 82 additions & 0 deletions corelib/src/ops/range.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,88 @@ impl RangeIntoIterator<
}
}

/// Represents the range [start, end].
#[derive(Clone, Drop, PartialEq)]
pub struct RangeInclusive<T> {
/// The lower bound of the range (inclusive).
pub start: T,
/// The upper bound of the range (inclusive).
pub end: T,
}

#[derive(Clone, Drop)]
pub struct RangeInclusiveIterator<T> {
/// The current value of the iterator.
pub(crate) cur: T,
/// The upper bound of the range (inclusive).
pub(crate) end: T,
// This field is:
// - `false` upon construction
// - `false` when iteration has yielded an element and the iterator is not exhausted
// - `true` when iteration has been used to exhaust the iterator
//
// This is required to differentiate between the last element and the end of the range.
pub(crate) exhausted: bool,
}

/// Handles the range inclusive operator (`..=`).
#[generate_trait]
pub impl RangeInclusiveOpImpl<T> of RangeInclusiveOp<T> {
/// Handles the `..=` operator. Returns the value of the expression `start..=end`.
fn range_inclusive(start: T, end: T) -> RangeInclusive<T> {
RangeInclusive { start, end }
}
}

impl RangeInclusiveDebug<
T, impl TDebug: crate::fmt::Debug<T>,
> of crate::fmt::Debug<RangeInclusive<T>> {
fn fmt(
self: @RangeInclusive<T>, ref f: crate::fmt::Formatter,
) -> Result<(), crate::fmt::Error> {
self.start.fmt(ref f)?;
write!(f, "..=")?;
self.end.fmt(ref f)?;
Result::Ok(())
}
}

impl RangeInclusiveIteratorImpl<
T, +One<T>, +Add<T>, +Copy<T>, +Drop<T>, +PartialEq<T>, +PartialOrd<T>,
> of Iterator<RangeInclusiveIterator<T>> {
type Item = T;

fn next(ref self: RangeInclusiveIterator<T>) -> Option<T> {
if self.exhausted {
return Option::None;
}

let current = self.cur;

// If this is the last element, mark as exhausted for next iteration
if current == self.end {
self.exhausted = true;
return Option::Some(current);
}

// We know current < self.end here, because the iterator is not exhausted
self.cur = current + One::one();
Option::Some(current)
}
}

pub impl RangeInclusiveIntoIterator<
T, +One<T>, +Add<T>, +Copy<T>, +Drop<T>, +PartialEq<T>, +PartialOrd<T>,
> of IntoIterator<RangeInclusive<T>> {
type IntoIter = RangeInclusiveIterator<T>;

fn into_iter(self: RangeInclusive<T>) -> Self::IntoIter {
let exhausted = self.start > self.end;
Self::IntoIter { cur: self.start, end: self.end, exhausted }
}
}


// Sierra optimization.

mod internal {
Expand Down
26 changes: 26 additions & 0 deletions corelib/src/test/range_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,29 @@ fn test_range_contains() {
fn test_range_format() {
assert!(format!("{:?}", 1..5) == "1..5");
}

#[test]
fn test_range_inclusive_iterator() {
let mut iter = (1_usize..=3).into_iter();
assert!(iter.next() == Option::Some(1));
assert!(iter.next() == Option::Some(2));
assert!(iter.next() == Option::Some(3));
assert!(iter.next() == Option::None);
}

#[test]
fn test_range_inclusive_iterator_range_end() {
let mut iter = (253_u8..=255).into_iter();
assert!(iter.next() == Option::Some(253));
assert!(iter.next() == Option::Some(254));
assert!(iter.next() == Option::Some(255));
assert!(iter.next() == Option::None);
}

#[test]
fn test_range_inclusive_empty_ranges() {
let mut iter = (255_u8..=125).into_iter();
assert!(iter.next() == Option::None);
let mut iter = (255_u8..=0).into_iter();
assert!(iter.next() == Option::None);
}
6 changes: 3 additions & 3 deletions crates/cairo-lang-formatter/src/node_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ impl SyntaxNodeFormat for SyntaxNode {
| SyntaxKind::TokenLParen
| SyntaxKind::TokenLBrack
| SyntaxKind::TokenImplicits => true,
SyntaxKind::TerminalDotDot
SyntaxKind::TerminalDotDot | SyntaxKind::TerminalDotDotEq
if matches!(parent_kind(db, self), Some(SyntaxKind::ExprBinary)) =>
{
true
Expand Down Expand Up @@ -171,7 +171,7 @@ impl SyntaxNodeFormat for SyntaxNode {
{
true
}
SyntaxKind::TokenDotDot
SyntaxKind::TokenDotDot | SyntaxKind::TokenDotDotEq
if grandparent_kind(db, self) == Some(SyntaxKind::StructArgTail) =>
{
true
Expand Down Expand Up @@ -761,7 +761,7 @@ impl SyntaxNodeFormat for SyntaxNode {
true,
))
}
SyntaxKind::TerminalDotDot
SyntaxKind::TerminalDotDot | SyntaxKind::TerminalDotDotEq
if matches!(parent_kind(db, self), Some(SyntaxKind::ExprBinary)) =>
{
BreakLinePointsPositions::Leading(BreakLinePointProperties::new(
Expand Down
1 change: 1 addition & 0 deletions crates/cairo-lang-parser/src/colored_printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn set_color(text: SmolStr, kind: SyntaxKind) -> ColoredString {
| SyntaxKind::TokenColon
| SyntaxKind::TokenColonColon
| SyntaxKind::TokenDotDot
| SyntaxKind::TokenDotDotEq
| SyntaxKind::TokenSemicolon
| SyntaxKind::TokenAnd
| SyntaxKind::TokenAndAnd
Expand Down
10 changes: 9 additions & 1 deletion crates/cairo-lang-parser/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,13 @@ impl<'a> Lexer<'a> {
']' => self.take_token_of_kind(TokenKind::RBrack),
'(' => self.take_token_of_kind(TokenKind::LParen),
')' => self.take_token_of_kind(TokenKind::RParen),
'.' => self.pick_kind('.', TokenKind::DotDot, TokenKind::Dot),
'.' => {
self.take();
match self.peek() {
Some('.') => self.pick_kind('=', TokenKind::DotDotEq, TokenKind::DotDot),
_ => TokenKind::Dot,
}
}
'*' => self.pick_kind('=', TokenKind::MulEq, TokenKind::Mul),
'/' => self.pick_kind('=', TokenKind::DivEq, TokenKind::Div),
'%' => self.pick_kind('=', TokenKind::ModEq, TokenKind::Mod),
Expand Down Expand Up @@ -422,6 +428,7 @@ enum TokenKind {
Comma,
Dot,
DotDot,
DotDotEq,
Eq,
Hash,
Semicolon,
Expand Down Expand Up @@ -503,6 +510,7 @@ fn token_kind_to_terminal_syntax_kind(kind: TokenKind) -> SyntaxKind {
TokenKind::Comma => SyntaxKind::TerminalComma,
TokenKind::Dot => SyntaxKind::TerminalDot,
TokenKind::DotDot => SyntaxKind::TerminalDotDot,
TokenKind::DotDotEq => SyntaxKind::TerminalDotDotEq,
TokenKind::Eq => SyntaxKind::TerminalEq,
TokenKind::Hash => SyntaxKind::TerminalHash,
TokenKind::Semicolon => SyntaxKind::TerminalSemicolon,
Expand Down
5 changes: 3 additions & 2 deletions crates/cairo-lang-parser/src/lexer_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fn terminal_kind_to_text(kind: SyntaxKind) -> Vec<&'static str> {
SyntaxKind::TerminalModEq => vec!["%="],
SyntaxKind::TerminalDot => vec!["."],
SyntaxKind::TerminalDotDot => vec![".."],
SyntaxKind::TerminalDotDotEq => vec!["..="],
SyntaxKind::TerminalEq => vec!["="],
SyntaxKind::TerminalEqEq => vec!["=="],
SyntaxKind::TerminalGE => vec![">="],
Expand Down Expand Up @@ -162,6 +163,7 @@ fn terminal_kinds() -> Vec<SyntaxKind> {
SyntaxKind::TerminalComma,
SyntaxKind::TerminalDot,
SyntaxKind::TerminalDotDot,
SyntaxKind::TerminalDotDotEq,
SyntaxKind::TerminalEq,
SyntaxKind::TerminalSemicolon,
SyntaxKind::TerminalQuestionMark,
Expand Down Expand Up @@ -218,7 +220,7 @@ fn need_separator(
|| (text0 == "." && text1.starts_with('.'))
|| (text0 == "-" && (text1.starts_with('>') || text1.starts_with('=')))
|| ((text0 == "+" || text0 == "*" || text0 == "/" || text0 == "%")
&& text1.starts_with('='))
|| (text0 == "..") && text1.starts_with('='))
|| (kind0 == SyntaxKind::TerminalLiteralNumber && kind0 == kind1)
{
return true;
Expand Down Expand Up @@ -295,7 +297,6 @@ fn test_lex_double_token() {
for separator in separators {
let text = format!("{text0}{separator}{text1}");
let mut lexer = Lexer::from_text(db, text.as_str());

let terminal = lexer.next().unwrap();
let token_text = terminal.text;
assert_eq!(
Expand Down
2 changes: 1 addition & 1 deletion crates/cairo-lang-parser/src/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub fn get_post_operator_precedence(kind: SyntaxKind) -> Option<usize> {
| SyntaxKind::TerminalGE => Some(7),
SyntaxKind::TerminalAndAnd => Some(8),
SyntaxKind::TerminalOrOr => Some(9),
SyntaxKind::TerminalDotDot => Some(10),
SyntaxKind::TerminalDotDot | SyntaxKind::TerminalDotDotEq => Some(10),
SyntaxKind::TerminalEq
| SyntaxKind::TerminalPlusEq
| SyntaxKind::TerminalMinusEq
Expand Down
1 change: 1 addition & 0 deletions crates/cairo-lang-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,7 @@ impl<'a> Parser<'a> {
SyntaxKind::TerminalOr => self.take::<TerminalOr>().into(),
SyntaxKind::TerminalXor => self.take::<TerminalXor>().into(),
SyntaxKind::TerminalDotDot => self.take::<TerminalDotDot>().into(),
SyntaxKind::TerminalDotDotEq => self.take::<TerminalDotDotEq>().into(),
_ => unreachable!(),
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! > test_runner_name
test_partial_parser_tree(expect_diagnostics: false)

//! > cairo_code
fn f() {
for i in 1..=x {}
}

//! > top_level_kind
ExprFor

//! > ignored_kinds

//! > expected_diagnostics

//! > expected_tree
└── Top level kind: ExprFor
├── for_kw (kind: TokenFor): 'for'
├── pattern (kind: ExprPath)
│ └── item #0 (kind: PathSegmentSimple)
│ └── ident (kind: TokenIdentifier): 'i'
├── identifier (kind: TokenIdentifier): 'in'
├── expr (kind: ExprBinary)
│ ├── lhs (kind: TokenLiteralNumber): '1'
│ ├── op (kind: TokenDotDotEq): '..='
│ └── rhs (kind: ExprPath)
│ └── item #0 (kind: PathSegmentSimple)
│ └── ident (kind: TokenIdentifier): 'x'
└── body (kind: ExprBlock)
├── lbrace (kind: TokenLBrace): '{'
├── statements (kind: StatementList) []
└── rbrace (kind: TokenRBrace): '}'

//! > ==========================================================================

//! > Test range inclusive operator precedence

//! > test_runner_name
test_partial_parser_tree(expect_diagnostics: false)

//! > cairo_code
fn f() {
x += false && true..=1 + 2
}

//! > top_level_kind
ExprBinary

//! > ignored_kinds

//! > expected_diagnostics

//! > expected_tree
└── Top level kind: ExprBinary
├── lhs (kind: ExprPath)
│ └── item #0 (kind: PathSegmentSimple)
│ └── ident (kind: TokenIdentifier): 'x'
├── op (kind: TokenPlusEq): '+='
└── rhs (kind: ExprBinary)
├── lhs (kind: ExprBinary)
│ ├── lhs (kind: TokenFalse): 'false'
│ ├── op (kind: TokenAndAnd): '&&'
│ └── rhs (kind: TokenTrue): 'true'
├── op (kind: TokenDotDotEq): '..='
└── rhs (kind: ExprBinary)
├── lhs (kind: TokenLiteralNumber): '1'
├── op (kind: TokenPlus): '+'
└── rhs (kind: TokenLiteralNumber): '2'
3 changes: 3 additions & 0 deletions crates/cairo-lang-semantic/src/corelib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,9 @@ pub fn core_binary_operator(
BinaryOperator::Or(_) => ("BitOr", "bitor", false, CoreTraitContext::TopLevel),
BinaryOperator::Xor(_) => ("BitXor", "bitxor", false, CoreTraitContext::TopLevel),
BinaryOperator::DotDot(_) => ("RangeOp", "range", false, CoreTraitContext::Ops),
BinaryOperator::DotDotEq(_) => {
("RangeInclusiveOp", "range_inclusive", false, CoreTraitContext::Ops)
}
_ => return Ok(Err(SemanticDiagnosticKind::UnknownBinaryOperator)),
};
Ok(Ok((
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! > Test range

//! > test_runner_name
test_expr_semantics(expect_diagnostics: false)

//! > function_body

//! > expr_code
1..=10_u8

//! > module_code

//! > expected_semantics
FunctionCall(
ExprFunctionCall {
function: core::ops::range::RangeInclusiveOpImpl::<core::integer::u8>::range_inclusive,
args: [
Value(
Literal(
ExprLiteral {
value: 1,
ty: core::integer::u8,
},
),
),
Value(
Literal(
ExprLiteral {
value: 10,
ty: core::integer::u8,
},
),
),
],
coupon_arg: None,
ty: core::ops::range::RangeInclusive::<core::integer::u8>,
},
)

//! > expected_diagnostics
2 changes: 2 additions & 0 deletions crates/cairo-lang-syntax-codegen/src/cairo_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ pub fn get_spec() -> Vec<Node> {
.node_with_explicit_kind("LT", "TerminalLT")
.node_with_explicit_kind("GT", "TerminalGT")
.node_with_explicit_kind("DotDot", "TerminalDotDot")
.node_with_explicit_kind("DotDotEq", "TerminalDotDotEq")
)
.add_struct(StructBuilder::new("ExprListParenthesized")
.node("lparen", "TerminalLParen")
Expand Down Expand Up @@ -870,6 +871,7 @@ pub fn get_spec() -> Vec<Node> {
.add_token_and_terminal("DivEq")
.add_token_and_terminal("Dot")
.add_token_and_terminal("DotDot")
.add_token_and_terminal("DotDotEq")
.add_token_and_terminal("EndOfFile")
.add_token_and_terminal("Eq")
.add_token_and_terminal("EqEq")
Expand Down
Loading
Loading