From da6d47ea67adf0937ac28dc35c978a30e0a3e50c Mon Sep 17 00:00:00 2001 From: "Guillem L. Jara" <4lon3ly0@tutanota.com> Date: Mon, 4 May 2026 15:55:14 +0200 Subject: [PATCH 1/2] parser, lexer: solve function call ambiguity I really wanted not to do it this way, but it causes an ambiguity with `getline foo(x)` and GNU seems to have bailed on this too. --- lexer/src/lib.rs | 15 +++++++++------ parser/src/lex.rs | 6 ++++-- parser/src/lib.rs | 28 +++++++++++++--------------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lexer/src/lib.rs b/lexer/src/lib.rs index cc8de2d..c5ae782 100644 --- a/lexer/src/lib.rs +++ b/lexer/src/lib.rs @@ -126,9 +126,12 @@ pub enum Token<'a> { RlengthVariable, #[token("ENVIRON", accept_expression)] EnvironVariable, - #[regex("(?&identifier)", Identifier::without_namespace)] - #[regex(r"(?&identifier)::(?&identifier)", Identifier::with_namespace)] + #[regex("(?&identifier)", |lex| Identifier::without_namespace::<0>(lex))] + #[regex(r"(?&identifier)::(?&identifier)", |lex| Identifier::with_namespace::<0>(lex))] Identifier(Identifier<'a>), + #[regex(r"(?&identifier)\(", |lex| Identifier::without_namespace::<1>(lex))] + #[regex(r"(?&identifier)::(?&identifier)\(", |lex| Identifier::with_namespace::<1>(lex))] + FunctionCall(Identifier<'a>), #[token("+", accept_expression)] Plus, #[token("-", accept_expression)] @@ -376,19 +379,19 @@ fn parse_float(lex: &mut Lexer<'_>) -> f64 { } impl<'a> Identifier<'a> { - fn without_namespace(lex: &mut Lexer<'a>) -> Self { + fn without_namespace(lex: &mut Lexer<'a>) -> Self { Self { namespace: None, - literal: parse_ident(lex, ..), + literal: parse_ident(lex, ..lex.slice().len() - TRIM), } } - fn with_namespace(lex: &mut Lexer<'a>) -> Self { + fn with_namespace(lex: &mut Lexer<'a>) -> Self { // SAFETY: The regex matching ensures it is present and well-formed. let separator = unsafe { memchr(b':', lex.slice()).unwrap_unchecked() }; Self { namespace: Some(parse_ident(lex, ..separator)), - literal: parse_ident(lex, separator + 2..), + literal: parse_ident(lex, separator + 2..lex.slice().len() - TRIM), } } } diff --git a/parser/src/lex.rs b/parser/src/lex.rs index 94eecda..c7c991b 100644 --- a/parser/src/lex.rs +++ b/parser/src/lex.rs @@ -190,8 +190,10 @@ impl TokenExt for Token<'_> { fn is_expr_start(&self) -> bool { self.is_atom() || self.is_prefix_op() - || self == &Token::OpenParent - || self == &Token::Getline + || matches!( + self, + Token::OpenParent | Token::FunctionCall(_) | Token::Getline + ) } fn is_place(&self) -> bool { matches!( diff --git a/parser/src/lib.rs b/parser/src/lib.rs index 6c89920..688e371 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -465,7 +465,17 @@ impl<'a> Parser<'a> { #[tracing::instrument] fn parse_function(&mut self, lex: &mut Lexer<'a>) -> Result<()> { - let name = lex.expect_identifier()?.qualify(self.namespace); + let name = match lex.expect_next()? { + Token::FunctionCall(ident) => ident, + Token::Identifier(ident) => { + lex.expect(&Token::OpenParent, |span| { + ParsingError::NoFunctionSignature(span, ident.literal.to_string()) + })?; + ident + } + _ => return Err(ParsingError::ExpectedIdentifier(lex.span())), + } + .qualify(self.namespace); let args = self.parse_signature(lex, &name)?; lex.consume(&Token::Newline); let body = self.parse_body(lex)?; @@ -481,9 +491,6 @@ impl<'a> Parser<'a> { name: &Identifier<'a>, ) -> Result>> { let mut args = Vec::new_in(self.arena); - lex.expect(&Token::OpenParent, |s| { - ParsingError::NoFunctionSignature(s, name.to_string()) - })?; if lex.consume(&Token::ClosedParent) { return Ok(args); @@ -567,10 +574,7 @@ impl<'a> Parser<'a> { } } else { let next = lex.expect_next()?; - if let Token::Identifier(name) = next - && lex.peek_is(&Token::OpenParent) - { - // TODO: use spans to check there is no space between ident, (. + if let Token::FunctionCall(name) = next { self.parse_function_call(lex, name.qualify(self.namespace), lex.span())? } else { Expr::leaf(self.parse_atom(lex, next)?) @@ -691,10 +695,6 @@ impl<'a> Parser<'a> { name: Identifier<'a>, span: Span, ) -> Result> { - lex.expect(&Token::OpenParent, ParsingError::ExpectedOpeningParenthesis)?; - if lex.span().start != span.end { - return Err(ParsingError::FunctionCallSeparatedIdent(span)); - } let expr = ExprNode::FunctionCall( name, self.parse_arguments(lex, |t| t == &Token::ClosedParent)?, @@ -724,9 +724,7 @@ impl<'a> Parser<'a> { #[tracing::instrument] fn get_place(&self, lex: &mut Lexer<'a>, token: Token<'a>) -> Option> { match token { - Token::Identifier(a) if !lex.peek_is(&Token::OpenParent) => { - Some(a.qualify(self.namespace).into()) - } + Token::Identifier(a) => Some(a.qualify(self.namespace).into()), Token::NrVariable => Some(Variable::Nr), Token::NfVariable => Some(Variable::Nf), Token::FsVariable => Some(Variable::Fs), From d10f779a7b01a218fadbd596197ac72a903ff10b Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Mon, 4 May 2026 23:33:32 +0900 Subject: [PATCH 2/2] Cargo.toml: remove a clippy config --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 79c7cc4..ef673cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,6 @@ unexpected_cfgs = { level = "warn", check-cfg = [ unused_qualifications = "warn" [workspace.lints.clippy] -collapsible_if = { level = "allow", priority = 127 } # remove me # The counts were generated with this command: # cargo clippy --all-targets --workspace --message-format=json --quiet \ # | jq -r '.message.code.code | select(. != null and startswith("clippy::"))' \