Skip to content

Commit

Permalink
Merge pull request #362 from alephium/import_parser
Browse files Browse the repository at this point in the history
`Import` statement parser
  • Loading branch information
simerplaha authored Jan 30, 2025
2 parents 7a1c529 + 8bbc2c5 commit fedfc94
Show file tree
Hide file tree
Showing 11 changed files with 522 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private object BlockParser {
EventParser.parseOrFail |
StructParser.parseOrFail |
FunctionParser.parseOrFail |
ImportParser.parseOrFail |
ExpressionParser.parseOrFail |
CommentParser.parseOrFail |
UnresolvedParser.parseOrFail(stop: _*)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ object Demo extends App {
val ast =
compiler.parseSoft {
"""
|import "a/b"
|
|event Event(a: Type)
|struct Struct { a: Type }
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ private object ExpressionParser {
NumberParser.parseOrFail |
BooleanParser.parseOrFail |
BStringParser.parseOrFail |
StringLiteralParser.parseOrFail |
IdentifierParser.parseOrFail
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.alephium.ralph.lsp.access.compiler.parser.soft

import fastparse._
import fastparse.NoWhitespace.noWhitespaceImplicit
import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra.range
import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token}

private object ImportParser {

/**
* Parses an import statement.
*
* Example syntax:
*
* {{{
* import "some text"
* import "a/b/c/d"
* }}}
*/
def parseOrFail[Unknown: P]: P[SoftAST.Import] =
P {
Index ~
TokenParser.parseOrFail(Token.Import) ~
&(TokenParser.WhileInOrFail(Token.Space, Token.Newline, Token.Quote) | End) ~
SpaceParser.parseOrFail.? ~
StringLiteralParser.parseOrFail.? ~
Index
} map {
case (from, importToken, space, endQuote, to) =>
SoftAST.Import(
index = range(from, to),
importToken = importToken,
postImportSpace = space,
string = endQuote
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.alephium.ralph.lsp.access.compiler.parser.soft

import fastparse._
import fastparse.NoWhitespace.noWhitespaceImplicit
import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra.{point, range}
import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token}

object StringLiteralParser {

/**
* Parses text enclosed in quotes, e.g. `"some text"`.
*
* If the text follows a path format (i.e., it contains forward slashes [[Token.ForwardSlash]]),
* it is parsed as a path.
*/
def parseOrFail[Unknown: P]: P[SoftAST.StringLiteral] =
P {
Index ~
TokenParser.parseOrFail(Token.Quote) ~
Index ~
TextParser.parseOrFail(Token.ForwardSlash, Token.Quote).? ~
path.rep ~
TokenParser.parse(Token.Quote) ~
Index
} map {
case (from, startQuote, headIndex, head, tail, endQuote, to) =>
val headResult =
if (head.isEmpty && tail.nonEmpty)
// if the tail-path is provided, e.g. `import "/abc"` but the head-path is empty,
// report error at the head-path.
Some(SoftAST.CodeStringExpected(point(headIndex)))
else
head

SoftAST.StringLiteral(
index = range(from, to),
startQuote = startQuote,
head = headResult,
tail = tail,
endQuote = endQuote
)
}

/** A string-literal could also define a path if it contains forward slashes */
private def path[Unknown: P]: P[SoftAST.Path] =
P {
Index ~
TokenParser.parseOrFail(Token.ForwardSlash) ~
TextParser.parse(Token.ForwardSlash, Token.Quote) ~
Index
} map {
case (from, slash, text, to) =>
SoftAST.Path(
index = range(from, to),
slash = slash,
text = text
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,34 @@ package org.alephium.ralph.lsp.access.compiler.parser.soft

import fastparse._
import fastparse.NoWhitespace.noWhitespaceImplicit
import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra.range
import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra.{point, range}
import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token}

private object TextParser {

/**
* Parses a text string, stopping at the specified tokens.
*
* @param stop The tokens at which parsing should stop.
* @return One of the following:
* - [[SoftAST.CodeStringExpected]] if the text is empty.
* - [[SoftAST.CodeString]] for non-empty text.
*/
def parse[Unknown: P](stop: Token*): P[SoftAST.CodeStringAST] =
P(Index ~ parseOrFail(stop: _*).?) map {
case (from, None) =>
SoftAST.CodeStringExpected(point(from))

case (_, Some(code)) =>
code
}

/**
* Parses a text string, stopping at the specified tokens.
*
* @param stop The tokens at which parsing should stop.
* @return Non-empty text string.
*/
def parseOrFail[Unknown: P](stop: Token*): P[SoftAST.CodeString] =
P(Index ~ TokenParser.WhileNotOrFail(stop: _*).! ~ Index) map {
case (from, text, to) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ object SoftAST {
params: Group[Token.OpenCurly.type, Token.CloseCurly.type])
extends BodyPartAST

case class Import(
index: SourceIndex,
importToken: TokenDocumented[Token.Import.type],
postImportSpace: Option[Space],
string: Option[StringLiteral])
extends BodyPartAST

/** Syntax: `implements or extends contract(arg1, arg2 ...)` */
case class Inheritance(
index: SourceIndex,
Expand Down Expand Up @@ -463,6 +470,20 @@ object SoftAST {
endTick: TokenDocExpectedAST[Token.Tick.type])
extends ExpressionAST

case class StringLiteral(
index: SourceIndex,
startQuote: TokenDocumented[Token.Quote.type],
head: Option[CodeStringAST],
tail: Seq[Path],
endQuote: TokenDocExpectedAST[Token.Quote.type])
extends ExpressionAST

case class Path(
index: SourceIndex,
slash: TokenDocumented[Token.ForwardSlash.type],
text: CodeStringAST)
extends SoftAST

sealed trait SpaceAST extends SoftAST

case class Space(
Expand Down Expand Up @@ -495,6 +516,8 @@ object SoftAST {

}

sealed trait CodeStringAST extends SoftAST

/**
* Represents a string within a segment of code.
*
Expand All @@ -504,7 +527,14 @@ object SoftAST {
case class CodeString(
index: SourceIndex,
text: String)
extends Code
extends CodeStringAST
with Code

/** Represents a location where a code symbol is expected, but an empty value was provided. */
case class CodeStringExpected(
index: SourceIndex)
extends ExpectedErrorAST("Symbol")
with CodeStringAST

/**
* Represents a token within a segment of code.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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.compiler.parser.soft.ast.TestSoftAST._
import org.alephium.ralph.lsp.access.util.TestCodeUtil._
import org.alephium.ralph.lsp.access.util.TestFastParse.assertIsFastParseError
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class ImportParserSpec extends AnyWordSpec with Matchers {

"fail" when {
"import is a prefix of an identifier" in {
assertIsFastParseError {
parseImport("important")
}
}
}

"succeed" when {
"only the import token is defined" in {
val importAST =
parseImport("import")

importAST shouldBe
SoftAST.Import(
index = indexOf(">>import<<"),
importToken = Import(indexOf(">>import<<")),
postImportSpace = None,
string = None
)
}

"tail space is defined" in {
val importAST =
parseImport("import ")

importAST shouldBe
SoftAST.Import(
index = indexOf(">>import <<"),
importToken = Import(indexOf(">>import<< ")),
postImportSpace = Some(SpaceOne(indexOf("import>> <<"))),
string = None
)
}

"starting quote is defined without space" in {
val importAST =
parseImport("import\"")

importAST shouldBe
SoftAST.Import(
index = indexOf(">>import\"<<"),
importToken = Import(indexOf(">>import<<")),
postImportSpace = None,
string = Some(
SoftAST.StringLiteral(
index = indexOf("import>>\"<<"),
startQuote = Quote(indexOf("import>>\"<<")),
head = None,
tail = Seq.empty,
endQuote = SoftAST.TokenExpected(indexOf("import\">><<"), Token.Quote)
)
)
)
}

"import is fully defined" in {
val importAST =
parseImport("""import "folder/file.ral"""")

importAST shouldBe
SoftAST.Import(
index = indexOf(""">>import "folder/file.ral"<<"""),
importToken = Import(indexOf(""">>import<< "folder/file.ral"""")),
postImportSpace = Some(SpaceOne(indexOf("""import>> <<"folder/file.ral""""))),
string = Some(
SoftAST.StringLiteral(
index = indexOf("""import >>"folder/file.ral"<<"""),
startQuote = Quote(indexOf("""import >>"<<folder/file.ral"""")),
head = Some(SoftAST.CodeString(indexOf("""import ">>folder<</file.ral""""), "folder")),
tail = Seq(
SoftAST.Path(
index = indexOf("""import "folder>>/file.ral<<""""),
slash = ForwardSlash(indexOf("""import "folder>>/<<file.ral"""")),
text = SoftAST.CodeString(indexOf("""import "folder/>>file.ral<<""""), "file.ral")
)
),
endQuote = Quote(indexOf("""import "folder/file.ral>>"<<"""))
)
)
)
}
}

}
Loading

0 comments on commit fedfc94

Please sign in to comment.