From c58b29031ea757ba81b97b14f9a7991a9259e4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Thu, 18 Jul 2024 02:08:05 +0200 Subject: [PATCH] probably my worst attempt at anything --- build.sbt | 2 + .../playground/smithyql/parser/v3/Demo.scala | 616 +++++++++++++----- 2 files changed, 473 insertions(+), 145 deletions(-) diff --git a/build.sbt b/build.sbt index 2c1437d5..2c9bd822 100644 --- a/build.sbt +++ b/build.sbt @@ -99,6 +99,8 @@ lazy val parser = module("parser") "io.circe" %% "circe-generic" % "0.14.9" % Test, "io.circe" %% "circe-parser" % "0.14.9" % Test, "co.fs2" %% "fs2-io" % "3.10.2" % Test, + // TODO: this is temp + "com.lihaoyi" %% "pprint" % "0.9.0", ) ) .enablePlugins(Antlr4Plugin) diff --git a/modules/parser/src/main/scala/playground/smithyql/parser/v3/Demo.scala b/modules/parser/src/main/scala/playground/smithyql/parser/v3/Demo.scala index 17b4e33e..785f8699 100644 --- a/modules/parser/src/main/scala/playground/smithyql/parser/v3/Demo.scala +++ b/modules/parser/src/main/scala/playground/smithyql/parser/v3/Demo.scala @@ -1,24 +1,74 @@ package playground.smithyql.parser.v3 +import cats.Applicative +import cats.Id import cats.Monad import cats.Parallel +import cats.StackSafeMonad +import cats.arrow.FunctionK import cats.data.EitherNel import cats.data.NonEmptyList import cats.implicits._ import org.antlr.v4.runtime.BaseErrorListener import org.antlr.v4.runtime.CharStreams import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.Parser +import org.antlr.v4.runtime.ParserRuleContext import org.antlr.v4.runtime.RecognitionException import org.antlr.v4.runtime.Recognizer -import org.antlr.v4.runtime.tree.ErrorNode -import org.antlr.v4.runtime.tree.TerminalNode +import playground.smithyql.QualifiedIdentifier +import playground.smithyql.parser.v3.Demo.Schema.Mapped +import playground.smithyql.parser.v3.Demo.Schema.Node +import playground.smithyql.parser.v3.Demo.Schema.Repeat +import playground.smithyql.parser.v3.Demo.Schema.Repeat1 +import playground.smithyql.parser.v3.Demo.Schema.Struct +import playground.smithyql.parser.v3.Demo.Schema.Terminal +import playground.smithyql.parser.v3.Yikes.NamespaceContext import playground.smithyql.parser.v3.Yikes.Source_fileContext import playground.smithyql.parser.v3.Yikes.Use_clauseContext import scala.jdk.CollectionConverters._ +import scala.reflect.ClassTag + +case class SourceFile[F[_]]( + clauses: F[List[F[UseClause[F]]]] +) { + + def sequence( + implicit F: Parallel[F], + M: Monad[F], + ): F[SourceFile[Id]] = clauses.flatMap { + _.parTraverse(_.flatMap(_.sequence)) + .map(SourceFile[Id](_)) + } + +} + +case class UseClause[F[_]]( + namespace: F[NonEmptyList[F[String]]], + service: F[String], +) { + + def sequence( + implicit F: Parallel[F], + M: Monad[F], + ): F[UseClause[Id]] = (namespace.flatMap(_.parSequence), service) + .parMapN(UseClause[Id](_, _)) + +} object Demo { + implicit class NullableOps[T]( + t: T + ) { + + def requireOr( + msg: String + ): EitherNel[String, T] = Option(t).toRightNel(msg) + + } + def main( args: Array[String] ): Unit = { @@ -32,100 +82,54 @@ object Demo { use service #foo """.stripMargin - val l = new Tokens(CharStreams.fromString(input)) - - val p = new Yikes(new CommonTokenStream(l)) - - case class SourceFile[F[_]]( - clauses: List[F[UseClause[F]]] - ) { - - def sequence( - implicit F: Parallel[F], - M: Monad[F], - ): F[SourceFile[cats.Id]] = clauses - .parTraverse(_.flatMap(_.sequence)) - .map(SourceFile[cats.Id](_)) - - } - - case class UseClause[F[_]]( - namespace: F[NonEmptyList[F[String]]], - service: F[String], - ) { - - def sequence( - implicit F: Parallel[F], - M: Monad[F], - ): F[UseClause[cats.Id]] = (namespace.flatMap(_.parSequence), service) - .parMapN(UseClause[cats.Id](_, _)) - - } - - implicit class NullableOps[T]( - t: T - ) { + def tokenize( + s: String + ) = { + val l = new Tokens(CharStreams.fromString(s)) - def requireOr( - msg: String - ): EitherNel[String, T] = Option(t).toRightNel(msg) + val p = new Yikes(new CommonTokenStream(l)) + p.removeErrorListeners() + p } - def checkTerminal( - p: TerminalNode - ): EitherNel[String, String] = p.accept( - new YikesBaseVisitor[EitherNel[String, String]] { - override protected def defaultResult( - ): EitherNel[String, String] = sys.error("unsupported") - - override def visitTerminal( - node: TerminalNode - ): EitherNel[String, String] = node.getText().asRight - - override def visitErrorNode( - node: ErrorNode - ): EitherNel[String, String] = s"error node: ${node.getText()}".leftNel - } - ) - - def parseFull( - p: Yikes - ): EitherNel[String, SourceFile[EitherNel[String, *]]] = p - .source_file() - .requireOr("no source file") - .map { sf => - SourceFile[EitherNel[String, *]]( - sf - .use_clause() - .asScala - .toList - .map { useClause => - UseClause[EitherNel[String, *]]( - namespace = NonEmptyList - .fromList( - useClause - .qualified_identifier() - .namespace() - .ID() - .asScala - .toList - .map(_.requireOr("invalid namespace segment").flatMap(checkTerminal(_))) - ) - .toRightNel("missing namespace"), - service = useClause - .qualified_identifier() - .ID() - .requireOr("missing ident node") - .flatMap { - checkTerminal - }, - ).asRight - } - ) - } - - p.removeErrorListeners() + // val visitor = + // new YikesBaseVisitor[EitherNel[String, Any]] { + // override def visitSource_file( + // ctx: Source_fileContext + // ): EitherNel[String, SourceFile[EitherNel[String, *]]] = ctx + // .use_clause() + // .requireOr("no use clauses") + // .map(_.asScala.toList.map(visitUse_clause(_))) + // .map(SourceFile(_)) + + // override def visitUse_clause( + // ctx: Use_clauseContext + // ): EitherNel[String, UseClause[EitherNel[String, *]]] = ctx + // .qualified_identifier() + // .requireOr("missing qualified identifier") + // .flatMap { qi => + // UseClause[EitherNel[String, *]]( + // namespace = NonEmptyList + // .fromList( + // qi.namespace() + // .ID() + // .asScala + // .toList + // .map(_.requireOr("invalid namespace segment").map(_.getText())) + // ) + // .toRightNel("missing namespace"), + // service = qi.ID().requireOr("missing ident node").map(_.getText()), + // ).asRight + // } + + // override protected def defaultResult( + // ): EitherNel[String, Nothing] = sys.error("unsupported branch") + // } + + var errors: List[String] = Nil + + val p = tokenize(input) p.addErrorListener(new BaseErrorListener { @@ -148,67 +152,389 @@ object Demo { val nextLinesRange: String = input.linesWithSeparators.toList.slice(line, line + 2).mkString - println( - s"""ERROR $line:$charPositionInLine @ $msg - |${previousLinesRange}${Console.GREEN}${beforeError}${Console.RED}${afterError}${Console.RESET} - |${" " * charPositionInLine}^HERE${nextLinesRange}""".stripMargin - ) + errors = + errors :+ ( + s"""ERROR $line:$charPositionInLine @ $msg + |${previousLinesRange}${Console.GREEN}${beforeError}${Console.RED}${afterError}${Console.RESET} + |${" " * charPositionInLine}^HERE${nextLinesRange}""".stripMargin + ) } }) - val r = parseFull(p) - println("parsed N rules: " + r.toOption.get.clauses.size) - r.toOption.get.clauses.foreach(println) - println("result: " + r.map(_.sequence)) - - p.reset() - val r2 = - new YikesBaseVisitor[EitherNel[String, Any]] { - override def visitSource_file( - ctx: Source_fileContext - ): EitherNel[String, SourceFile[EitherNel[String, *]]] = ctx - .use_clause() - .requireOr("no use clauses") - .map { - _.asScala.toList.map(visitUse_clause(_)) - } - .map(SourceFile(_)) - - override def visitUse_clause( - ctx: Use_clauseContext - ): EitherNel[String, UseClause[EitherNel[String, *]]] = ctx - .qualified_identifier() - .requireOr("missing qualified identifier") - .flatMap { qi => - UseClause[EitherNel[String, *]]( - namespace = NonEmptyList - .fromList( - qi.namespace() - .ID() - .asScala - .toList - .map(_.requireOr("invalid namespace segment").flatMap(checkTerminal(_))) - ) - .toRightNel("missing namespace"), - service = qi.ID().requireOr("missing ident node").flatMap { - checkTerminal - }, - ).asRight - } + // val resultVisitor = visitor + // .visitSource_file( + // p.source_file() + // ) + + // errors.foreach(println) + + // println(resultVisitor) + // println(resultVisitor.flatMap(_.sequence)) + + // p.reset() + // p.removeErrorListeners() + + // def parseManual( + // p: Yikes + // ): EitherNel[String, SourceFile[EitherNel[String, *]]] = p + // .source_file() + // .requireOr("no source file") + // .map { sf => + // SourceFile { + // sf + // .use_clause() + // .asScala + // .toList + // .map { useClause => + // UseClause( + // namespace = useClause + // .qualified_identifier() + // .namespace() + // .ID() + // .asScala + // .toList + // .map(_.requireOr("invalid namespace segment").map(_.getText())) + // .toNel + // .toRightNel("missing namespace"), + // service = useClause + // .qualified_identifier() + // .ID() + // .requireOr("missing ident node") + // .map(_.getText()), + // ).asRight + // } + // } + // } + + trait R[A] { + protected def underlying: EitherNel[String, A] + + def flatMap[B]( + f: A => R[B] + ): R[B] = R.wrap(underlying.flatMap(a => f(a).underlying)) + + def map[B]( + f: A => B + ): R[B] = flatMap(a => R.pure(f(a))) + + override def toString( + ): String = underlying.toString() + } + + object R { + private def wrap[A]( + e: EitherNel[String, A] + ): R[A] = + new R[A] { + protected def underlying: EitherNel[String, A] = e + } + + def pure[A]( + a: A + ): R[A] = wrap(Right(a)) + + def required[A]( + a: A + ): R[A] = { + assert(a != null, "required value is null") + pure(a) + } + + def nullable[A]( + a: A + ): R[Option[A]] = pure(Option(a)) - override protected def defaultResult( - ): EitherNel[String, Nothing] = sys.error("unsupported branch") + def collection[A]( + as: java.util.Collection[A] + ): R[List[A]] = { + assert(as != null, "required collection is null") + pure(as.asScala.toList) } - .visitSource_file( - p.source_file() - ) - println(r) - println(r2) - println(r == r2) + implicit class ROptionOps[A]( + ro: R[Option[A]] + ) { + def ifNull( + msg: String + ): R[A] = ro.flatMap { + case Some(a) => R.pure(a) + case None => R.wrap(Left(NonEmptyList.one(msg))) + } + } + + implicit class RListOps[A]( + rl: R[List[A]] + ) { + def mapEach[B]( + f: A => B + ): R[List[B]] = rl.map(_.map(f)) + + def nonEmptyOr( + msg: String + ): R[NonEmptyList[A]] = rl.map(_.toNel).ifNull(msg) + } - println(r2.flatMap(_.sequence)) + def parAp: Applicative[R] = + new Applicative[R] { + def pure[A]( + x: A + ): R[A] = R.pure(x) + + def ap[A, B]( + ff: R[A => B] + )( + fa: R[A] + ): R[B] = wrap( + Parallel.parAp(ff.underlying)(fa.underlying) + ) + } + + implicit val mon: Monad[R] = + new StackSafeMonad[R] { + def flatMap[A, B]( + fa: R[A] + )( + f: A => R[B] + ): R[B] = fa.flatMap(f) + + def pure[A]( + x: A + ): R[A] = R.pure(x) + } + + implicit val par: Parallel[R] = + new Parallel[R] { + type F[A] = R[A] + + def applicative: Applicative[R] = parAp + def monad: Monad[R] = mon + + def parallel: FunctionK[R, R] = FunctionK.id + def sequential: FunctionK[R, R] = FunctionK.id + } + } + + def nullCheck[A]( + name: String + )( + a: A + ): A = { assert(a != null, s"$name is null"); a } + + // rules of thumb: ID can always be null if singular + // def parseManual2( + // p: Yikes + // ): R[SourceFile[R]] = R + // .nullable { + // p.source_file() + // } + // .ifNull("no source file") + // .flatMap { sf => + // R.collection(sf.use_clause()) + // .mapEach { useClause => + // UseClause( + // namespace = R + // .nullable(useClause.qualified_identifier()) + // .ifNull("no qualified identifier") + // .flatMap(qi => + // R.collection(qi.namespace().ID()) + // .mapEach(R.required(_).map(_.getText())) + // ) + // .nonEmptyOr("no namespace"), + // service = R + // .nullable(useClause.qualified_identifier()) + // .ifNull("no qualified identifier") + // .map(_.ID()) + // .map(_.getText()), + // ) + // } + // } + // .map(_.map(R.pure(_))) + // .map(SourceFile(_)) + + object schemas { + + val id: Schema[XR[String]] = Schema.token(Tokens.ID) + val namespace: Schema[XR[NonEmptyList[XR[String]]]] = id + .repeat1 + .at[Yikes.NamespaceContext] + .map(_.flatten) + + val qi = Schema + .struct2( + namespace, + id, + )(UseClause.apply[XR]) + .at[Yikes.Qualified_identifierContext] + .at[Yikes.Use_clauseContext] + .map(_.flatten) + + val file: Schema[XR[SourceFile[XR]]] = Schema + .struct1( + qi.repeat + )(SourceFile.apply[XR]) + .at[Yikes.Source_fileContext] + + } + pprint.pprintln(schemas.file) + + println(decode(schemas.file, p)) + // val resultManual = parseManual(p) + // println(resultVisitor == resultManual) + + // println(parseManual2(tokenize("use foo.bar#baz"))) + // println(parseManual2(tokenize("use foo.bar#baz")).map(_.sequence)) + // println(tokenize("").source_file().use_clause().asScala.toList.map(_.getText())) } + trait XR[A] { + + def map[B]( + f: A => B + ): XR[B] = ??? + + def flatten[B]( + implicit ev: A <:< XR[B] + ): XR[B] = ??? + + } + + object XR { + + def pure[A]( + a: A + ): XR[A] = ??? + + } + + type StringK = XR[String] + + sealed trait Schema[Alg] { + + def map[A]( + f: Alg => A + ): Schema[A] = Schema.Mapped(this, f) + + def repeat1: Schema[XR[NonEmptyList[Alg]]] = Schema.Repeat1(this) + def repeat: Schema[XR[List[Alg]]] = Schema.Repeat(this) + + def at[A: ClassTag]: Schema.Field[XR[Alg]] = Schema.Node( + this, + implicitly[ClassTag[A]].runtimeClass.asInstanceOf[Class[_ <: ParserRuleContext]], + ) + + } + + object Schema { + + case class Terminal( + token: Int + ) extends Schema[StringK] + + case class Repeat[Alg]( + item: Schema[Alg] + ) extends Schema[XR[List[Alg]]] + + case class Repeat1[Alg]( + item: Schema[Alg] + ) extends Schema[XR[NonEmptyList[Alg]]] + + type Field[Alg] = Schema[Alg] + + case class Struct[Alg]( + fields: List[Field[Any]], + // fields: List[Schema[Any]], + make: List[Any] => Alg, + ) extends Schema[Alg] + + case class Node[Alg]( + schema: Schema[Alg], + tag: Class[_ <: ParserRuleContext], + ) extends Schema[XR[Alg]] + + case class Mapped[A, B]( + schema: Schema[A], + f: A => B, + ) extends Schema[B] + + def token( + tok: Int + ): Schema[StringK] = Terminal(tok) + + // TODO param for kind + def struct1[Alg, A1]( + a1: Field[A1] + )( + f: A1 => Alg + ): Schema[Alg] = Struct( + List(a1).asInstanceOf[List[Field[Any]]], + items => f(items(0).asInstanceOf[A1]), + ) + + // TODO param for kind + def struct2[Alg, A1, A2]( + a1: Field[A1], + a2: Field[A2], + )( + f: ( + A1, + A2, + ) => Alg + ): Schema[Alg] = Struct( + List(a1, a2).asInstanceOf[List[Field[Any]]], + items => + f( + items(0).asInstanceOf[A1], + items(1).asInstanceOf[A2], + ), + ) + + } + + def decode[A]( + schema: Schema[A], + parser: Yikes, + ): A = + schema match { + case n: Node[x] => + parser.source_file() match { + case node if n.tag.isInstance(node) => XR.pure(decodeInner(n.schema, node)) + } + } + + def decodeInner[A]( + schema: Schema[A], + node: ParserRuleContext, + ): A = + schema match { + case s: Struct[aa] => + def handleField[X]( + f: Schema.Field[X] + ): X = { + f match { + case rrrr: Repeat[a1] => + XR.pure { + rrrr.item match { + case m: Mapped[a2, b2] => + m.f { + schema match { + case n: Node[a3] => + Option(node.getRuleContexts(n.tag)) match { + case None => ??? + case Some(items) => items.asScala.toList.asInstanceOf[a2] + } + } + } + } + } + } + }.asInstanceOf[X] + + // sys.error(f.toString()) /* node.getRuleContexts() */ + + val allFieldsDecoded = s.fields.map(handleField(_)) + + s.make(allFieldsDecoded) + } + }