Skip to content

Commit

Permalink
Merge pull request #937 from ross-weir/impl-contract-template-compiler
Browse files Browse the repository at this point in the history
[WIP] Contract template compiler
  • Loading branch information
aslesarenko authored Dec 13, 2023
2 parents 4a01b4e + 33c50c2 commit 2097bc1
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 0 deletions.
196 changes: 196 additions & 0 deletions parsers/shared/src/main/scala/sigmastate/lang/ContractParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package sigmastate.lang

import sigmastate.SType
import fastparse._
import fastparse.NoWhitespace._
import SigmaParser._
import sigmastate.Values.SValue
import sigmastate.lang.syntax.Basic

/**
* Represents a docstring line.
*/
case class DocumentationToken(kind: DocumentationToken.Kind, name: Option[String], body: Option[String])

/**
* Companion object containing the classes required for describing an docstring token.
*/
object DocumentationToken {
def apply(kind: Kind): DocumentationToken = new DocumentationToken(kind, None, None)

def apply(kind: Kind, body: String): DocumentationToken = new DocumentationToken(kind, None, Option(body))

def apply(kind: TagKind, name: String, body: String): DocumentationToken =
new DocumentationToken(kind, Option(name), Option(body))

/**
* Represents a documentation remark.
*/
sealed abstract class Kind

/**
* Documents an untagged doc description, this is simply a line of text anywhere in the docstring.
*/
case object Description extends Kind

/**
* Represents an empty doc line.
*/
case object EmptyLine extends Kind

/**
* Represents a line starting with a tag (@) but is not supported.
*/
case object UnsupportedTag extends Kind

/**
* Represents a labeled documentation remark.
*/
sealed abstract class TagKind(val label: String) extends Kind

/**
* Documents a specific value parameter of the contract template.
*/
case object Param extends TagKind("@param")

/**
* Documents the return value of the contract template.
*/
case object Return extends TagKind("@returns")
}

/**
* Holds values extracted from a `@param` tag line in docstrings.
*
* @param name Name of the parameter.
* @param description Description of the parameter.
*/
case class ParameterDoc(name: String, description: String)

/**
* Contract template documentation extracted from the preceding docstring.
*
* @param description Top level contract template description.
* @param params Contract template parameters as defined in the docstring.
*/
case class ContractDoc(description: String, params: Seq[ParameterDoc])

object ContractDoc {
def apply(tokens: Seq[DocumentationToken]): ContractDoc = {
val nonEmptyTokens = tokens.dropWhile(_.kind == DocumentationToken.EmptyLine)

val (description, paramTokens) = nonEmptyTokens.span(_.kind == DocumentationToken.Description)
val descriptionText = description.flatMap(_.body).mkString(" ")

def extractParamDocs(tokens: Seq[DocumentationToken]): Seq[ParameterDoc] = {
tokens match {
case DocumentationToken(kind: DocumentationToken.TagKind, Some(name), Some(body)) +: tail if kind == DocumentationToken.Param =>
// grab the succeeding description tokens if there are any, this is multiline description of params
val (descTokens, remainingTokens) = tail.span(_.kind == DocumentationToken.Description)
val fullDescription = (body +: descTokens.flatMap(_.body)).mkString(" ")
ParameterDoc(name, fullDescription) +: extractParamDocs(remainingTokens)
case _ +: tail => extractParamDocs(tail)
case Seq() => Seq()
}
}

ContractDoc(descriptionText, extractParamDocs(paramTokens))
}
}

