From 023a0766199d0051238f52542bf6e2d32c1cd0bc Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 14:24:51 +1100 Subject: [PATCH 01/10] parse reserved tokens --- .../compiler/parser/soft/TokenParser.scala | 25 +++ .../compiler/parser/soft/ast/Token.scala | 153 +++++++++++------- .../compiler/parser/soft/TestParser.scala | 67 ++++---- .../parser/soft/ast/ReservedTokenSpec.scala | 29 ++++ 4 files changed, 186 insertions(+), 88 deletions(-) create mode 100644 compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/ReservedTokenSpec.scala diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TokenParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TokenParser.scala index 71ca29478..fb6b85e27 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TokenParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TokenParser.scala @@ -355,6 +355,31 @@ private object TokenParser { ) } + /** + * Parses all reserved tokens defined in [[Token.reserved]] and returns the first match. + */ + def Reserved[Unknown: P]: P[Token.Reserved] = { + val it = + Token.reserved.iterator + + val head = + P(it.next().lexeme) map { + _ => + Token.reserved.head + } + + it.foldLeft(head) { + case (parser, keyword) => + def nextParser = + P(keyword.lexeme) map { + _ => + keyword + } + + parser | nextParser + } + } + def OperatorOrFail[Unknown: P]: P[SoftAST.Operator] = P { buildOperatorParser(Token.Or) | diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala index 962306ddd..b2c07eab2 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala @@ -16,6 +16,8 @@ package org.alephium.ralph.lsp.access.compiler.parser.soft.ast +import org.alephium.macros.EnumerationMacros + sealed trait Token { self => def lexeme: String @@ -24,81 +26,88 @@ sealed trait Token { self => object Token { + /** + * Represents tokens that are specifically reserved for use in Ralph's grammar. + * + * These tokens cannot be used as identifier [[SoftAST.Identifier]]. + */ + sealed trait Reserved extends Token + sealed abstract class Operator(override val lexeme: String) extends Token - case object Minus extends Operator("-") - case object Plus extends Operator("+") - case object Asterisk extends Operator("*") - case object ForwardSlash extends Operator("/") - case object GreaterThan extends Operator(">") - case object LessThan extends Operator("<") - case object Equality extends Operator("==") - case object GreaterThanOrEqual extends Operator(">=") - case object PlusEquals extends Operator("+=") - case object MinusEquals extends Operator("-=") - case object LessThanOrEqual extends Operator("<=") - case object Equal extends Operator("=") - case object NotEqual extends Operator("!=") - case object Exclamation extends Operator("!") - case object Bar extends Operator("|") - case object Ampersand extends Operator("&") - case object Caret extends Operator("^") - case object Percent extends Operator("%") - case object ForwardArrow extends Operator("->") - case object Or extends Operator("||") - case object And extends Operator("&&") + case object Equality extends Operator("==") with Reserved + case object GreaterThanOrEqual extends Operator(">=") with Reserved + case object PlusEquals extends Operator("+=") with Reserved + case object MinusEquals extends Operator("-=") with Reserved + case object LessThanOrEqual extends Operator("<=") with Reserved + case object NotEqual extends Operator("!=") with Reserved + case object ForwardArrow extends Operator("->") with Reserved + case object Or extends Operator("||") with Reserved + case object And extends Operator("&&") with Reserved + case object Minus extends Operator("-") with Reserved + case object Plus extends Operator("+") with Reserved + case object Asterisk extends Operator("*") with Reserved + case object ForwardSlash extends Operator("/") with Reserved + case object GreaterThan extends Operator(">") with Reserved + case object LessThan extends Operator("<") with Reserved + case object Equal extends Operator("=") with Reserved + case object Exclamation extends Operator("!") with Reserved + case object Bar extends Operator("|") with Reserved + case object Ampersand extends Operator("&") with Reserved + case object Caret extends Operator("^") with Reserved + case object Percent extends Operator("%") with Reserved sealed abstract class Delimiter(override val lexeme: String) extends Token - case object OpenParen extends Delimiter("(") - case object CloseParen extends Delimiter(")") - case object OpenCurly extends Delimiter("{") - case object CloseCurly extends Delimiter("}") - case object OpenBracket extends Delimiter("[") - case object BlockBracket extends Delimiter("]") - case object Comma extends Delimiter(",") - case object Dot extends Delimiter(".") - case object Colon extends Delimiter(":") - case object Semicolon extends Delimiter(";") + case object OpenParen extends Delimiter("(") with Reserved + case object CloseParen extends Delimiter(")") with Reserved + case object OpenCurly extends Delimiter("{") with Reserved + case object CloseCurly extends Delimiter("}") with Reserved + case object OpenBracket extends Delimiter("[") with Reserved + case object BlockBracket extends Delimiter("]") with Reserved + case object Comma extends Delimiter(",") with Reserved + case object Dot extends Delimiter(".") with Reserved + case object Colon extends Delimiter(":") with Reserved + case object Semicolon extends Delimiter(";") with Reserved case object Newline extends Delimiter(System.lineSeparator()) case object Space extends Delimiter(" ") - case object DoubleForwardSlash extends Delimiter("//") + case object DoubleForwardSlash extends Delimiter("//") with Reserved sealed abstract class Punctuator(override val lexeme: String) extends Token - case object Hash extends Punctuator("#") + case object Hash extends Punctuator("#") with Reserved case object Underscore extends Punctuator("_") - case object At extends Punctuator("@") - case object Tick extends Punctuator("`") - case object Quote extends Punctuator("\"") + case object At extends Punctuator("@") with Reserved + case object Tick extends Punctuator("`") with Reserved + case object Quote extends Punctuator("\"") with Reserved sealed abstract class Data(override val lexeme: String) extends Token - case object Let extends Data("let") - case object Mut extends Data("mut") - case object Struct extends Data("struct") - case object Const extends Data("const") - case object Enum extends Data("enum") - case object Event extends Data("event") + case object Let extends Data("let") with Reserved + case object Mut extends Data("mut") with Reserved + case object Struct extends Data("struct") with Reserved + case object Const extends Data("const") with Reserved + case object Enum extends Data("enum") with Reserved + case object Event extends Data("event") with Reserved sealed abstract class Control(override val lexeme: String) extends Token - case object If extends Control("if") - case object Else extends Control("else") - case object While extends Control("while") - case object Return extends Control("return") - case object For extends Control("for") + case object If extends Control("if") with Reserved + case object Else extends Control("else") with Reserved + case object While extends Control("while") with Reserved + case object Return extends Control("return") with Reserved + case object For extends Control("for") with Reserved sealed abstract class AccessModifier(override val lexeme: String) extends Token - case object Pub extends AccessModifier("pub") + case object Pub extends AccessModifier("pub") with Reserved sealed abstract class Definition(override val lexeme: String) extends Token - case object Fn extends Definition("fn") - case object Import extends Definition("import") - case object Contract extends Definition("Contract") - case object Abstract extends Definition("Abstract") - case object TxScript extends Definition("TxScript") - case object AssetScript extends Definition("AssetScript") - case object Interface extends Definition("Interface") + case object Fn extends Definition("fn") with Reserved + case object Import extends Definition("import") with Reserved + case object Contract extends Definition("Contract") with Reserved + case object Abstract extends Definition("Abstract") with Reserved + case object TxScript extends Definition("TxScript") with Reserved + case object AssetScript extends Definition("AssetScript") with Reserved + case object Interface extends Definition("Interface") with Reserved sealed abstract class Inheritance(override val lexeme: String) extends Token - case object Extends extends Inheritance("extends") - case object Implements extends Inheritance("implements") + case object Extends extends Inheritance("extends") with Reserved + case object Implements extends Inheritance("implements") with Reserved sealed abstract class Native(override val lexeme: String) extends Token case object Using extends Native("using") @@ -106,14 +115,36 @@ object Token { case object Embeds extends Native("embeds") sealed abstract class Primitive(override val lexeme: String) extends Token - case object True extends Primitive("true") - case object False extends Primitive("false") - case object Alph_Small extends Primitive("alph") - case object Alph_Big extends Primitive("ALPH") + case object True extends Primitive("true") with Reserved + case object False extends Primitive("false") with Reserved + case object Alph_Small extends Primitive("alph") with Reserved + case object Alph_Big extends Primitive("ALPH") with Reserved sealed trait Term extends Token case class Name(lexeme: String) extends Term case class NumberLiteral(lexeme: String) extends Term case class StringLiteral(lexeme: String) extends Term + /** + * Order such that tokens such as: + * - `||` come before `|` + * - `+=` come before `+` and `=` + */ + implicit val reservedDescendingOrdering: Ordering[Reserved] = { + case (x: Reserved, y: Reserved) => + val lengthCompare = + y.lexeme.length compare x.lexeme.length + + if (lengthCompare == 0) + y.lexeme compare x.lexeme + else + lengthCompare + } + + val reserved: Array[Reserved] = + EnumerationMacros + .sealedInstancesOf[Reserved] + .toArray + .sorted + } diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala index fee68b753..d4c48266f 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala @@ -17,56 +17,45 @@ package org.alephium.ralph.lsp.access.compiler.parser.soft import fastparse.{P, Parsed} -import org.alephium.ralph.lsp.access.compiler.message.error.FastParseError -import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.SoftAST +import org.alephium.ralph.error.CompilerError +import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token} import org.scalatest.matchers.should.Matchers._ object TestParser { def parseSoft(code: String): SoftAST.BlockBody = - runParser(SoftParser.parse(_))(code) + runSoftParser(SoftParser.parse(_))(code) def parseTemplate(code: String): SoftAST.Template = - runParser(TemplateParser.parseOrFail(_))(code) + runSoftParser(TemplateParser.parseOrFail(_))(code) def parseFunction(code: String): SoftAST.Function = - runParser(FunctionParser.parseOrFail(_))(code) + runSoftParser(FunctionParser.parseOrFail(_))(code) def parseTuple(code: String): SoftAST.Tuple = - runParser(TupleParser.parse(_))(code) + runSoftParser(TupleParser.parse(_))(code) def parseBlockClause(mandatory: Boolean)(code: String): SoftAST.BlockClause = - runParser(BlockParser.clause(mandatory)(_))(code) + runSoftParser(BlockParser.clause(mandatory)(_))(code) def parseBlockBody(code: String): SoftAST.BlockBody = - runParser(BlockParser.body(_))(code) + runSoftParser(BlockParser.body(_))(code) def parseComment(code: String): SoftAST.Comments = - runParser(CommentParser.parseOrFail(_))(code) + runSoftParser(CommentParser.parseOrFail(_))(code) def parseType(code: String): SoftAST.TypeAST = - runParser(TypeParser.parse(_))(code) + runSoftParser(TypeParser.parse(_))(code) - private def runParser[T <: SoftAST](parser: P[_] => P[T])(code: String): T = { - // invoke .get to ensure that the parser should NEVER fail - val result = - fastparse.parse(code, parser) match { - case Parsed.Success(ast, _) => - Right(ast) + def parseReservedToken(code: String): Token.Reserved = + runAnyParser(TokenParser.Reserved(_))(code) - case failure: Parsed.Failure => - Left(FastParseError(failure)) - } + def parseReservedTokenOrError(code: String): Either[Parsed.Failure, Token.Reserved] = + runAnyParserOrError(TokenParser.Reserved(_))(code) + private def runSoftParser[T <: SoftAST](parser: P[_] => P[T])(code: String): T = { val ast = - result match { - case Left(error) => - // Print a formatted error so it's easier to debug. - fail(error.error.toFormatter().format(Some(Console.RED))) - - case Right(ast) => - ast - } + runAnyParser(parser)(code) val astToCode = ast.toCode() @@ -88,4 +77,28 @@ object TestParser { ast } + private def runAnyParser[T](parser: P[_] => P[T])(code: String): T = { + val result: Either[Parsed.Failure, T] = + runAnyParserOrError(parser)(code) + + // invoke .get to ensure that the parser should NEVER fail + result match { + case Left(error) => + // Print a formatted error so it's easier to debug. + fail(CompilerError.FastParseError(error).toFormatter().format(Some(Console.RED))) + + case Right(ast) => + ast + } + } + + private def runAnyParserOrError[T](parser: P[_] => P[T])(code: String): Either[Parsed.Failure, T] = + fastparse.parse(code, parser) match { + case Parsed.Success(ast, _) => + Right(ast) + + case failure: Parsed.Failure => + Left(failure) + } + } diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/ReservedTokenSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/ReservedTokenSpec.scala new file mode 100644 index 000000000..8adc7ecdd --- /dev/null +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/ReservedTokenSpec.scala @@ -0,0 +1,29 @@ +package org.alephium.ralph.lsp.access.compiler.parser.soft.ast + +import fastparse.Parsed +import org.alephium.ralph.lsp.access.compiler.parser.soft.TestParser._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.EitherValues._ + +import scala.util.Random + +class ReservedTokenSpec extends AnyWordSpec with Matchers { + + "parse reserved tokens" in { + val tokens = + Random.shuffle(Token.reserved.toList) + + tokens should not be empty + + tokens foreach { + token => + parseReservedToken(token.lexeme) shouldBe token + } + } + + "parse reserved without spaces" in { + parseReservedTokenOrError("blah").left.value shouldBe a[Parsed.Failure] + } + +} From c2c468c220205466fd3f32b230b8861027503a0c Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 14:27:34 +1100 Subject: [PATCH 02/10] disallow reserved tokens to be used as identifier --- .../compiler/parser/soft/CommonParser.scala | 1 + .../parser/soft/FnDecelerationSpec.scala | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala index 668670652..3eb83bd09 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala @@ -81,6 +81,7 @@ private object CommonParser { P { Index ~ CommentParser.parseOrFail.? ~ + !TokenParser.Reserved ~ toCodeOrFail(isLetterDigitOrUnderscore.!) ~ Index } map { diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala index 56de3bec5..4d9ebb017 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala @@ -135,27 +135,23 @@ class FnDecelerationSpec extends AnyWordSpec with Matchers { |""".stripMargin } - val (functions, identifiers) = + val functions = root .toNode() .walkDown .map(_.data) .collect { case function: SoftAST.Function => - Left(function) - - case identifier: SoftAST.Identifier if identifier.code.text == "fn" => - Right(identifier) + function } .toList - .partitionMap(identity) + + functions should have size 2 /** - * Test function + * Test first function */ - functions should have size 1 - val function = functions.head - function.signature.fnName shouldBe + functions.head.signature.fnName shouldBe SoftAST.Identifier( SoftAST.Code( index = indexOf("fn >>function<<"), @@ -164,11 +160,10 @@ class FnDecelerationSpec extends AnyWordSpec with Matchers { ) /** - * Test Identifiers + * Test second function */ - identifiers should have size 1 - identifiers.head shouldBe - SoftAST.Identifier( + functions.last.fn shouldBe + SoftAST.Fn( SoftAST.Code( index = indexOf { """fn function( @@ -176,9 +171,20 @@ class FnDecelerationSpec extends AnyWordSpec with Matchers { |} |""".stripMargin }, - text = "fn" + text = Token.Fn.lexeme ) ) + + functions.last.signature.fnName shouldBe + SoftAST.IdentifierExpected( + indexOf { + """fn function( + | fn >><< + |} + |""".stripMargin + } + ) + } } From 7b62742f1dfae0c596d174bcb6c4ecae19614072 Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 14:30:11 +1100 Subject: [PATCH 03/10] Test-cases: `AnnotationSpec` --- .../parser/soft/AnnotationParser.scala | 6 +- .../compiler/parser/soft/ast/SoftAST.scala | 14 +- .../compiler/parser/soft/AnnotationSpec.scala | 138 ++++++++++++++++++ .../compiler/parser/soft/TestParser.scala | 3 + 4 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala index 560d3d7bb..302d54648 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala @@ -32,16 +32,18 @@ private object AnnotationParser { identifier ~ spaceOrFail.? ~ TupleParser.parseOrFail.? ~ + spaceOrFail.? ~ Index } map { - case (from, at, preIdentifierSpace, identifier, postIdentifierSpace, tuple, to) => + case (from, at, preIdentifierSpace, identifier, postIdentifierSpace, tuple, postTupleSpace, to) => SoftAST.Annotation( index = range(from, to), at = at, preIdentifierSpace = preIdentifierSpace, identifier = identifier, postIdentifierSpace = postIdentifierSpace, - tuple = tuple + tuple = tuple, + postTupleSpace = postTupleSpace ) } diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala index 780f8796b..5a74cb27b 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala @@ -186,6 +186,17 @@ object SoftAST { code: Code) extends TokenDocumentedAST + object At { + + def apply(code: Code): At = + At( + index = code.index, + documentation = None, + code = code + ) + + } + case class At( index: SourceIndex, documentation: Option[Comments], @@ -729,7 +740,8 @@ object SoftAST { preIdentifierSpace: Option[Space], identifier: IdentifierAST, postIdentifierSpace: Option[Space], - tuple: Option[Tuple]) + tuple: Option[Tuple], + postTupleSpace: Option[Space]) extends ExpressionAST sealed trait SpaceAST extends SoftAST diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala new file mode 100644 index 000000000..9654cd67c --- /dev/null +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala @@ -0,0 +1,138 @@ +// Copyright 2024 The Alephium Authors +// This file is part of the alephium project. +// +// The library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the library. If not, see http://www.gnu.org/licenses/. + +package org.alephium.ralph.lsp.access.compiler.parser.soft + +import org.alephium.ralph.lsp.access.compiler.parser.soft.TestParser._ +import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token} +import org.alephium.ralph.lsp.access.util.TestCodeUtil._ +import org.alephium.ralph.lsp.utils.Node +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.OptionValues._ + +class AnnotationSpec extends AnyWordSpec with Matchers { + + "error cases" should { + "report missing identifier" in { + val annotation = + parseAnnotation("@") + + annotation shouldBe + SoftAST.Annotation( + index = indexOf(">>@<<"), + at = SoftAST.At( + SoftAST.Code( + index = indexOf(">>@<<"), + text = Token.At.lexeme + ) + ), + preIdentifierSpace = None, + identifier = SoftAST.IdentifierExpected(indexOf("@>><<")), + postIdentifierSpace = None, + tuple = None, + postTupleSpace = None + ) + } + + "reject reserved keyword as annotation identifier" in { + val body = + // `fn` is a reserved keyword and cannot be used as an annotation identifier. + // Similarly, `Contract TxScript etc` also cannot be used as an identifier + parseSoft("@fn function()") + + val annotation = + body + .toNode() + .walkDown + .collectFirst { + case Node(annotation: SoftAST.Annotation, _) => + annotation + } + .value + + annotation shouldBe + SoftAST.Annotation( + index = indexOf(">>@<>@<><>@anno<<"), + at = SoftAST.At( + SoftAST.Code( + index = indexOf(">>@<>anno<<"), + text = "anno" + ) + ), + postIdentifierSpace = None, + tuple = None, + postTupleSpace = None + ) + } + + "parsed annotation tuples" in { + val annotation = + parseAnnotation("@anno(a, b, c + d)") + + annotation shouldBe + SoftAST.Annotation( + index = indexOf(">>@anno(a, b, c + d)<<"), + at = SoftAST.At( + SoftAST.Code( + index = indexOf(">>@<>anno<<(a, b, c + d)"), + text = "anno" + ) + ), + postIdentifierSpace = None, + // No need to test the AST for the Tuple. Simply test that a tuple is defined + tuple = Some(annotation.tuple.value), + postTupleSpace = None + ) + } + +} diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala index d4c48266f..1f04fbbaf 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala @@ -26,6 +26,9 @@ object TestParser { def parseSoft(code: String): SoftAST.BlockBody = runSoftParser(SoftParser.parse(_))(code) + def parseAnnotation(code: String): SoftAST.Annotation = + runSoftParser(AnnotationParser.parseOrFail(_))(code) + def parseTemplate(code: String): SoftAST.Template = runSoftParser(TemplateParser.parseOrFail(_))(code) From 4788c4d2cfda2e7183247f2ef6ccdb4c9d920637 Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 14:40:40 +1100 Subject: [PATCH 04/10] Testcases: `FnAnnotationSpec`. Allow multiple annotations. --- .../compiler/parser/soft/FunctionParser.scala | 22 +- .../compiler/parser/soft/ast/SoftAST.scala | 2 +- .../parser/soft/FnAnnotationSpec.scala | 306 ++++++++++++++++++ .../compiler/parser/soft/TestParser.scala | 18 ++ 4 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnAnnotationSpec.scala diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FunctionParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FunctionParser.scala index 9fefbbfae..55142115a 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FunctionParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FunctionParser.scala @@ -24,10 +24,16 @@ import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.SoftAST private object FunctionParser { + /** + * Parses a function. + * + * @return A parsed [[SoftAST.Function]] containing all function information, + * such as its documentation, name, parameters, return type and block expressions. + */ def parseOrFail[Unknown: P]: P[SoftAST.Function] = P { Index ~ - AnnotationParser.parseOrFail.? ~ + AnnotationParser.parseOrFail.rep ~ spaceOrFail.? ~ AccessModifierParser.parseOrFail.? ~ TokenParser.FnOrFail ~ @@ -40,7 +46,7 @@ private object FunctionParser { case (from, annotation, postAnnotationSpace, pub, fnDeceleration, headSpace, signature, tailSpace, block, to) => SoftAST.Function( index = range(from, to), - annotation = annotation, + annotations = annotation, postAnnotationSpace = postAnnotationSpace, pub = pub, fn = fnDeceleration, @@ -51,6 +57,12 @@ private object FunctionParser { ) } + /** + * Parses a function's signature. + * + * @return A parsed [[SoftAST.FunctionSignature]] containing the details of the function signature, + * such as its name, parameters and return type. + */ private def signature[Unknown: P]: P[SoftAST.FunctionSignature] = P(Index ~ identifier ~ spaceOrFail.? ~ ParameterParser.parse ~ spaceOrFail.? ~ returnSignature ~ Index) map { case (from, fnName, headSpace, params, tailSpace, returns, to) => @@ -64,6 +76,12 @@ private object FunctionParser { ) } + /** + * Parses a function's return type. + * + * @return The parsed [[SoftAST.FunctionReturnAST]], either containing the return + * type or an error indicating that the return type is expected. + */ private def returnSignature[Unknown: P]: P[SoftAST.FunctionReturnAST] = P(Index ~ (TokenParser.ForwardArrow ~ spaceOrFail.? ~ TypeParser.parse).? ~ Index) map { case (from, Some((forwardArrow, space, tpe)), to) => diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala index 5a74cb27b..6b9ce685e 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala @@ -512,7 +512,7 @@ object SoftAST { case class Function( index: SourceIndex, - annotation: Option[Annotation], + annotations: Seq[Annotation], postAnnotationSpace: Option[Space], pub: Option[AccessModifier], fn: Fn, diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnAnnotationSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnAnnotationSpec.scala new file mode 100644 index 000000000..73b5025fe --- /dev/null +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnAnnotationSpec.scala @@ -0,0 +1,306 @@ +// Copyright 2024 The Alephium Authors +// This file is part of the alephium project. +// +// The library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the library. If not, see http://www.gnu.org/licenses/. + +package org.alephium.ralph.lsp.access.compiler.parser.soft + +import org.alephium.ralph.lsp.access.compiler.parser.soft.TestParser._ +import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token} +import org.alephium.ralph.lsp.access.util.TestCodeUtil._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.OptionValues._ + +class FnAnnotationSpec extends AnyWordSpec with Matchers { + + "annotations are not defined" in { + val function = + parseFunction("fn function() -> ()") + + function.annotations shouldBe empty + } + + "report missing identifier" in { + val function = + parseFunction { + """@ + |fn function() -> () + |""".stripMargin + } + + function.annotations should have size 1 + function.annotations.head shouldBe + SoftAST.Annotation( + index = indexOf { + """>>@ + |< () + |""".stripMargin + }, + at = SoftAST.At( + SoftAST.Code( + index = indexOf { + """>>@<< + |fn function() -> () + |""".stripMargin + }, + text = Token.At.lexeme + ) + ), + preIdentifierSpace = Some( + SoftAST.Space( + SoftAST.Code( + index = indexOf { + """@>> + |< () + |""".stripMargin + }, + text = Token.Newline.lexeme + ) + ) + ), + identifier = SoftAST.IdentifierExpected( + indexOf { + """@ + |>>< () + |""".stripMargin + } + ), + postIdentifierSpace = None, + tuple = None, + postTupleSpace = None + ) + } + + "annotations are defined" when { + "without head space and empty parameters" in { + val function = + parseFunction { + """@annotation + |fn function() -> () + |""".stripMargin + } + + val expected = + SoftAST.Annotation( + index = indexOf { + """>>@annotation + |< () + |""".stripMargin + }, + at = SoftAST.At( + SoftAST.Code( + indexOf { + """>>@< () + |""".stripMargin + }, + Token.At.lexeme + ) + ), + preIdentifierSpace = None, + identifier = SoftAST.Identifier( + SoftAST.Code( + indexOf { + """@>>annotation<< + |fn function() -> () + |""".stripMargin + }, + "annotation" + ) + ), + postIdentifierSpace = Some( + SoftAST.Space( + code = SoftAST.Code( + indexOf { + """@annotation>> + |< () + |""".stripMargin + }, + Token.Newline.lexeme + ) + ) + ), + tuple = None, + postTupleSpace = None + ) + + function.annotations should have size 1 + function.annotations.head shouldBe expected + } + + "two annotations with expressions and spaces" in { + val code = + """@ annotation (a, b + c, c = 4) + |@ last () + |fn function() -> () + |""".stripMargin + + val function = + parseFunction(code) + + function.annotations should have size 2 + + function.annotations.head shouldBe + SoftAST.Annotation( + index = indexOf { + """>>@ annotation (a, b + c, c = 4) + |<<@ last () + |fn function() -> () + |""".stripMargin + }, + at = SoftAST.At( + SoftAST.Code( + index = indexOf { + """>>@<< annotation (a, b + c, c = 4) + |@ last () + |fn function() -> () + |""".stripMargin + }, + text = Token.At.lexeme + ) + ), + preIdentifierSpace = Some( + SoftAST.Space( + SoftAST.Code( + index = indexOf { + """@>> < () + |""".stripMargin + }, + text = " " + ) + ) + ), + identifier = SoftAST.Identifier( + SoftAST.Code( + index = indexOf { + """@ >>annotation<< (a, b + c, c = 4) + |@ last () + |fn function() -> () + |""".stripMargin + }, + text = "annotation" + ) + ), + postIdentifierSpace = Some( + SoftAST.Space( + code = SoftAST.Code( + index = indexOf { + """@ annotation>> <<(a, b + c, c = 4) + |@ last () + |fn function() -> () + |""".stripMargin + }, + text = " " + ) + ) + ), + tuple = + // This test case is not targeting Tuples AST, simply parse it. + Some(findAnnotation("annotation")(code).flatMap(_.tuple).value), + postTupleSpace = Some( + SoftAST.Space( + SoftAST.Code( + index = indexOf { + """@ annotation (a, b + c, c = 4)>> + |<<@ last () + |fn function() -> () + |""".stripMargin + }, + text = Token.Newline.lexeme + ) + ) + ) + ) + + function.annotations.last shouldBe + SoftAST.Annotation( + index = indexOf { + """@ annotation (a, b + c, c = 4) + |>>@ last () + |< () + |""".stripMargin + }, + at = SoftAST.At( + SoftAST.Code( + index = indexOf { + """@ annotation (a, b + c, c = 4) + |>>@<< last () + |fn function() -> () + |""".stripMargin + }, + text = Token.At.lexeme + ) + ), + preIdentifierSpace = Some( + SoftAST.Space( + SoftAST.Code( + index = indexOf { + """@ annotation (a, b + c, c = 4) + |@>> < () + |""".stripMargin + }, + text = " " + ) + ) + ), + identifier = SoftAST.Identifier( + SoftAST.Code( + index = indexOf { + """@ annotation (a, b + c, c = 4) + |@ >>last<< () + |fn function() -> () + |""".stripMargin + }, + text = "last" + ) + ), + postIdentifierSpace = Some( + SoftAST.Space( + code = SoftAST.Code( + index = indexOf { + """@ annotation (a, b + c, c = 4) + |@ last>> <<() + |fn function() -> () + |""".stripMargin + }, + text = " " + ) + ) + ), + tuple = + // This test case is not targeting Tuples AST, simply parse it. + Some(findAnnotation("last")(code).flatMap(_.tuple).value), + postTupleSpace = Some( + SoftAST.Space( + SoftAST.Code( + index = indexOf { + """@ annotation (a, b + c, c = 4) + |@ last ()>> + |< () + |""".stripMargin + }, + text = Token.Newline.lexeme + ) + ) + ) + ) + + } + } + +} diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala index 1f04fbbaf..064476612 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala @@ -19,6 +19,7 @@ package org.alephium.ralph.lsp.access.compiler.parser.soft import fastparse.{P, Parsed} import org.alephium.ralph.error.CompilerError import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token} +import org.alephium.ralph.lsp.utils.Node import org.scalatest.matchers.should.Matchers._ object TestParser { @@ -56,6 +57,23 @@ object TestParser { def parseReservedTokenOrError(code: String): Either[Parsed.Failure, Token.Reserved] = runAnyParserOrError(TokenParser.Reserved(_))(code) + def findAnnotation(identifier: String)(code: String): Option[SoftAST.Annotation] = + findAnnotation( + identifier = identifier, + body = parseSoft(code) + ) + + def findAnnotation( + identifier: String, + body: SoftAST.BlockBody): Option[SoftAST.Annotation] = + body + .toNode() + .walkDown + .collectFirst { + case Node(annotation @ SoftAST.Annotation(_, _, _, id: SoftAST.Identifier, _, _, _), _) if id.code.text == identifier => + annotation + } + private def runSoftParser[T <: SoftAST](parser: P[_] => P[T])(code: String): T = { val ast = runAnyParser(parser)(code) From 6a113d78010203f1a79c9cb518beb66c69244df3 Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 14:59:59 +1100 Subject: [PATCH 05/10] Testcase: Documentation of `Annotation` --- .../compiler/parser/soft/AnnotationSpec.scala | 78 ++++++++++++++++++- .../compiler/parser/soft/TestParser.scala | 15 +++- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala index 9654cd67c..8a7dc3688 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala @@ -82,7 +82,7 @@ class AnnotationSpec extends AnyWordSpec with Matchers { } } - "parsed annotation identifier" in { + "parse annotation identifier" in { val annotation = parseAnnotation("@anno") @@ -108,7 +108,7 @@ class AnnotationSpec extends AnyWordSpec with Matchers { ) } - "parsed annotation tuples" in { + "parse annotation tuples" in { val annotation = parseAnnotation("@anno(a, b, c + d)") @@ -135,4 +135,78 @@ class AnnotationSpec extends AnyWordSpec with Matchers { ) } + "parse annotation documentation" in { + val code = + """// documentation + |@anno + |""".stripMargin + + val annotation = + parseAnnotation(code) + + val expectedComment = + findFirstComment(annotation).value + + // Test that the Comment tree's code is parsed. + // No need to assert the comments AST here. + // These test-cases are for annotations. + expectedComment.toCode() shouldBe + """// documentation + |""".stripMargin + + annotation shouldBe + SoftAST.Annotation( + index = indexOf { + """>>// documentation + |@anno + |<<""".stripMargin + }, + at = SoftAST.At( + indexOf { + """>>// documentation + |@<>@<>anno<< + |""".stripMargin + }, + text = "anno" + ) + ), + postIdentifierSpace = Some( + SoftAST.Space( + SoftAST.Code( + index = indexOf { + """// documentation + |@anno>> + |<<""".stripMargin + }, + text = Token.Newline.lexeme + ) + ) + ), + tuple = None, + postTupleSpace = None + ) + } + } diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala index 064476612..a107c5f61 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala @@ -60,13 +60,13 @@ object TestParser { def findAnnotation(identifier: String)(code: String): Option[SoftAST.Annotation] = findAnnotation( identifier = identifier, - body = parseSoft(code) + ast = parseSoft(code) ) def findAnnotation( identifier: String, - body: SoftAST.BlockBody): Option[SoftAST.Annotation] = - body + ast: SoftAST): Option[SoftAST.Annotation] = + ast .toNode() .walkDown .collectFirst { @@ -74,6 +74,15 @@ object TestParser { annotation } + def findFirstComment(body: SoftAST): Option[SoftAST.Comments] = + body + .toNode() + .walkDown + .collectFirst { + case Node(comments @ SoftAST.Comments(_, _, _, _), _) => + comments + } + private def runSoftParser[T <: SoftAST](parser: P[_] => P[T])(code: String): T = { val ast = runAnyParser(parser)(code) From 01839389e581a7bb9895ea75a8b0f8ba3521c800 Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 15:06:22 +1100 Subject: [PATCH 06/10] Testcase: Report missing closing paren --- .../compiler/parser/soft/AnnotationSpec.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala index 8a7dc3688..0820cd147 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationSpec.scala @@ -48,6 +48,23 @@ class AnnotationSpec extends AnyWordSpec with Matchers { ) } + "report missing closing parenthesis" in { + val annotation = + parseAnnotation("@anno(") + + // opening paren is parsed + annotation.tuple.value.openParen shouldBe + SoftAST.OpenParen( + SoftAST.Code( + index = indexOf("@anno>>(<<"), + text = Token.OpenParen.lexeme + ) + ) + + // closing paren is reported as expected + annotation.tuple.value.closeParen shouldBe SoftAST.CloseParenExpected(indexOf("@anno(>><<")) + } + "reject reserved keyword as annotation identifier" in { val body = // `fn` is a reserved keyword and cannot be used as an annotation identifier. From 6fef54393c2a6b149e731252cb65fa3934ddd2d6 Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 15:19:21 +1100 Subject: [PATCH 07/10] Document `AnnotationParser` --- .../compiler/parser/soft/AnnotationParser.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala index 302d54648..825a397c2 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/AnnotationParser.scala @@ -24,6 +24,18 @@ import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.SoftAST private object AnnotationParser { + /** + * Parses a single annotation and its trailing space. + * + * The configuration within an `Annotation` syntax allows for various expressions [[SoftAST.ExpressionAST]]. + * For example, the following will parse successfully: + * + * {{{ + * @using(a = functionCall(), b = object.getConfig(param), 1 + 1, blah) + * }}} + * + * @return Returns a [[SoftAST.Annotation]] representation the annotation and its documentation. + */ def parseOrFail[Unknown: P]: P[SoftAST.Annotation] = P { Index ~ From ab09cd59f2e67c70f0cbd069cffae91ef410172a Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 15:24:51 +1100 Subject: [PATCH 08/10] Document `identifierOrFail` --- .../access/compiler/parser/soft/CommonParser.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala index 3eb83bd09..583ad7c12 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala @@ -77,6 +77,18 @@ private object CommonParser { SoftAST.IdentifierExpected(point(from)) } + /** + * Parses identifiers as long as they are not reserved tokens [[Token.Reserved]]. + * + * For example, the following code will result in an [[SoftAST.IdentifierExpected]] error + * because `let` is a reserved token [[Token.Let]]: + * + * {{{ + * fn let() -> () + * }}} + * + * @return A successfully parsed identifier instance [[SoftAST.Identifier]] or a parser error. + */ def identifierOrFail[Unknown: P]: P[SoftAST.Identifier] = P { Index ~ From e6e57bb3c365083024d20b4638930d71726a1e06 Mon Sep 17 00:00:00 2001 From: simerplaha Date: Wed, 11 Dec 2024 15:51:53 +1100 Subject: [PATCH 09/10] TODO --- .../ralph/lsp/access/compiler/parser/soft/CommonParser.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala index 583ad7c12..d59126d89 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/CommonParser.scala @@ -87,6 +87,8 @@ private object CommonParser { * fn let() -> () * }}} * + * TODO: Handle cases such as `fn letter() -> ()` + * * @return A successfully parsed identifier instance [[SoftAST.Identifier]] or a parser error. */ def identifierOrFail[Unknown: P]: P[SoftAST.Identifier] = From 7287fd28e3c2a4d4833e7170eb1ffa5c33166bb9 Mon Sep 17 00:00:00 2001 From: simerplaha Date: Thu, 12 Dec 2024 08:06:54 +1100 Subject: [PATCH 10/10] fix windows build --- .../lsp/access/compiler/parser/soft/FnDecelerationSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala index 4d9ebb017..ab04d198a 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/FnDecelerationSpec.scala @@ -179,8 +179,8 @@ class FnDecelerationSpec extends AnyWordSpec with Matchers { SoftAST.IdentifierExpected( indexOf { """fn function( - | fn >><< - |} + | fn + |>><<} |""".stripMargin } )