Skip to content

Support custom val symbol in ValDef.let #13936

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

cchantep
Copy link
Contributor

Related to #13929

Currently ValDef.let doesn't allow to define Flags for the val definition.

Usage:

val tpe = TypeRepr.of[String]

val valSym = Symbol.newVal(
  Symbol.spliceOwner,
  "myValName",
  tpe,
  Flags.Lazy,
  Symbol.noSymbol
)

ValDef.let(Symbol.spliceOwner, valSym, Expr("foo")) { v =>
  '{ println(v) }.asTerm
}

In this way, it's possible to create an Ref to the val before its definition (required for recursive/lazy definitions).

// After `tpe` and `valSym` but before `let` call
val earlyRef = Typed(Ref(valSym), Inferred(tpe))

@cchantep cchantep force-pushed the feature/13929-valdef_let_sym branch from 8873ec9 to afc5af3 Compare November 13, 2021 16:48
@nicolasstucki
Copy link
Contributor

If you already created the Symbol you should use the normal ValDef.apply constructor. The ValDef.let constructors are the ones that create the symbol for you.

@nicolasstucki
Copy link
Contributor

nicolasstucki commented Nov 15, 2021

Maybe an example in the documentation of how to use ValDef.apply would be helpful.

@nicolasstucki
Copy link
Contributor

Also, if you have the symbol of the val you can recover its owner.

@cchantep
Copy link
Contributor Author

If you already created the Symbol you should use the normal ValDef.apply constructor. The ValDef.let constructors are the ones that create the symbol for you.

Yes you can, but that's quite error prone.

@nicolasstucki
Copy link
Contributor

The quotes API will always be more error-prone than the quotes API. By using the symbol constructor we drove into an error-prone territory, the let does not help much there. If you want to make your code less error-prone consider using quotes instead. For example, creating a lazy val can be done with

def lazyVal[T: Type, U: Type](rhs: Expr[T])(body: Expr[T] => Expr[U])(using Quotes): Expr[U] =
  '{ lazy val x: T = $rhs; ${body('x)} }

def lazyVal[T: Type, U: Type](name: String, rhs: Expr[T])(body: Expr[T] => Expr[U])(using Quotes): Expr[U] =
  // implement using reflection

// ...
// other variants of val definitions

Then your example can be simplified to

- val tpe = TypeRepr.of[String]

- val valSym = Symbol.newVal(
-  Symbol.spliceOwner,
- "myValName",
-  tpe,
-  Flags.Lazy,
-  Symbol.noSymbol
- )

- ValDef.let(Symbol.spliceOwner, valSym, Expr("foo").asTerm) { v =>
-  '{ println(v) }.asTerm
- }
+  lazyVal("myValName", Expr("foo")) { x => 
+   '{ println(v) }
+ }

I have always been against the addition of the ValDef.let helpers because they make it look like they are the simple version of how to generate ValDef in blocks. But it hides the even simpler version that I showed above.

The only place where once needs to use the ValDef constructor is when creating members or parameters which is a completely different story.

@cchantep cchantep closed this Nov 15, 2021
@cchantep cchantep reopened this Nov 15, 2021
@cchantep
Copy link
Contributor Author

The quotes API will always be more error-prone than the quotes API.

?

By using the symbol constructor we drove into an error-prone territory, the let does not help much there. If you want to make your code less error-prone consider using quotes instead. For example, creating a lazy val can be done with

def lazyVal[T: Type, U: Type](rhs: Expr[T])(body: Expr[T] => Expr[U])(using Quotes): Expr[U] =
  '{ lazy val x: T = $rhs; ${body('x)} }

def lazyVal[T: Type, U: Type](name: String, rhs: Expr[T])(body: Expr[T] => Expr[U])(using Quotes): Expr[U] =
  // implement using reflection

// ...
// other variants of val definitions

Using the quotes syntax requires to have Type[T], the Expr[T] "world" rather than the Term/TypeRepr one.
So for me those are not alternative, neither to the current ValDef.let, nor to the one draft there.

@nicolasstucki
Copy link
Contributor

If you have a Term you can always recover and Expr[Any] with asExpr and then recover its Type.
The type can be recovered as shown in the Macro Tutorial with this code

