From 9858f891728ffa891e20ddfdbd5a767852920e64 Mon Sep 17 00:00:00 2001 From: "P. Oscar Boykin" Date: Fri, 22 Nov 2024 11:52:28 -1000 Subject: [PATCH] Pass main or test options when generating c code --- .github/workflows/ci.yml | 2 +- .../bosatsu/codegen/clang/ClangGenTest.scala | 3 +- .../scala/org/bykn/bosatsu/MainModule.scala | 28 +---- .../scala/org/bykn/bosatsu/PackageMap.scala | 9 ++ .../scala/org/bykn/bosatsu/PackageName.scala | 9 ++ .../main/scala/org/bykn/bosatsu/Parser.scala | 20 ++++ .../bykn/bosatsu/codegen/clang/ClangGen.scala | 58 ++++++++-- .../codegen/clang/ClangTranspiler.scala | 109 +++++++++++++++--- .../codegen/python/PythonTranspiler.scala | 8 +- .../bosatsu/codegen/clang/ClangGenTest.scala | 3 +- 10 files changed, 182 insertions(+), 67 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff44d97f8..9d6a34a8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: - name: "build assembly" run: "sbt \"++${{matrix.scala}}; cli/assembly\"" - name: "generate c code" - run: "./bosatsuj transpile --input_dir test_workspace/ --package_root test_workspace/ --outdir c_out c" + run: "./bosatsuj transpile --input_dir test_workspace/ --package_root test_workspace/ --outdir c_out c --test" - name: "compile generated c code" run: | cp c_runtime/* c_out diff --git a/cli/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala b/cli/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala index a98b4f761..110e9bf60 100644 --- a/cli/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala +++ b/cli/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala @@ -28,8 +28,7 @@ class ClangGenTest extends munit.FunSuite { val res = ClangGen.renderMain( sortedEnv = sortedEnv, externals = ClangGen.ExternalResolver.FromJvmExternals, - value = (PackageName.PredefName, Identifier.Name("ignored")), - evaluator = (Code.Include(true, "eval.h"), Code.Ident("evaluator_run")) + value = (PackageName.PredefName, Identifier.Name("ignored")) ) res match { diff --git a/core/src/main/scala/org/bykn/bosatsu/MainModule.scala b/core/src/main/scala/org/bykn/bosatsu/MainModule.scala index 93478b5c7..2125115de 100644 --- a/core/src/main/scala/org/bykn/bosatsu/MainModule.scala +++ b/core/src/main/scala/org/bykn/bosatsu/MainModule.scala @@ -6,6 +6,7 @@ import com.monovore.decline.{Argument, Command, Help, Opts} import cats.parse.{Parser0 => P0, Parser => P} import org.typelevel.paiges.Doc import scala.util.{Failure, Success, Try} +import org.bykn.bosatsu.Parser.argFromParser import codegen.Transpiler @@ -1236,33 +1237,6 @@ abstract class MainModule[IO[_]](implicit val opts: Opts[MainCommand] = { - def argFromParser[A]( - p: P0[A], - defmeta: String, - typeName: String, - suggestion: String - ): Argument[A] = - new Argument[A] { - def defaultMetavar: String = defmeta - def read(string: String): ValidatedNel[String, A] = - p.parseAll(string) match { - case Right(a) => Validated.valid(a) - case _ => - val sugSpace = if (suggestion.nonEmpty) s" $suggestion" else "" - Validated.invalidNel( - s"could not parse $string as a $typeName." + sugSpace - ) - } - } - - implicit val argPack: Argument[PackageName] = - argFromParser( - PackageName.parser, - "packageName", - "package name", - "Must be capitalized strings separated by /" - ) - implicit val argValue: Argument[(PackageName, Option[Bindable])] = argFromParser( (PackageName.parser ~ (P.string( diff --git a/core/src/main/scala/org/bykn/bosatsu/PackageMap.scala b/core/src/main/scala/org/bykn/bosatsu/PackageMap.scala index 29ad31053..f37d9d086 100644 --- a/core/src/main/scala/org/bykn/bosatsu/PackageMap.scala +++ b/core/src/main/scala/org/bykn/bosatsu/PackageMap.scala @@ -54,6 +54,15 @@ case class PackageMap[A, B, C, +D]( }) }.toMap + def testValues( + implicit ev: Package[A, B, C, D] <:< Package.Typed[Any] + ): Map[PackageName, Identifier.Bindable] = + toMap.iterator.flatMap { case (n, pack) => + Package.testValue(ev(pack)).iterator.map { case (bn, _, _) => + (n, bn) + } + }.toMap + def topoSort( implicit ev: Package[A, B, C, D] <:< Package.Typed[Any] ): Toposort.Result[PackageName] = { diff --git a/core/src/main/scala/org/bykn/bosatsu/PackageName.scala b/core/src/main/scala/org/bykn/bosatsu/PackageName.scala index e4af69079..92b207c42 100644 --- a/core/src/main/scala/org/bykn/bosatsu/PackageName.scala +++ b/core/src/main/scala/org/bykn/bosatsu/PackageName.scala @@ -4,6 +4,7 @@ import cats.Order import cats.data.NonEmptyList import cats.implicits._ import cats.parse.{Parser => P} +import com.monovore.decline.Argument import org.typelevel.paiges.{Doc, Document} import Parser.upperIdent @@ -41,4 +42,12 @@ object PackageName { val PredefName: PackageName = PackageName(NonEmptyList.of("Bosatsu", "Predef")) + + implicit val argPack: Argument[PackageName] = + Parser.argFromParser( + parser, + "packageName", + "package name", + "Must be capitalized strings separated by /" + ) } diff --git a/core/src/main/scala/org/bykn/bosatsu/Parser.scala b/core/src/main/scala/org/bykn/bosatsu/Parser.scala index 3bb3e3a7e..befdcda8b 100644 --- a/core/src/main/scala/org/bykn/bosatsu/Parser.scala +++ b/core/src/main/scala/org/bykn/bosatsu/Parser.scala @@ -2,6 +2,7 @@ package org.bykn.bosatsu import cats.data.{Kleisli, Validated, ValidatedNel, NonEmptyList} import cats.parse.{Parser0 => P0, Parser => P} +import com.monovore.decline.Argument import org.typelevel.paiges.Doc import scala.collection.immutable.SortedMap @@ -480,4 +481,23 @@ object Parser { def parser[A](p: P[A]): P[MaybeTupleOrParens[A]] = tupleOrParens(p) | p.map(Bare(_)) } + + def argFromParser[A]( + p: P0[A], + defmeta: String, + typeName: String, + suggestion: String + ): Argument[A] = + new Argument[A] { + def defaultMetavar: String = defmeta + def read(string: String): ValidatedNel[String, A] = + p.parseAll(string) match { + case Right(a) => Validated.valid(a) + case _ => + val sugSpace = if (suggestion.nonEmpty) s" $suggestion" else "" + Validated.invalidNel( + s"could not parse $string as a $typeName." + sugSpace + ) + } + } } diff --git a/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangGen.scala b/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangGen.scala index a96e8be52..b94df223a 100644 --- a/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangGen.scala +++ b/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangGen.scala @@ -140,23 +140,52 @@ object ClangGen { } } + private def renderDeps( + env: Impl.Env, + sortedEnv: Vector[NonEmptyList[(PackageName, List[(Bindable, Expr)])]] + ): env.T[Unit] = { + import env._ + + Traverse[Vector].compose[NonEmptyList] + .traverse_(sortedEnv) { case (pn, values) => + values.traverse_ { case (bindable, expr) => + renderTop(pn, bindable, expr) + } + } + } + def renderMain( sortedEnv: Vector[NonEmptyList[(PackageName, List[(Bindable, Expr)])]], externals: ExternalResolver, - value: (PackageName, Bindable), - evaluator: (Code.Include, Code.Ident) + value: (PackageName, Bindable) ): Either[Error, Doc] = { val env = Impl.Env.impl - import env._ + import env.monadImpl - val trav2 = Traverse[Vector].compose[NonEmptyList] + val res = renderDeps(env, sortedEnv) *> env.renderMain(value._1, value._2) - val res = - trav2.traverse_(sortedEnv) { case (pn, values) => - values.traverse_ { case (bindable, expr) => - renderTop(pn, bindable, expr) + val allValues: Impl.AllValues = + sortedEnv + .iterator.flatMap(_.iterator) + .flatMap { case (p, vs) => + vs.iterator.map { case (b, e) => + (p, b) -> (e, generatedName(p, b)) + } } - } *> env.renderMain(value._1, value._2, evaluator._1, evaluator._2) + .toMap + + env.run(allValues, externals, res) + } + + def renderTests( + sortedEnv: Vector[NonEmptyList[(PackageName, List[(Bindable, Expr)])]], + externals: ExternalResolver, + values: List[(PackageName, Bindable)] + ): Either[Error, Doc] = { + val env = Impl.Env.impl + import env.monadImpl + + val res = renderDeps(env, sortedEnv) *> env.renderTests(values) val allValues: Impl.AllValues = sortedEnv @@ -168,7 +197,7 @@ object ClangGen { } .toMap - run(allValues, externals, res) + env.run(allValues, externals, res) } private def fullName(p: PackageName, b: Bindable): String = @@ -1135,7 +1164,8 @@ object ClangGen { } } - def renderMain(p: PackageName, b: Bindable, evalInc: Code.Include, evalFn: Code.Ident): T[Unit] + def renderMain(p: PackageName, b: Bindable): T[Unit] + def renderTests(values: List[(PackageName, Bindable)]): T[Unit] } object Env { @@ -1364,7 +1394,11 @@ object ClangGen { def constructorFn(p: PackageName, b: Bindable): T[Code.Ident] = monadImpl.pure(Code.Ident(Idents.escape("___bsts_c_", fullName(p, b)))) - def renderMain(p: PackageName, b: Bindable, evalInc: Code.Include, evalFn: Code.Ident): T[Unit] = + def renderMain(p: PackageName, b: Bindable): T[Unit] = + // TODO ??? + monadImpl.unit + + def renderTests(values: List[(PackageName, Bindable)]): T[Unit] = // TODO ??? monadImpl.unit } diff --git a/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangTranspiler.scala b/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangTranspiler.scala index 62224fa22..0eca2de43 100644 --- a/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangTranspiler.scala +++ b/core/src/main/scala/org/bykn/bosatsu/codegen/clang/ClangTranspiler.scala @@ -1,35 +1,89 @@ package org.bykn.bosatsu.codegen.clang import cats.Traverse -import cats.data.{Const, NonEmptyList} +import cats.data.{Const, NonEmptyList, Validated} import com.monovore.decline.{Argument, Opts} +import java.util.regex.{Pattern => RegexPat} import org.bykn.bosatsu.codegen.Transpiler -import org.bykn.bosatsu.{Identifier, MatchlessFromTypedExpr, PackageName, PackageMap, Par} +import org.bykn.bosatsu.{Identifier, MatchlessFromTypedExpr, Package, PackageName, PackageMap, Par, TypedExpr} import org.typelevel.paiges.Doc import scala.util.{Failure, Success, Try} +import Identifier.Bindable + +import cats.syntax.all._ + case object ClangTranspiler extends Transpiler { - case class Arguments() + sealed abstract class Mode(val name: String) + object Mode { + case class Main(pack: PackageName) extends Mode("main") + case class Test(filter: Option[PackageName => Boolean], filterRegex: String) extends Mode("test") { + def values(p: PackageMap.Typed[Any]): List[(PackageName, Bindable)] = + (filter match { + case None => + p.testValues.toList + case Some(k) => + p.testValues.iterator.filter { case (p, _) => k(p) }.toList + }).sortBy(_._1) + } + + val opts: Opts[Mode] = + Opts.option[PackageName]("main", "the package to use as an entry point") + .map(Main(_)) + .orElse( + Opts.flag("test", "compile the tests") *> + (Opts.option[String]("filter", "regular expression to filter package names").orNone) + .mapValidated { + case None => Validated.valid(Test(None, ".*")) + case Some(pat) => + Try(RegexPat.compile(pat)) match { + case Success(p) => Validated.valid(Test(Some(pn => p.matcher(pn.asString).matches()), pat)) + case Failure(e) => Validated.invalidNel(s"could not parse pattern: $pat\n\n${e.getMessage}") + } + } + ) + } + + case class Arguments(mode: Mode) type Args[P] = Const[Arguments, P] def traverseArgs: Traverse[Args] = implicitly def opts[P](pathArg: Argument[P]): Opts[Transpiler.Optioned[P]] = Opts.subcommand("c", "generate c code") { - Opts(Transpiler.optioned(this)(Const[Arguments, P](Arguments()))) + Mode.opts.map { m => + Transpiler.optioned(this)(Const[Arguments, P](Arguments(m))) + } } case class GenError(error: ClangGen.Error) extends Exception(s"clang gen error: ${error.display.render(80)}") + private def spacePackList(ps: Iterable[PackageName]): Doc = + (Doc.line + Doc.intercalate(Doc.comma + Doc.line, ps.map(p => Doc.text(p.asString)))) + .nested(4) + .grouped + case class CircularPackagesFound(loop: NonEmptyList[PackageName]) - extends Exception(s"circular dependencies found in packages: ${ - loop.map(_.asString).toList.mkString(", ") - }") + extends Exception( + (Doc.text("circular dependencies found in packages:") + spacePackList(loop.toList)).render(80) + ) + + case class InvalidMainValue(pack: PackageName, message: String) extends + Exception(s"invalid main ${pack.asString}: $message.") + + case class NoTestsFound(packs: List[PackageName], regex: String) extends + Exception( + (Doc.text("no tests found in:") + spacePackList(packs)).render(80) + ) def externalsFor(pm: PackageMap.Typed[Any]): ClangGen.ExternalResolver = ClangGen.ExternalResolver.stdExternals(pm) + def validMain[A](te: TypedExpr[A]): Either[String, Unit] = + // TODO: need to check this + Right(()) + def renderAll( pm: PackageMap.Typed[Any], args: Args[String] @@ -40,18 +94,39 @@ case object ClangTranspiler extends Transpiler { case Some(loop) => Failure(CircularPackagesFound(loop)) case None => val matchlessMap = MatchlessFromTypedExpr.compile(pm) + val sortedEnv = cats.Functor[Vector] + .compose[NonEmptyList] + .map(sorted.layers) { pn => pn -> matchlessMap(pn) } val ext = externalsFor(pm) - val doc = ClangGen.renderMain( - sortedEnv = cats.Functor[Vector] - .compose[NonEmptyList] - .map(sorted.layers) { pn => pn -> matchlessMap(pn) }, - externals = ext, - // TODO: this is currently ignored - value = (PackageName.PredefName, Identifier.Name("todo")), - // TODO: this is also ignored currently - evaluator = (Code.Include(true, "eval.h"), Code.Ident("evaluator_run")) - ) + val doc = args.getConst.mode match { + case Mode.Main(p) => + pm.toMap.get(p).flatMap(Package.mainValue(_)) match { + case Some((b, _, t)) => + validMain(t) match { + case Right(_) => + ClangGen.renderMain( + sortedEnv = sortedEnv, + externals = ext, + // TODO: this is currently ignored + value = (p, b)) + case Left(invalid) => + return Failure(InvalidMainValue(p, invalid)) + } + case None => + return Failure(InvalidMainValue(p, "empty package")) + } + case test @ Mode.Test(_, re) => + test.values(pm) match { + case Nil => + return Failure(NoTestsFound(pm.toMap.keySet.toList.sorted, re)) + case nonEmpty => + ClangGen.renderTests( + sortedEnv = sortedEnv, + externals = ext, + values = nonEmpty) + } + } doc match { case Left(err) => Failure(GenError(err)) diff --git a/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonTranspiler.scala b/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonTranspiler.scala index abea1e14a..b9f029d95 100644 --- a/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonTranspiler.scala +++ b/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonTranspiler.scala @@ -6,7 +6,7 @@ import cats.{Eval, Traverse} import com.monovore.decline.{Argument, Opts} import org.bykn.bosatsu.CollectionUtils.listToUnique import org.bykn.bosatsu.codegen.Transpiler -import org.bykn.bosatsu.{Package, PackageMap, Par, Parser, MatchlessFromTypedExpr} +import org.bykn.bosatsu.{PackageMap, Par, Parser, MatchlessFromTypedExpr} import org.typelevel.paiges.Doc import scala.util.Try @@ -90,11 +90,7 @@ case object PythonTranspiler extends Transpiler { }.toList if (missingExternals.isEmpty) { - val tests = pm.toMap.iterator.flatMap { case (n, pack) => - Package.testValue(pack).iterator.map { case (bn, _, _) => - (n, bn) - } - }.toMap + val tests = pm.testValues val parsedEvals = evaluators.map(Parser.unsafeParse(PythonGen.evaluatorParser, _)) diff --git a/core/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala b/core/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala index 2ab5c401c..94dd47f78 100644 --- a/core/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala +++ b/core/src/test/scala/org/bykn/bosatsu/codegen/clang/ClangGenTest.scala @@ -23,8 +23,7 @@ x = 1 NonEmptyList.one(PackageName.PredefName -> matchlessMap(PackageName.PredefName)), ), externals = ClangGen.ExternalResolver.FromJvmExternals, - value = (PackageName.PredefName, Identifier.Name(fns.last)), - evaluator = (Code.Include(true, "eval.h"), Code.Ident("evaluator_run")) + value = (PackageName.PredefName, Identifier.Name(fns.last)) ) res match {