diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..27bb0f7a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +modules/treesitter/src/main/scala/playground/generated/** linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2267fc96..b12178df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,5 +55,22 @@ jobs: run: nix develop --command bash -c 'cd vscode-extension && yarn && SERVER_VERSION=$(cat ../.version) xvfb-run --auto-servernum yarn test' - name: Show extension test logs - if: job.status == 'failure' + if: always() && job.status == 'failure' run: cat vscode-extension/fixture/smithyql-log.txt | tail --lines 1000 + + build-parser: + name: "Build parser" + strategy: + matrix: + os: [ubuntu-latest, macos-latest, macos-13] + runs-on: ${{matrix.os}} + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4.1.1 + - uses: coursier/setup-action@v1 + with: + apps: sbt + jvm: adoptium:1.21 + - name: Parser tests + # intentionally not setting up nix + run: sbt treesitter/test diff --git a/build.sbt b/build.sbt index ba2bdc2e..10a7a231 100644 --- a/build.sbt +++ b/build.sbt @@ -87,6 +87,7 @@ val commonSettings = Seq( Test / scalacOptions += "-Wconf:cat=deprecation:silent,msg=Specify both message and version:silent", scalacOptions += "-release:11", mimaFailOnNoPrevious := false, + resolvers += "Sonatype S01 snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots", ) def module( @@ -133,8 +134,28 @@ lazy val parser = module("parser") .dependsOn( ast % "test->test;compile->compile", source % "test->test;compile->compile", + treesitter % "test->compile", ) +lazy val treesitter = module("treesitter") + .settings( + libraryDependencies ++= Seq( + "org.polyvariant.treesitter4s" %% "core" % "0.4.0" + ) + ) + +lazy val parsergen = module("parser-gen") + .settings( + libraryDependencies ++= Seq( + "dev.optics" %% "monocle-core" % "3.3.0", + "com.disneystreaming.smithy4s" %% "smithy4s-json" % smithy4sVersion.value, + ("org.scalameta" %% "scalameta" % "4.11.0").cross(CrossVersion.for3Use2_13), + "org.polyvariant.treesitter4s" %% "core" % "0.4.0", + ), + scalacOptions -= "-release:11", + ) + .enablePlugins(Smithy4sCodegenPlugin) + // Formatter for the SmithyQL language constructs lazy val formatter = module("formatter") .settings( @@ -189,6 +210,7 @@ lazy val core = module("core") examples % "test->compile", pluginCore, ast, + treesitter, source % "test->test;compile->compile", parser % "test->compile;test->test", formatter, @@ -228,17 +250,27 @@ lazy val e2e = module("e2e") buildInfoKeys ++= Seq[BuildInfoKey.Entry[_]]( // do you know how to simplify this? let me know please! Def - .task((lsp / Compile / fullClasspath).value.map(_.data).map(_.toString)) - .taskValue - .named("lspClassPath"), - Def - .task( - (lsp / Compile / mainClass).value.getOrElse(sys.error("didn't find main class in lsp")) + .task { + s"""${(lsp / organization).value}::${(lsp / moduleName).value}:${(lsp / version).value}""" + } + // todo: replace with a full publishLocal before e2e in particular gets run (but not before tests run normally) + .dependsOn( + lsp / publishLocal, + languageSupport / publishLocal, + core / publishLocal, + parser / publishLocal, + pluginCore / publishLocal, + source / publishLocal, + treesitter / publishLocal, + ast / publishLocal, + formatter / publishLocal, + protocol4s / publishLocal, ) .taskValue - .named("lspMainClass"), + .named("lspArtifact") ), publish / skip := true, + Test / fork := true, ) .dependsOn(lsp) @@ -260,6 +292,7 @@ lazy val root = project core, examples, parser, + parsergen, formatter, languageSupport, lsp, @@ -267,4 +300,5 @@ lazy val root = project pluginCore, pluginSample, e2e, + treesitter, ) diff --git a/flake.lock b/flake.lock index 42bd7c08..0ef3c0b0 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730272153, - "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", + "lastModified": 1730602179, + "narHash": "sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", + "rev": "3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixpkgs-unstable", + "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 35a33a30..0ee4b41b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,10 +1,10 @@ { inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05"; flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { nixpkgs, flake-utils, ... }: + outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem ( system: let @@ -18,10 +18,40 @@ pkgs.sbt pkgs.jless pkgs.gnupg + (pkgs.tree-sitter.override { webUISupport = true; }) # temporary, while we don't download coursier ourselves pkgs.coursier ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.xvfb-run ]; }; + packages.tree-sitter-smithyql = + pkgs.stdenv.mkDerivation { + name = "tree-sitter-smithyql"; + src = ./tree-sitter-smithyql; + buildInputs = [pkgs.tree-sitter pkgs.nodejs]; + buildPhase = '' + tree-sitter generate + make + ''; + installPhase = if system == "x86_64-darwin" || system == "aarch64-darwin" then '' + cp libtree-sitter-smithyql.dylib $out + '' else '' + cp libtree-sitter-smithyql.so $out + ''; + }; + packages.tree-sitter-smithyql-all = + pkgs.stdenv.mkDerivation { + name = "tree-sitter-smithyql-all"; + src = ./tree-sitter-smithyql; + dontBuild=true; + installPhase = '' + mkdir $out + cd $out + mkdir darwin-aarch64 && cp ${self.packages.aarch64-darwin.tree-sitter-smithyql} darwin-aarch64/libtree-sitter-smithyql.dylib + mkdir darwin-x86-64 && cp ${self.packages.x86_64-darwin.tree-sitter-smithyql} darwin-x86-64/libtree-sitter-smithyql.dylib + mkdir linux-aarch64 && cp ${self.packages.aarch64-linux.tree-sitter-smithyql} linux-aarch64/libtree-sitter-smithyql.so + mkdir linux-x86-64 && cp ${self.packages.x86_64-linux.tree-sitter-smithyql} linux-x86-64/libtree-sitter-smithyql.so + ''; + }; } ); } diff --git a/modules/ast/src/main/scala/playground/smithyql/AST.scala b/modules/ast/src/main/scala/playground/smithyql/AST.scala index a542a1c7..1895de91 100644 --- a/modules/ast/src/main/scala/playground/smithyql/AST.scala +++ b/modules/ast/src/main/scala/playground/smithyql/AST.scala @@ -4,6 +4,7 @@ import cats.Applicative import cats.Functor import cats.Id import cats.Show +import cats.arrow.FunctionK import cats.data.NonEmptyList import cats.kernel.Eq import cats.kernel.Order @@ -87,7 +88,7 @@ final case class SourceFile[F[_]]( def mapK[G[_]: Functor]( fk: F ~> G - ): AST[G] = SourceFile( + ): SourceFile[G] = SourceFile( prelude = prelude.mapK(fk), statements = fk(statements).map(_.map(_.mapK(fk))), ) @@ -132,9 +133,9 @@ final case class OperationName[F[_]]( text: String ) extends AST[F] { - def mapK[G[_]: Functor]( - fk: F ~> G - ): OperationName[G] = copy() + def mapK[G[_]: Functor](fk: FunctionK[F, G]): OperationName[G] = retag[G] + + def retag[G[_]]: OperationName[G] = copy() } @@ -189,7 +190,7 @@ final case class QueryOperationName[F[_]]( fk: F ~> G ): QueryOperationName[G] = QueryOperationName( identifier.map(fk(_)), - fk(operationName).map(_.mapK(fk)), + fk(operationName).map(_.retag[G]), ) } diff --git a/modules/ast/src/test/scala/playground/Assertions.scala b/modules/ast/src/test/scala/playground/Assertions.scala index ea0f7ddd..ac2bd7b4 100644 --- a/modules/ast/src/test/scala/playground/Assertions.scala +++ b/modules/ast/src/test/scala/playground/Assertions.scala @@ -23,7 +23,7 @@ object Assertions extends Expectations.Helpers { val stringWithResets = d.show()(conf).linesWithSeparators.map(Console.RESET + _).mkString failure( - s"Diff failed:\n${Console.RESET}(${conf.left("expected")}, ${conf.right("actual")})\n\n" + stringWithResets + s"Diff failed:\n${Console.RESET}(${conf.left("actual")}, ${conf.right("expected")})\n\n" + stringWithResets ) } diff --git a/modules/core/src/main/scala/playground/ASTAdapter.scala b/modules/core/src/main/scala/playground/ASTAdapter.scala new file mode 100644 index 00000000..c7d6816f --- /dev/null +++ b/modules/core/src/main/scala/playground/ASTAdapter.scala @@ -0,0 +1,12 @@ +package playground + +import cats.syntax.all.* +import playground.smithyql.QualifiedIdentifier + +object ASTAdapter { + + def decodeQI(qi: playground.generated.nodes.QualifiedIdentifier): Option[QualifiedIdentifier] = + (qi.namespace.map(_.source).toNel, qi.selection.map(_.source)) + .mapN(QualifiedIdentifier.apply) + +} diff --git a/modules/core/src/main/scala/playground/MultiServiceResolver.scala b/modules/core/src/main/scala/playground/MultiServiceResolver.scala index 15c5fc7c..ecd4f696 100644 --- a/modules/core/src/main/scala/playground/MultiServiceResolver.scala +++ b/modules/core/src/main/scala/playground/MultiServiceResolver.scala @@ -9,6 +9,7 @@ import playground.smithyql.UseClause import playground.smithyql.WithSource object MultiServiceResolver { + import playground.smithyql.tsutils.* /** Determines which service should be used for a query. The rules are: * - If the operation name has a service identifier, there MUST be a service with that name @@ -37,6 +38,40 @@ object MultiServiceResolver { case None => resolveImplicit(queryOperationName.operationName, serviceIndex, useClauses) } + /** Determines which service should be used for a query. The rules are: + * - If the operation name has a service identifier, there MUST be a service with that name + * that contains the given operation. + * - If there's no service identifier, find all matching services that are included in the use + * clauses. MUST find exactly one entry. + * + * In other cases, such as when we can't find a unique entry, or the explicitly referenced + * service doesn't have an operation with a matching name, we fail. The latter might eventually + * be refactored to a separate piece of code. + * + * **Important**! + * + * This method assumes that all of the use clauses match the available service set. It does NOT + * perform a check on that. For the actual check, see PreludeCompiler. + */ + def resolveServiceTs( + queryOperationName: playground.generated.nodes.QueryOperationName, + serviceIndex: ServiceIndex, + useClauses: List[playground.generated.nodes.UseClause], + ): EitherNel[CompilationError, QualifiedIdentifier] = + queryOperationName.name match { + case Some(opName) => + // todo: this should be an Option in codegen. might be a bad grammar + queryOperationName.identifier.headOption match { + case Some(explicitRef) => resolveExplicitTs(serviceIndex, explicitRef, opName) + + case None => resolveImplicitTs(opName, serviceIndex, useClauses) + } + case None => + // TODO: operation name is invalid or something like that + ??? + + } + private def resolveExplicit( index: ServiceIndex, explicitRef: WithSource[QualifiedIdentifier], @@ -66,6 +101,41 @@ object MultiServiceResolver { case Some(_) => explicitRef.value.asRight } + private def resolveExplicitTs( + index: ServiceIndex, + explicitRef: playground.generated.nodes.QualifiedIdentifier, + operationName: playground.generated.nodes.OperationName, + ): EitherNel[CompilationError, QualifiedIdentifier] = + ASTAdapter.decodeQI(explicitRef) match { + case None => ??? /* todo - I don't really know xD */ + // explicit reference exists, but doesn't parse + case Some(ref) => + index.getService(ref) match { + // explicit reference exists, but the service doesn't + case None => + CompilationError + .error( + CompilationErrorDetails.UnknownService(index.serviceIds.toList), + explicitRef.range, + ) + .leftNel + + // the service exists, but doesn't have the requested operation + case Some(service) + if !service.operationNames.contains_(OperationName(operationName.source)) => + CompilationError + .error( + CompilationErrorDetails.OperationMissing(service.operationNames.toList), + operationName.range, + ) + .leftNel + + // all good + case Some(_) => ref.asRight + } + + } + private def resolveImplicit( operationName: WithSource[OperationName[WithSource]], index: ServiceIndex, @@ -90,4 +160,28 @@ object MultiServiceResolver { } } + private def resolveImplicitTs( + operationName: playground.generated.nodes.OperationName, + index: ServiceIndex, + useClauses: List[playground.generated.nodes.UseClause], + ): EitherNel[CompilationError, QualifiedIdentifier] = { + val matchingServices = index + .getServices(useClauses.flatMap(_.identifier).flatMap(ASTAdapter.decodeQI).toSet) + .filter(_.hasOperation(OperationName(operationName.source))) + + matchingServices match { + case one :: Nil => one.id.asRight + case _ => + CompilationError + .error( + CompilationErrorDetails + .AmbiguousService( + workspaceServices = index.serviceIds.toList + ), + operationName.range, + ) + .leftNel + } + } + } diff --git a/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala b/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala index e651290c..144fd96e 100644 --- a/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala +++ b/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala @@ -1,166 +1,96 @@ package playground.smithyql import cats.syntax.all.* +import tsutils.* +import util.chaining.* trait RangeIndex { def findAtPosition( pos: Position - ): NodeContext + ): Option[NodeContext] } object RangeIndex { - def build( - sf: SourceFile[WithSource] - ): RangeIndex = - new RangeIndex { - - private val allRanges: List[ContextRange] = { - val path = NodeContext.EmptyPath - - val preludeRanges: List[ContextRange] = sf - .prelude - .useClauses - .toNel - .foldMap { useClauses => - val newBase = path.inPrelude - - ContextRange(useClauses.map(_.range).reduceLeft(_.fakeUnion(_)), newBase) :: - sf.prelude - .useClauses - .mapWithIndex { - ( - uc, - i, - ) => - findInUseClause(uc, newBase.inUseClause(i)) - } - .combineAll - } + def build(parsed: playground.generated.nodes.SourceFile): RangeIndex = fromRanges { - val queryRanges = sf.queries(WithSource.unwrap).zipWithIndex.flatMap { case (rq, index) => - findInQuery(rq.query, path.inQuery(index)) - } + val root = NodeContext.EmptyPath - preludeRanges ++ queryRanges + val preludeRanges = parsed + .prelude + .toList + .flatMap { prelude => + val newBase = root.inPrelude + ContextRange(prelude.range, newBase) :: + prelude.use_clause.zipWithIndex.map { (useClause, i) => + ContextRange(useClause.range, newBase.inUseClause(i)) + } } - // Console - // .err - // .println( - // s"""Found ${allRanges.size} ranges for query ${q.operationName.value.text}: - // |${allRanges - // .map(_.render) - // .mkString("\n")}""".stripMargin - // ) - - def findAtPosition( - pos: Position - ): NodeContext = allRanges - .filter(_.range.contains(pos)) - .maxByOption(_.ctx.length) - .map(_.ctx) - // By default, we're on root level - .getOrElse(NodeContext.EmptyPath) - - } - - private def findInQuery( - q: WithSource[Query[WithSource]], - path: NodeContext, - ) = { - val qv = q.value - - List(ContextRange(q.range, path)) ++ - findInOperationName(qv.operationName, path.inOperationName) ++ - findInNode(qv.input, path.inOperationInput) - } - - private def findInUseClause( - useClause: WithSource[UseClause[WithSource]], - path: NodeContext, - ): List[ContextRange] = ContextRange(useClause.value.identifier.range, path) :: Nil - - private def findInOperationName( - operationName: WithSource[QueryOperationName[WithSource]], - path: NodeContext, - ): List[ContextRange] = - ContextRange( - operationName.value.operationName.range, - path, - ) :: Nil - - private def findInNode( - node: WithSource[InputNode[WithSource]], - ctx: NodeContext, - ): List[ContextRange] = { - def entireNode( - ctx: NodeContext - ) = ContextRange(node.range, ctx) - - val default = Function.const( - // Default case: can be triggered e.g. inside a string literal - // which would affect completions of enum values and timestamps. - entireNode(ctx) :: Nil + def inputNodeRanges(node: playground.generated.nodes.InputNode, base: NodeContext) + : List[ContextRange] = node.visit( + new playground.generated.nodes.InputNode.Visitor.Default[List[ContextRange]] { + def default: List[ContextRange] = Nil + + override def onString(node: playground.generated.nodes.String_): List[ContextRange] = + ContextRange(node.range.shrink1, base.inQuotes) :: Nil + + override def onList(node: playground.generated.nodes.List_): List[ContextRange] = + ContextRange(node.range.shrink1, base.inCollectionEntry(None)) :: + node.list_fields.zipWithIndex.flatMap { (inputNode, i) => + ContextRange(inputNode.range, base.inCollectionEntry(Some(i))) :: + inputNodeRanges(inputNode, base.inCollectionEntry(Some(i))) + } + + override def onStruct(node: playground.generated.nodes.Struct): List[ContextRange] = + ContextRange(node.range.shrink1, base.inStructBody) :: + node.bindings.toList.flatMap { binding => + (binding.key, binding.value).tupled.toList.flatMap { (key, value) => + ContextRange(value.range, base.inStructBody.inStructValue(key.source)) :: + inputNodeRanges(value, base.inStructBody.inStructValue(key.source)) + } + } + } ) - node - .value - .fold( - listed = l => entireNode(ctx) :: findInList(l, ctx), - struct = s => entireNode(ctx) :: findInStruct(s, ctx.inStructBody), - string = { _ => - val inQuotes = ContextRange( - node.range.shrink1, - ctx.inQuotes, - ) - - inQuotes :: entireNode(ctx) :: Nil - }, - int = default, - bool = default, - nul = default, - ) + val queryRanges = parsed.statements.zipWithIndex.flatMap { (stat, statementIndex) => + stat.run_query.toList.flatMap { runQuery => + ContextRange(runQuery.range, root.inQuery(statementIndex)) :: runQuery + .operation_name + .toList + .flatMap { operationName => + ContextRange(operationName.range, root.inQuery(statementIndex).inOperationName) :: Nil + } ++ + runQuery.input.toList.flatMap { input => + inputNodeRanges( + playground.generated.nodes.InputNode(input), + root.inQuery(statementIndex).inOperationInput, + ) - } + } + } - private def findInList( - list: Listed[WithSource], - ctx: NodeContext, - ): List[ContextRange] = { - val inItems = list - .values - .value - .zipWithIndex - .flatMap { case (entry, index) => findInNode(entry, ctx.inCollectionEntry(index.some)) } - - val inBody = ContextRange( - list - .values - .range, - ctx.inCollectionEntry(None), - ) + } - inBody :: inItems + preludeRanges ++ queryRanges } - private def findInStruct( - struct: Struct[WithSource], - ctx: NodeContext, - ): List[ContextRange] = { - // Struct fields that allow nesting in them - val inFields = struct - .fields - .value - .value - .flatMap { binding => - findInNode(binding.value, ctx.inStructValue(binding.identifier.value.text)) - } - - ContextRange(struct.fields.range, ctx) :: inFields - } + def fromRanges(allRanges: List[ContextRange]): RangeIndex = + pos => + allRanges + .filter(_.range.contains(pos)) + .tap { ranges => + // println() + // println("=======") + // println(s"all ranges: ${allRanges.map(_.render).mkString(", ")}") + // println(s"ranges for position ${pos.index}: ${ranges.map(_.render).mkString(", ")}") + // println("=======") + // println() + } + .maxByOption(_.ctx.length) + .map(_.ctx) } diff --git a/modules/core/src/main/scala/playground/smithyql/tsutils.scala b/modules/core/src/main/scala/playground/smithyql/tsutils.scala new file mode 100644 index 00000000..5cb55005 --- /dev/null +++ b/modules/core/src/main/scala/playground/smithyql/tsutils.scala @@ -0,0 +1,11 @@ +package playground.smithyql + +import org.polyvariant.treesitter4s.Node + +object tsutils { + + extension (node: Node) { + def range: SourceRange = SourceRange(Position(node.startByte), Position(node.endByte)) + } + +} diff --git a/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala b/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala index 1c335c7d..9e06eeea 100644 --- a/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala +++ b/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala @@ -1,5 +1,7 @@ package playground.smithyql +import cats.syntax.all.* +import org.polyvariant.treesitter4s.TreeSitterAPI import playground.Assertions.* import playground.Diffs.given import playground.smithyql.parser.SourceParser @@ -24,6 +26,12 @@ object AtPositionTests extends FunSuite { text: String ): NodeContext = { val (extracted, position) = extractCursor(text) + val parsedTs = playground + .generated + .nodes + .SourceFile + .unsafeApply(TreeSitterAPI.make("smithyql").parse(extracted).rootNode.get) + val parsed = SourceParser[SourceFile] .parse(extracted) @@ -31,8 +39,9 @@ object AtPositionTests extends FunSuite { .get RangeIndex - .build(parsed) + .build(parsedTs) .findAtPosition(position) + .getOrElse(NodeContext.EmptyPath) } // tests for before/after/between queries diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index e4440b0a..9592492d 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -74,10 +74,9 @@ object E2ETests extends SimpleIOSuite { val builder = new ProcessBuilder( - "java", - "-cp", - BuildInfo.lspClassPath.mkString(":"), - BuildInfo.lspMainClass, + "cs", + "launch", + BuildInfo.lspArtifact, ) // Watch process stderr in test runner .redirectError(Redirect.INHERIT) diff --git a/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala b/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala index 58c99f4e..65338216 100644 --- a/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala +++ b/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala @@ -3,6 +3,8 @@ package playground.language import cats.Id import cats.kernel.Order.catsKernelOrderingForOrder import cats.syntax.all.* +import org.polyvariant.treesitter4s.TreeSitterAPI +import playground.ASTAdapter import playground.MultiServiceResolver import playground.ServiceIndex import playground.smithyql.NodeContext @@ -11,11 +13,7 @@ import playground.smithyql.NodeContext.^^: import playground.smithyql.OperationName import playground.smithyql.Position import playground.smithyql.QualifiedIdentifier -import playground.smithyql.Query import playground.smithyql.RangeIndex -import playground.smithyql.SourceFile -import playground.smithyql.WithSource -import playground.smithyql.parser.SourceParser import playground.smithyql.syntax.* import smithy4s.dynamic.DynamicSchemaIndex @@ -75,14 +73,15 @@ object CompletionProvider { } def completeRootOperationName( - file: SourceFile[WithSource], + file: playground.generated.nodes.SourceFile, insertBodyStruct: CompletionItem.InsertBodyStruct, ) = { // double-check test coverage. // there's definitely a test missing for N>1 clauses. // https://github.com/kubukoz/smithy-playground/issues/161 - val presentServiceIds - : List[QualifiedIdentifier] = file.prelude.useClauses.map(_.value.identifier.value) + val presentServiceIds: List[QualifiedIdentifier] = file + .select(_.prelude.use_clause.identifier) + .flatMap(ASTAdapter.decodeQI) // for operations on root level we show: // - completions for ops from the service being used, which don't insert a use clause and don't show the service ID @@ -112,17 +111,18 @@ object CompletionProvider { // we're definitely in an existing query, so we don't insert a brace in either case. def completeOperationNameFor( - q: Query[WithSource], - sf: SourceFile[WithSource], + q: playground.generated.nodes.RunQuery, + sf: playground.generated.nodes.SourceFile, serviceId: Option[QualifiedIdentifier], ): List[CompletionItem] = serviceId match { case Some(serviceId) => // includes the current query's service reference // as it wouldn't result in ading a use clause - val presentServiceIdentifiers = - q.operationName.value.mapK(WithSource.unwrap).identifier.toList ++ - sf.prelude.useClauses.map(_.value.identifier.value) + val presentServiceIdentifiers = { + q.select(_.operation_name.identifier) ++ + sf.select(_.prelude.use_clause.identifier) + }.flatMap(ASTAdapter.decodeQI) completeOperationName( serviceId, @@ -134,16 +134,16 @@ object CompletionProvider { } def completeInQuery( - q: Query[WithSource], - sf: SourceFile[WithSource], + q: playground.generated.nodes.RunQuery, + sf: playground.generated.nodes.SourceFile, ctx: NodeContext, - ): List[CompletionItem] = { + ): List[CompletionItem] = q.operation_name.toList.flatMap { operationName => val resolvedServiceId = MultiServiceResolver - .resolveService( - q.operationName.value, + .resolveServiceTs( + operationName, serviceIndex, - sf.prelude.useClauses.map(_.value), + sf.select(_.prelude.use_clause), ) .toOption @@ -154,10 +154,12 @@ object CompletionProvider { case NodeContext.PathEntry.AtOperationInput ^^: ctx => resolvedServiceId match { case Some(serviceId) => - inputCompletions(serviceId)( - q.operationName.value.operationName.value.mapK(WithSource.unwrap) - ) - .getCompletions(ctx) + q.select(_.operation_name.name) + .map(id => OperationName[Id](id.source)) + .flatMap { + inputCompletions(serviceId)(_) + .getCompletions(ctx) + } case None => Nil } @@ -169,45 +171,48 @@ object CompletionProvider { ( doc, pos, - ) => - SourceParser[SourceFile].parse(doc) match { - case Left(_) => - // we can try to deal with this later - Nil - - case Right(sf) => - val matchingNode = RangeIndex - .build(sf) - .findAtPosition(pos) - - // System.err.println("matchingNode: " + matchingNode.render) - - matchingNode match { - case NodeContext.PathEntry.InQuery(n) ^^: rest => - val q = - sf - .queries(WithSource.unwrap) - .get(n.toLong) - .getOrElse(sys.error(s"Fatal error: no query at index $n")) - .query - .value - - completeInQuery(q, sf, rest) - - case NodeContext.PathEntry.AtPrelude ^^: - NodeContext.PathEntry.AtUseClause(_) ^^: - EmptyPath => - servicesById - .toList - .sortBy(_._1) - .map(CompletionItem.useServiceClause.tupled) - - case EmptyPath => completeRootOperationName(sf, CompletionItem.InsertBodyStruct.Yes) - - case _ => Nil - } + ) => { + val parsedTs = playground + .generated + .nodes + .SourceFile + .unsafeApply(TreeSitterAPI.make("smithyql").parse(doc).rootNode.get) + + val matchingNode = RangeIndex + .build(parsedTs) + .findAtPosition(pos) + .getOrElse(NodeContext.EmptyPath) + + // System.err.println("matchingNode: " + matchingNode.render) + + matchingNode match { + case NodeContext.PathEntry.InQuery(n) ^^: rest => + val q = parsedTs + .statements + .flatMap(_.run_query) + .get(n.toLong) + .getOrElse(sys.error(s"Fatal error: no query at index $n")) + + completeInQuery(q, parsedTs, rest) + + case NodeContext.PathEntry.AtPrelude ^^: + NodeContext.PathEntry.AtUseClause(_) ^^: + EmptyPath => + servicesById + .toList + .sortBy(_._1) + .map(CompletionItem.useServiceClause.tupled) + + case EmptyPath => + completeRootOperationName( + parsedTs, + CompletionItem.InsertBodyStruct.Yes, + ) + case _ => Nil } + + } } } diff --git a/modules/parser-gen/src/main/scala/playground/parsergen/IR.scala b/modules/parser-gen/src/main/scala/playground/parsergen/IR.scala new file mode 100644 index 00000000..e2c959d7 --- /dev/null +++ b/modules/parser-gen/src/main/scala/playground/parsergen/IR.scala @@ -0,0 +1,50 @@ +package playground.parsergen + +import cats.data.NonEmptyList +import treesittersmithy.FieldName +import treesittersmithy.NodeType +import treesittersmithy.TypeName + +enum Type { + case Union(name: TypeName, subtypes: NonEmptyList[Subtype]) + case Product(name: TypeName, fields: List[Field], children: Option[Children]) +} + +case class Field(name: FieldName, targetTypes: NonEmptyList[TypeName], repeated: Boolean) +case class Children(targetTypes: NonEmptyList[TypeName], repeated: Boolean) + +case class Subtype(name: TypeName) + +object IR { + + def from(nt: NodeType): Type = + if nt.subtypes.nonEmpty then fromUnion(nt) + else + fromProduct(nt) + + private def fromUnion(nt: NodeType): Type.Union = Type.Union( + name = nt.tpe, + subtypes = NonEmptyList.fromListUnsafe(nt.subtypes.map(subtype => Subtype(name = subtype.tpe))), + ) + + private def fromProduct(nt: NodeType): Type.Product = Type.Product( + name = nt.tpe, + fields = + nt.fields + .map { (fieldName, fieldInfo) => + Field( + name = fieldName, + targetTypes = NonEmptyList.fromListUnsafe(fieldInfo.types.map(_.tpe)), + repeated = fieldInfo.multiple, + ) + } + .toList, + children = nt.children.map { children => + Children( + targetTypes = NonEmptyList.fromListUnsafe(children.types.map(_.tpe)), + repeated = children.multiple, + ) + }, + ) + +} diff --git a/modules/parser-gen/src/main/scala/playground/parsergen/ParserGen.scala b/modules/parser-gen/src/main/scala/playground/parsergen/ParserGen.scala new file mode 100644 index 00000000..43811d57 --- /dev/null +++ b/modules/parser-gen/src/main/scala/playground/parsergen/ParserGen.scala @@ -0,0 +1,376 @@ +package playground.parsergen + +import cats.data.NonEmptyList +import cats.syntax.all.* +import monocle.syntax.all.* +import org.polyvariant.treesitter4s.Node +import smithy4s.Blob +import smithy4s.json.Json +import treesittersmithy.FieldName +import treesittersmithy.NodeType +import treesittersmithy.NodeTypes +import treesittersmithy.TypeName +import util.chaining.* + +import java.nio.file.Files +import java.nio.file.Paths +import scala.annotation.targetName +import scala.jdk.CollectionConverters.* +import scala.meta.Dialect + +extension (tn: TypeName) { + @targetName("renderTypeName") + def render: String = tn.value.dropWhile(_ == '_').fromSnakeCase.ident + def renderProjection: String = show"as${tn.prettyName}".ident + def renderVisitorMethod: String = show"on${tn.prettyName}".ident + private def prettyName = tn.value.dropWhile(_ == '_').fromSnakeCase + def asChildName: FieldName = FieldName(tn.value) +} + +extension (fn: FieldName) { + @targetName("renderFieldName") + def render: String = fn.value.ident +} + +extension (tpe: NodeType) { + + def render: String = + IR.from(tpe) match { + case union: Type.Union => renderUnion(union) + case product: Type.Product => renderProduct(product) + } + +} + +private def renderUnion(u: Type.Union): String = { + val name = u.name.render + val underlyingType = u.subtypes.map(_.name.render).mkString_(" | ") + + val projections = u.subtypes.map { sub => + // format: off + show"""def ${sub.name.renderProjection}: Option[${sub.name.render}] = ${sub.name.render}.unapply(node)""" + // format: on + } + + val instanceMethods = + show"""extension (node: $name) { + |${projections.mkString_("\n").indentTrim(2)} + | def visit[A](visitor: Visitor[A]): A = visitor.visit(node) + |}""".stripMargin + + val applyMethod = { + val cases = u + .subtypes + .map(nodeType => show"""case ${nodeType.name.render}(node) => Right(node)""") + + show"""def apply(node: Node): Either[String, $name] = node match { + |${cases.mkString_("\n").indentTrim(2)} + | case _ => Left(s"Expected $name, got $${node.tpe}") + |}""".stripMargin + } + + val typedApplyMethod = show"""def apply(node: $underlyingType): $name = node""".stripMargin + + val visitor = + show""" + |trait Visitor[A] { + |${u + .subtypes + .map(sub => show"def ${sub.name.renderVisitorMethod}(node: ${sub.name.render}): A") + .mkString_("\n") + .indentTrim(2)} + | + | def visit(node: $name): A = (node: @nowarn("msg=match may not be exhaustive")) match { + |${u + .subtypes + .map(sub => show"case ${sub.name.render}(node) => ${sub.name.renderVisitorMethod}(node)") + .mkString_("\n") + .indentTrim(4)} + | } + |} + | + |object Visitor { + | abstract class Default[A] extends Visitor[A] { + | def default: A + | + |${u + .subtypes + .map(sub => + show"def ${sub.name.renderVisitorMethod}(node: ${sub.name.render}): A = default" + ) + .mkString_("\n") + .indentTrim(4)} + | } + |} + |""".stripMargin + + val selectorMethods = u + .subtypes + .map { subtype => + // format: off + show"""def ${subtype.name.asChildName.render} : ${subtype.name.render}.Selector = ${subtype.name.render}.Selector(path.flatMap(_.${subtype.name.renderProjection}))""" + // format: on + } + .mkString_("\n") + + show"""// Generated code! Do not modify by hand. + |package playground.generated.nodes + | + |import ${classOf[Node].getName()} + |import playground.treesitter4s.std.Selection + |import annotation.nowarn + | + |opaque type $name <: Node = $underlyingType + | + |object $name { + | + |${instanceMethods.indentTrim(2)} + | + |${applyMethod.indentTrim(2)} + | + |${typedApplyMethod.indentTrim(2)} + | + | def unsafeApply(node: Node): $name = apply(node).fold(sys.error, identity) + | + | def unapply(node: Node): Option[$name] = apply(node).toOption + | + |${visitor.indentTrim(2)} + | + | final case class Selector(path: List[$name]) extends Selection[$name] { + |${selectorMethods.indentTrim(4)} + | + | type Self = Selector + | protected val remake = Selector.apply + | } + |} + |""".stripMargin +} + +private def renderProduct(p: Type.Product): String = { + val name = p.name.render + + def renderTypeUnion(types: NonEmptyList[TypeName]) = types + .map(_.render) + .reduceLeft(_ + " | " + _) + + def renderFieldType(field: Field): String = renderTypeUnion(field.targetTypes).pipe { + case s if field.repeated => show"List[$s]" + case s => show"Option[$s]" + } + + def renderChildrenType(children: Children): String = renderTypeUnion(children.targetTypes).pipe { + case s if children.repeated => show"List[$s]" + case s => show"Option[$s]" + } + + def renderChildType(tpe: TypeName, repeated: Boolean): String = tpe.render.pipe { + case s if repeated => show"List[$s]" + case s => show"Option[$s]" + } + + val fieldGetters = p + .fields + .map { field => + val allFields = show"""node.fields.getOrElse(${field.name.value.literal}, Nil)""" + + val cases = field.targetTypes.map { tpe => + show"""case ${tpe.render}(node) => node""" + } + + val fieldValue = + if field.repeated then show"""$allFields.toList.collect { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + else + show"""$allFields.headOption.map { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + + show"""def ${field.name.render}: ${renderFieldType(field)} = $fieldValue""" + } + + val typedChildren = p.children.map { children => + val fieldTypeAnnotation = renderChildrenType(children) + + val allChildren = show"""node.children""" + + val cases = children.targetTypes.map { tpe => + show"""case ${tpe.render}(node) => node""" + } + + val fieldValue = + if children.repeated then show"""$allChildren.toList.collect { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + else + show"""$allChildren.collectFirst { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + + show"""def typedChildren: ${fieldTypeAnnotation} = $fieldValue""" + } + + val typedChildrenPrecise = p + .children + .toList + .flatMap { fieldInfo => + fieldInfo.targetTypes.map((fieldInfo.repeated, _)).toList + } + .map { (repeated, fieldType) => + val fieldTypeAnnotation = renderChildType(fieldType, repeated) + val childValue = + if repeated then show"""node.children.toList.collect { + | case ${fieldType.render}(node) => node + |}""".stripMargin + else + show"""node.children.collectFirst { + | case ${fieldType.render}(node) => node + |}""".stripMargin + + show"""def ${fieldType.asChildName.render}: $fieldTypeAnnotation = $childValue""".stripMargin + } + + val instanceMethods = + if (fieldGetters.nonEmpty || typedChildren.nonEmpty || typedChildrenPrecise.nonEmpty) { + show"""extension (node: $name) { + | def select[A](f: $name.Selector => Selection[A]): List[A] = f($name.Selector(List(node))).path + | // fields + |${fieldGetters.mkString_("\n\n").indentTrim(2)} + | // typed children + |${typedChildren.foldMap(_.indentTrim(2)): String} + | // precise typed children + |${typedChildrenPrecise.mkString_("\n\n").indentTrim(2)} + |}""".stripMargin + } else + "" + + val selectorMethods = p + .fields + .flatMap { + case field if field.targetTypes.size == 1 => + // format: off + show"""def ${field.name.render}: ${field.targetTypes.head.render}.Selector = ${field.targetTypes.head.render}.Selector(path.flatMap(_.${field.name.render}))""".stripMargin.some + // format: on + + case f => + System + .err + .println( + s"Skipping selector for field ${f.name} in product $name as it has multiple target types" + ) + none + } + .concat( + p.children.toList.flatMap(_.targetTypes.toList).map { tpe => + // format: off + show"""def ${tpe.asChildName.render}: ${tpe.render}.Selector = ${tpe.render}.Selector(path.flatMap(_.${tpe.asChildName.render}))""".stripMargin + // format: on + } + ) + .mkString_("\n") + + show"""// Generated code! Do not modify by hand. + |package playground.generated.nodes + | + |import ${classOf[Node].getName()} + |import playground.treesitter4s.std.Selection + | + |opaque type $name <: Node = Node + | + |object $name { + |${instanceMethods.indentTrim(2)} + | + | def apply(node: Node): Either[String, $name] = + | if node.tpe == ${p.name.value.literal} + | then Right(node) + | else Left(s"Expected ${p.name.render}, got $${node.tpe}") + | + | def unsafeApply(node: Node): $name = apply(node).fold(sys.error, identity) + | + | def unapply(node: Node): Option[$name] = apply(node).toOption + | + | final case class Selector(path: List[$name]) extends Selection[$name] { + |${selectorMethods.indentTrim(4)} + | + | type Self = Selector + | protected val remake = Selector.apply + | } + |} + |""".stripMargin + +} + +@main def parserGen = { + val types = + Json + .read[NodeTypes]( + Blob(Files.readString(Paths.get("tree-sitter-smithyql/src/node-types.json"))) + ) + .toTry + .get + .value + + val base = Paths.get(s"modules/treesitter/src/main/scala/playground/generated/nodes") + + val rendered = types + .filter(_.named) + .map( + // only render field types that are named + _.focus(_.fields.each.types) + .modify(_.filter(_.named)) + // don't render the field if it has no types + .focus(_.fields) + .modify(_.filter((_, v) => v.types.nonEmpty)) + ) + .fproduct( + _.render + ) + + Files.createDirectories(base) + + Files.walk(base).iterator().asScala.filter(Files.isRegularFile(_)).foreach(Files.delete) + + rendered + .foreach { (tpe, code) => + Files.writeString( + base.resolve(s"${tpe.tpe.render}.scala"), + code, + ) + } +} + +extension (s: String) { + + def indentTrim(n: Int): String = s + .linesIterator + .map { + case line if line.nonEmpty => " " * n + line + case line => line + } + .mkString("\n") + + def trimLines: String = s.linesIterator.map(_.stripTrailing()).mkString("\n") + + def literal: String = scala.meta.Lit.String(s).printSyntaxFor(scala.meta.dialects.Scala3) + + def ident: String = { + // etc. + val reserved = Set("List", "String", "Boolean", "Null") + if reserved(s) then s + "_" + else + scala.meta.Name(s).printSyntaxFor(scala.meta.dialects.Scala3) + } + + def fromSnakeCase: String = s.split('_').map(_.capitalize).mkString + +} + +extension [A](l: List[A]) { + + def requireOnly: A = + l match { + case a :: Nil => a + case _ => throw new IllegalArgumentException(s"Expected exactly one element, got $l") + } + +} diff --git a/modules/parser-gen/src/main/smithy/treesitter.smithy b/modules/parser-gen/src/main/smithy/treesitter.smithy new file mode 100644 index 00000000..3c7c687e --- /dev/null +++ b/modules/parser-gen/src/main/smithy/treesitter.smithy @@ -0,0 +1,58 @@ +$version: "2" + +namespace treesittersmithy + +list NodeTypes { + member: NodeType +} + +structure NodeType { + @required + @jsonName("type") + tpe: TypeName + + @required + named: Boolean + + @required + fields: NodeFields = {} + + children: FieldInfo + + @required + subtypes: NodeTypes = [] +} + +string TypeName + +map NodeFields { + key: FieldName + value: FieldInfo +} + +string FieldName + +structure FieldInfo { + @required + multiple: Boolean + + @required + required: Boolean + + @required + types: TypeList +} + +list TypeList { + member: TypeInfo +} + +// https://github.com/disneystreaming/smithy4s/issues/1618 +structure TypeInfo { + @required + @jsonName("type") + tpe: TypeName + + @required + named: Boolean +} diff --git a/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala b/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala index 95ea1188..9b15eda0 100644 --- a/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala +++ b/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala @@ -8,6 +8,8 @@ import fs2.io.file.Path import io.circe.Codec import io.circe.Decoder import io.circe.syntax.* +import org.polyvariant.treesitter4s.Node +import org.polyvariant.treesitter4s.TreeSitterAPI import playground.Assertions.* import playground.smithyql.* import playground.smithyql.parser.v2.scanner.Scanner @@ -20,6 +22,8 @@ import java.nio.file.Paths trait ParserSuite extends SimpleIOSuite { + def treeSitterWrap(fileSource: String): String = fileSource + def loadParserTests[Alg[_[_]]: SourceParser]( prefix: String, // this isn't on by default because whitespace in full files (currently, 1-1 mapping to queries) is significant and should not be trimmed before parsing. @@ -56,6 +60,7 @@ trait ParserSuite extends SimpleIOSuite { } validTokensTest(testCase, trimWhitespace) + treeSitterTest(testCase, trimWhitespace) } private def validTokensTest( @@ -73,6 +78,27 @@ trait ParserSuite extends SimpleIOSuite { } } + private def treeSitterTest( + testCase: TestCase, + trimWhitespace: Boolean, + ) = + test(testCase.name + " (tree-sitter no errors)") { + testCase.readInput(trimWhitespace).map { input => + val src = treeSitterWrap(input) + val scanned = TreeSitterAPI.make("smithyql").parse(src).rootNode.get + + val errors = scanned + .fold[List[Node]](_ :: _.flatten.toList) + .filter(_.isError) + .map { node => + s"${node.source} (${node.startByte} to ${node.endByte}, ${node.selfAndParents.map(_.tpe).mkString(" -> ")})" + } + .mkString("\n") + + assert(errors.isEmpty) || failure("error in file: " + testCase.base) || failure(src) + } + } + // invalidTokens: a flag that tells the suite whether the file should contain invalid tokens. def loadNegativeParserTests[Alg[_[_]]: SourceParser]( prefix: String, @@ -91,8 +117,23 @@ trait ParserSuite extends SimpleIOSuite { if (!invalidTokens) validTokensTest(testCase, trimWhitespace) + treeSitterNegativeTest(testCase, trimWhitespace) } + private def treeSitterNegativeTest( + testCase: TestCase, + trimWhitespace: Boolean, + ) = + test(testCase.name + " (tree-sitter require errors)") { + testCase.readInput(trimWhitespace).map { input => + val scanned = TreeSitterAPI.make("smithyql").parse(input).rootNode.get + + val errors = scanned.fold[List[Node]](_ :: _.flatten.toList).find(_.isError) + + assert(errors.nonEmpty) + } + } + private def readText( path: Path ) = @@ -144,11 +185,12 @@ trait ParserSuite extends SimpleIOSuite { outputExtension: String, ) { + private val inputPath = base / "input.smithyql-test" private val outputPath = base / s"output$outputExtension" def readInput( trimWhitespace: Boolean - ): IO[String] = readText(base / "input.smithyql-test").map( + ): IO[String] = readText(inputPath).map( if (trimWhitespace) _.strip else diff --git a/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala b/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala index 8458527b..f70cf43f 100644 --- a/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala +++ b/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala @@ -7,5 +7,11 @@ import playground.smithyql.parser.ParserSuite import playground.smithyql.parser.SourceParser object ListParserTests extends ParserSuite { + + override def treeSitterWrap(fileSource: String): String = + s"""FakeCall { + | fakeField = $fileSource + |}""".stripMargin + loadParserTests[Listed]("listed", trimWhitespace = true) } diff --git a/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala b/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala index 1eebed2f..9aba2774 100644 --- a/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala +++ b/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala @@ -6,5 +6,7 @@ import playground.smithyql.parser.Codecs.given import playground.smithyql.parser.ParserSuite object StructParserTests extends ParserSuite { + + override def treeSitterWrap(fileSource: String): String = s"FakeCall $fileSource" loadParserTests[Struct]("struct", trimWhitespace = true) } diff --git a/modules/source/src/main/scala/playground/smithyql/WithSource.scala b/modules/source/src/main/scala/playground/smithyql/WithSource.scala index 8004d272..a95e9768 100644 --- a/modules/source/src/main/scala/playground/smithyql/WithSource.scala +++ b/modules/source/src/main/scala/playground/smithyql/WithSource.scala @@ -64,8 +64,8 @@ final case class SourceRange( // Assuming this range corresponds to a bracket/brace/quote etc., // shrink it by one character on each side. def shrink1: SourceRange = copy( - start = start.copy(index = start.index + 1), - end = end.copy(index = end.index - 1), + start = start.moveRight(1), + end = end.moveLeft(1), ) def render: String = s"${start.index}-${end.index}" diff --git a/modules/treesitter/src/main/resources/darwin-aarch64/libtree-sitter-smithyql.dylib b/modules/treesitter/src/main/resources/darwin-aarch64/libtree-sitter-smithyql.dylib new file mode 100755 index 00000000..0f570b11 Binary files /dev/null and b/modules/treesitter/src/main/resources/darwin-aarch64/libtree-sitter-smithyql.dylib differ diff --git a/modules/treesitter/src/main/resources/darwin-x86-64/libtree-sitter-smithyql.dylib b/modules/treesitter/src/main/resources/darwin-x86-64/libtree-sitter-smithyql.dylib new file mode 100755 index 00000000..3652c855 Binary files /dev/null and b/modules/treesitter/src/main/resources/darwin-x86-64/libtree-sitter-smithyql.dylib differ diff --git a/modules/treesitter/src/main/resources/linux-aarch64/libtree-sitter-smithyql.so b/modules/treesitter/src/main/resources/linux-aarch64/libtree-sitter-smithyql.so new file mode 100755 index 00000000..e981ba4f Binary files /dev/null and b/modules/treesitter/src/main/resources/linux-aarch64/libtree-sitter-smithyql.so differ diff --git a/modules/treesitter/src/main/resources/linux-x86-64/libtree-sitter-smithyql.so b/modules/treesitter/src/main/resources/linux-x86-64/libtree-sitter-smithyql.so new file mode 100755 index 00000000..8dc3d5bd Binary files /dev/null and b/modules/treesitter/src/main/resources/linux-x86-64/libtree-sitter-smithyql.so differ diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Binding.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Binding.scala new file mode 100644 index 00000000..ecfb9bfd --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Binding.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Binding <: Node = Node + +object Binding { + extension (node: Binding) { + def select[A](f: Binding.Selector => Selection[A]): List[A] = f(Binding.Selector(List(node))).path + // fields + def key: Option[Identifier] = node.fields.getOrElse("key", Nil).headOption.map { + case Identifier(node) => node + } + + def value: Option[InputNode] = node.fields.getOrElse("value", Nil).headOption.map { + case InputNode(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, Binding] = + if node.tpe == "binding" + then Right(node) + else Left(s"Expected Binding, got ${node.tpe}") + + def unsafeApply(node: Node): Binding = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Binding] = apply(node).toOption + + final case class Selector(path: List[Binding]) extends Selection[Binding] { + def key: Identifier.Selector = Identifier.Selector(path.flatMap(_.key)) + def value: InputNode.Selector = InputNode.Selector(path.flatMap(_.value)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Boolean_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Boolean_.scala new file mode 100644 index 00000000..295034fc --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Boolean_.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Boolean_ <: Node = Node + +object Boolean_ { + + + def apply(node: Node): Either[String, Boolean_] = + if node.tpe == "boolean" + then Right(node) + else Left(s"Expected Boolean_, got ${node.tpe}") + + def unsafeApply(node: Node): Boolean_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Boolean_] = apply(node).toOption + + final case class Selector(path: List[Boolean_]) extends Selection[Boolean_] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Comment.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Comment.scala new file mode 100644 index 00000000..936272cf --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Comment.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Comment <: Node = Node + +object Comment { + + + def apply(node: Node): Either[String, Comment] = + if node.tpe == "comment" + then Right(node) + else Left(s"Expected Comment, got ${node.tpe}") + + def unsafeApply(node: Node): Comment = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Comment] = apply(node).toOption + + final case class Selector(path: List[Comment]) extends Selection[Comment] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Identifier.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Identifier.scala new file mode 100644 index 00000000..e942fe9b --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Identifier.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Identifier <: Node = Node + +object Identifier { + + + def apply(node: Node): Either[String, Identifier] = + if node.tpe == "identifier" + then Right(node) + else Left(s"Expected Identifier, got ${node.tpe}") + + def unsafeApply(node: Node): Identifier = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Identifier] = apply(node).toOption + + final case class Selector(path: List[Identifier]) extends Selection[Identifier] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/InputNode.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/InputNode.scala new file mode 100644 index 00000000..41c64fdc --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/InputNode.scala @@ -0,0 +1,81 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection +import annotation.nowarn + +opaque type InputNode <: Node = Boolean_ | List_ | Null_ | Number | String_ | Struct + +object InputNode { + + extension (node: InputNode) { + def asBoolean: Option[Boolean_] = Boolean_.unapply(node) + def asList: Option[List_] = List_.unapply(node) + def asNull: Option[Null_] = Null_.unapply(node) + def asNumber: Option[Number] = Number.unapply(node) + def asString: Option[String_] = String_.unapply(node) + def asStruct: Option[Struct] = Struct.unapply(node) + def visit[A](visitor: Visitor[A]): A = visitor.visit(node) + } + + def apply(node: Node): Either[String, InputNode] = node match { + case Boolean_(node) => Right(node) + case List_(node) => Right(node) + case Null_(node) => Right(node) + case Number(node) => Right(node) + case String_(node) => Right(node) + case Struct(node) => Right(node) + case _ => Left(s"Expected InputNode, got ${node.tpe}") + } + + def apply(node: Boolean_ | List_ | Null_ | Number | String_ | Struct): InputNode = node + + def unsafeApply(node: Node): InputNode = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[InputNode] = apply(node).toOption + + + trait Visitor[A] { + def onBoolean(node: Boolean_): A + def onList(node: List_): A + def onNull(node: Null_): A + def onNumber(node: Number): A + def onString(node: String_): A + def onStruct(node: Struct): A + + def visit(node: InputNode): A = (node: @nowarn("msg=match may not be exhaustive")) match { + case Boolean_(node) => onBoolean(node) + case List_(node) => onList(node) + case Null_(node) => onNull(node) + case Number(node) => onNumber(node) + case String_(node) => onString(node) + case Struct(node) => onStruct(node) + } + } + + object Visitor { + abstract class Default[A] extends Visitor[A] { + def default: A + + def onBoolean(node: Boolean_): A = default + def onList(node: List_): A = default + def onNull(node: Null_): A = default + def onNumber(node: Number): A = default + def onString(node: String_): A = default + def onStruct(node: Struct): A = default + } + } + + final case class Selector(path: List[InputNode]) extends Selection[InputNode] { + def boolean : Boolean_.Selector = Boolean_.Selector(path.flatMap(_.asBoolean)) + def list : List_.Selector = List_.Selector(path.flatMap(_.asList)) + def `null` : Null_.Selector = Null_.Selector(path.flatMap(_.asNull)) + def number : Number.Selector = Number.Selector(path.flatMap(_.asNumber)) + def string : String_.Selector = String_.Selector(path.flatMap(_.asString)) + def struct : Struct.Selector = Struct.Selector(path.flatMap(_.asStruct)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/List_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/List_.scala new file mode 100644 index 00000000..679214ff --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/List_.scala @@ -0,0 +1,37 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type List_ <: Node = Node + +object List_ { + extension (node: List_) { + def select[A](f: List_.Selector => Selection[A]): List[A] = f(List_.Selector(List(node))).path + // fields + def list_fields: List[InputNode] = node.fields.getOrElse("list_fields", Nil).toList.collect { + case InputNode(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, List_] = + if node.tpe == "list" + then Right(node) + else Left(s"Expected List_, got ${node.tpe}") + + def unsafeApply(node: Node): List_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[List_] = apply(node).toOption + + final case class Selector(path: List[List_]) extends Selection[List_] { + def list_fields: InputNode.Selector = InputNode.Selector(path.flatMap(_.list_fields)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Null_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Null_.scala new file mode 100644 index 00000000..5e438bb4 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Null_.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Null_ <: Node = Node + +object Null_ { + + + def apply(node: Node): Either[String, Null_] = + if node.tpe == "null" + then Right(node) + else Left(s"Expected Null_, got ${node.tpe}") + + def unsafeApply(node: Node): Null_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Null_] = apply(node).toOption + + final case class Selector(path: List[Null_]) extends Selection[Null_] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Number.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Number.scala new file mode 100644 index 00000000..66014bb1 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Number.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Number <: Node = Node + +object Number { + + + def apply(node: Node): Either[String, Number] = + if node.tpe == "number" + then Right(node) + else Left(s"Expected Number, got ${node.tpe}") + + def unsafeApply(node: Node): Number = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Number] = apply(node).toOption + + final case class Selector(path: List[Number]) extends Selection[Number] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/OperationName.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/OperationName.scala new file mode 100644 index 00000000..f5ad7ad4 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/OperationName.scala @@ -0,0 +1,39 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type OperationName <: Node = Node + +object OperationName { + extension (node: OperationName) { + def select[A](f: OperationName.Selector => Selection[A]): List[A] = f(OperationName.Selector(List(node))).path + // fields + + // typed children + def typedChildren: Option[Identifier] = node.children.collectFirst { + case Identifier(node) => node + } + // precise typed children + def identifier: Option[Identifier] = node.children.collectFirst { + case Identifier(node) => node + } + } + + def apply(node: Node): Either[String, OperationName] = + if node.tpe == "operation_name" + then Right(node) + else Left(s"Expected OperationName, got ${node.tpe}") + + def unsafeApply(node: Node): OperationName = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[OperationName] = apply(node).toOption + + final case class Selector(path: List[OperationName]) extends Selection[OperationName] { + def identifier: Identifier.Selector = Identifier.Selector(path.flatMap(_.identifier)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Prelude.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Prelude.scala new file mode 100644 index 00000000..f5fd7dbd --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Prelude.scala @@ -0,0 +1,39 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Prelude <: Node = Node + +object Prelude { + extension (node: Prelude) { + def select[A](f: Prelude.Selector => Selection[A]): List[A] = f(Prelude.Selector(List(node))).path + // fields + + // typed children + def typedChildren: List[UseClause] = node.children.toList.collect { + case UseClause(node) => node + } + // precise typed children + def use_clause: List[UseClause] = node.children.toList.collect { + case UseClause(node) => node + } + } + + def apply(node: Node): Either[String, Prelude] = + if node.tpe == "prelude" + then Right(node) + else Left(s"Expected Prelude, got ${node.tpe}") + + def unsafeApply(node: Node): Prelude = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Prelude] = apply(node).toOption + + final case class Selector(path: List[Prelude]) extends Selection[Prelude] { + def use_clause: UseClause.Selector = UseClause.Selector(path.flatMap(_.use_clause)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/QualifiedIdentifier.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/QualifiedIdentifier.scala new file mode 100644 index 00000000..0cde7e20 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/QualifiedIdentifier.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type QualifiedIdentifier <: Node = Node + +object QualifiedIdentifier { + extension (node: QualifiedIdentifier) { + def select[A](f: QualifiedIdentifier.Selector => Selection[A]): List[A] = f(QualifiedIdentifier.Selector(List(node))).path + // fields + def namespace: List[Identifier] = node.fields.getOrElse("namespace", Nil).toList.collect { + case Identifier(node) => node + } + + def selection: Option[Identifier] = node.fields.getOrElse("selection", Nil).headOption.map { + case Identifier(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, QualifiedIdentifier] = + if node.tpe == "qualified_identifier" + then Right(node) + else Left(s"Expected QualifiedIdentifier, got ${node.tpe}") + + def unsafeApply(node: Node): QualifiedIdentifier = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[QualifiedIdentifier] = apply(node).toOption + + final case class Selector(path: List[QualifiedIdentifier]) extends Selection[QualifiedIdentifier] { + def namespace: Identifier.Selector = Identifier.Selector(path.flatMap(_.namespace)) + def selection: Identifier.Selector = Identifier.Selector(path.flatMap(_.selection)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/QueryOperationName.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/QueryOperationName.scala new file mode 100644 index 00000000..6e4ba9ad --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/QueryOperationName.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type QueryOperationName <: Node = Node + +object QueryOperationName { + extension (node: QueryOperationName) { + def select[A](f: QueryOperationName.Selector => Selection[A]): List[A] = f(QueryOperationName.Selector(List(node))).path + // fields + def identifier: List[QualifiedIdentifier] = node.fields.getOrElse("identifier", Nil).toList.collect { + case QualifiedIdentifier(node) => node + } + + def name: Option[OperationName] = node.fields.getOrElse("name", Nil).headOption.map { + case OperationName(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, QueryOperationName] = + if node.tpe == "query_operation_name" + then Right(node) + else Left(s"Expected QueryOperationName, got ${node.tpe}") + + def unsafeApply(node: Node): QueryOperationName = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[QueryOperationName] = apply(node).toOption + + final case class Selector(path: List[QueryOperationName]) extends Selection[QueryOperationName] { + def identifier: QualifiedIdentifier.Selector = QualifiedIdentifier.Selector(path.flatMap(_.identifier)) + def name: OperationName.Selector = OperationName.Selector(path.flatMap(_.name)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/RunQuery.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/RunQuery.scala new file mode 100644 index 00000000..fb741774 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/RunQuery.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type RunQuery <: Node = Node + +object RunQuery { + extension (node: RunQuery) { + def select[A](f: RunQuery.Selector => Selection[A]): List[A] = f(RunQuery.Selector(List(node))).path + // fields + def input: Option[Struct] = node.fields.getOrElse("input", Nil).headOption.map { + case Struct(node) => node + } + + def operation_name: Option[QueryOperationName] = node.fields.getOrElse("operation_name", Nil).headOption.map { + case QueryOperationName(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, RunQuery] = + if node.tpe == "run_query" + then Right(node) + else Left(s"Expected RunQuery, got ${node.tpe}") + + def unsafeApply(node: Node): RunQuery = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[RunQuery] = apply(node).toOption + + final case class Selector(path: List[RunQuery]) extends Selection[RunQuery] { + def input: Struct.Selector = Struct.Selector(path.flatMap(_.input)) + def operation_name: QueryOperationName.Selector = QueryOperationName.Selector(path.flatMap(_.operation_name)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/SourceFile.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/SourceFile.scala new file mode 100644 index 00000000..dba19b71 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/SourceFile.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type SourceFile <: Node = Node + +object SourceFile { + extension (node: SourceFile) { + def select[A](f: SourceFile.Selector => Selection[A]): List[A] = f(SourceFile.Selector(List(node))).path + // fields + def prelude: Option[Prelude] = node.fields.getOrElse("prelude", Nil).headOption.map { + case Prelude(node) => node + } + + def statements: List[TopLevelStatement] = node.fields.getOrElse("statements", Nil).toList.collect { + case TopLevelStatement(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, SourceFile] = + if node.tpe == "source_file" + then Right(node) + else Left(s"Expected SourceFile, got ${node.tpe}") + + def unsafeApply(node: Node): SourceFile = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[SourceFile] = apply(node).toOption + + final case class Selector(path: List[SourceFile]) extends Selection[SourceFile] { + def prelude: Prelude.Selector = Prelude.Selector(path.flatMap(_.prelude)) + def statements: TopLevelStatement.Selector = TopLevelStatement.Selector(path.flatMap(_.statements)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/String_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/String_.scala new file mode 100644 index 00000000..dc03603d --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/String_.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type String_ <: Node = Node + +object String_ { + + + def apply(node: Node): Either[String, String_] = + if node.tpe == "string" + then Right(node) + else Left(s"Expected String_, got ${node.tpe}") + + def unsafeApply(node: Node): String_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[String_] = apply(node).toOption + + final case class Selector(path: List[String_]) extends Selection[String_] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Struct.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Struct.scala new file mode 100644 index 00000000..5523d639 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Struct.scala @@ -0,0 +1,37 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Struct <: Node = Node + +object Struct { + extension (node: Struct) { + def select[A](f: Struct.Selector => Selection[A]): List[A] = f(Struct.Selector(List(node))).path + // fields + def bindings: List[Binding] = node.fields.getOrElse("bindings", Nil).toList.collect { + case Binding(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, Struct] = + if node.tpe == "struct" + then Right(node) + else Left(s"Expected Struct, got ${node.tpe}") + + def unsafeApply(node: Node): Struct = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Struct] = apply(node).toOption + + final case class Selector(path: List[Struct]) extends Selection[Struct] { + def bindings: Binding.Selector = Binding.Selector(path.flatMap(_.bindings)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/TopLevelStatement.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/TopLevelStatement.scala new file mode 100644 index 00000000..26f9468f --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/TopLevelStatement.scala @@ -0,0 +1,39 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type TopLevelStatement <: Node = Node + +object TopLevelStatement { + extension (node: TopLevelStatement) { + def select[A](f: TopLevelStatement.Selector => Selection[A]): List[A] = f(TopLevelStatement.Selector(List(node))).path + // fields + + // typed children + def typedChildren: Option[RunQuery] = node.children.collectFirst { + case RunQuery(node) => node + } + // precise typed children + def run_query: Option[RunQuery] = node.children.collectFirst { + case RunQuery(node) => node + } + } + + def apply(node: Node): Either[String, TopLevelStatement] = + if node.tpe == "top_level_statement" + then Right(node) + else Left(s"Expected TopLevelStatement, got ${node.tpe}") + + def unsafeApply(node: Node): TopLevelStatement = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[TopLevelStatement] = apply(node).toOption + + final case class Selector(path: List[TopLevelStatement]) extends Selection[TopLevelStatement] { + def run_query: RunQuery.Selector = RunQuery.Selector(path.flatMap(_.run_query)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/UseClause.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/UseClause.scala new file mode 100644 index 00000000..6d7c13b2 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/UseClause.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type UseClause <: Node = Node + +object UseClause { + extension (node: UseClause) { + def select[A](f: UseClause.Selector => Selection[A]): List[A] = f(UseClause.Selector(List(node))).path + // fields + def identifier: Option[QualifiedIdentifier] = node.fields.getOrElse("identifier", Nil).headOption.map { + case QualifiedIdentifier(node) => node + } + // typed children + def typedChildren: List[Whitespace] = node.children.toList.collect { + case Whitespace(node) => node + } + // precise typed children + def whitespace: List[Whitespace] = node.children.toList.collect { + case Whitespace(node) => node + } + } + + def apply(node: Node): Either[String, UseClause] = + if node.tpe == "use_clause" + then Right(node) + else Left(s"Expected UseClause, got ${node.tpe}") + + def unsafeApply(node: Node): UseClause = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[UseClause] = apply(node).toOption + + final case class Selector(path: List[UseClause]) extends Selection[UseClause] { + def identifier: QualifiedIdentifier.Selector = QualifiedIdentifier.Selector(path.flatMap(_.identifier)) + def whitespace: Whitespace.Selector = Whitespace.Selector(path.flatMap(_.whitespace)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Whitespace.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Whitespace.scala new file mode 100644 index 00000000..ee663890 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Whitespace.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Whitespace <: Node = Node + +object Whitespace { + + + def apply(node: Node): Either[String, Whitespace] = + if node.tpe == "whitespace" + then Right(node) + else Left(s"Expected Whitespace, got ${node.tpe}") + + def unsafeApply(node: Node): Whitespace = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Whitespace] = apply(node).toOption + + final case class Selector(path: List[Whitespace]) extends Selection[Whitespace] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/treesitter4s/std/Selection.scala b/modules/treesitter/src/main/scala/playground/treesitter4s/std/Selection.scala new file mode 100644 index 00000000..fbf2016a --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/treesitter4s/std/Selection.scala @@ -0,0 +1,10 @@ +package playground.treesitter4s.std + +trait Selection[A] { + type Self <: Selection[A] + def path: List[A] + protected def remake: List[A] => Self + + def transform(f: List[A] => List[A]): Self = remake(f(path)) + def find(f: A => Boolean): Self = transform(_.find(f).toList) +} diff --git a/modules/treesitter/src/test/scala/playground/smithyql/parser/v3/TreeSitterParserTests.scala b/modules/treesitter/src/test/scala/playground/smithyql/parser/v3/TreeSitterParserTests.scala new file mode 100644 index 00000000..10900b12 --- /dev/null +++ b/modules/treesitter/src/test/scala/playground/smithyql/parser/v3/TreeSitterParserTests.scala @@ -0,0 +1,96 @@ +package playground.smithyql.parser.v3 + +import org.polyvariant.treesitter4s.Node +import org.polyvariant.treesitter4s.TreeSitterAPI +import playground.generated.nodes.SourceFile +import weaver.* + +object TreeSitterParserTests extends FunSuite { + + private def parse(s: String): SourceFile = { + val p = TreeSitterAPI.make("smithyql") + SourceFile.unsafeApply(p.parse(s).rootNode.get) + } + + test("SourceFile fields") { + val in = parse("""use service foo.bar.baz.bax#Baz + |GetBaz{}""".stripMargin) + + assert.eql(in.prelude.map(_.use_clause.size), Some(1)) && + assert(in.statements.nonEmpty) + } + + test("All parents of deep child") { + val allNodes = parse("""use service foo.bar.baz.bax#Baz + |GetBaz { a = { x = 42 }}""".stripMargin) + .fold[List[Node]](_ :: _.flatten.toList) + + val parentTypesAndSources = allNodes + .find(_.source == "x") + .get + .selfAndParents + .map(n => n.tpe -> n.source) + .mkString("\n") + + val expected = List( + "identifier" -> "x", + "binding" -> "x = 42", + "struct" -> "{ x = 42 }", + "binding" -> "a = { x = 42 }", + "struct" -> "{ a = { x = 42 }}", + "run_query" -> "GetBaz { a = { x = 42 }}", + "top_level_statement" -> "GetBaz { a = { x = 42 }}", + "source_file" -> "use service foo.bar.baz.bax#Baz\nGetBaz { a = { x = 42 }}", + ).mkString("\n") + + assert.same(expected, parentTypesAndSources) + } + + test("Deep insight into field") { + val in = parse("""use service foo.bar.baz.bax#Baz + |GetBaz { a = { x = 42 } }""".stripMargin) + + val valueOfX = + in.select( + _.statements + .run_query + .input + .bindings + .find(_.key.get.source == "a") + .value + .struct + .bindings + .find(_.key.get.source == "x") + .value + .number + ).head + .source + .toInt + + assert.eql(42, valueOfX) + } + + test("Deep insight into field, but the file isn't valid") { + val in = parse("""use service fo o.b ar.b/az.bax/#//B//,,{}az + |GetBa z { a = { x = 42, 50 }, z, 42 }""".stripMargin) + + val valueOfX = + in.select( + _.statements + .run_query + .input + .bindings + .find(_.key.get.source == "a") + .value + .struct + .bindings + .find(_.key.get.source == "x") + .value + .number + ).head + .source + .toInt + + assert.eql(42, valueOfX) + } +} diff --git a/smithy-build.json b/smithy-build.json index b029b2a4..4364f960 100644 --- a/smithy-build.json +++ b/smithy-build.json @@ -1,5 +1,5 @@ { - "sources": ["modules/core/src/test/smithy"], + "sources": ["modules/examples/src/main/smithy"], "mavenDependencies": [ "com.disneystreaming.alloy:alloy-core:0.3.14", "com.disneystreaming.smithy4s:smithy4s-protocol:0.18.26", diff --git a/tree-sitter-smithyql/.gitattributes b/tree-sitter-smithyql/.gitattributes new file mode 100644 index 00000000..4cb10583 --- /dev/null +++ b/tree-sitter-smithyql/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +src/*.json linguist-generated +src/parser.c linguist-generated +src/tree_sitter/* linguist-generated + +bindings/** linguist-generated +binding.gyp linguist-generated +setup.py linguist-generated +Makefile linguist-generated +Package.swift linguist-generated diff --git a/tree-sitter-smithyql/.gitignore b/tree-sitter-smithyql/.gitignore new file mode 100644 index 00000000..5d11846d --- /dev/null +++ b/tree-sitter-smithyql/.gitignore @@ -0,0 +1,54 @@ +# Rust artifacts +Cargo.lock +target/ + +# Node artifacts +build/ +prebuilds/ +node_modules/ +*.tgz + +# Swift artifacts +.build/ +Package.resolved + +# Go artifacts +go.sum +_obj/ + +# Python artifacts +.venv/ +dist/ +*.egg-info +*.whl + +# C artifacts +*.a +*.so +*.so.* +*.dylib +*.dll +*.pc + +# Example dirs +/examples/*/ + +# Grammar volatiles +*.wasm +*.obj +*.o + +src/ + +.editorconfig +Cargo.toml +Makefile +Package.swift +binding.gyp +bindings/ +go.mod +package.json +pyproject.toml +setup.py + +a.out.js diff --git a/tree-sitter-smithyql/example.smithyql b/tree-sitter-smithyql/example.smithyql new file mode 100644 index 00000000..8124bf33 --- /dev/null +++ b/tree-sitter-smithyql/example.smithyql @@ -0,0 +1,25 @@ +use service a.b#C + +hello { + a = 42, + b = 50, + c = { + d = "foo", + e = false, + f = true, + list = [ + 50, + 100, + 100, + [ + null, + false, + true, + null, + "a", + 40, + ], + ], + }, + nul = null, +} diff --git a/tree-sitter-smithyql/grammar.js b/tree-sitter-smithyql/grammar.js new file mode 100644 index 00000000..54b663f2 --- /dev/null +++ b/tree-sitter-smithyql/grammar.js @@ -0,0 +1,91 @@ +// Comma-separated sequence of field, with an optional trailing comma. +function comma_separated_trailing(field_grammar) { + return prec.left( + 1, + seq(field_grammar, repeat(seq(",", field_grammar)), optional(",")) + ); +} + +module.exports = grammar({ + name: "smithyql", + + extras: ($) => [$.whitespace, $.comment], + rules: { + source_file: ($) => + seq( + field("prelude", optional($.prelude)), + field("statements", repeat($.top_level_statement)) + ), + + prelude: ($) => repeat1($.use_clause), + + // todo: use token.immediate to prevent comments? + // or just allow comments everywhere? + use_clause: ($) => + seq( + "use", + $.whitespace, + "service", + $.whitespace, + field("identifier", $.qualified_identifier) + ), + + top_level_statement: ($) => choice($.run_query), + + run_query: ($) => + seq( + field("operation_name", $.query_operation_name), + field("input", $.struct) + ), + + _namespace: ($) => seq($.identifier, repeat(seq(".", $.identifier))), + + qualified_identifier: ($) => + seq( + field("namespace", $._namespace), + "#", + field("selection", $.identifier) + ), + + query_operation_name: ($) => + seq( + field( + "identifier", + optional(prec.left(seq($.qualified_identifier, "."))) + ), + field("name", $.operation_name) + ), + + operation_name: ($) => $.identifier, + + _input_node: ($) => + choice($.struct, $.list, $.number, $.string, $.boolean, $.null), + + struct: ($) => seq("{", field("bindings", optional($._bindings)), "}"), + list: ($) => seq("[", field("list_fields", optional($._list_fields)), "]"), + + _bindings: ($) => comma_separated_trailing($.binding), + + binding: ($) => + seq( + field("key", $.identifier), + choice("=", ":"), + field("value", $._input_node) + ), + + _list_fields: ($) => comma_separated_trailing($._input_node), + + identifier: ($) => /[a-zA-Z_][a-zA-Z0-9_]*/, + + boolean: ($) => choice("true", "false"), + number: ($) => /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/, + string: ($) => /"([^"\\]|\\.)*"/, + + null: ($) => "null", + + comment: ($) => token(seq("//", /.*/)), + whitespace: ($) => /\s+/, + }, + supertypes: ($) => [$._input_node], +}); +// diff --git a/tree-sitter-smithyql/test/corpus/simple-complete.txt b/tree-sitter-smithyql/test/corpus/simple-complete.txt new file mode 100644 index 00000000..24a05445 --- /dev/null +++ b/tree-sitter-smithyql/test/corpus/simple-complete.txt @@ -0,0 +1,44 @@ +================================================================================ +Simple complete valid example +================================================================================ +use service foo.bar#baz +Hello { a = 42, b = "false", c = true } +-------------------------------------------------------------------------------- +(source_file + (use_clause + (whitespace) + (whitespace) + (qualified_identifier + (identifier) + (identifier) + (identifier))) + (whitespace) + (top_level_statement + (operation_call + (operation_name + (identifier)) + (whitespace) + (struct + (whitespace) + (bindings + (binding + (identifier) + (whitespace) + (whitespace) + (input_node + (number))) + (whitespace) + (binding + (identifier) + (whitespace) + (whitespace) + (input_node + (string))) + (whitespace) + (binding + (identifier) + (whitespace) + (whitespace) + (input_node + (boolean)))) + (whitespace))))) diff --git a/update-libs.sh b/update-libs.sh new file mode 100755 index 00000000..1fe5c084 --- /dev/null +++ b/update-libs.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +LIBS_PATH=$(nix build .#tree-sitter-smithyql-all --no-link --print-out-paths --print-build-logs) +mkdir -p modules/treesitter/src/main/resources +cp -R "$LIBS_PATH"/* modules/treesitter/src/main/resources +chmod -R 755 modules/treesitter/src/main/resources