diff --git a/analysis/src/main/scala/org/polystat/odin/analysis/inlining/Context.scala b/analysis/src/main/scala/org/polystat/odin/analysis/inlining/Context.scala new file mode 100644 index 00000000..f0bdbeb2 --- /dev/null +++ b/analysis/src/main/scala/org/polystat/odin/analysis/inlining/Context.scala @@ -0,0 +1,126 @@ +package org.polystat.odin.analysis.inlining + +import higherkindness.droste.data.Fix +import org.polystat.odin.core.ast._ +import org.polystat.odin.parser.eo.Parser +import org.polystat.odin.backend.eolang.ToEO.instances.progToEO +import org.polystat.odin.backend.eolang.ToEO.ops.ToEOOps + +// 0. Resolve explicit BigInt chains (^.^.aboba) during parsing +// 1. Create the first context that includes names of all top-lvl EOObjs +// 1.1. Replace All top-level applications (a > b) with application of +// 0-locators ($.a > b) +// [pending approval] +// 2. Recursively explore the AST +// 2.1 Upon meeting an EOSimpleAPP() -> resolve it using the current context +// into EOSimpleAppWithLocator() +// 2.2 Upon meeting a EOObj() -> use its contents to update the context and pass +// it in the next recursive call + +object Context { + + type Context = Map[String, BigInt] + + def resolveLocator( + ctx: Context, + app: EOSimpleApp[Fix[EOExpr]], + currentDepth: BigInt + ): EOSimpleAppWithLocator[Fix[EOExpr]] = { + val name: String = app.name + val depth: BigInt = ctx.getOrElse(name, 0) + + EOSimpleAppWithLocator(app.name, currentDepth - depth) + } + + def rebuildContext( + ctx: Context, + currentDepth: BigInt, + objs: Vector[EOBnd[Fix[EOExpr]]], + freeAttrs: Option[Vector[LazyName]] + ): Context = { + val objCtx = objs + .flatMap { + case bndExpr: EOBndExpr[Fix[EOExpr]] => Some(bndExpr) + case _ => None + } + .map(bnd => bnd.bndName.name.name -> currentDepth) + .toMap + val argCtx = freeAttrs match { + case Some(value) => value.map(lName => lName.name -> currentDepth).toMap + case None => Map.empty + } + + ctx ++ objCtx ++ argCtx + } + + def setLocators(code: EOProg[Fix[EOExpr]]): EOProg[Fix[EOExpr]] = { + def exprHelper(ctx: Context, depth: BigInt)( + expr: EOExpr[Fix[EOExpr]] + ): EOExpr[Fix[EOExpr]] = + expr match { + case obj @ EOObj(freeAttrs, _, bndAttrs) => + val newDepth = depth + 1 + val newCtx = rebuildContext(ctx, newDepth, bndAttrs, Some(freeAttrs)) + + obj.copy(bndAttrs = bndAttrs.map(bndExprHelper(newCtx, newDepth))) + + case app: EOSimpleApp[Fix[EOExpr]] => resolveLocator(ctx, app, depth) + case EOCopy(Fix(trg), args) => + EOCopy( + trg = Fix(exprHelper(ctx, depth)(trg)), + args = args.map(bndHelper(ctx, depth)) + ) + case dot @ EODot(Fix(src), _) => + dot.copy(src = Fix(exprHelper(ctx, depth)(src))) + case other => other + } + + def bndExprHelper(ctx: Context, depth: BigInt)( + bnd: EOBndExpr[Fix[EOExpr]] + ): EOBndExpr[Fix[EOExpr]] = + bnd match { + case bnd @ EOBndExpr(_, Fix(expr)) => + bnd.copy(expr = Fix(exprHelper(ctx, depth)(expr))) + } + + def bndHelper(ctx: Context, depth: BigInt)( + bnd: EOBnd[Fix[EOExpr]] + ): EOBnd[Fix[EOExpr]] = + bnd match { + case EOAnonExpr(Fix(expr)) => + EOAnonExpr(Fix(exprHelper(ctx, depth)(expr))) + case bnd @ EOBndExpr(_, Fix(expr)) => + bnd.copy(expr = Fix(exprHelper(ctx, depth)(expr))) + } + + val initialCtx = + rebuildContext(Map(), 0, code.bnds, None) + code.copy(bnds = code.bnds.map(bndHelper(initialCtx, 0))) + } + + def main(args: Array[String]): Unit = { + val code: String = + """ + |[] > outer + | [] > self + | 256 > magic + | [] > dummy + | [outer] > cock + | outer > @ + | outer.self > @ + | self "yahoo" > @ + | [self] > method + | self.magic > @ + | + |""".stripMargin + + Parser + .parse(code) + .map(setLocators) match { + case Left(value) => println(value) + case Right(value) => println(value.toEOPretty) + } + + } + +} diff --git a/backends/eolang/src/main/scala/org/polystat/odin/backend/eolang/ToEO.scala b/backends/eolang/src/main/scala/org/polystat/odin/backend/eolang/ToEO.scala index 5ce18af6..e7496eaf 100644 --- a/backends/eolang/src/main/scala/org/polystat/odin/backend/eolang/ToEO.scala +++ b/backends/eolang/src/main/scala/org/polystat/odin/backend/eolang/ToEO.scala @@ -1,6 +1,5 @@ package org.polystat.odin.backend.eolang -import cats.implicits.toBifunctorOps import higherkindness.droste.data.Fix import EOBndRepr.instances._ import ToEO.ops.ToEOOps @@ -10,7 +9,7 @@ import inlineorlines._ import inlineorlines.ops._ import org.polystat.odin.core.ast.astparams.EOExprOnly import org.polystat.odin.core.ast._ -import org.polystat.odin.utils.text.indent +import org.polystat.odin.utils.text.{escape, indent} trait ToEO[T, R] { def toEO(node: T): R @@ -66,7 +65,7 @@ object ToEO { override def toEO(node: EOMetas): Lines = Lines( node.pack.map(p => s"${Constants.SYMBS.META_PREFIX}package $p") ++ - node.metas.map(_.toEO.value) + node.metas.map(_.toEO.value).appended("\n") ) } @@ -194,8 +193,9 @@ object ToEO { override def toEO(node: EOApp[EOExprOnly]): InlineOrLines = node match { case n: EOSimpleApp[EOExprOnly] => n.toEO: Inline - case n: EODot[EOExprOnly] => n.toEO - case n: EOCopy[EOExprOnly] => n.toEO: Lines + case n: EOSimpleAppWithLocator[EOExprOnly] => n.toEO: Inline + case n: EODot[EOExprOnly] => n.toEO: Inline + case n: EOCopy[EOExprOnly] => n.toEO } } @@ -208,51 +208,154 @@ object ToEO { } - implicit val dotToEO: ToEO[EODot[EOExprOnly], InlineOrLines] = - new ToEO[EODot[EOExprOnly], InlineOrLines] { - def usualDotNotation(n: String)(s: String): String = s"$s.$n" + implicit val simpleAppWithLocatorToEO: ToEO[EOSimpleAppWithLocator[EOExprOnly], Inline] = + new ToEO[EOSimpleAppWithLocator[EOExprOnly], Inline] { - def reverseDotNotation(n: String)( - ls: Iterable[String] - ): Iterable[String] = - Vector(s"$n.") ++ ls.map(indent) + override def toEO(node: EOSimpleAppWithLocator[EOExprOnly]): Inline = + Inline( + if (node.locator == 0) s"$$.${node.name}" + else List + .fill(node.locator.toInt)("^") + .appended(node.name) + .mkString(".") + ) + + } + + implicit val dotToEO: ToEO[EODot[EOExprOnly], Inline] = + new ToEO[EODot[EOExprOnly], Inline] { - def dotNotation(n: String)(eoExpr: InlineOrLines): InlineOrLines = - eoExpr.bimap( - usualDotNotation(n), - reverseDotNotation(n) + override def toEO(node: EODot[EOExprOnly]): Inline = { + Inline( + List[String]( + renderArgSingleLine(EOAnonExpr(node.src)), + node.name + ).mkString(".") ) + } - def objCases(name: String)(obj: EOObj[EOExprOnly]): InlineOrLines = - dotNotation(name)(obj.toEO) + } - def appCases(name: String)(app: EOApp[EOExprOnly]): InlineOrLines = - app match { - case n: EOSimpleApp[EOExprOnly] => dotNotation(name)(n.toEO: Inline) - case n: EODot[EOExprOnly] => dotNotation(name)(n.src.toEO) - case n: EOCopy[EOExprOnly] => dotNotation(name)(n.toEO: Lines) - } + def renderObjSingleLine(obj: EOObj[EOExprOnly]): Inline = { + val params: String = Constants.SYMBS.FREE_ATTR_DECL_ST + + obj.freeAttrs.map(_.name).mkString(" ") + + obj + .varargAttr + .fold[String]("") { vararg => + val prefix = if (obj.freeAttrs.isEmpty) "" else " " + prefix + vararg.name + Constants.SYMBS.VARARG_MOD + } + + Constants.SYMBS.FREE_ATTR_DECL_ED + + val bndAttrs: String = + if (obj.bndAttrs.isEmpty) + "" + else { + " " + + obj + .bndAttrs + .map(bnd => s"(${renderEOBndSingleLine(bnd).value})") + .mkString(" ") + } - def dataCases(name: String)(data: EOData[EOExprOnly]): InlineOrLines = - dotNotation(name)(data.toEO) + Inline(List(params, bndAttrs).mkString) + } - override def toEO(node: EODot[EOExprOnly]): InlineOrLines = { - Fix.un(node.src) match { - case n: EOObj[EOExprOnly] => objCases(node.name)(n) - case n: EOApp[EOExprOnly] => appCases(node.name)(n) - case n: EOData[EOExprOnly] => dataCases(node.name)(n) + def renderAppSingleLine(app: EOApp[EOExprOnly]): Inline = { + app match { + case app: EOSimpleApp[EOExprOnly] => simpleAppToEO.toEO(app) + case awl: EOSimpleAppWithLocator[EOExprOnly] => + simpleAppWithLocatorToEO.toEO(awl) + case dot: EODot[EOExprOnly] => dotToEO.toEO(dot) + case cp: EOCopy[EOExprOnly] => renderCopySingleLine(cp) + } + } + + def renderEOExprSingleLine(expr: EOExprOnly): Inline = { + Fix.un(expr) match { + case obj: EOObj[EOExprOnly] => renderObjSingleLine(obj) + case app: EOApp[EOExprOnly] => renderAppSingleLine(app) + case data: EOData[EOExprOnly] => renderDataSingleLine(data) + } + } + + def renderNamedBndSingleLine(name: EONamedBnd): String = { + name match { + case EOAnyNameBnd(name) => name match { + case LazyName(name) => name + case ConstName(name) => name + Constants.SYMBS.CONST_MOD } - } + case EODecoration => Constants.ATTRS.DECORATION + } + } + def renderEOBndSingleLine(bnd: EOBnd[EOExprOnly]): Inline = { + bnd match { + case EOAnonExpr(expr) => renderEOExprSingleLine(expr) + case EOBndExpr(bndName, expr) => + Inline( + List[String]( + renderEOExprSingleLine(expr).value, + Constants.SYMBS.BINDING, + renderNamedBndSingleLine(bndName) + ).mkString(" ") + ) + } + } + + def renderArraySingleLine(arr: EOArray[EOExprOnly]): Inline = { + val elems: String = arr.elems.map(renderArgSingleLine).mkString(" ") + Inline(s"${Constants.SYMBS.ARRAY_START} $elems") + } + + def renderDataSingleLine(data: EOData[EOExprOnly]): Inline = { + data match { + case arr: EOArray[EOExprOnly] => renderArraySingleLine(arr) + case d: EORegexData[EOExprOnly] => d.toEO + case d: EOStrData[EOExprOnly] => d.toEO + case d: EOIntData[EOExprOnly] => d.toEO + case d: EOCharData[EOExprOnly] => d.toEO + case d: EOBoolData[EOExprOnly] => d.toEO + case d: EOFloatData[EOExprOnly] => d.toEO + case d: EOBytesData[EOExprOnly] => d.toEO } + } + + def renderArgSingleLine(arg: EOBnd[EOExprOnly]): String = + arg match { + case bnd: EOBndExpr[EOExprOnly] => + "(" + renderEOBndSingleLine(bnd).value + ")" + case eoCopy @ EOAnonExpr(_ @Fix(EOCopy(_, _))) => + "(" + renderEOBndSingleLine(eoCopy).value + ")" + case array @ EOAnonExpr(_ @Fix(EOArray(_))) => + "(" + renderEOBndSingleLine(array).value + ")" + case array @ EOAnonExpr(_ @Fix(EOObj(_, _, _))) => + "(" + renderEOBndSingleLine(array).value + ")" + case other => renderEOBndSingleLine(other).value + } + + def renderCopySingleLine(copy: EOCopy[EOExprOnly]): Inline = { + val trg: String = renderArgSingleLine(EOAnonExpr(copy.trg)) + val args: String = copy.args.map(renderArgSingleLine).mkString(" ") - implicit val copyToEO: ToEO[EOCopy[EOExprOnly], Lines] = - new ToEO[EOCopy[EOExprOnly], Lines] { + Inline(List(trg, args).mkString(" ")) + } + + implicit val copyToEO: ToEO[EOCopy[EOExprOnly], InlineOrLines] = + new ToEO[EOCopy[EOExprOnly], InlineOrLines] { - override def toEO(node: EOCopy[EOExprOnly]): Lines = Lines( - node.trg.toEO.toIterable ++ + override def toEO(node: EOCopy[EOExprOnly]): InlineOrLines = { + val outerArgsString = node.args.flatMap(_.toEO.toIterable).map(indent) - ) + + Fix.un(node.trg) match { + case EOObj(_, _, _) => renderCopySingleLine(node) + case trg => + Lines( + renderArgSingleLine(EOAnonExpr(Fix(trg))) +: outerArgsString + ) + } + } } @@ -294,7 +397,9 @@ object ToEO { new ToEO[EOStrData[EOExprOnly], Inline] { override def toEO(node: EOStrData[EOExprOnly]): Inline = - Inline(s"\"${node.str}\"") + Inline( + "\"" + escape('"', node.str) + "\"" + ) } @@ -327,7 +432,9 @@ object ToEO { new ToEO[EOCharData[EOExprOnly], Inline] { override def toEO(node: EOCharData[EOExprOnly]): Inline = - Inline(s"'${node.char}'") + Inline( + "\'" + escape('\'', node.char.toString) + "\'" + ) } diff --git a/build.sbt b/build.sbt index 88e966d7..063bcd7b 100644 --- a/build.sbt +++ b/build.sbt @@ -119,7 +119,10 @@ lazy val core = project lazy val parser = project .settings(commonSettings) .settings(publishSettings) - .dependsOn(core) + .dependsOn( + core, + `eolang-backend` + ) .settings( name := "parser", libraryDependencies ++= Dependencies.parser diff --git a/core/src/main/scala/org/polystat/odin/core/ast/ast.scala b/core/src/main/scala/org/polystat/odin/core/ast/ast.scala index 45532a3c..106b82b1 100644 --- a/core/src/main/scala/org/polystat/odin/core/ast/ast.scala +++ b/core/src/main/scala/org/polystat/odin/core/ast/ast.scala @@ -1,6 +1,8 @@ package org.polystat.odin.core.ast import com.github.tarao.nonempty.collection.NonEmpty +// import eu.timepit.refined.numeric._ +// import eu.timepit.refined.api.Refined import scala.util.matching.Regex @@ -83,6 +85,11 @@ sealed trait EOApp[+A] extends EOExpr[A] sealed case class EOSimpleApp[+A](name: String) extends EOApp[A] +sealed case class EOSimpleAppWithLocator[+A]( + name: String, + locator: BigInt +) extends EOApp[A] + sealed case class EODot[+A](src: A, name: String) extends EOApp[A] sealed case class EOCopy[+A]( diff --git a/parser/src/main/scala/org/polystat/odin/parser/eo/Anon.scala b/parser/src/main/scala/org/polystat/odin/parser/eo/Anon.scala index 2e3fee9f..293b9d43 100644 --- a/parser/src/main/scala/org/polystat/odin/parser/eo/Anon.scala +++ b/parser/src/main/scala/org/polystat/odin/parser/eo/Anon.scala @@ -52,8 +52,8 @@ object Anon { } P.defer( - application | - abstraction | + abstraction.backtrack | + application | verticalArray ) } diff --git a/parser/src/main/scala/org/polystat/odin/parser/eo/JsonStringUtil.scala b/parser/src/main/scala/org/polystat/odin/parser/eo/JsonStringUtil.scala deleted file mode 100644 index 022c8cfc..00000000 --- a/parser/src/main/scala/org/polystat/odin/parser/eo/JsonStringUtil.scala +++ /dev/null @@ -1,176 +0,0 @@ -package org.polystat.odin.parser.eo - -import cats.parse.{Parser => P, Parser0 => P0} - -/** - * borrowed from: - * https://github.com/typelevel/cats-parse/blob/main/bench/src/main/scala/cats/parse/bench/self.scala - */ -object JsonStringUtil extends GenericStringUtil { - - // Here are the rules for escaping in json - lazy val decodeTable: Map[Char, Char] = - Map( - ('\\', '\\'), - ('\'', '\''), - ('\"', '\"'), - ('b', '\b'), // backspace - ('f', '\f'), // form-feed - ('n', '\n'), - ('r', '\r'), - ('t', '\t') - ) - -} - -abstract class GenericStringUtil { - protected def decodeTable: Map[Char, Char] - - private val encodeTable = - decodeTable.iterator.map { case (v, k) => (k, s"\\$v") }.toMap - - private val nonPrintEscape: Array[String] = - (0 until 32).map { c => - val strHex = c.toHexString - val strPad = List.fill(4 - strHex.length)('0').mkString - s"\\u$strPad$strHex" - }.toArray - - val escapedToken: P[Char] = { - val escapes = P.charIn(decodeTable.keys.toSeq).map(_.toString) - - val oct: P[Char] = P.charIn('0' to '7') - val octP: P[String] = (P.char('o').as('o') ~ oct.rep(2, 2)).map { - case (o, octs) => (o :: octs).toList.mkString - } - - val hex: P[Char] = P.ignoreCaseCharIn(('0' to '9') ++ ('a' to 'f')) - val hex2: P[String] = hex.rep(2, 2).map(_.toList.mkString) - val hexP: P[String] = (P.char('x').as("x") ~ hex2).map { case (x, hex) => - x + hex - } - - val hex4: P[String] = (hex2 ~ hex2).map { case (hex1, hex2) => - hex1 + hex2 - } - val u4: P[String] = (P.char('u').as("u") ~ hex4).map { case (u, hex) => - u + hex - } - - val after = P.oneOf(escapes :: octP :: hexP :: u4 :: Nil) - (P.char('\\').as("\\") ~ after).flatMap { case (bs, after) => - unescape(bs + after).fold( - _ => P.fail, - char => { - require(char.length == 1) - P.pure(char.charAt(0)) - } - ) - } - } - - /** - * String content without the delimiter - */ - def undelimitedString(endP: P[Unit]): P[String] = - escapedToken - .backtrack - .orElse((!endP).with1 ~ P.anyChar) - .rep - .string - .flatMap { str => - unescape(str) match { - case Right(str1) => P.pure(str1) - case Left(_) => P.fail - } - } - - private val simpleString: P0[String] = - P.charsWhile0(c => c >= ' ' && c != '"' && c != '\\') - - def escapedString(q: Char): P[String] = { - val end: P[Unit] = P.char(q) - end *> (simpleString <* end) - .backtrack - .orElse(undelimitedString(end) <* end) - } - - def escape(quoteChar: Char, str: String): String = { - // We can ignore escaping the opposite character used for the string - // x isn't escaped anyway and is kind of a hack here - val ignoreEscape = - if (quoteChar == '\'') '"' else if (quoteChar == '"') '\'' else 'x' - str.flatMap { c => - if (c == ignoreEscape) c.toString - else - encodeTable.get(c) match { - case None => - if (c < ' ') nonPrintEscape(c.toInt) - else c.toString - case Some(esc) => esc - } - } - } - - def unescape(str: String): Either[Int, String] = { - val sb = new java.lang.StringBuilder - def decodeNum(idx: Int, size: Int, base: Int): Int = { - val end = idx + size - if (end <= str.length) { - val intStr = str.substring(idx, end) - val asInt = - try Integer.parseInt(intStr, base) - catch { case _: NumberFormatException => ~idx } - sb.append(asInt.toChar) - end - } else ~str.length - } - @annotation.tailrec - def loop(idx: Int): Int = - if (idx >= str.length) { - // done - idx - } else if (idx < 0) { - // error from decodeNum - idx - } else { - val c0 = str.charAt(idx) - if (c0 != '\\') { - sb.append(c0) - loop(idx + 1) - } else { - // str(idx) == \ - val nextIdx = idx + 1 - if (nextIdx >= str.length) { - // error we expect there to be a character after \ - ~idx - } else { - val c = str.charAt(nextIdx) - decodeTable.get(c) match { - case Some(d) => - sb.append(d) - loop(idx + 2) - case None => - c match { - case 'o' => loop(decodeNum(idx + 2, 2, 8)) - case 'x' => loop(decodeNum(idx + 2, 2, 16)) - case 'u' => loop(decodeNum(idx + 2, 4, 16)) - case 'U' => loop(decodeNum(idx + 2, 8, 16)) - case other => - // \c is interpreted as just \c, if the character isn't - // escaped - sb.append('\\') - sb.append(other) - loop(idx + 2) - } - } - } - } - } - - val res = loop(0) - if (res < 0) Left(~res) - else Right(sb.toString) - } - -} diff --git a/parser/src/main/scala/org/polystat/odin/parser/eo/Named.scala b/parser/src/main/scala/org/polystat/odin/parser/eo/Named.scala index d1e5271f..39b2877d 100644 --- a/parser/src/main/scala/org/polystat/odin/parser/eo/Named.scala +++ b/parser/src/main/scala/org/polystat/odin/parser/eo/Named.scala @@ -12,16 +12,6 @@ import org.polystat.odin.parser.eo.Tokens._ object Named { - val name: P[EONamedBnd] = ( - (optWsp.with1.soft *> (P.char('>') *> optWsp) *> - (identifier | P.string("@").string)) ~ - (optWsp *> P.char('!').?) <* optWsp - ).map { - case ("@", None) => EODecoration - case (name, Some(_)) => EOAnyNameBnd(ConstName(name)) - case (name, None) => EOAnyNameBnd(LazyName(name)) - } - type ApplicationArgs = NonEmpty[EOBnd[EOExprOnly], Vector[EOBnd[EOExprOnly]]] def `object`( @@ -31,7 +21,7 @@ object Named { val abstraction = ( - params.soft ~ name ~ + params.soft ~ SingleLine.bndName ~ boundAttributes(indent, indentationStep).? ).map { case (((params, vararg), name), attrs) => EOBndExpr( @@ -41,21 +31,21 @@ object Named { } val inverseDotApplication = ( - (identifier.soft <* P.char('.')).soft ~ name ~ + (identifier.soft <* P.char('.')).soft ~ SingleLine.bndName ~ verticalApplicationArgs(indent, indentationStep) ).map { case ((attr, name), args) => EOBndExpr(name, createInverseDot(attr, args)) } val verticalArray = ( - (P.char('*').soft *> name).soft ~ + (P.char('*').soft *> SingleLine.bndName).soft ~ verticalApplicationArgs(indent, indentationStep) ).map { case (name, args) => EOBndExpr(name, createArrayFromNonEmpty(Some(args))) } val regularApplication = ( - singleLineApplication.soft ~ name ~ + singleLineApplication.soft ~ SingleLine.bndName ~ verticalApplicationArgs(indent, indentationStep).? ).map { case ((trg, name), Some(args)) => diff --git a/parser/src/main/scala/org/polystat/odin/parser/eo/Prettyprint.scala b/parser/src/main/scala/org/polystat/odin/parser/eo/Prettyprint.scala index a1f6782d..d355f8d7 100644 --- a/parser/src/main/scala/org/polystat/odin/parser/eo/Prettyprint.scala +++ b/parser/src/main/scala/org/polystat/odin/parser/eo/Prettyprint.scala @@ -8,7 +8,7 @@ package org.polystat.odin.parser.eo import cats.data.NonEmptyList import cats.parse.Parser.Expectation import cats.parse._ -import org.polystat.odin.parser.eo.JsonStringUtil.escape +import org.polystat.odin.utils.text.escape class Prettyprint(filename: String = "", input: String) { private val locmap = LocationMap(input) diff --git a/parser/src/main/scala/org/polystat/odin/parser/eo/SingleLine.scala b/parser/src/main/scala/org/polystat/odin/parser/eo/SingleLine.scala index 73829a64..d3218041 100644 --- a/parser/src/main/scala/org/polystat/odin/parser/eo/SingleLine.scala +++ b/parser/src/main/scala/org/polystat/odin/parser/eo/SingleLine.scala @@ -27,7 +27,7 @@ object SingleLine { ) val bndName: P[EONamedBnd] = ( - P.char('>').surroundedBy(optWsp) *> + (optWsp.with1.soft *> P.char('>') *> optWsp) *> ((identifier | P.string("@").string) <* optWsp) ~ (P.charIn('!') <* optWsp).? ).map { @@ -36,11 +36,7 @@ object SingleLine { case (name, None) => EOAnyNameBnd(LazyName(name)) } - val attributeName: P[String] = - identifier | - P.string("@").string | - P.string("$").string | - P.string("^").string + val attributeName: P[String] = identifier | P.string("@").string val data: P[EOExprOnly] = ( float.map(EOFloatData(_)) | @@ -52,31 +48,65 @@ object SingleLine { val singleLineApplication: P[EOExprOnly] = P.recursive[EOExprOnly](recurse => { + val self: P[String] = P.char('$').string + + val parent: P[String] = P.char('^').string + + val nameWithLocator: P[EOExprOnly] = + (self *> P.char('.') *> identifier) + .map(name => Fix[EOExpr](EOSimpleAppWithLocator(name, 0))) + .orElse( + ((parent.void.repSep(P.char('.')) <* P.char('.')) ~ identifier) + .map { case (parents, name) => + Fix[EOExpr](EOSimpleAppWithLocator(name, parents.length)) + } + ) + val simpleApplicationTarget: P[EOExprOnly] = - data | attributeName.map(name => Fix[EOExpr](EOSimpleApp(name))) + data | + attributeName.map(name => Fix[EOExpr](EOSimpleApp(name))) | + nameWithLocator.backtrack | + self.map(parent => Fix[EOExpr](EOSimpleApp(parent))) | + parent.map(self => Fix[EOExpr](EOSimpleApp(self))) + + val parenthesized: P[EOExprOnly] = + recurse.between(P.char('('), P.char(')')) + + val singleLineBndExpr: P[EOBndExpr[EOExprOnly]] = ( + (P.char('(').soft *> (recurse ~ bndName)) <* P.char(')') + ).map { case (expr, name) => + EOBndExpr(name, expr) + } + val singleLineAbstraction: P[EOExprOnly] = ( + params.soft ~ (wsp.soft *> singleLineBndExpr.repSep(wsp)).? + ).map { case ((freeAttrs, vararg), bnds) => + Fix( + EOObj( + freeAttrs, + vararg, + bnds.map(_.toList.toVector).getOrElse(Vector.empty) + ) + ) + } val attributeChain: P[EOExprOnly] = ( - (simpleApplicationTarget.soft <* P.char('.')).soft ~ + ((parenthesized | simpleApplicationTarget).soft <* P.char('.')).soft ~ attributeName.repSep(1, P.char('.')) ).map { case (trg, attrs) => attrs.foldLeft(trg)((acc, id) => Fix[EOExpr](EODot(acc, id))) } - val parenthesized: P[EOExprOnly] = - recurse.between(P.char('('), P.char(')')) - val applicationTarget: P[EOExprOnly] = - parenthesized | attributeChain | simpleApplicationTarget + attributeChain | parenthesized | simpleApplicationTarget val justApplication: P[EOExprOnly] = ( - (applicationTarget.soft <* wsp) ~ - applicationTarget.repSep0(0, wsp) + (applicationTarget.soft <* wsp).soft ~ + (applicationTarget.backtrack.map(EOAnonExpr(_)) | singleLineBndExpr) + .repSep(1, wsp) ).map { case (trg, args) => NonEmpty - .from(args) - .map(args => - Fix[EOExpr](EOCopy(trg, args.map(EOAnonExpr(_)).toVector)) - ) + .from(args.toList) + .map(args => Fix[EOExpr](EOCopy(trg, args.toVector))) .getOrElse(trg) } @@ -92,6 +122,7 @@ object SingleLine { } singleLineArray | + singleLineAbstraction | justApplication | applicationTarget diff --git a/parser/src/main/scala/org/polystat/odin/parser/eo/StringUtils.scala b/parser/src/main/scala/org/polystat/odin/parser/eo/StringUtils.scala new file mode 100644 index 00000000..19f4ad22 --- /dev/null +++ b/parser/src/main/scala/org/polystat/odin/parser/eo/StringUtils.scala @@ -0,0 +1,72 @@ +package org.polystat.odin.parser.eo + +import cats.parse.{Parser => P, Parser0 => P0} +import org.polystat.odin.utils.text.{decodeTable, unescape} + +/** + * borrowed from: + * https://github.com/typelevel/cats-parse/blob/main/bench/src/main/scala/cats/parse/bench/self.scala + */ + +object StringUtils { + + val escapedToken: P[Char] = { + val escapes = P.charIn(decodeTable.keys.toSeq).map(_.toString) + + val oct: P[Char] = P.charIn('0' to '7') + val octP: P[String] = (P.char('o').as('o') ~ oct.rep(2, 2)).map { + case (o, octs) => (o :: octs).toList.mkString + } + + val hex: P[Char] = P.ignoreCaseCharIn(('0' to '9') ++ ('a' to 'f')) + val hex2: P[String] = hex.rep(2, 2).map(_.toList.mkString) + val hexP: P[String] = (P.char('x').as("x") ~ hex2).map { case (x, hex) => + x + hex + } + + val hex4: P[String] = (hex2 ~ hex2).map { case (hex1, hex2) => + hex1 + hex2 + } + val u4: P[String] = (P.char('u').as("u") ~ hex4).map { case (u, hex) => + u + hex + } + + val after = P.oneOf(escapes :: octP :: hexP :: u4 :: Nil) + (P.char('\\').as("\\") ~ after).flatMap { case (bs, after) => + unescape(bs + after).fold( + _ => P.fail, + char => { + require(char.length == 1) + P.pure(char.charAt(0)) + } + ) + } + } + + /** + * String content without the delimiter + */ + def undelimitedString(endP: P[Unit]): P[String] = + escapedToken + .backtrack + .orElse((!endP).with1 ~ P.anyChar) + .rep + .string + .flatMap { str => + unescape(str) match { + case Right(str1) => P.pure(str1) + case Left(_) => P.fail + } + } + + private val simpleString: P0[String] = + P.charsWhile0(c => c >= ' ' && c != '"' && c != '\\') + + def escapedString(q: Char): P[String] = { + val end: P[Unit] = P.char(q) + end *> (simpleString <* end) + .backtrack + .orElse(undelimitedString(end) <* end) + } + +} diff --git a/parser/src/main/scala/org/polystat/odin/parser/eo/Tokens.scala b/parser/src/main/scala/org/polystat/odin/parser/eo/Tokens.scala index bb88988b..f766b79c 100644 --- a/parser/src/main/scala/org/polystat/odin/parser/eo/Tokens.scala +++ b/parser/src/main/scala/org/polystat/odin/parser/eo/Tokens.scala @@ -32,11 +32,11 @@ object Tokens { }.map(_.toFloat) val integer: P[Int] = Numbers.signedIntString.map(_.toInt) - val string: P[String] = JsonStringUtil.escapedString('\"') + val string: P[String] = StringUtils.escapedString('\"') val char: P[Char] = ( - JsonStringUtil.escapedToken.backtrack | + StringUtils.escapedToken.backtrack | P.charWhere(c => c != '\n' && c != '\'') ) .surroundedBy(P.char('\'')) diff --git a/parser/src/main/scala/org/polystat/odin/parser/xmir/XmirToAst.scala b/parser/src/main/scala/org/polystat/odin/parser/xmir/XmirToAst.scala index 79731d9d..82310392 100644 --- a/parser/src/main/scala/org/polystat/odin/parser/xmir/XmirToAst.scala +++ b/parser/src/main/scala/org/polystat/odin/parser/xmir/XmirToAst.scala @@ -2,8 +2,6 @@ package org.polystat.odin.parser.xmir import cats.effect.Sync import cats.implicits._ -import cats.Traverse -import cats.data.Validated import com.github.tarao.nonempty.collection.NonEmpty import higherkindness.droste.data.Fix import org.polystat.odin.core.ast._ @@ -136,15 +134,23 @@ object XmirToAst { } } + private[this] def transformEoDot(dot: EODot[EOExprOnly]): EOExprOnly = { + Fix(Fix.un(dot.src) match { + case src: EODot[EOExprOnly] => dot.copy(src = transformEoDot(src)) + case EOSimpleApp("^") => + EOSimpleAppWithLocator[EOExprOnly](dot.name, 1) + case EOSimpleApp("$") => + EOSimpleAppWithLocator[EOExprOnly](dot.name, 0) + case EOSimpleAppWithLocator("^", loc) => + EOSimpleAppWithLocator[EOExprOnly](dot.name, loc + 1) + case _ => dot + }) + } + private[this] def combineErrors[A]( eithers: Seq[Either[String, A]] - ): Either[String, Seq[A]] = { - Traverse[Seq] - .traverse(eithers)(either => - Validated.fromEither(either.leftMap(_ + "\n")) - ) - .toEither - } + ): Either[String, Seq[A]] = + eithers.parSequence private[this] val bndFromTuple: ((Option[EONamedBnd], EOExprOnly)) => EOBnd[EOExprOnly] = { case (Some(name), value) => EOBndExpr(name, value) @@ -250,7 +256,7 @@ object XmirToAst { name <- name of <- parsedOf (_, expr) <- of - } yield (None, Fix[EOExpr](EODot(expr, name))) + } yield (None, transformEoDot(EODot(expr, name))) case Elem(_, "abstraction", attrs, _, children @ _*) => val name = extractName(attrs.asAttrMap) val (varargSeq, freeSeq) = children diff --git a/parser/src/test/scala/org/polystat/odin/parser/EOParserTestSuite.scala b/parser/src/test/scala/org/polystat/odin/parser/EOParserTestSuite.scala index 010ffe35..f37f1eca 100644 --- a/parser/src/test/scala/org/polystat/odin/parser/EOParserTestSuite.scala +++ b/parser/src/test/scala/org/polystat/odin/parser/EOParserTestSuite.scala @@ -5,6 +5,10 @@ import org.scalatest.wordspec.AnyWordSpec import TestUtils._ import org.polystat.odin.core.ast.EOProg import org.polystat.odin.core.ast.astparams.EOExprOnly +import org.polystat.odin.parser.ast_tests.{ + FullProgramExamples, + SingleLineExamples +} import org.scalacheck.{Gen, Prop, Test} import org.scalatestplus.scalacheck.Checkers @@ -102,17 +106,9 @@ trait EOParserTestSuite extends AnyWordSpec with Checkers { "/eo_sources" ).map(filename => TestCase(fileNameOf(filename), readCodeFrom(filename))) - val mutualRecursionExample: Tests[EOProg[EOExprOnly]] = List( - TestCase( - "Mutual Recursion Example", - MutualRecExample.code, - Some(MutualRecExample.ast) - ) - ) - - "existing programs" should { + "full programs" should { runParserTests(programParser, correctTests = examplesFromSources) - runParserTests(programParser, correctTests = mutualRecursionExample) + runParserTests(programParser, correctTests = FullProgramExamples.correct) } diff --git a/parser/src/test/scala/org/polystat/odin/parser/MutualRecExample.scala b/parser/src/test/scala/org/polystat/odin/parser/MutualRecExample.scala deleted file mode 100644 index af7f22d4..00000000 --- a/parser/src/test/scala/org/polystat/odin/parser/MutualRecExample.scala +++ /dev/null @@ -1,144 +0,0 @@ -package org.polystat.odin.parser - -import com.github.tarao.nonempty.collection.NonEmpty -import org.polystat.odin.core.ast.astparams.EOExprOnly -import org.polystat.odin.core.ast._ -import higherkindness.droste.data.Fix - -object MutualRecExample { - - val ast: EOProg[EOExprOnly] = EOProg( - EOMetas( - pack = Some("sandbox"), - metas = Vector( - EOAliasMeta("stdout", "org.eolang.io.stdout"), - EOAliasMeta("sprintf", "org.eolang.txt.sprintf"), - ) - ), - Vector( - EOBndExpr( - EOAnyNameBnd(LazyName("base")), - Fix[EOExpr]( - EOObj( - freeAttrs = Vector(), - varargAttr = None, - bndAttrs = Vector( - EOBndExpr( - EOAnyNameBnd(LazyName("x")), - Fix[EOExpr](EOSimpleApp("memory")) - ), - EOBndExpr( - EOAnyNameBnd(LazyName("f")), - Fix[EOExpr]( - EOObj( - freeAttrs = Vector(LazyName("self"), LazyName("v")), - varargAttr = None, - bndAttrs = Vector( - EOBndExpr( - EODecoration, - Fix[EOExpr]( - EOCopy( - Fix[EOExpr]( - EODot(Fix[EOExpr](EOSimpleApp("x")), "write") - ), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("v"))) - ) - ) - ) - ) - ) - ) - ) - ), - EOBndExpr( - EOAnyNameBnd(LazyName("g")), - Fix[EOExpr]( - EOObj( - freeAttrs = Vector(LazyName("self"), LazyName("v")), - varargAttr = None, - bndAttrs = Vector( - EOBndExpr( - EODecoration, - Fix[EOExpr]( - EOCopy( - Fix[EOExpr]( - EODot(Fix[EOExpr](EOSimpleApp("self")), "f") - ), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("self"))), - EOAnonExpr(Fix[EOExpr](EOSimpleApp("v"))) - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) - ), - EOBndExpr( - EOAnyNameBnd(LazyName("derived")), - Fix[EOExpr]( - EOObj( - freeAttrs = Vector(), - varargAttr = None, - bndAttrs = Vector( - EOBndExpr(EODecoration, Fix[EOExpr](EOSimpleApp("base"))), - EOBndExpr( - EOAnyNameBnd(LazyName("f")), - Fix[EOExpr]( - EOObj( - freeAttrs = Vector(LazyName("self"), LazyName("v")), - varargAttr = None, - bndAttrs = Vector( - EOBndExpr( - EODecoration, - Fix[EOExpr]( - EOCopy( - Fix[EOExpr]( - EODot(Fix[EOExpr](EOSimpleApp("self")), "g") - ), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("self"))), - EOAnonExpr(Fix[EOExpr](EOSimpleApp("v"))) - ) - ) - ) - ) - ), - ) - ) - ) - ) - ) - ) - ), - ) - ) - - val code: String = - """+package sandbox - |+alias stdout org.eolang.io.stdout - |+alias sprintf org.eolang.txt.sprintf - |[] > base - | memory > x - | [self v] > f - | x.write > @ - | v - | [self v] > g - | self.f > @ - | self - | v - |[] > derived - | base > @ - | [self v] > f - | self.g > @ - | self - | v - |""".stripMargin - -} diff --git a/parser/src/test/scala/org/polystat/odin/parser/SingleLineExamples.scala b/parser/src/test/scala/org/polystat/odin/parser/SingleLineExamples.scala deleted file mode 100644 index 032628ee..00000000 --- a/parser/src/test/scala/org/polystat/odin/parser/SingleLineExamples.scala +++ /dev/null @@ -1,96 +0,0 @@ -package org.polystat.odin.parser - -import com.github.tarao.nonempty.collection.NonEmpty -import org.polystat.odin.core.ast._ -import org.polystat.odin.core.ast.astparams.EOExprOnly -import higherkindness.droste.data.Fix -import org.polystat.odin.parser.TestUtils.TestCase - -object SingleLineExamples { - - val correct: List[TestCase[EOExprOnly]] = - List( - TestCase( - label = "justA", - code = "a", - ast = Some(Fix[EOExpr](EOSimpleApp("a"))) - ), - TestCase( - label = "normal application", - code = "a b c d", - ast = Some( - Fix[EOExpr]( - EOCopy( - Fix[EOExpr](EOSimpleApp("a")), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("b"))), - EOAnonExpr(Fix[EOExpr](EOSimpleApp("c"))), - EOAnonExpr(Fix[EOExpr](EOSimpleApp("d"))) - ) - ) - ) - ) - ), - TestCase( - label = "rightAssociative", - code = "a (b (c d))", - ast = Some( - Fix[EOExpr]( - EOCopy( - Fix[EOExpr](EOSimpleApp("a")), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr( - Fix[EOExpr]( - EOCopy( - Fix[EOExpr](EOSimpleApp("b")), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr( - Fix[EOExpr]( - EOCopy( - Fix[EOExpr](EOSimpleApp("c")), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("d"))) - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) - ), - TestCase( - label = "leftAssociative", - code = "((a b) c) d", - ast = Some( - Fix[EOExpr]( - EOCopy( - Fix[EOExpr]( - EOCopy( - Fix[EOExpr]( - EOCopy( - Fix[EOExpr](EOSimpleApp("a")), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("b"))) - ) - ) - ), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("c"))) - ) - ) - ), - NonEmpty[Vector[EOBnd[EOExprOnly]]]( - EOAnonExpr(Fix[EOExpr](EOSimpleApp("d"))) - ) - ) - ) - ) - ) - ) - -} diff --git a/parser/src/test/scala/org/polystat/odin/parser/ast_tests/FullProgramExamples.scala b/parser/src/test/scala/org/polystat/odin/parser/ast_tests/FullProgramExamples.scala new file mode 100644 index 00000000..ba8c40b8 --- /dev/null +++ b/parser/src/test/scala/org/polystat/odin/parser/ast_tests/FullProgramExamples.scala @@ -0,0 +1,225 @@ +package org.polystat.odin.parser.ast_tests + +import com.github.tarao.nonempty.collection.NonEmpty +import higherkindness.droste.data.Fix +import org.polystat.odin.core.ast.astparams.EOExprOnly +import org.polystat.odin.core.ast._ +import org.polystat.odin.parser.TestUtils.TestCase + +object FullProgramExamples { + + val correct: List[TestCase[EOProg[EOExprOnly]]] = List( + mutualRecursionExample, + dirWalk, + ) + + private lazy val dirWalk: TestCase[EOProg[EOExprOnly]] = TestCase( + label = "dir walk", + code = + """(dir "/tmp").walk + | * ([f] (f.is-dir > @)) + |""".stripMargin, + ast = Some( + EOProg( + metas = EOMetas(pack = None, metas = Vector()), + bnds = Vector( + EOAnonExpr( + Fix( + EOCopy( + trg = Fix( + EODot( + src = Fix( + EOCopy( + trg = Fix(EOSimpleApp[EOExprOnly]("dir")), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix(EOStrData[EOExprOnly]("/tmp"))) + ) + ) + ), + name = "walk" + ) + ), + args = NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr( + Fix( + EOArray( + elems = Vector( + EOAnonExpr( + Fix( + EOObj( + freeAttrs = Vector(LazyName("f")), + varargAttr = None, + bndAttrs = Vector( + EOBndExpr( + bndName = EODecoration, + expr = Fix( + EODot( + src = Fix(EOSimpleApp[EOExprOnly]("f")), + name = "is-dir" + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + private lazy val mutualRecursionExample: TestCase[EOProg[Fix[EOExpr]]] = + TestCase( + label = "mutual recursion example", + code = + """+package sandbox + |+alias stdout org.eolang.io.stdout + |+alias sprintf org.eolang.txt.sprintf + |[] > base + | memory > x + | [self v] > f + | x.write > @ + | v + | [self v] > g + | self.f > @ + | self + | v + |[] > derived + | base > @ + | [self v] > f + | self.g > @ + | self + | v + |""".stripMargin, + ast = Some( + EOProg( + EOMetas( + pack = Some("sandbox"), + metas = Vector( + EOAliasMeta("stdout", "org.eolang.io.stdout"), + EOAliasMeta("sprintf", "org.eolang.txt.sprintf"), + ) + ), + Vector( + EOBndExpr( + EOAnyNameBnd(LazyName("base")), + Fix[EOExpr]( + EOObj( + freeAttrs = Vector(), + varargAttr = None, + bndAttrs = Vector( + EOBndExpr( + EOAnyNameBnd(LazyName("x")), + Fix[EOExpr](EOSimpleApp("memory")) + ), + EOBndExpr( + EOAnyNameBnd(LazyName("f")), + Fix[EOExpr]( + EOObj( + freeAttrs = Vector(LazyName("self"), LazyName("v")), + varargAttr = None, + bndAttrs = Vector( + EOBndExpr( + EODecoration, + Fix[EOExpr]( + EOCopy( + Fix[EOExpr]( + EODot( + Fix[EOExpr](EOSimpleApp("x")), + "write" + ) + ), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix[EOExpr](EOSimpleApp("v"))) + ) + ) + ) + ) + ) + ) + ) + ), + EOBndExpr( + EOAnyNameBnd(LazyName("g")), + Fix[EOExpr]( + EOObj( + freeAttrs = Vector(LazyName("self"), LazyName("v")), + varargAttr = None, + bndAttrs = Vector( + EOBndExpr( + EODecoration, + Fix[EOExpr]( + EOCopy( + Fix[EOExpr]( + EODot(Fix[EOExpr](EOSimpleApp("self")), "f") + ), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr( + Fix[EOExpr](EOSimpleApp("self")) + ), + EOAnonExpr(Fix[EOExpr](EOSimpleApp("v"))) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ), + EOBndExpr( + EOAnyNameBnd(LazyName("derived")), + Fix[EOExpr]( + EOObj( + freeAttrs = Vector(), + varargAttr = None, + bndAttrs = Vector( + EOBndExpr(EODecoration, Fix[EOExpr](EOSimpleApp("base"))), + EOBndExpr( + EOAnyNameBnd(LazyName("f")), + Fix[EOExpr]( + EOObj( + freeAttrs = Vector(LazyName("self"), LazyName("v")), + varargAttr = None, + bndAttrs = Vector( + EOBndExpr( + EODecoration, + Fix[EOExpr]( + EOCopy( + Fix[EOExpr]( + EODot(Fix[EOExpr](EOSimpleApp("self")), "g") + ), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr( + Fix[EOExpr](EOSimpleApp("self")) + ), + EOAnonExpr(Fix[EOExpr](EOSimpleApp("v"))) + ) + ) + ) + ) + ), + ) + ) + ) + ) + ) + ) + ), + ) + ) + ), + ) + +} diff --git a/parser/src/test/scala/org/polystat/odin/parser/ast_tests/SingleLineExamples.scala b/parser/src/test/scala/org/polystat/odin/parser/ast_tests/SingleLineExamples.scala new file mode 100644 index 00000000..2c77b28e --- /dev/null +++ b/parser/src/test/scala/org/polystat/odin/parser/ast_tests/SingleLineExamples.scala @@ -0,0 +1,153 @@ +package org.polystat.odin.parser.ast_tests + +import com.github.tarao.nonempty.collection.NonEmpty +import higherkindness.droste.data.Fix +import org.polystat.odin.core.ast.astparams.EOExprOnly +import org.polystat.odin.core.ast._ +import org.polystat.odin.parser.TestUtils.TestCase + +object SingleLineExamples { + + val correct: List[TestCase[EOExprOnly]] = + List( + justA, + normalApp, + rightAssoc, + leftAssoc, + singleLineAbstraction, + ) + + private lazy val singleLineAbstraction: TestCase[EOExprOnly] = + TestCase( + label = "single line abstraction", + code = "[x] (x.add 1 > succ) (x.sub 1 > prev)", + ast = Some( + Fix( + EOObj( + freeAttrs = Vector(LazyName("x")), + varargAttr = None, + bndAttrs = Vector( + EOBndExpr( + bndName = EOAnyNameBnd(LazyName("succ")), + expr = Fix( + EOCopy( + trg = Fix( + EODot( + src = Fix(EOSimpleApp[EOExprOnly]("x")), + name = "add" + ) + ), + args = NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix(EOIntData[EOExprOnly](1))) + ) + ) + ) + ), + EOBndExpr( + bndName = EOAnyNameBnd(LazyName("prev")), + expr = Fix( + EOCopy( + trg = Fix( + EODot( + src = Fix(EOSimpleApp[EOExprOnly]("x")), + name = "sub" + ) + ), + args = NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix(EOIntData[EOExprOnly](1))) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + private lazy val justA: TestCase[Fix[EOExpr]] = TestCase( + label = "justA", + code = "a", + ast = Some(Fix[EOExpr](EOSimpleApp("a"))) + ) + + private lazy val normalApp: TestCase[Fix[EOExpr]] = TestCase( + label = "normal application", + code = "a b c d", + ast = Some( + Fix[EOExpr]( + EOCopy( + Fix[EOExpr](EOSimpleApp("a")), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix[EOExpr](EOSimpleApp("b"))), + EOAnonExpr(Fix[EOExpr](EOSimpleApp("c"))), + EOAnonExpr(Fix[EOExpr](EOSimpleApp("d"))) + ) + ) + ) + ) + ) + + private lazy val rightAssoc: TestCase[Fix[EOExpr]] = TestCase( + label = "rightAssociative", + code = "a (b (c d))", + ast = Some( + Fix[EOExpr]( + EOCopy( + Fix[EOExpr](EOSimpleApp("a")), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr( + Fix[EOExpr]( + EOCopy( + Fix[EOExpr](EOSimpleApp("b")), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr( + Fix[EOExpr]( + EOCopy( + Fix[EOExpr](EOSimpleApp("c")), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix[EOExpr](EOSimpleApp("d"))) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + private lazy val leftAssoc: TestCase[Fix[EOExpr]] = TestCase( + label = "leftAssociative", + code = "((a b) c) d", + ast = Some( + Fix[EOExpr]( + EOCopy( + Fix[EOExpr]( + EOCopy( + Fix[EOExpr]( + EOCopy( + Fix[EOExpr](EOSimpleApp("a")), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix[EOExpr](EOSimpleApp("b"))) + ) + ) + ), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix[EOExpr](EOSimpleApp("c"))) + ) + ) + ), + NonEmpty[Vector[EOBnd[EOExprOnly]]]( + EOAnonExpr(Fix[EOExpr](EOSimpleApp("d"))) + ) + ) + ) + ) + ) + +} diff --git a/parser/src/test/scala/org/polystat/odin/parser/eo/ParserTests.scala b/parser/src/test/scala/org/polystat/odin/parser/eo/ParserTests.scala index 4efde4c5..62ad6e70 100644 --- a/parser/src/test/scala/org/polystat/odin/parser/eo/ParserTests.scala +++ b/parser/src/test/scala/org/polystat/odin/parser/eo/ParserTests.scala @@ -1,11 +1,13 @@ package org.polystat.odin.parser.eo import cats.parse.{Parser => P, Parser0 => P0} +import org.polystat.odin.backend.eolang.ToEO.ops._ +import org.polystat.odin.backend.eolang.ToEO.instances._ import org.polystat.odin.core.ast._ import org.polystat.odin.core.ast.astparams.EOExprOnly import org.polystat.odin.parser.EOParserTestSuite import org.polystat.odin.parser.TestUtils.TestCase -import org.polystat.odin.parser.Gens +import org.polystat.odin.parser.gens._ class ParserTests extends EOParserTestSuite { @@ -40,7 +42,7 @@ class ParserTests extends EOParserTestSuite { "comments or empty lines" in { runParserTestsGen( Left(Tokens.emptyLinesOrComments), - Gens.emptyLinesOrComments + eo.emptyLinesOrComments ) } @@ -72,13 +74,13 @@ class ParserTests extends EOParserTestSuite { runParserTests(Right(Tokens.string), stringTests) "pass" in { - runParserTestsGen(Right(Tokens.string), Gens.string) + runParserTestsGen(Right(Tokens.string), eo.string) } } "chars" should { "pass" in { - runParserTestsGen(Right(Tokens.char), Gens.char) + runParserTestsGen(Right(Tokens.char), eo.char) } val charTests = List( TestCase(label = "new line", code = "\'\\n\'", Some('\n')), @@ -92,39 +94,39 @@ class ParserTests extends EOParserTestSuite { } "identifiers" in { - runParserTestsGen(Right(Tokens.identifier), Gens.identifier) + runParserTestsGen(Right(Tokens.identifier), eo.identifier) } "integers" in { - runParserTestsGen(Right(Tokens.integer), Gens.integer) + runParserTestsGen(Right(Tokens.integer), eo.integer) } "floats" in { - runParserTestsGen(Right(Tokens.float), Gens.float) + runParserTestsGen(Right(Tokens.float), eo.float) } } "metas" should { "package meta" in { - runParserTestsGen(Right(Metas.packageMeta), Gens.packageMeta) + runParserTestsGen(Right(Metas.packageMeta), eo.packageMeta) } "alias meta" in { - runParserTestsGen(Right(Metas.aliasMeta), Gens.aliasMeta) + runParserTestsGen(Right(Metas.aliasMeta), eo.aliasMeta) } "rt meta" in { - runParserTestsGen(Right(Metas.rtMeta), Gens.rtMeta) + runParserTestsGen(Right(Metas.rtMeta), eo.rtMeta) } "all metas" in { - runParserTestsGen(Left(Metas.metas), Gens.metas) + runParserTestsGen(Left(Metas.metas), eo.metas) } } "abstraction params" should { "pass" in { - runParserTestsGen(Right(SingleLine.params), Gens.abstractionParams) + runParserTestsGen(Right(SingleLine.params), eo.abstractionParams) } "fail" in { shouldFailParsing(Right(SingleLine.params), "[...]") @@ -137,7 +139,7 @@ class ParserTests extends EOParserTestSuite { "binding name" should { "pass" in { - runParserTestsGen(Right(Named.name), Gens.bndName) + runParserTestsGen(Right(SingleLine.bndName), eo.bndName) } val incorrectTests = List[TestCase[EONamedBnd]]( @@ -146,7 +148,7 @@ class ParserTests extends EOParserTestSuite { ) runParserTests[EONamedBnd]( - Right(Named.name), + Right(SingleLine.bndName), incorrectTests = incorrectTests ) } @@ -155,7 +157,7 @@ class ParserTests extends EOParserTestSuite { "pass" in { runParserTestsGen( singleLineApplicationParser, - Gens.singleLineApplication(recDepthMax = 4) + eo.singleLineApplication(maxDepth = 4) ) } } @@ -164,20 +166,33 @@ class ParserTests extends EOParserTestSuite { "pass" in { runParserTestsGen( Right(Parser.`object`(0, 4)), - Gens.`object`( + eo.`object`( named = true, indentationStep = 4, - recDepthMax = 2 + maxDepth = 4 ) ) } } "program" should { - "pass" in { + "pass auto-generated programs" in { runParserTestsGen( Left(Parser.program(0, indentationStep = 2)), - Gens.program(indentationStep = 2, recDepthMax = 2) + eo.program(indentationStep = 2, maxDepth = 4) + ) + } + + "prog->pretty == prog->pretty->parsed->pretty" in { + import org.scalacheck.Prop + check( + Prop.forAll(ast.eoProg(4)) { prog => + val expected: Either[String, String] = Right(prog.toEOPretty) + val actual: Either[String, String] = + Parser.parse(prog.toEOPretty).map(_.toEOPretty) + expected == actual + }, + scalacheckParams ) } } @@ -187,14 +202,33 @@ class ParserTests extends EOParserTestSuite { object ParserTests { def main(args: Array[String]): Unit = { - for (_ <- 1 to 1) { - println( - Gens - .program(indentationStep = 4, recDepthMax = 2) - .sample - .get - ) + val code = + """ + |[a b c] (123 > a) (123 > a) + |[a v v] > a + | 1 > a + | 2 > v + | 3 > a + |^.^.^.aboba + |$.self + |$ + |^ + | + | + |""".stripMargin + + val parsed = Parser.parse(code) + + parsed.foreach(pprint.pprintln(_)) + + parsed match { + case Left(value) => println(value) + case Right(value) => { + println(value.toEOPretty) + println(Parser.parse(value.toEOPretty).map(_ == value)) + } } + } } diff --git a/parser/src/test/scala/org/polystat/odin/parser/gens/ast.scala b/parser/src/test/scala/org/polystat/odin/parser/gens/ast.scala new file mode 100644 index 00000000..992d73ed --- /dev/null +++ b/parser/src/test/scala/org/polystat/odin/parser/gens/ast.scala @@ -0,0 +1,159 @@ +package org.polystat.odin.parser.gens + +import com.github.tarao.nonempty.collection.NonEmpty +import org.polystat.odin.core.ast._ +import org.polystat.odin.core.ast.astparams._ +import higherkindness.droste.data.Fix +import org.scalacheck.Gen + +object ast { + + val stringData: Gen[EOStrData[EOExprOnly]] = + eo.undelimitedString.map(EOStrData[EOExprOnly]) + + val charData: Gen[EOCharData[EOExprOnly]] = + Gen.oneOf('\u0000' to '\u9999').map(EOCharData[EOExprOnly]) + + val integerData: Gen[EOIntData[EOExprOnly]] = + eo.integer.map(int => EOIntData[EOExprOnly](int.toInt)) + + val floatData: Gen[EOFloatData[EOExprOnly]] = + eo.float.map(float => EOFloatData[EOExprOnly](float.toFloat)) + + val simpleApp: Gen[EOSimpleApp[EOExprOnly]] = + Gen + .oneOf( + eo.attributeName, + Gen.const("^"), + Gen.const("$") + ) + .map(EOSimpleApp[EOExprOnly]) + + val simpleAppWithLocator: Gen[EOSimpleAppWithLocator[EOExprOnly]] = + Gen + .choose(0, 5) + .flatMap(n => + eo.identifier.map(name => EOSimpleAppWithLocator[EOExprOnly](name, n)) + ) + + val eoData: Gen[EOData[EOExprOnly]] = Gen.oneOf( + stringData, + charData, + integerData, + floatData + ) + + val rtMeta: Gen[EORTMeta] = + for { + alias <- eo.identifier + artifact <- eo.artifactName + } yield EORTMeta(alias, artifact) + + val aliasMeta: Gen[EOAliasMeta] = for { + alias <- eo.identifier + artifact <- eo.packageName + } yield EOAliasMeta(alias, artifact) + + val metas: Gen[EOMetas] = + for { + pkgMeta <- Gen.option(eo.packageName) + otherMetas <- between( + 0, + 4, + Gen.oneOf( + aliasMeta, + rtMeta, + ) + ).map(_.toVector) + } yield EOMetas(pkgMeta, otherMetas) + + def eoProg(maxDepth: Int): Gen[EOProg[EOExprOnly]] = for { + metas <- metas + bnds <- between( + 0, + 4, + Gen.frequency( + 1 -> anonExpr(maxDepth), + 9 -> bndExpr(maxDepth) + ) + ).map(_.toVector) + } yield EOProg(metas, bnds) + + def eoDot(maxDepth: Int, depth: Int = 0): Gen[EODot[EOExprOnly]] = for { + trg <- + if (depth < maxDepth) + eoExpr(maxDepth, depth + 1) + else + simpleApp.map(Fix(_)) + name <- eo.attributeName + } yield EODot(trg, name) + + def eoApp(maxDepth: Int, depth: Int = 0): Gen[EOApp[EOExprOnly]] = + if (depth < maxDepth) + Gen.oneOf( + eoCopy(maxDepth, depth + 1), + eoDot(maxDepth, depth + 1), + simpleApp, + simpleAppWithLocator + ) + else + simpleApp + + def eoExpr(maxDepth: Int, depth: Int = 0): Gen[EOExprOnly] = { + Gen + .oneOf(eoData, eoApp(maxDepth, depth), eoObj(maxDepth, depth)) + .map(Fix(_)) + } + + val anyNameBnd: Gen[EOAnyNameBnd] = for { + name <- eo.identifier + namedBnd <- Gen.oneOf(LazyName(name), ConstName(name)) + } yield EOAnyNameBnd(namedBnd) + + val namedBnd: Gen[EONamedBnd] = Gen.oneOf(anyNameBnd, Gen.const(EODecoration)) + + def anonExpr(maxDepth: Int, depth: Int = 0): Gen[EOAnonExpr[EOExprOnly]] = + eoExpr(maxDepth, depth).map(EOAnonExpr[EOExprOnly]) + + def bndExpr(maxDepth: Int, depth: Int = 0): Gen[EOBndExpr[EOExprOnly]] = for { + expr <- eoExpr(maxDepth, depth) + namedBnd <- namedBnd + } yield EOBndExpr(namedBnd, expr) + + def eoCopy(maxDepth: Int, depth: Int = 0): Gen[EOCopy[EOExprOnly]] = { + + for { + trg <- + if (depth < maxDepth) + eoExpr(maxDepth, depth + 1) + else + simpleApp.map(Fix(_)) + args <- between( + min = 1, + max = 3, + gen = Gen.frequency( + (1, bndExpr(maxDepth, depth + 1)), + (9, anonExpr(maxDepth, depth + 1)) + ) + ) + .flatMap(lst => + NonEmpty.from(lst.toVector) match { + case Some(value) => Gen.const(value) + case None => + anonExpr(maxDepth, depth + 1).map(it => + NonEmpty[Vector[EOBnd[EOExprOnly]]](it) + ) + } + ) + } yield EOCopy(trg, args) + } + + def eoObj(maxDepth: Int, depth: Int = 0): Gen[EOObj[EOExprOnly]] = { + for { + params <- between(0, 4, eo.paramName).map(_.map(LazyName).toVector) + vararg <- Gen.option(eo.paramName.map(LazyName)) + bnds <- between(0, 4, bndExpr(maxDepth, depth + 1)).map(_.toVector) + } yield EOObj(params, vararg, bnds) + } + +} diff --git a/parser/src/test/scala/org/polystat/odin/parser/Gens.scala b/parser/src/test/scala/org/polystat/odin/parser/gens/eo.scala similarity index 62% rename from parser/src/test/scala/org/polystat/odin/parser/Gens.scala rename to parser/src/test/scala/org/polystat/odin/parser/gens/eo.scala index 02d80438..02998da9 100644 --- a/parser/src/test/scala/org/polystat/odin/parser/Gens.scala +++ b/parser/src/test/scala/org/polystat/odin/parser/gens/eo.scala @@ -1,30 +1,11 @@ -package org.polystat.odin.parser +package org.polystat.odin.parser.gens import org.scalacheck.Gen -object Gens { - - def between[T]( - min: Int, - max: Int, - gen: Gen[T], - sep: Gen[String] = "" - ): Gen[String] = { - for { - len <- Gen.choose(min, max) - gens <- Gen.listOfN(len, gen) - sep <- sep - } yield gens.mkString(sep) - } - - def surroundedBy[T, S](gen: Gen[T], sur: Gen[S]): Gen[String] = for { - before <- sur - thing <- gen - after <- sur - } yield s"$before$thing$after" +object eo { val wsp: Gen[String] = - between( + betweenStr( 1, 2, Gen.frequency( @@ -33,7 +14,7 @@ object Gens { ) ).map(_.mkString) - val optWsp: Gen[String] = between( + val optWsp: Gen[String] = betweenStr( 0, 2, Gen.frequency( @@ -57,20 +38,20 @@ object Gens { } yield s"$wsp$eol" val comment = for { before <- optWsp - comment <- between(0, 15, Gen.alphaLowerChar) + comment <- betweenStr(0, 15, Gen.alphaLowerChar) eol <- eol } yield s"$before#$comment$eol" - between(0, 2, Gen.oneOf(emptyLine, comment)) + betweenStr(0, 2, Gen.oneOf(emptyLine, comment)) } val digit: Gen[Char] = Gen.numChar - val digits: Gen[String] = between(1, 5, digit) + val digits: Gen[String] = betweenStr(1, 5, digit) val nonZeroDigit: Gen[Char] = Gen.oneOf('1' to '9') val nonZeroInteger: Gen[String] = for { sign <- Gen.oneOf("", "-") first <- nonZeroDigit - rest <- between(0, 1, digits) + rest <- betweenStr(0, 1, digits) } yield (sign :: first :: rest :: Nil).mkString val integer: Gen[String] = Gen.frequency( @@ -83,7 +64,7 @@ object Gens { after <- digits } yield s"$before.$after" - val escapedUnicode: Gen[String] = between(4, 4, digit).map("\\u" + _) + val escapedUnicode: Gen[String] = betweenStr(4, 4, digit).map("\\u" + _) val javaEscape: Gen[String] = Gen.frequency( (1, "\\t"), @@ -108,12 +89,15 @@ object Gens { ) ) - val string: Gen[String] = - between( + val undelimitedString: Gen[String] = + betweenStr( 0, 15, undelimitedChar - ).map(str => s"\"$str\"") + ) + + val string: Gen[String] = + undelimitedString.map(str => s"\"$str\"") val char: Gen[String] = undelimitedChar .retryUntil(_ != "'") @@ -130,11 +114,11 @@ object Gens { val identifier: Gen[String] = for { fst <- smallLetter - rest <- between(0, 3, identifierChar) + rest <- betweenStr(0, 3, identifierChar) } yield fst +: rest val packageName: Gen[String] = - between(1, 3, identifier, sep = ".") + betweenStr(1, 3, identifier, sep = ".") val packageMeta: Gen[String] = for { name <- packageName @@ -150,7 +134,7 @@ object Gens { pkgName <- packageName artifactName <- identifier version <- - between(3, 3, digits.retryUntil(s => !s.startsWith("0")), sep = ".") + betweenStr(3, 3, digits.retryUntil(s => !s.startsWith("0")), sep = ".") } yield s"$pkgName:$artifactName:$version" val rtMeta: Gen[String] = for { @@ -160,9 +144,9 @@ object Gens { val metas: Gen[String] = for { header <- emptyLinesOrComments - pkg <- between(0, 1, packageMeta) + pkg <- betweenStr(0, 1, packageMeta) pkgEol <- eol - metas <- between( + metas <- betweenStr( 0, 5, for { @@ -181,18 +165,16 @@ object Gens { val bndName: Gen[String] = for { op <- surroundedBy(">", optWsp) id <- paramName - exclamationMark <- surroundedBy(between(0, 1, "!"), optWsp) + exclamationMark <- surroundedBy(betweenStr(0, 1, "!"), optWsp) } yield (op :: id :: exclamationMark :: Nil).mkString val abstractionParams: Gen[String] = for { - params <- between(0, 5, paramName, sep = wsp) - vararg <- between(0, 1, if (params.nonEmpty) "..." else "") + params <- betweenStr(0, 5, paramName, sep = wsp) + vararg <- betweenStr(0, 1, if (params.nonEmpty) "..." else "") } yield s"[$params$vararg]" val attributeName: Gen[String] = Gen.frequency( (1, "@"), - (1, "$"), - (1, "^"), (10, identifier) ) @@ -204,70 +186,108 @@ object Gens { ) def singleLineApplication( - recDepthMax: Int, - recDepth: Int = 0, + maxDepth: Int, + depth: Int = 0, ): Gen[String] = { + val nameWithLocator = Gen.oneOf( + identifier.map(name => "$." + name), + Gen + .choose(1, 5) + .flatMap(n => + identifier.map(name => List.fill(n)("^").mkString(".") + "." + name) + ) + ) + val simpleApplicationTarget = Gen.oneOf( data, - attributeName + attributeName, + Gen.const("^"), + Gen.const("$"), + nameWithLocator ) - val attributeChain = for { - trg <- simpleApplicationTarget - attrs <- between(1, 3, attributeName, sep = ".") + def parenthesized(maxDepth: Int, depth: Int): Gen[String] = + if (depth < maxDepth) + singleLineApplication( + depth = depth, + maxDepth = maxDepth + ) + .map(s => s"($s)") + else + simpleApplicationTarget + + def singleLineEoBnd(maxDepth: Int, depth: Int): Gen[String] = for { + expr <- singleLineApplication(maxDepth, depth) + bndName <- eo.bndName + } yield List("(", expr, bndName, ")").mkString + + def singleLineAbstraction(maxDepth: Int, depth: Int): Gen[String] = for { + params <- eo.abstractionParams + args <- between(0, 4, singleLineEoBnd(maxDepth, depth + 1)) + wsp <- wsp + } yield { + if (args.isEmpty) + List[String]("(", params, ")").mkString + else + List[String](params, wsp, args.mkString(" ")).mkString + } + + def attributeChain(maxDepth: Int, depth: Int): Gen[String] = for { + trg <- + Gen.oneOf(simpleApplicationTarget, parenthesized(maxDepth, depth + 1)) + attrs <- betweenStr(1, 3, attributeName, sep = ".") } yield s"$trg.$attrs" - val applicationTarget = Gen.oneOf( + def applicationTarget(maxDepth: Int, depth: Int): Gen[String] = Gen.oneOf( + parenthesized(maxDepth, depth), simpleApplicationTarget, - attributeChain + attributeChain(maxDepth, depth) ) - val parenthesized = Gen.lzy( - singleLineApplication( - recDepth = recDepth + 1, - recDepthMax = recDepthMax - ) - .map(s => s"($s)") - ) - - val horizontalApplicationArgs = { + def horizontalApplicationArgs(maxDepth: Int, depth: Int): Gen[String] = { val arg = - if (recDepth < recDepthMax) - Gen.oneOf(parenthesized, applicationTarget) - else { - applicationTarget - } - between(1, 5, arg, sep = wsp) + if (depth < maxDepth) + applicationTarget(maxDepth, depth + 1) + else + simpleApplicationTarget + betweenStr(1, 5, arg, sep = wsp) } - val justApplication = for { + def justApplication(maxDepth: Int, depth: Int): Gen[String] = for { trg <- - if (recDepth < recDepthMax) - Gen.oneOf(parenthesized, applicationTarget) - else applicationTarget + if (depth < maxDepth) + applicationTarget(maxDepth, depth + 1) + else + simpleApplicationTarget sep <- wsp - args <- horizontalApplicationArgs + args <- horizontalApplicationArgs(maxDepth, depth + 1) } yield (trg :: sep :: args :: Nil).mkString - val singleLineArray = for { + def singleLineArray(maxDepth: Int, depth: Int): Gen[String] = for { sep <- optWsp elem = - if (recDepth < recDepthMax) - Gen.oneOf(parenthesized, applicationTarget) - else applicationTarget - elems <- between(0, 4, elem, sep = wsp) + if (depth < maxDepth) + Gen.oneOf( + parenthesized(maxDepth, depth + 1), + applicationTarget(maxDepth, depth + 1) + ) + else simpleApplicationTarget + elems <- betweenStr(0, 4, elem, sep = wsp) } yield s"*$sep$elems" - Gen.oneOf( - justApplication, - applicationTarget, - parenthesized, - singleLineArray - ) + if (depth < maxDepth) + Gen.oneOf( + justApplication(maxDepth, depth), + applicationTarget(maxDepth, depth + 1), + singleLineArray(maxDepth, depth), + singleLineAbstraction(maxDepth, depth), + ) + else + simpleApplicationTarget } - val nothing: Gen[String] = Gen.oneOf("" :: Nil) + val nothing: Gen[String] = Gen.const("") def wspBetweenObjs( recDepth: Int, @@ -280,7 +300,7 @@ object Gens { def `object`( named: Boolean, indentationStep: Int, - recDepthMax: Int, + maxDepth: Int, recDepth: Int = 0, includeInverseDot: Boolean = true, ): Gen[String] = { @@ -289,11 +309,11 @@ object Gens { name <- if (named) bndName else nothing ifAttrs <- Gen.oneOf(true, false) attrs <- - if (recDepth < recDepthMax && ifAttrs) + if (recDepth < maxDepth && ifAttrs) boundAttributes( recDepth = recDepth + 1, indentationStep = indentationStep, - recDepthMax = recDepthMax, + maxDepth = maxDepth, ) else nothing } yield (params :: name :: attrs :: Nil).mkString @@ -304,21 +324,21 @@ object Gens { attrs <- verticalApplicationArgs( recDepth = recDepth + 1, indentationStep = indentationStep, - recDepthMax = recDepthMax, - includeInverseDot = recDepth < recDepthMax + maxDepth = maxDepth, + includeInverseDot = recDepth < maxDepth ) } yield (id :: "." :: name :: attrs :: Nil).mkString val regularApplication = for { - trg <- singleLineApplication(recDepthMax = recDepthMax) + trg <- singleLineApplication(maxDepth = maxDepth) name <- if (named) bndName else nothing ifArgs <- Gen.oneOf(true, false) args <- - if (recDepth < recDepthMax && ifArgs) + if (recDepth < maxDepth && ifArgs) verticalApplicationArgs( recDepth = recDepth + 1, indentationStep = indentationStep, - recDepthMax = recDepthMax, + maxDepth = maxDepth, ) else nothing } yield (trg :: name :: args :: Nil).mkString @@ -327,11 +347,11 @@ object Gens { name <- if (named) bndName else nothing ifHasItems <- Gen.oneOf(true, false) items <- - if (recDepth < recDepthMax && ifHasItems) + if (recDepth < maxDepth && ifHasItems) verticalApplicationArgs( recDepth = recDepth + 1, indentationStep = indentationStep, - recDepthMax = recDepthMax, + maxDepth = maxDepth, ) else nothing } yield ("*" :: name :: items :: Nil).mkString @@ -348,19 +368,19 @@ object Gens { def verticalApplicationArgs( recDepth: Int, indentationStep: Int, - recDepthMax: Int, + maxDepth: Int, includeInverseDot: Boolean = true, ): Gen[String] = for { before <- wspBetweenObjs(recDepth, indentationStep) named <- Gen.oneOf(true, false) - objs <- between( + objs <- betweenStr( 1, 3, `object`( recDepth = recDepth, named = named, indentationStep = indentationStep, - recDepthMax = recDepthMax, + maxDepth = maxDepth, includeInverseDot = includeInverseDot ), sep = wspBetweenObjs(recDepth, indentationStep) @@ -370,17 +390,17 @@ object Gens { def boundAttributes( recDepth: Int, indentationStep: Int, - recDepthMax: Int, + maxDepth: Int, ): Gen[String] = for { before <- wspBetweenObjs(recDepth, indentationStep) - objs <- between( + objs <- betweenStr( 1, 3, `object`( recDepth = recDepth, named = true, indentationStep = indentationStep, - recDepthMax = recDepthMax + maxDepth = maxDepth ), sep = wspBetweenObjs(recDepth, indentationStep) ) @@ -388,10 +408,10 @@ object Gens { def program( indentationStep: Int, - recDepthMax: Int, + maxDepth: Int, ): Gen[String] = for { metas <- metas - objs <- between( + objs <- betweenStr( 0, 4, Gen @@ -400,7 +420,7 @@ object Gens { `object`( named = named, indentationStep = indentationStep, - recDepthMax = recDepthMax + maxDepth = maxDepth ) ), sep = wspBetweenObjs(0, indentationStep = indentationStep) diff --git a/parser/src/test/scala/org/polystat/odin/parser/gens/package.scala b/parser/src/test/scala/org/polystat/odin/parser/gens/package.scala new file mode 100644 index 00000000..72216701 --- /dev/null +++ b/parser/src/test/scala/org/polystat/odin/parser/gens/package.scala @@ -0,0 +1,33 @@ +package org.polystat.odin.parser + +import org.scalacheck.Gen + +package object gens { + + def betweenStr[T]( + min: Int, + max: Int, + gen: Gen[T], + sep: Gen[String] = "" + ): Gen[String] = { + sep.flatMap(sep => between(min, max, gen).map(_.mkString(sep))) + } + + def between[T]( + min: Int, + max: Int, + gen: Gen[T], + ): Gen[List[T]] = { + for { + len <- Gen.choose(min, max) + gens <- Gen.listOfN(len, gen) + } yield gens + } + + def surroundedBy[T, S](gen: Gen[T], sur: Gen[S]): Gen[String] = for { + before <- sur + thing <- gen + after <- sur + } yield s"$before$thing$after" + +} diff --git a/parser/src/test/scala/org/polystat/odin/parser/xmir/ParserTests.scala b/parser/src/test/scala/org/polystat/odin/parser/xmir/ParserTests.scala index 64efe632..325fc3d7 100644 --- a/parser/src/test/scala/org/polystat/odin/parser/xmir/ParserTests.scala +++ b/parser/src/test/scala/org/polystat/odin/parser/xmir/ParserTests.scala @@ -5,10 +5,11 @@ import cats.effect.{IO, Sync} import cats.implicits._ import org.polystat.odin.core.ast.EOBnd import org.polystat.odin.core.ast.astparams.EOExprOnly -import org.polystat.odin.parser.MutualRecExample import org.polystat.odin.parser.EoParser.sourceCodeEoParser +import org.polystat.odin.parser.ast_tests.FullProgramExamples import org.scalatest.Assertion import org.scalatest.wordspec.AsyncWordSpec + import scala.xml.Elem class ParserTests extends AsyncWordSpec with AsyncIOSpec { @@ -96,7 +97,9 @@ class ParserTests extends AsyncWordSpec with AsyncIOSpec { |[] > derived | base > @ | 0 > a - | base.^.f > stuff + | ^.^.^.base.f > stuff + | $.base > lmao + | ^.base > rofl |"str" > str |'c' > char |123 @@ -132,8 +135,7 @@ class ParserTests extends AsyncWordSpec with AsyncIOSpec { "very simple" -> verySimple, "simple" -> simple, "division by zero" -> divByZero, - "mutual_recursion_example" -> MutualRecExample.code - ) + ) ++ FullProgramExamples.correct.init.map(tc => (tc.label, tc.code)) tests.foreach { case (label, code) => registerAsyncTest(label) { diff --git a/utils/src/main/scala/org/polystat/odin/utils/text.scala b/utils/src/main/scala/org/polystat/odin/utils/text.scala index ceebcd49..c3d8a328 100644 --- a/utils/src/main/scala/org/polystat/odin/utils/text.scala +++ b/utils/src/main/scala/org/polystat/odin/utils/text.scala @@ -7,4 +7,105 @@ object text { def indent: String => String = indentWith(' ')(2) + // Here are the rules for escaping in json + val decodeTable: Map[Char, Char] = + Map( + ('\\', '\\'), + ('\'', '\''), + ('\"', '\"'), + ('b', '\b'), // backspace + ('f', '\f'), // form-feed + ('n', '\n'), + ('r', '\r'), + ('t', '\t') + ) + + private val encodeTable = + decodeTable.iterator.map { case (v, k) => (k, s"\\$v") }.toMap + + private val nonPrintEscape: Array[String] = + (0 until 32).map { c => + val strHex = c.toHexString + val strPad = List.fill(4 - strHex.length)('0').mkString + s"\\u$strPad$strHex" + }.toArray + + def escape(quoteChar: Char, str: String): String = { + // We can ignore escaping the opposite character used for the string + // x isn't escaped anyway and is kind of a hack here + val ignoreEscape = + if (quoteChar == '\'') '"' else if (quoteChar == '"') '\'' else 'x' + str.flatMap { c => + if (c == ignoreEscape) c.toString + else + encodeTable.get(c) match { + case None => + if (c < ' ') nonPrintEscape(c.toInt) + else c.toString + case Some(esc) => esc + } + } + } + + def unescape(str: String): Either[Int, String] = { + val sb = new java.lang.StringBuilder + def decodeNum(idx: Int, size: Int, base: Int): Int = { + val end = idx + size + if (end <= str.length) { + val intStr = str.substring(idx, end) + val asInt = + try Integer.parseInt(intStr, base) + catch { case _: NumberFormatException => ~idx } + sb.append(asInt.toChar) + end + } else ~str.length + } + @annotation.tailrec + def loop(idx: Int): Int = + if (idx >= str.length) { + // done + idx + } else if (idx < 0) { + // error from decodeNum + idx + } else { + val c0 = str.charAt(idx) + if (c0 != '\\') { + sb.append(c0) + loop(idx + 1) + } else { + // str(idx) == \ + val nextIdx = idx + 1 + if (nextIdx >= str.length) { + // error we expect there to be a character after \ + ~idx + } else { + val c = str.charAt(nextIdx) + decodeTable.get(c) match { + case Some(d) => + sb.append(d) + loop(idx + 2) + case None => + c match { + case 'o' => loop(decodeNum(idx + 2, 2, 8)) + case 'x' => loop(decodeNum(idx + 2, 2, 16)) + case 'u' => loop(decodeNum(idx + 2, 4, 16)) + case 'U' => loop(decodeNum(idx + 2, 8, 16)) + case other => + // \c is interpreted as just \c, if the character isn't + // escaped + sb.append('\\') + sb.append(other) + loop(idx + 2) + } + } + } + } + } + + val res = loop(0) + if (res < 0) Left(~res) + else Right(sb.toString) + } + }