From bcf1552fbb31799b02f572c33f9572fad11553fe Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Mon, 18 May 2026 13:11:34 +1000 Subject: [PATCH 1/6] Convert `get_operator_precendence` to a trait routine --- .../optimising_line_formatter/contexts.rs | 11 ++- .../rules/optimising_line_formatter/mod.rs | 68 ++++++++++--------- .../optimising_line_formatter/requirements.rs | 6 +- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/core/src/rules/optimising_line_formatter/contexts.rs b/core/src/rules/optimising_line_formatter/contexts.rs index 8f09bcad..4a0d3b5d 100644 --- a/core/src/rules/optimising_line_formatter/contexts.rs +++ b/core/src/rules/optimising_line_formatter/contexts.rs @@ -648,7 +648,7 @@ impl<'a> SpecificContextStack<'a> { } } (prev, Some(op @ (TT::Op(_) | TT::Keyword(_)))) - if super::get_operator_precedence(op).is_some() && is_binary(op, prev) => + if op.get_operator_precedence().is_some() && is_binary(op, prev) => { self.update_operator_precedences(node, is_break); } @@ -933,7 +933,7 @@ impl<'a> LineFormattingContexts<'a> { TT::ConditionalDirective(kind) if kind.is_else() => { contexts.push_operators(); } - op if super::get_operator_precedence(op).is_some() + op if op.get_operator_precedence().is_some() && is_binary(op, prev_prev_token_type) => { contexts.push_operators(); @@ -1108,10 +1108,9 @@ impl<'a> LineFormattingContexts<'a> { contexts.fluent(contexts.current_context.clone()); } } - op if super::get_operator_precedence(op).is_some() - && is_binary(op, prev_token_type) => - { - let op_prec = super::get_operator_precedence(op).unwrap(); + + op if op.get_operator_precedence().is_some() && is_binary(op, prev_token_type) => { + let op_prec = op.get_operator_precedence().unwrap(); contexts.pop_until_and_retain(CT::Precedence(op_prec)); } TT::Keyword(KK::Of) if matches!(next_token_type, Some(TT::Keyword(KK::Object))) => { diff --git a/core/src/rules/optimising_line_formatter/mod.rs b/core/src/rules/optimising_line_formatter/mod.rs index 6bd926e8..4afa4bf3 100644 --- a/core/src/rules/optimising_line_formatter/mod.rs +++ b/core/src/rules/optimising_line_formatter/mod.rs @@ -1258,41 +1258,47 @@ impl<'this> InternalOptimisingLineFormatter<'this, '_> { const HIGHEST_PRECEDENCE: u8 = 0; const LOWEST_PRECEDENCE: u8 = 5; -fn get_operator_precedence(token_type: TokenType) -> Option { - match token_type { - TT::Op(OK::Dot) => Some(0), +trait OperatorPrecedence { + fn get_operator_precedence(self) -> Option; +} - TT::Op(OK::AddressOf) | TT::Keyword(KK::Not) => Some(1), +impl OperatorPrecedence for TokenType { + fn get_operator_precedence(self) -> Option { + match self { + TT::Op(OK::Dot) => Some(0), - TT::Op(OK::Star | OK::Slash) - | TT::Keyword(KK::Div | KK::Mod | KK::And | KK::Shl | KK::Shr | KK::As) => Some(2), + TT::Op(OK::AddressOf) | TT::Keyword(KK::Not) => Some(1), - TT::Op(OK::Plus | OK::Minus) | TT::Keyword(KK::Or | KK::Xor) => Some(3), + TT::Op(OK::Star | OK::Slash) + | TT::Keyword(KK::Div | KK::Mod | KK::And | KK::Shl | KK::Shr | KK::As) => Some(2), - TT::Op( - OK::Equal(EqKind::Comp) - | OK::NotEqual - | OK::LessThan(ChK::Comp) - | OK::GreaterThan(ChK::Comp) - | OK::LessEqual - | OK::GreaterEqual, - ) - | TT::Keyword(KK::In(InKind::Op) | KK::Is) => Some(4), - // The import clause `in`s is most simply represented as a precedence - // relationship - TT::Keyword(KK::In(InKind::Import)) => Some(4), - TT::Op(OK::DotDot) => Some(5), - - TT::Op(_) - | TT::Identifier - | TT::Keyword(_) - | TT::TextLiteral(_) - | TT::NumberLiteral(_) - | TT::ConditionalDirective(_) - | TT::CompilerDirective - | TT::Comment(_) - | TT::Eof - | TT::Unknown => None, + TT::Op(OK::Plus | OK::Minus) | TT::Keyword(KK::Or | KK::Xor) => Some(3), + + TT::Op( + OK::Equal(EqKind::Comp) + | OK::NotEqual + | OK::LessThan(ChK::Comp) + | OK::GreaterThan(ChK::Comp) + | OK::LessEqual + | OK::GreaterEqual, + ) + | TT::Keyword(KK::In(InKind::Op) | KK::Is) => Some(4), + // The import clause `in`s is most simply represented as a precedence + // relationship + TT::Keyword(KK::In(InKind::Import)) => Some(4), + TT::Op(OK::DotDot) => Some(5), + + TT::Op(_) + | TT::Identifier + | TT::Keyword(_) + | TT::TextLiteral(_) + | TT::NumberLiteral(_) + | TT::ConditionalDirective(_) + | TT::CompilerDirective + | TT::Comment(_) + | TT::Eof + | TT::Unknown => None, + } } } diff --git a/core/src/rules/optimising_line_formatter/requirements.rs b/core/src/rules/optimising_line_formatter/requirements.rs index 8d352021..5b3e716a 100644 --- a/core/src/rules/optimising_line_formatter/requirements.rs +++ b/core/src/rules/optimising_line_formatter/requirements.rs @@ -1,10 +1,10 @@ use super::InternalOptimisingLineFormatter; use super::SpecificContextDataStack; use super::contexts::*; -use super::get_operator_precedence; use super::is_binary; use super::types::DecisionRequirement; use crate::lang::*; +use crate::rules::optimising_line_formatter::OperatorPrecedence; use super::contexts::ContextType as CT; use super::types::DecisionRequirement as DR; @@ -214,7 +214,7 @@ impl InternalOptimisingLineFormatter<'_, '_> { .map(|(_, data)| data.is_broken | data.is_child_broken) .if_else_or_default(DR::MustBreak, DR::Indifferent), (prev, Some(op @ (TT::Op(_) | TT::Keyword(_)))) - if get_operator_precedence(op).is_some() && is_binary(op, prev) => + if op.get_operator_precedence().is_some() && is_binary(op, prev) => { contexts_data .iter() @@ -239,7 +239,7 @@ impl InternalOptimisingLineFormatter<'_, '_> { .map(|(_, data)| data.is_broken | data.is_child_broken) .if_else_or_default(DR::MustBreak, DR::Indifferent), (Some(op @ (TT::Op(_) | TT::Keyword(_))), _) - if get_operator_precedence(op).is_some() => + if op.get_operator_precedence().is_some() => { DR::MustNotBreak } From 63503f5e82bf52e958e0abad110fd15724a81f94 Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Mon, 18 May 2026 15:57:32 +1000 Subject: [PATCH 2/6] Use stacks of token types in formatting context construction --- .../optimising_line_formatter/contexts.rs | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/core/src/rules/optimising_line_formatter/contexts.rs b/core/src/rules/optimising_line_formatter/contexts.rs index 4a0d3b5d..8d830198 100644 --- a/core/src/rules/optimising_line_formatter/contexts.rs +++ b/core/src/rules/optimising_line_formatter/contexts.rs @@ -775,12 +775,6 @@ impl<'a> LineFormattingContexts<'a> { token_types: &'a [TokenType], context_tree: &'a ParentPointerTree, ) -> Self { - let get_token_type_from_line_index = |line_index| { - token_types - .get(*line.get_tokens().get(line_index as usize)?) - .cloned() - }; - let builder_context_tree = Self::new_tree(); let mut contexts = LineFormattingContextsBuilder::new(&builder_context_tree); @@ -822,18 +816,35 @@ impl<'a> LineFormattingContexts<'a> { } } - let mut prev_prev_token_type = None; - let mut prev_token_type = None; - let mut prev_semantic_token_type = None; - let mut current = get_token_type_from_line_index(0); - let mut next_token_type = get_token_type_from_line_index(1); + let mut prev_token_types: Vec = Vec::with_capacity(line.get_tokens().len()); + macro_rules! last_semantic_token_type { + () => { + last_semantic_token_type!(0) + }; + ($i: expr) => { + prev_token_types + .iter() + .rev() + .filter(|tt| !tt.is_comment_or_directive()) + .nth($i) + }; + } + let mut next_token_types = line + .get_tokens() + .iter() + .rev() + .map(|id| token_types[*id]) + .collect::>(); + let mut current = next_token_types.pop(); while let Some(current_token_type) = current { if !current_token_type.is_comment_or_compiler_directive() { let last_context_type = contexts.current_context.get().context_type; // New contexts relating to the previous token are pushed here // to avoid including any leading comments - if let (Some(prev_token_type), Some(prev_directive_token_type)) = - (prev_token_type, prev_semantic_token_type) + if let Some(prev_token_type) = prev_token_types + .iter() + .rev() + .find(|tt| !tt.is_comment_or_compiler_directive()) { match (prev_token_type, last_context_type) { (TT::Op(OK::LParen | OK::LBrack | OK::LessThan(ChK::Generic)), _) @@ -858,7 +869,7 @@ impl<'a> LineFormattingContexts<'a> { } _ => {} } - match prev_directive_token_type { + match prev_token_type { TT::Keyword(KK::Of) => { contexts.push(CT::Subject); contexts.push_expression(); @@ -908,7 +919,10 @@ impl<'a> LineFormattingContexts<'a> { contexts.push_expression(); } TT::Keyword(KK::Abstract) - if matches!(prev_prev_token_type, Some(TT::Keyword(KK::Class))) => {} + if matches!( + last_semantic_token_type!(1), + Some(TT::Keyword(KK::Class)) + ) => {} TT::Keyword(kk) if kk.is_directive() => { contexts.push_expression(); } @@ -934,7 +948,7 @@ impl<'a> LineFormattingContexts<'a> { contexts.push_operators(); } op if op.get_operator_precedence().is_some() - && is_binary(op, prev_prev_token_type) => + && is_binary(*op, last_semantic_token_type!(1).cloned()) => { contexts.push_operators(); } @@ -952,7 +966,7 @@ impl<'a> LineFormattingContexts<'a> { TT::Op(OK::LessThan(ChevronKind::Generic)) => BracketKind::Angle, _ => BracketKind::Round, }; - let (typ, cont_delta) = match prev_token_type { + let (typ, cont_delta) = match last_semantic_token_type!() { // routine invocations Some(TT::Identifier | TT::Op(OK::GreaterThan(ChevronKind::Generic))) => { (BracketStyle::BreakClose, 1) @@ -1094,7 +1108,10 @@ impl<'a> LineFormattingContexts<'a> { } TT::Op(OK::Dot) if CT::Precedence(0) == last_context_type => { contexts.retain_current(); - if matches!(prev_token_type, Some(TT::Op(OK::RParen | OK::RBrack))) { + if matches!( + last_semantic_token_type!(), + Some(TT::Op(OK::RParen | OK::RBrack)) + ) { /* Fluency is considered after () and [] because they allow for arbitrary computation which will @@ -1109,11 +1126,15 @@ impl<'a> LineFormattingContexts<'a> { } } - op if op.get_operator_precedence().is_some() && is_binary(op, prev_token_type) => { + op if op.get_operator_precedence().is_some() + && is_binary(op, last_semantic_token_type!().cloned()) => + { let op_prec = op.get_operator_precedence().unwrap(); contexts.pop_until_and_retain(CT::Precedence(op_prec)); } - TT::Keyword(KK::Of) if matches!(next_token_type, Some(TT::Keyword(KK::Object))) => { + TT::Keyword(KK::Of) + if matches!(next_token_types.last(), Some(TT::Keyword(KK::Object))) => + { contexts.pop_until_after(CT::AnonHeader); } TT::Keyword(KK::Then | KK::Do | KK::Of) => { @@ -1134,7 +1155,7 @@ impl<'a> LineFormattingContexts<'a> { contexts.pop_until_after(CT::AnonHeader); } TT::Keyword(KK::Abstract) - if matches!(prev_token_type, Some(TT::Keyword(KK::Class))) => {} + if matches!(last_semantic_token_type!(), Some(TT::Keyword(KK::Class))) => {} TT::Keyword(kk) if kk.is_directive() => { if contexts.pop_until(CT::DirectiveList) != Some(CT::DirectiveList) { if contexts @@ -1179,15 +1200,8 @@ impl<'a> LineFormattingContexts<'a> { _ => {} } - if !current_token_type.is_comment_or_directive() { - prev_prev_token_type = prev_token_type; - prev_token_type = current; - } - if !current_token_type.is_comment_or_compiler_directive() { - prev_semantic_token_type = current; - } - current = next_token_type; - next_token_type = get_token_type_from_line_index(contexts.line_index + 1); + prev_token_types.extend(current); + current = next_token_types.pop(); } contexts.finalise(); From cfd8e39ffabb022eed025fd9c4a43541a3d477ce Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Tue, 26 May 2026 10:25:14 +1000 Subject: [PATCH 3/6] Add reference to `optimising_line_formatter::is_binary` argument --- core/src/rules/optimising_line_formatter/contexts.rs | 6 +++--- core/src/rules/optimising_line_formatter/mod.rs | 2 +- core/src/rules/optimising_line_formatter/requirements.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/rules/optimising_line_formatter/contexts.rs b/core/src/rules/optimising_line_formatter/contexts.rs index 8d830198..5778f11b 100644 --- a/core/src/rules/optimising_line_formatter/contexts.rs +++ b/core/src/rules/optimising_line_formatter/contexts.rs @@ -648,7 +648,7 @@ impl<'a> SpecificContextStack<'a> { } } (prev, Some(op @ (TT::Op(_) | TT::Keyword(_)))) - if op.get_operator_precedence().is_some() && is_binary(op, prev) => + if op.get_operator_precedence().is_some() && is_binary(op, prev.as_ref()) => { self.update_operator_precedences(node, is_break); } @@ -948,7 +948,7 @@ impl<'a> LineFormattingContexts<'a> { contexts.push_operators(); } op if op.get_operator_precedence().is_some() - && is_binary(*op, last_semantic_token_type!(1).cloned()) => + && is_binary(*op, last_semantic_token_type!(1)) => { contexts.push_operators(); } @@ -1127,7 +1127,7 @@ impl<'a> LineFormattingContexts<'a> { } op if op.get_operator_precedence().is_some() - && is_binary(op, last_semantic_token_type!().cloned()) => + && is_binary(op, last_semantic_token_type!()) => { let op_prec = op.get_operator_precedence().unwrap(); contexts.pop_until_and_retain(CT::Precedence(op_prec)); diff --git a/core/src/rules/optimising_line_formatter/mod.rs b/core/src/rules/optimising_line_formatter/mod.rs index 4afa4bf3..469751ae 100644 --- a/core/src/rules/optimising_line_formatter/mod.rs +++ b/core/src/rules/optimising_line_formatter/mod.rs @@ -1307,7 +1307,7 @@ impl OperatorPrecedence for TokenType { /// /// Returns whether the given [`TokenType`] is a binary operator by looking at /// its previous (real) token type -fn is_binary(token_type: TokenType, prev_token_type: Option) -> bool { +fn is_binary(token_type: TokenType, prev_token_type: Option<&TokenType>) -> bool { match token_type { TT::Op(OK::Plus | OK::Minus | OK::AddressOf) | TT::Keyword(KK::Not) => {} _ => return true, diff --git a/core/src/rules/optimising_line_formatter/requirements.rs b/core/src/rules/optimising_line_formatter/requirements.rs index 5b3e716a..9c16b1dd 100644 --- a/core/src/rules/optimising_line_formatter/requirements.rs +++ b/core/src/rules/optimising_line_formatter/requirements.rs @@ -214,7 +214,7 @@ impl InternalOptimisingLineFormatter<'_, '_> { .map(|(_, data)| data.is_broken | data.is_child_broken) .if_else_or_default(DR::MustBreak, DR::Indifferent), (prev, Some(op @ (TT::Op(_) | TT::Keyword(_)))) - if op.get_operator_precedence().is_some() && is_binary(op, prev) => + if op.get_operator_precedence().is_some() && is_binary(op, prev.as_ref()) => { contexts_data .iter() From a3aa9b2df6d81cc0a9a4669e0be40ebfd9fcd93c Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Fri, 22 May 2026 14:53:45 +1000 Subject: [PATCH 4/6] Improve single-token formatting context pruning --- .../src/rules/optimising_line_formatter/contexts.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/core/src/rules/optimising_line_formatter/contexts.rs b/core/src/rules/optimising_line_formatter/contexts.rs index 5778f11b..588a664f 100644 --- a/core/src/rules/optimising_line_formatter/contexts.rs +++ b/core/src/rules/optimising_line_formatter/contexts.rs @@ -1493,9 +1493,12 @@ impl<'builder> LineFormattingContextsBuilder<'builder> { self.contexts_to_remove .extend(self.contexts.into_iter().filter(|node_ref| { let ctx = node_ref.get(); - ctx.ending_token == Some(ctx.starting_token) - || ctx.ending_token.is_none() - && matches!(ctx.context_type, CT::CommaList | CT::Assignment) + let is_single_token = ctx.ending_token == Some(ctx.starting_token) + // `self.line_index` is the the index after the last token + || ctx.starting_token + 1 == self.line_index; + let is_commalist_or_assignment = ctx.ending_token.is_none() + && matches!(ctx.context_type, CT::CommaList | CT::Assignment); + is_single_token || is_commalist_or_assignment })); // If an `Brackets` context is in an expression or guard clause, then @@ -2100,7 +2103,6 @@ mod tests { 1 Assignment ^----$-------- 1 AssignLHS ^----$ 1 BGenerics ^--$ - 1 AssignRHS ^---- "}, commas = {" type AA = class end; @@ -2109,7 +2111,6 @@ mod tests { 1 AssignLHS ^------------$ 1 BGenerics ^----------$ 0 CommaList ^--------$ - 1 AssignRHS ^---- "}, semicolons = {" type AA = class end; @@ -2118,7 +2119,6 @@ mod tests { 1 AssignLHS ^------------$ 1 BGenerics ^----------$ 0 SemicolonList ^--------$ - 1 AssignRHS ^---- "}, constraints = {" type AA = class end; @@ -2130,7 +2130,6 @@ mod tests { 1 SemicolonElem ^--------------------$ 0 CommaList ^----------------$ 1 SemicolonElem ^--------$ - 1 AssignRHS ^---- "}, )] fn generics(input: &str) -> Result<(), DslParseError> { From 14ffb98b9028a1bffb8725faf1970b29c83aab26 Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Tue, 26 May 2026 09:21:19 +1000 Subject: [PATCH 5/6] Fix syntax errors in else_if_then statement tests --- core/datatests/generators/optimising_line_formatter.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/datatests/generators/optimising_line_formatter.rs b/core/datatests/generators/optimising_line_formatter.rs index d7d0f00d..66bb1843 100644 --- a/core/datatests/generators/optimising_line_formatter.rs +++ b/core/datatests/generators/optimising_line_formatter.rs @@ -3013,16 +3013,16 @@ mod control_flows { inline = " if A then else if AAAAAA and BBBBBB then - Bar; + Bar else if AAAAAAAAA and BBBBBBBBB then - Bar; + Bar else if AAAAAAAAAA and BBBBBBBBBB and CCCCCCCCCC then - Bar; + Bar else if Foo(AAA, BB, CCC) then - Bar; + Bar else if Foo( AAAA, BBBBBB, @@ -3052,7 +3052,7 @@ mod control_flows { end else if AA(A, B, C) then begin Bar; - end; + end else if AAA(AAA, BB, CCC) then begin Bar; From 75bc30ab5e7f10d4f474316b0b42093b784849bc Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Tue, 26 May 2026 09:48:43 +1000 Subject: [PATCH 6/6] Add support for ternary expressions --- CHANGELOG.md | 4 + core/CHANGELOG.md | 4 + .../generators/logical_line_parser.rs | 20 +++ .../generators/optimising_line_formatter.rs | 134 +++++++++++++++++ .../optimising_line_formatter/contexts.rs | 136 +++++++++++++++++- .../optimising_line_formatter/requirements.rs | 15 +- 6 files changed, 306 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b9b2f00..9561bca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added + +- Added support for `if else` ternary expressions + ## [0.7.0] - 2025-11-11 ### Added diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 417eabaa..abf24816 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for `if else` ternary expressions + ## 0.7.0 - 2025-11-11 ### Added diff --git a/core/datatests/generators/logical_line_parser.rs b/core/datatests/generators/logical_line_parser.rs index cf2d6584..8bff8dc6 100644 --- a/core/datatests/generators/logical_line_parser.rs +++ b/core/datatests/generators/logical_line_parser.rs @@ -31,6 +31,7 @@ pub fn generate_test_files(root_dir: &Path) { routine_implementations::generate(root_dir); control_flows::generate(root_dir); statements::generate(root_dir); + ternary::generate(root_dir); attributes::generate(root_dir); semicolons::generate(root_dir); regression::generate(root_dir); @@ -3258,6 +3259,25 @@ mod statements { } } +mod ternary { + use super::*; + + pub fn generate(root_dir: &Path) { + generate_test_cases!( + root_dir, + assignment = " + _|A := if A then B else C; + ", + nested = " + _|A := if if A then B else C then + if A then B else C + else + if A then B else C; + ", + ); + } +} + mod attributes { use super::*; diff --git a/core/datatests/generators/optimising_line_formatter.rs b/core/datatests/generators/optimising_line_formatter.rs index 66bb1843..a2cf1053 100644 --- a/core/datatests/generators/optimising_line_formatter.rs +++ b/core/datatests/generators/optimising_line_formatter.rs @@ -432,6 +432,31 @@ mod comments { and FFFFFFFFFFFF then ; ", + ternary = " + A := + if A then + B + else // + if C then D else E; + A := + if A then + B + else // + if CCCCC then + D + else + E; + A := + if A then + B + else // + if C then + DDDDD // + else if a then + E + else + d; + ", ); } } @@ -4238,6 +4263,7 @@ mod expressions { unary::generate(root_dir); set::generate(root_dir); strings::generate(root_dir); + ternary::generate(root_dir); } mod boolean { @@ -4859,6 +4885,114 @@ mod expressions { ); } } + + mod ternary { + use super::*; + + pub fn generate(root_dir: &Path) { + generate_test_cases!( + root_dir, + basic_assignment = " + A := if B then C else D; + AAAAAAAA := + if B then C else D; + AAAAAAAA := + if BBBBBBB then + CC + else + DD; + ", + nested_must_wrap = " + // wrap_column=41 | + // A := if B then if C then D else E else F; + A := + if B then + if C then D else E + else + F; + ", + nested = " + A := + if B then + if CCCCC then D else E + else + F; + A := + if B then + if CCCCCC then + D + else + E + else + F; + A := + if BBBBBBB + BBBBBBBB then + if CCCCCC + CCCCC then + DDDDDDDDDD + DDDDD + else + EEEEEEEEE + EEEEEE + else + FFFFFFFF + FFFFFFFFFF; + A := + if BBBBBBBB + + BBBBBBBB then + if CCCCC + CCCCCC then + DDDDDDDDDD + DDDDD + else + EEEEEEEEE + EEEEEE + else + FFFFFFFF + FFFFFFFFFF; + A := + if BBBBBBB + BBBBBBBB then + if CCCCCCC + + CCCCCCC then + DDDDDDDDDD + DDDDD + else + EEEEEEEEE + EEEEEE + else + FFFFFFFF + FFFFFFFFFF; + A := + if BBBBBBB + BBBBBBBB then + if CCCCC + CCCCCC then + DDDDDDDDDD + + DDDDDD + else + EEEEEEEEE + EEEEEE + else + FFFFFFFF + FFFFFFFFFF; + A := + if BBBBBBB + BBBBBBBB then + if CCCCC + CCCCCC then + DDDDDDDDDD + DDDDD + else + EEEEEEEEE + + EEEEEEE + else + FFFFFFFF + FFFFFFFFFF; + A := + if BBBBBBB + BBBBBBBB then + if CCCCCC + CCCCC then + DDDDDDDDDD + DDDDD + else + EEEEEEEEE + EEEEEE + else + FFFFFFFFF + + FFFFFFFFFF; + A := + if BBBBBBBB + + BBBBBBBB then + if CCCCCC + + CCCCCC then + DDDDDDDDDD + DDDDD + else + EEEEEEEEE + + EEEEEEE + else + FFFFFFFFF + FFFFFFFFF; + ", + ); + } + } } mod attributes { diff --git a/core/src/rules/optimising_line_formatter/contexts.rs b/core/src/rules/optimising_line_formatter/contexts.rs index 588a664f..f8661678 100644 --- a/core/src/rules/optimising_line_formatter/contexts.rs +++ b/core/src/rules/optimising_line_formatter/contexts.rs @@ -97,6 +97,7 @@ pub(super) enum ContextType { // Nested Type, Precedence(u8), + IfElse, AssignLHS, AssignRHS, ControlFlowBegin, @@ -105,6 +106,7 @@ pub(super) enum ContextType { MemberAccess, Subject, GuardClause, + ExpressionGuardClause, AnonHeader, } use ContextType as CT; @@ -519,6 +521,15 @@ impl<'a> SpecificContextStack<'a> { (Some(TT::Op(OK::Colon)), _) => { self.update_last_matching_context(node, context_matches!(_), apply_pivotal_break); } + (Some(TT::Keyword(KK::Then)), _) => { + // if-else expression `then` + self.update_last_matching_context(node, CT::IfElse, |_, data| { + data.is_broken |= is_break; + data.can_break &= is_break; + data.one_element_per_line.get_or_insert(is_break); + }); + self.update_last_matching_context(node, CT::Subject, apply_pivotal_break); + } (Some(TT::Keyword(KK::If | KK::While | KK::Until | KK::On | KK::Case)), _) => { self.update_last_matching_context(node, CT::ControlFlowBegin, |_, data| { data.is_broken |= is_break; @@ -540,9 +551,13 @@ impl<'a> SpecificContextStack<'a> { }); } (Some(TT::Keyword(KK::Else)), _) => { - self.update_last_matching_context(node, CT::ControlFlowBegin, |_, data| { - data.is_broken |= is_break; - }); + self.update_last_matching_context( + node, + context_matches!(CT::ControlFlowBegin | CT::Subject), + |_, data| { + data.is_broken |= is_break; + }, + ); } (_, Some(TT::Keyword(KK::Begin | KK::End))) => { self.update_last_matching_context(node, CT::ControlFlowBegin, |_, data| { @@ -690,6 +705,7 @@ impl<'a> SpecificContextStack<'a> { CT::TypedAssignment | CT::ForLoop | CT::RaiseAt => { data.is_broken |= data.is_child_broken } + CT::ExpressionGuardClause => data.is_broken |= data.is_child_broken, CT::AssignLHS if self.formatting_contexts.line.get_line_type() == LLT::Assignment => { @@ -895,7 +911,26 @@ impl<'a> LineFormattingContexts<'a> { contexts.push_expression(); } TT::Keyword(KK::If | KK::While | KK::On | KK::Until | KK::Case) => { - contexts.push(CT::GuardClause); + contexts.push( + if prev_token_types + .iter() + .filter(|tt| !tt.is_comment_or_compiler_directive()) + .count() + == 1 + { + CT::GuardClause + } else { + CT::ExpressionGuardClause + }, + ); + contexts.push_expression(); + } + TT::Keyword(KK::Then) => { + // if-else expressions + contexts.push_expression(); + } + TT::Keyword(KK::Else) => { + // if-else expressions contexts.push_expression(); } TT::Keyword(KK::With) => { @@ -1077,12 +1112,48 @@ impl<'a> LineFormattingContexts<'a> { _ => {} } } + TT::Keyword(KK::If) if contexts.line_index != 0 => { + // if-else expression + if let Some(tt) = prev_token_types.last() + && tt != &TT::Keyword(KK::Else) + { + // Don't push another context in the `else if` case + contexts.push((CT::IfElse, 0)); + } + } TT::Keyword(KK::If | KK::While | KK::With | KK::On) => { contexts.push_utility((CT::ControlFlowBegin, 0)); contexts.push(CT::ControlFlow); } TT::Keyword(KK::Else) => { - contexts.pop_until(CT::ControlFlowBegin); + if let Some(CT::IfElse) = + contexts.pop_until(context_matches!(CT::ControlFlowBegin | CT::IfElse)) + { + // This is to ensure nested if-else expressions are popped of the stack. + // They will already have their `ending_token` set. + while contexts.last_context_mut().ending_token.is_some() { + contexts.pop(); + contexts.pop_until(CT::IfElse); + } + if contexts.last_context_mut().context_type == CT::IfElse + && next_token_types + .iter() + .rev() + .find(|tt| !tt.is_comment_or_compiler_directive()) + .is_some_and(|tt| tt != &TT::Keyword(KK::If)) + { + // Only set the ending on bare `else`, i.e., not `else if` + contexts.last_context_mut().ending_token = Some(contexts.line_index) + } + } + if next_token_types + .iter() + .next_back() + .is_some_and(|tt| tt != &TT::Keyword(KK::If)) + { + // `Subject` shouldn't be pushed if it is an `else if` + contexts.push(CT::Subject); + } } TT::Keyword(KK::Case | KK::Until) => { contexts.push(CT::ControlFlow); @@ -1137,7 +1208,13 @@ impl<'a> LineFormattingContexts<'a> { { contexts.pop_until_after(CT::AnonHeader); } - TT::Keyword(KK::Then | KK::Do | KK::Of) => { + TT::Keyword(KK::Then) => { + contexts.pop_until_and_retain(context_matches!(CT::GuardClause)); + contexts.pop_until(context_matches!(CT::IfElse | CT::ControlFlow)); + contexts.push(CT::Subject); + } + TT::Keyword(KK::Do | KK::Of) => { + contexts.pop_until_and_retain(context_matches!(CT::GuardClause)); contexts.pop_until(context_matches!(CT::ControlFlow | CT::ForLoop)); } TT::Keyword(KK::Function | KK::Procedure) @@ -1795,9 +1872,11 @@ mod tests { "TypedAssignment" => Ok(CT::TypedAssignment), "AssignLHS" => Ok(CT::AssignLHS), "AssignRHS" => Ok(CT::AssignRHS), + "IfElse" => Ok(CT::IfElse), "ControlFlow" => Ok(CT::ControlFlow), "GuardClause" => Ok(CT::GuardClause), + "EGuardClause" => Ok(CT::ExpressionGuardClause), "ControlFlowBegin" => Ok(CT::ControlFlowBegin), "MemberAccess" => Ok(CT::MemberAccess), "ForLoop" => Ok(CT::ForLoop), @@ -2225,6 +2304,51 @@ mod tests { assert_contexts(input) } + #[yare::parameterized( + assignment = {" + A := if AA = BB then CC + DD else EE - FF; + 1 Base ^----------------------------------------- + 1 AssignRHS ^----------------------------------$ + 0 IfElse ^--------------------------$-------- + 1 EGuardClause ^-----$ + 1 Precedence(4) ^-----$ + 1 Subject ^----------$ + 1 Precedence(3) ^-----$ + 1 Subject ^----------$ + 1 Precedence(3) ^-----$ + "}, + nested = {" + A := if AA = BB then if CC = DD then EE + FF else GG - HH else HH * II; + 1 Base ^---------------------------------------------------------------------- + 1 AssignRHS ^---------------------------------------------------------------$ + 0 IfElse ^-------------------------------------------------------$-------- + 1 EGuardClause ^-----$ + 1 Precedence(4) ^-----$ + 1 Subject ^---------------------------------------$ + 0 IfElse ^--------------------------$-------- + 1 EGuardClause ^-----$ + 1 Precedence(4) ^-----$ + 1 Subject ^----------$ + 1 Precedence(3) ^-----$ + 1 Subject ^----------$ + 1 Precedence(3) ^-----$ + 1 Subject ^----------$ + 1 Precedence(2) ^-----$ + "}, + else_if = {" + A := if AA then BB else if CC then DD else EE; + 1 Base ^--------------------------------------------- + 1 AssignRHS ^--------------------------------------$ + 0 IfElse ^-----------------------------------$--- + 1 Subject ^-----$ + 1 Subject ^-----$ + 1 Subject ^-----$ + "}, + )] + fn ternary(input: &str) -> Result<(), DslParseError> { + assert_contexts(input) + } + #[yare::parameterized( inline = {" with AA do; diff --git a/core/src/rules/optimising_line_formatter/requirements.rs b/core/src/rules/optimising_line_formatter/requirements.rs index 9c16b1dd..5de062d1 100644 --- a/core/src/rules/optimising_line_formatter/requirements.rs +++ b/core/src/rules/optimising_line_formatter/requirements.rs @@ -278,7 +278,20 @@ impl InternalOptimisingLineFormatter<'_, '_> { .get_last_context(CT::ControlFlowBegin) .map(|(_, data)| data.is_child_broken) .if_else_or_default(DR::MustBreak, DR::Indifferent), - (_, Some(TT::Keyword(KK::Else))) => DR::MustBreak, + (_, Some(TT::Keyword(KK::Else))) => contexts_data + .get_last_context(context_matches!(CT::IfElse)) + .map(|(_, data)| data.is_broken | data.is_child_broken) + .if_else_or(DR::MustBreak, DR::MustNotBreak, DR::MustBreak), + (Some(TT::Keyword(KK::Then)), Some(TT::Keyword(KK::If))) => DR::MustBreak, + (Some(TT::Keyword(KK::Then)), _) => contexts_data + .get_last_context(context_matches!(CT::IfElse)) + .and_then(|(_, data)| data.one_element_per_line) + .if_else_or_default(DR::MustBreak, DR::MustNotBreak), + (Some(TT::Keyword(KK::Else)), Some(TT::Keyword(KK::If))) => DR::MustNotBreak, + (Some(TT::Keyword(KK::Else)), _) => contexts_data + .get_last_context(context_matches!(CT::IfElse)) + .map(|(_, data)| data.is_broken | data.is_child_broken) + .if_else_or(DR::MustBreak, DR::MustNotBreak, DR::MustBreak), (Some(_), Some(TT::Keyword(KK::Begin))) => contexts_data .get_last_context(context_matches!(CT::CommaElem | CT::AssignRHS)) .and_then(|(_, data)| data.break_anonymous_routine)