Skip to content

String interpolator parsers#339

Merged
johnynek merged 6 commits intotypelevel:mainfrom
stephenjudkins:sdj/string-interpolator-parsers
Dec 31, 2021
Merged

String interpolator parsers#339
johnynek merged 6 commits intotypelevel:mainfrom
stephenjudkins:sdj/string-interpolator-parsers

Conversation

@stephenjudkins
Copy link
Contributor

@stephenjudkins stephenjudkins commented Dec 30, 2021

Fixes #329.

I just dragged this over the finish line and am open to any suggested changes. Was a useful if somewhat painful way to dip my toes into Scala 3

@stephenjudkins stephenjudkins force-pushed the sdj/string-interpolator-parsers branch from 1305673 to a2363f8 Compare December 30, 2021 23:19
@codecov-commenter
Copy link

codecov-commenter commented Dec 30, 2021

Codecov Report

Merging #339 (3d5164a) into main (ae38c13) will decrease coverage by 0.19%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #339      +/-   ##
==========================================
- Coverage   96.56%   96.37%   -0.20%     
==========================================
  Files           9        9              
  Lines        1049     1049              
  Branches       94       94              
==========================================
- Hits         1013     1011       -2     
- Misses         36       38       +2     
Impacted Files Coverage Δ
core/shared/src/main/scala/cats/parse/Parser.scala 96.18% <0.00%> (-0.24%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ae38c13...3d5164a. Read the comment docs.

@johnynek
Copy link
Collaborator

cc @s5bug

Copy link
Collaborator

@johnynek johnynek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really awesome! I'm so excited you tackled this.

object StringInterpolation {

implicit class Helper(val sc: StringContext) extends AnyVal {
def parser(args: Any*): Parser[Any] = macro StringInterpolationMacros.parser
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not very familiar with macros honestly... I always have to look up details.

So, I guess that since this is a white box macro that the type gets refined (as checked in the test below)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup correct, this needs to be a whitebox macro to get the type refined at each call site.

val (matched, last) = stringParts.splitAt(args.length)

val parsers: List[Tree] = matched.zip(args).map { case (a, b) =>
if (a.isEmpty) b.tree else q"${lit(a)} *> $b "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering about what happens if b is a Parser0. It seems we could often handle it (as long as there is at least one string value, or another Parser. But I guess we can't get the type of b right?

Copy link
Collaborator

@johnynek johnynek Dec 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we test that with Parser0? I guess we could make sure it works as long as there is a leading Parser or string?

assertEquals(
arrow.parseAll("spam,1234 -> quux"),
Right((true, BigInt(1234), 42))
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add a few assertions on compileErrors(...)

https://scalameta.org/munit/docs/assertions.html

it would be good to see some failures and make sure they fail in both scala 2 and 3 (although the messages may be different).

val foo: Parser[Boolean] = Parser.string("spam").as(true) | Parser.string("eggs").as(false)
val bar: Parser[BigInt] = Numbers.bigInt
val quux: Parser[Int] = Parser.string("quux").as(42)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we exercise with 0 args, 1 arg up to say 3 (to exercise non-tuples)
so, I guess val x: Parser[Unit] = parser"foo" should work. val x: Parser[A] = parser"foo$aParser" and then at 2 we should have tuples?

or do we require 2 or more (or 1 if we wrap with tuple1)?

Copy link
Contributor Author

@stephenjudkins stephenjudkins Dec 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrote tests to exercise these cases. Here's how it currently works:

  • empty string: compile error
  • non-empty string with no interpolated parsers: Parser[Unit] equivalent to Parser.string("...")
  • string with single interpolated Parser[A]: Parser[A]
  • string with n interpolated Parser[{A, B, C...}]: Parser[(A, B, C...)]

test("parser string interpolator") {
val foo: Parser[Boolean] = Parser.string("spam").as(true) | Parser.string("eggs").as(false)
val bar: Parser[BigInt] = Numbers.bigInt
val quux: Parser[Int] = Parser.string("quux").as(42)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we write this as parser"quux".as(42)? does parser bind tighter than method application?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does

@s5bug
Copy link

s5bug commented Dec 31, 2021

This is awesome! Thank you so much!

@stephenjudkins
Copy link
Contributor Author

Added more code to deal with Parser0. Turns out, in Scala 2.x since we're dealing with untyped trees things just work out like we'd expect. Scala 3 needs more type plumbing to get the same behavior working

A change from previous discussions is that parser"" now generates a Parser.unit:Parser0[Unit]

Copy link
Collaborator

@regadas regadas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks great! thanks for taking this 😄 just left some minor things.

I was thinking ... would it be worth separating this into it's own module?


object StringInterpolation {

implicit class Helper(val sc: StringContext) extends AnyVal {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
implicit class Helper(val sc: StringContext) extends AnyVal {
implicit class Helper(private val sc: StringContext) extends AnyVal {

transparent inline final def parser(inline args: Any*): Any = ${ parserImpl('sc, 'args) }
}

def parserImpl(sc: Expr[StringContext], args: Expr[Seq[Any]])(using q: Quotes) = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def parserImpl(sc: Expr[StringContext], args: Expr[Seq[Any]])(using q: Quotes) = {
private def parserImpl(sc: Expr[StringContext], args: Expr[Seq[Any]])(using q: Quotes) = {


}

test("including a Parser0") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we also assert val p: Parser0[Unit] = parser"" ?


}

class StringInterpolationMacros(val c: whitebox.Context) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we could make this one private

Suggested change
class StringInterpolationMacros(val c: whitebox.Context) {
private class StringInterpolationMacros(val c: whitebox.Context) {

@johnynek johnynek merged commit e0a50f4 into typelevel:main Dec 31, 2021
@johnynek
Copy link
Collaborator

Rather than nit on this, I'm going to merge and then will iterate.

I'll send a followup PR with @regadas 's suggestions.

Thanks so much!

I look forward to a release with this!

@stephenjudkins
Copy link
Contributor Author

Well, it's functional now in but I thought on it, and: the Parser vs Parser0 aspect of this is currently not ideal and inconsistent between Scala 2/3. I'm send a followup PR with some failing/non-compiling tests as a starting point for discussion

@johnynek
Copy link
Collaborator

@stephenjudkins take a look at my PR where I also tried to factor the code similarly and added a few more tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A parser string interpolator

5 participants