/**
* Represents a parsed parameter defined in a contracts signature.
*
* @param name The name of the parameter.
* @param tpe The type of the parameter.
* @param defaultValue The default value assigned to the parameter, if it exists.
*/
case class ContractParam(name: String, tpe: SType, defaultValue: Option[SType#WrappedType])

/**
* Represents the signature of a contract.
*
* @param name The name of the contract.
* @param params The parameters for the contract.
*/
case class ContractSignature(name: String, params: Seq[ContractParam])

/**
* The result of parsing a contract template.
* Includes the parsed docstring preceding the contract as well as the contract signature.
*
* @param docs Docstring parsed from the contract.
* @param signature Signature of the contract.
* @param body Parsed SValue of the contract.
*/
case class ParsedContractTemplate(docs: ContractDoc, signature: ContractSignature, body: SValue)

/**
* Parsers that handle parsing contract definitions excluding the contract body which is handled
* by [[SigmaParser]].
*/
object ContractParser {
def parse(source: String): Parsed[ParsedContractTemplate] = fastparse.parse(source, parse(_))

/**
* Parse a contract up until the open brace (i.e the beginning of the contract logic).
*/
def parse[_: P]: P[ParsedContractTemplate] = P(Docs.parse ~ Basic.Newline ~ Signature.parse ~ WL.? ~ "=" ~ WL.? ~ AnyChar.rep(1).!).map(s => ParsedContractTemplate(s._1, s._2, SigmaParser(s._3).get.value))

/**
* Parsers for contract docstrings.
* Contract docstrings follow similiar structure as scaladocs except only support a subset
* of documentation tags such as @param & @returns.
*/
object Docs {

import DocumentationToken._

def parse(source: String): Parsed[ContractDoc] = {
fastparse.parse(source, parse(_))
}

def parse[_: P]: P[ContractDoc] = P(" ".rep.? ~ "/*" ~ docLine.rep ~ " ".rep.? ~ "*/").map(ContractDoc.apply)

def linePrefix[_: P] = P(WL.? ~ "*" ~ " ".rep.? ~ !"/")

def word[_: P] = CharsWhile(c => c != ' ')

def charUntilNewLine[_: P] = CharsWhile(c => c != '\n')

def unsupportedTag[_: P] = P("@" ~ charUntilNewLine.?).map(_ => DocumentationToken(UnsupportedTag))

def returnTag[_: P] = P("@returns").map(_ => DocumentationToken(Return))

def paramTag[_: P] = P("@param" ~ WL ~ word.! ~ WL ~ charUntilNewLine.!).map(s => DocumentationToken(Param, s._1, s._2))

def tag[_: P] = P(returnTag | paramTag | unsupportedTag)

def emptyLine[_: P] = P(("" | " ".rep.?) ~ &(Basic.Newline)).map(_ => DocumentationToken(EmptyLine))

def description[_: P] = P(!"@" ~ charUntilNewLine.!).map(s => DocumentationToken(Description, s))

def docLine[_: P] = P(linePrefix ~ (emptyLine | description | tag) ~ Basic.Newline)
}

/**
* Parsers for contract signatures.
* A contract signature has essentially the same shape as a scala function signature.
*/
object Signature {

def parse(source: String): Parsed[ContractSignature] = {
fastparse.parse(source, parse(_))
}

def parse[_: P]: P[ContractSignature] = P(annotation ~ WL.? ~ `def` ~ WL.? ~ Id.! ~ params).map(s => ContractSignature(s._1, s._2.getOrElse(Seq())))

def annotation[_: P] = P("@contract")

def paramDefault[_: P] = P(WL.? ~ `=` ~ WL.? ~ ExprLiteral).map(s => s.asWrappedType)

def param[_: P] = P(WL.? ~ Id.! ~ ":" ~ Type ~ paramDefault.?).map(s => ContractParam(s._1, s._2, s._3))

def params[_: P] = P("(" ~ param.rep(1, ",").? ~ ")")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package sigmastate.lang

import org.scalatest.matchers.should.Matchers
import org.scalatest.propspec.AnyPropSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import sigmastate.Values._
import sigmastate._

class ContractParserSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matchers {
property("parses docstring") {
val doc =
"""/** This is my contracts description.
|* Here is another line describing what it does in more detail.
|*
|* @param p1 describe p1
|* @param p2 description of the 2nd parameter
|* which is pretty complex and on many
|* lines to describe functions
|* @param p3 the final parameter
|* @return
|*/""".stripMargin
val contractDoc = ContractParser.Docs.parse(doc).get.value

contractDoc.description shouldBe "This is my contracts description. Here is another line describing what it does in more detail."
contractDoc.params should contain theSameElementsInOrderAs Seq(
ParameterDoc("p1", "describe p1"),
ParameterDoc("p2", "description of the 2nd parameter which is pretty complex and on many lines to describe functions"),
ParameterDoc("p3", "the final parameter")
)
}

property("parses contract signature") {
val source = "@contract def contractName(p1: Int = 5, p2: String = \"default string\", param3: Long)"
val parsed = ContractParser.Signature.parse(source).get.value

parsed.name shouldBe "contractName"
parsed.params should contain theSameElementsInOrderAs Seq(
ContractParam("p1", SInt, Some(IntConstant(5).asWrappedType)),
ContractParam("p2", SString, Some(StringConstant("default string").asWrappedType)),
ContractParam("param3", SLong, None)
)
}

property("parses contract signature no params") {
val source = "@contract def contractName()"
val parsed = ContractParser.Signature.parse(source).get.value

parsed.name shouldBe "contractName"
parsed.params should contain theSameElementsInOrderAs Seq()
}
}
6 changes: 6 additions & 0 deletions sc/shared/src/main/scala/sigmastate/lang/SigmaCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ class SigmaCompiler private(settings: CompilerSettings) {
CompilerResult(env, "<no source code>", compiledGraph, compiledTree)
}

/** Compiles the given parsed contract source. */
def compileParsed(env: ScriptEnv, parsedExpr: SValue)(implicit IR: IRContext): CompilerResult[IR.type] = {
val typed = typecheck(env, parsedExpr)
compileTyped(env, typed)
}

/** Unlowering transformation, which replaces some operations with equivalent MethodCall
* node. This replacement is only defined for some operations.
* This is inverse to `lowering` which is performed during compilation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package sigmastate.lang

import fastparse.Parsed
import org.ergoplatform.sdk.ContractTemplate
import sigmastate.Values.SValue
import sigmastate.lang.syntax.ParserException
import org.ergoplatform.sdk.NetworkType
import sigmastate.eval.CompiletimeIRContext
import org.ergoplatform.sdk.Parameter
import sigmastate.interpreter.Interpreter.ScriptEnv

/** Compiler which compiles Ergo contract templates into a [[ContractTemplate]]. */
class SigmaTemplateCompiler(networkPrefix: Byte) {
val sigmaCompiler = new SigmaCompiler(networkPrefix)

/**
* Compiles the provided contract source code into a [[ContractTemplate]].
*
* @param source The ErgoScript contract source code.
* @return The contract template.
*/
def compile(env: ScriptEnv, source: String): ContractTemplate = {
ContractParser.parse(source) match {
case Parsed.Success(template, _) => {
implicit val ir = new CompiletimeIRContext
val result = sigmaCompiler.compileParsed(env, template.body)
assemble(template, result.buildTree)
}
case f: Parsed.Failure =>
throw new ParserException(s"Contract template syntax error: $f", Some(SourceContext.fromParserFailure(f)))
}
}

private def assemble(parsed: ParsedContractTemplate, expr: SValue): ContractTemplate = {
val (constTypes, constValueOpts) = parsed.signature.params.map(param => (param.tpe, param.defaultValue)).unzip
val allConstValuesAreNone = constValueOpts.forall(_.isEmpty)
val constValues =
if (allConstValuesAreNone) None
else Some(constValueOpts.toIndexedSeq)
val contractParams = parsed.signature.params.zipWithIndex.map { case (param, idx) =>
val desc: String = parsed.docs.params.find(docParam => docParam.name == param.name).map(_.description).getOrElse("")
Parameter(param.name, desc, idx)
}

ContractTemplate(
name = parsed.signature.name,
description = parsed.docs.description,
constTypes = constTypes.toIndexedSeq,
constValues = constValues,
parameters = contractParams.toIndexedSeq,
expressionTree = expr.toSigmaProp
)
}
}

object SigmaTemplateCompiler {
def apply(networkPrefix: Byte): SigmaTemplateCompiler = new SigmaTemplateCompiler(networkPrefix)
}
Loading

0 comments on commit 2097bc1

Please sign in to comment.