From a59043adbf08be30a74dc2488cb20fc29aecfd9d Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:08:15 +0000 Subject: [PATCH 1/2] fix(frontend): lex `<<` as two `Less` tokens to support nested generics The lexer previously combined `<<` into a single `ShiftLeft` token, which caused `Store<::Key>` to fail parsing since the type parser expected `Token::Less`. This applies the same fix already used for `>>`: always emit two separate tokens and reconstruct the shift operator in the expression parser. Closes #11553 --- compiler/noirc_frontend/src/lexer/lexer.rs | 8 +++---- .../noirc_frontend/src/parser/parser/infix.rs | 10 +++++++-- .../src/parser/parser/statement.rs | 4 +++- .../src/tests/traits/trait_bounds.rs | 22 +++++++++++++++++++ tooling/nargo_fmt/src/formatter/expression.rs | 10 +++++++-- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/compiler/noirc_frontend/src/lexer/lexer.rs b/compiler/noirc_frontend/src/lexer/lexer.rs index 2d3719a49f6..a25fe6e8d80 100644 --- a/compiler/noirc_frontend/src/lexer/lexer.rs +++ b/compiler/noirc_frontend/src/lexer/lexer.rs @@ -256,9 +256,8 @@ impl<'a> Lexer<'a> { if self.peek_char_is('=') { self.next_char(); Ok(Token::LessEqual.into_span(start, start + 1)) - } else if self.peek_char_is('<') { - self.next_char(); - Ok(Token::ShiftLeft.into_span(start, start + 1)) + // Note: There is deliberately no case for ShiftLeft. We always lex << as + // two separate Less tokens to help the parser parse nested generic types. } else { Ok(prev_token.into_single_span(start)) } @@ -963,7 +962,8 @@ mod tests { Token::Star, Token::Assign, Token::Equal, - Token::ShiftLeft, + Token::Less, + Token::Less, Token::Greater, Token::Greater, Token::EOF, diff --git a/compiler/noirc_frontend/src/parser/parser/infix.rs b/compiler/noirc_frontend/src/parser/parser/infix.rs index 10a94932782..c4a48101c78 100644 --- a/compiler/noirc_frontend/src/parser/parser/infix.rs +++ b/compiler/noirc_frontend/src/parser/parser/infix.rs @@ -118,7 +118,8 @@ impl Parser<'_> { parse_infix!( self, Parser::parse_shift, - if self.eat(Token::Less) { + if self.next_token.token() != &Token::LessEqual && self.eat(Token::Less) { + // Make sure to skip the `<<=` case, as `<<=` is lexed as `< <=`. BinaryOpKind::Less } else if self.eat(Token::LessEqual) { BinaryOpKind::LessEqual @@ -141,7 +142,12 @@ impl Parser<'_> { parse_infix!( self, Parser::parse_add_or_subtract, - if !self.next_is(Token::Assign) && self.eat(Token::ShiftLeft) { + if self.at(Token::Less) && self.next_is(Token::Less) { + // Left-shift (<<) is issued as two separate < tokens by the lexer as this makes it easier + // to parse nested generic types. For normal expressions however, it means we have to manually + // parse two less-than tokens as a single left-shift here. + self.bump(); + self.bump(); BinaryOpKind::ShiftLeft } else if self.at(Token::Greater) && self.next_is(Token::Greater) { // Right-shift (>>) is issued as two separate > tokens by the lexer as this makes it easier diff --git a/compiler/noirc_frontend/src/parser/parser/statement.rs b/compiler/noirc_frontend/src/parser/parser/statement.rs index 90d12c37327..5d303913ea7 100644 --- a/compiler/noirc_frontend/src/parser/parser/statement.rs +++ b/compiler/noirc_frontend/src/parser/parser/statement.rs @@ -234,13 +234,15 @@ impl Parser<'_> { Token::Percent => Some(BinaryOpKind::Modulo), Token::Ampersand => Some(BinaryOpKind::And), Token::Caret => Some(BinaryOpKind::Xor), - Token::ShiftLeft => Some(BinaryOpKind::ShiftLeft), Token::Pipe => Some(BinaryOpKind::Or), _ => None, } } else if self.at(Token::Greater) && self.next_is(Token::GreaterEqual) { // >>= Some(BinaryOpKind::ShiftRight) + } else if self.at(Token::Less) && self.next_is(Token::LessEqual) { + // <<= + Some(BinaryOpKind::ShiftLeft) } else { None }; diff --git a/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs b/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs index ef60b14e5dd..f2f98018080 100644 --- a/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs +++ b/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs @@ -649,3 +649,25 @@ fn where_clause_on_self_type_with_generic() { "#; assert_no_errors(src); } + +// Regression test for https://github.com/noir-lang/noir/issues/11553 +#[test] +fn nested_angle_brackets_in_type_position() { + let src = r#" + pub trait HasKey { + type Key; + } + + pub struct Store { + key: K, + } + + pub fn make_store(key: ::Key) -> Store<::Key> + where + T: HasKey, + { + Store { key } + } + "#; + assert_no_errors(src); +} diff --git a/tooling/nargo_fmt/src/formatter/expression.rs b/tooling/nargo_fmt/src/formatter/expression.rs index e9f35ef3c7a..ba890ff9896 100644 --- a/tooling/nargo_fmt/src/formatter/expression.rs +++ b/tooling/nargo_fmt/src/formatter/expression.rs @@ -811,8 +811,14 @@ impl ChunkFormatter<'_, '_> { group.space_or_line(); group.text(self.chunk(|formatter| { - let tokens_count = - if infix.operator.contents == BinaryOpKind::ShiftRight { 2 } else { 1 }; + let tokens_count = if matches!( + infix.operator.contents, + BinaryOpKind::ShiftRight | BinaryOpKind::ShiftLeft + ) { + 2 + } else { + 1 + }; for _ in 0..tokens_count { formatter.write_current_token(); formatter.bump(); From e274fb15dd094bbdcef33c4a6238243603bbc310 Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:26:09 +0000 Subject: [PATCH 2/2] chore: update tests as should pass --- .../noirc_frontend/src/tests/traits/trait_associated_items.rs | 3 +-- compiler/noirc_frontend/src/tests/traits/trait_bounds.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/noirc_frontend/src/tests/traits/trait_associated_items.rs b/compiler/noirc_frontend/src/tests/traits/trait_associated_items.rs index dff4dd5121f..09add80e7a0 100644 --- a/compiler/noirc_frontend/src/tests/traits/trait_associated_items.rs +++ b/compiler/noirc_frontend/src/tests/traits/trait_associated_items.rs @@ -1628,9 +1628,8 @@ fn associated_type_shorthand_in_param_position() { assert_no_errors(src); } -/// TODO(https://github.com/noir-lang/noir/issues/11549): remove should_panic once fixed +/// Regression test for https://github.com/noir-lang/noir/issues/11549 #[test] -#[should_panic(expected = "Expected no errors")] fn nested_associated_type_access_fails() { // Bug: nested associated type resolution fails let src = r#" diff --git a/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs b/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs index 74beba6b90b..06fc3b2a796 100644 --- a/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs +++ b/compiler/noirc_frontend/src/tests/traits/trait_bounds.rs @@ -1338,9 +1338,8 @@ fn where_clause_on_associated_type_of_generic_in_trait_impl() { assert_no_errors(src); } -/// TODO(https://github.com/noir-lang/noir/issues/11553): remove should_panic once fixed +/// Regression test for https://github.com/noir-lang/noir/issues/11553 #[test] -#[should_panic(expected = "Expected no errors")] fn associated_type_as_generic_trait_param_with_nested_angle_brackets() { // Bug: Parser fails on << in type position: Store<::Key> let src = r#"