From 6993a164dd57c57be9f79190824b774b162272eb Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Sun, 17 Aug 2025 07:31:44 +0200 Subject: parser: Fix disambiguation paren on first argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arguments can be put inside of enclosing parenthesis, but the parser was assuming that if an opening paren was found when parsing the first argument on an argument list, then that was all it was needed to be parsed. This though conflicts with situations like: .byte ($01 << 2) | ($01 << 1) In this case, the parser would have ignored everything past the first closing paren. This commit provides a fix in which if an operation is found past the first enclosing parenthesis, then this assumption is discarded in favor of a parenthesis being used for disambiguating on an arithmetical/logical expression. Signed-off-by: Miquel Sabaté Solà --- lib/xixanta/src/parser.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 3 deletions(-) (limited to 'lib/xixanta/src/parser.rs') diff --git a/lib/xixanta/src/parser.rs b/lib/xixanta/src/parser.rs index 8672923..d6d8d6f 100644 --- a/lib/xixanta/src/parser.rs +++ b/lib/xixanta/src/parser.rs @@ -879,9 +879,36 @@ impl Parser { self.next(); self.skip_whitespace(line); - // The end is actually the matching paren for the current opening - // one. - self.find_matching_paren(line, self.offset)? + // If there is a parenthesis it might be either that the command + // enclosed its arguments into parenthesis, or that the first + // argument is using parenthesis to disambiguate with an + // arithmetic/logical operation. + let e = self.find_matching_paren(line, self.offset)?; + match line.get(e + 1..line.len()) { + Some(rest) => { + let trimmed = rest.trim_start(); + if trimmed.is_empty() { + // The rest of the line was just empty: return the + // matching closing paren as there was no need to + // disambiguate. + e + } else if self.get_operation_from_line(trimmed).is_ok() { + // There is an arithmetic/logical operation in sight! In + // that case we assume it was a disambiguation scenario + // and so we rollback from the previous `next()` call + // and return the end of the line. + self.prev(); + line.len() + } else { + // No operation in sight, let's pick the matching paren + // as the end of the line. + e + } + } + // No arithmetic/logical operation in sight, let's return the + // matching closing paren as the end. + None => e, + } } else { line.len() }; @@ -1572,6 +1599,12 @@ impl Parser { self.column += 1; self.offset += 1; } + + // Decrement `self.column` and `self.offset` by one. + fn prev(&mut self) { + self.column -= 1; + self.offset -= 1; + } } #[cfg(test)] @@ -2334,6 +2367,69 @@ mod tests { assert_node(right_and, NodeType::Value, line, "3"); } + #[test] + fn parens_to_desambiguate() { + let line = ".byte ($01 << 4) | ($01 << 2) | ($01 << 1)"; + let mut parser = Parser::default(); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); + + let node = parser.nodes.last().unwrap().last().unwrap(); + assert_node(node, NodeType::Control(ControlType::Byte), line, ".byte"); + assert!(node.left.is_none()); + assert!(node.right.is_none()); + + let args = node.args.clone().unwrap(); + assert_eq!(args.len(), 1); + + let arg = args.first().unwrap(); + assert_eq!(arg.node_type, NodeType::Operation(OperationType::Or)); + + let first = arg.left.clone().unwrap(); + assert_eq!(first.node_type, NodeType::Operation(OperationType::Lshift)); + assert_node(&first.left.clone().unwrap(), NodeType::Literal, line, "$01"); + assert_node(&first.right.clone().unwrap(), NodeType::Value, line, "4"); + + let other = arg.right.clone().unwrap(); + + let second = other.left.clone().unwrap(); + assert_eq!(second.node_type, NodeType::Operation(OperationType::Lshift)); + assert_node( + &second.left.clone().unwrap(), + NodeType::Literal, + line, + "$01", + ); + assert_node(&second.right.clone().unwrap(), NodeType::Value, line, "2"); + + let third = other.right.clone().unwrap(); + assert_eq!(third.node_type, NodeType::Operation(OperationType::Lshift)); + assert_node(&third.left.clone().unwrap(), NodeType::Literal, line, "$01"); + assert_node(&third.right.clone().unwrap(), NodeType::Value, line, "1"); + } + + #[test] + fn parens_to_desambiguate2() { + let line = ".byte ($01 << 4) | ($01 << 2) | ($01 << 1), $02"; + let mut parser = Parser::default(); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); + + let node = parser.nodes.last().unwrap().last().unwrap(); + assert_node(node, NodeType::Control(ControlType::Byte), line, ".byte"); + assert!(node.left.is_none()); + assert!(node.right.is_none()); + + let args = node.args.clone().unwrap(); + assert_eq!(args.len(), 2); + + let arg = args.first().unwrap(); + assert_eq!(arg.node_type, NodeType::Operation(OperationType::Or)); + // NOTE: no further assertions needed as this is covered in + // `parens_to_disambiguate`. + + let second = args.last().unwrap(); + assert_node(second, NodeType::Literal, line, "$02"); + } + #[test] fn unary_operator_test() { let line = "ldx #