rhsTerm.asExpr match
  case '{ $rhs: t } => 
    lazyVal[t]("myValName", rhs){ x => '{ println(v) } } // `[t]` can be iferred

@cchantep
Copy link
Contributor Author

If you have a Term you can always recover and Expr[Any] with asExpr and then recover its Type. The type can be recovered as shown in the Macro Tutorial with this code

rhsTerm.asExpr match
  case '{ $rhs: t } => 
    lazyVal[t]("myValName", rhs){ x => '{ println(v) } } // `[t]` can be iferred

Recovering an Expr[Any] is not sufficient to use in a typed quotes '{ ... }.

@nicolasstucki
Copy link
Contributor

Recovering an Expr[Any] is not sufficient to use in a typed quotes '{ ... }.

Not sure when it is not sufficient. Could you provide an example?

We can use '{ ... } as in

rhsTerm.asExpr match
  case '{ $rhs: t } => '{ lazy val myValName: t = $rhs; println(v) }

@cchantep
Copy link
Contributor Author

Recovering an Expr[Any] is not sufficient to use in a typed quotes '{ ... }.

Not sure when it is not sufficient. Could you provide an example?

We can use '{ ... } as in

rhsTerm.asExpr match
  case '{ $rhs: t } => '{ lazy val myValName: t = $rhs; println(v) }
    1. In the use cases I see, it's not possible to do either .asExprOf[T] nor '{ val v: T = ... } because the Type is not statically known.
    1. println would work with Expr[Any], but as soon as you want to call a field/method specif to the type, as '{ v.foo }, the quotes will refuse.

@nicolasstucki
Copy link
Contributor

    1. In the use cases I see, it's not possible to do either .asExprOf[T] nor '{ val v: T = ... } because the Type is not statically known.
    1. println would work with Expr[Any], but as soon as you want to call a field/method specif to the type, as '{ v.foo }, the quotes will refuse.

Both those cases are possible and covered in the matching-types section of the Macro Tutorial.

To do .asExprOf[T] nor '{ val v: T = ... } need to make a statically known name for the type wich can be achieved with type variables in quoted patterns. The previous example already did that

rhsTerm.asExpr match
  case '{ $rhs: t } =>
    // The type of `rhs` is statically known as `t`
    // A given `Type[t]` is available here
    rhsTerm.asExprOf[t] // ok
    '{ val x: t = $rhs; ... } // `t` statically known to be the given `Type[t]`

Alternatively we could do

rhsTerm.tpe match
  case '[t] =>
    // A given `Type[t]` is available here
    val rhs = rhsTerm.asExprOf[t] // ok
    '{ val x: t = $rhs; ... } // `t` statically known to be the given `Type[t]`

Methods and field/method can also be statically known

trait Foo:
  def foo: Int = 2
rhsTerm.asExpr match
  case '{ expr: Foo } =>
    '{ $expr.foo }

or to get the precise type t

rhsTerm.asExpr match
  case '{ type t <: Foo; expr: `t` } =>
    '{ $expr.foo }

@cchantep
Copy link
Contributor Author

cchantep commented Nov 16, 2021

    1. In the use cases I see, it's not possible to do either .asExprOf[T] nor '{ val v: T = ... } because the Type is not statically known.
    1. println would work with Expr[Any], but as soon as you want to call a field/method specif to the type, as '{ v.foo }, the quotes will refuse.

Both those cases are possible and covered in the matching-types section of the Macro Tutorial.

To do .asExprOf[T] nor '{ val v: T = ... } need to make a statically known name for the type wich can be achieved with type variables in quoted patterns. The previous example already did that

rhsTerm.asExpr match
  case '{ $rhs: t } =>
    // The type of `rhs` is statically known as `t`
    // A given `Type[t]` is available here
    rhsTerm.asExprOf[t] // ok
    '{ val x: t = $rhs; ... } // `t` statically known to be the given `Type[t]`

We are not speaking of the same thing.
Having t extracted there doesn't help to select & apply functions/fields specific to this type.

v match {
  case '{ $rhs: t } =>
    '{
    val tv: t = $rhs
    tv.foreach { inner => println("inner=" + inner) }
/*
[error] ^^^^^^^^^
[error] value foreach is not a member of t.
 */
  }
}

Alternatively we could do

rhsTerm.tpe match
  case '[t] =>
    // A given `Type[t]` is available here
    val rhs = rhsTerm.asExprOf[t] // ok
    '{ val x: t = $rhs; ... } // `t` statically known to be the given `Type[t]`

Methods and field/method can also be statically known

trait Foo:
  def foo: Int = 2
rhsTerm.asExpr match
  case '{ expr: Foo } =>
    '{ $expr.foo }

or to get the precise type t

rhsTerm.asExpr match
  case '{ type t <: Foo; expr: `t` } =>
    '{ $expr.foo }

If it's possible to write such constraints : Foo or type t <: Foo, it's possible to directly write term.asExprOf[Foo].
The issues are not in such case.

@cchantep
Copy link
Contributor Author

Not related to this PR specifically, but I think the documentation could more clearly state, that the quote type matching could be use to extract static type from TypeRepr/Type[_].

  inline def foo[T](v: Any): Unit = ${ fooImpl[T]('v) }

  private def fooImpl[T: Type](v: Expr[Any])(using q: Quotes): Expr[Unit] = {
    import q.reflect.*

    TypeRepr.of[T].asType match {
      case '[t] =>
        val vt = Type.of[t]
        val typedExpr = v.asExprOf[t](using vt)

        '{
          val ve: t = ${typedExpr}

          println(s"ve = $ve")
        }
    }
  }

@nicolasstucki nicolasstucki self-assigned this Jan 13, 2022
@cchantep cchantep closed this Apr 2, 2023
@cchantep cchantep deleted the feature/13929-valdef_let_sym branch April 2, 2023 15:17
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.

2 participants