Skip to content

Compiler crash when quoting an inline argument of function type #4431

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
TomasMikula opened this issue May 1, 2018 · 28 comments
Closed

Compiler crash when quoting an inline argument of function type #4431

TomasMikula opened this issue May 1, 2018 · 28 comments

Comments

@TomasMikula
Copy link
Contributor

$ dotty-0.8.0-RC1/bin/dotr
Starting dotty REPL...
scala> inline def h(inline f: Int => String): String = ~ '(f(42)) 

Exception in thread "main" java.lang.AssertionError: assertion failed: unresolved symbols: value f$1 when pickling rs$line$1
	at dotty.DottyPredef$.assertFail(DottyPredef.scala:36)
	at dotty.tools.dotc.core.tasty.TreePickler.pickle(TreePickler.scala:634)
	at dotty.tools.dotc.core.quoted.PickledQuotes$.pickle(PickledQuotes.scala:98)
	at dotty.tools.dotc.core.quoted.PickledQuotes$.pickleQuote(PickledQuotes.scala:31)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.pickleAsTasty$1(ReifyQuotes.scala:394)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.pickledQuote(ReifyQuotes.scala:413)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.quotation(ReifyQuotes.scala:382)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:547)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transformWithCapturer$2(ReifyQuotes.scala:472)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.body$4(ReifyQuotes.scala:499)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.$anonfun$4(ReifyQuotes.scala:505)
	at dotty.tools.dotc.ast.tpd$.polyDefDef(tpd.scala:229)
	at dotty.tools.dotc.ast.tpd$.DefDef(tpd.scala:191)
	at dotty.tools.dotc.ast.tpd$.Closure(tpd.scala:103)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.makeLambda(ReifyQuotes.scala:505)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.split(ReifyQuotes.scala:523)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.splice(ReifyQuotes.scala:431)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:552)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:84)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:585)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.traverse$1(MacroTransformWithImplicits.scala:53)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transformStats(MacroTransformWithImplicits.scala:60)
	at dotty.tools.dotc.transform.MacroTransform$Transformer.transform(MacroTransform.scala:60)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:86)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1211)
	at dotty.tools.dotc.transform.MacroTransform$Transformer.transform(MacroTransform.scala:54)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:86)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform$$anonfun$2(Trees.scala:1231)
	at scala.collection.immutable.List.mapConserve(List.scala:176)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1231)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformStats(Trees.scala:1229)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1217)
	at dotty.tools.dotc.transform.MacroTransform$Transformer.transform(MacroTransform.scala:54)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:86)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.transform.MacroTransform.run(MacroTransform.scala:22)
	at dotty.tools.dotc.transform.ReifyQuotes.run(ReifyQuotes.scala:104)
	at dotty.tools.dotc.core.Phases$Phase.runOn$$anonfun$1(Phases.scala:295)
	at scala.collection.immutable.List.map(List.scala:283)
	at dotty.tools.dotc.core.Phases$Phase.runOn(Phases.scala:297)
	at dotty.tools.dotc.Run.runPhases$4$$anonfun$4(Run.scala:171)
	at scala.compat.java8.JProcedure1.apply(JProcedure1.java:18)
	at scala.compat.java8.JProcedure1.apply(JProcedure1.java:10)
	at scala.collection.IndexedSeqOptimized.foreach(IndexedSeqOptimized.scala:32)
	at scala.collection.IndexedSeqOptimized.foreach$(IndexedSeqOptimized.scala:29)
	at scala.collection.mutable.ArrayOps$ofRef.foreach(ArrayOps.scala:191)
	at dotty.tools.dotc.Run.runPhases$5(Run.scala:186)
	at dotty.tools.dotc.Run.compileUnits$$anonfun$1(Run.scala:194)
	at scala.compat.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
	at dotty.tools.dotc.util.Stats$.maybeMonitored(Stats.scala:88)
	at dotty.tools.dotc.Run.compileUnits(Run.scala:201)
	at dotty.tools.dotc.Run.compileUnits(Run.scala:141)
	at dotty.tools.repl.ReplCompiler.runCompilationUnit(ReplCompiler.scala:188)
	at dotty.tools.repl.ReplCompiler.compile(ReplCompiler.scala:197)
	at dotty.tools.repl.ReplDriver.compile(ReplDriver.scala:229)
	at dotty.tools.repl.ReplDriver.interpret(ReplDriver.scala:202)
	at dotty.tools.repl.ReplDriver.loop$1(ReplDriver.scala:150)
	at dotty.tools.repl.ReplDriver.runUntilQuit$$anonfun$1(ReplDriver.scala:154)
	at dotty.tools.repl.ReplDriver.withRedirectedOutput$$anonfun$2$$anonfun$1(ReplDriver.scala:163)
	at scala.util.DynamicVariable.withValue(DynamicVariable.scala:58)
	at scala.Console$.withErr(Console.scala:192)
	at dotty.tools.repl.ReplDriver.withRedirectedOutput$$anonfun$1(ReplDriver.scala:163)
	at scala.util.DynamicVariable.withValue(DynamicVariable.scala:58)
	at scala.Console$.withOut(Console.scala:163)
	at dotty.tools.repl.ReplDriver.withRedirectedOutput(ReplDriver.scala:163)
	at dotty.tools.repl.ReplDriver.runUntilQuit(ReplDriver.scala:154)
	at dotty.tools.repl.Main$.main(Main.scala:6)
	at dotty.tools.repl.Main.main(Main.scala)
@nicolasstucki
Copy link
Contributor

The issue here is that we do not support inline functions as they have no value we can use. In this stage, they are just code. One solution would be to simply disallow the inline keyword on method types.

@nicolasstucki
Copy link
Contributor

The same lack of support is cought correctly by the folowing assertion as stated in #4433

$ dotty-0.8.0-RC1/bin/dotr
Starting dotty REPL...
scala> inline def g(inline p: Int => Boolean): Boolean = ~{
         if(p(5)) '(true)
         else '(false)
       } 
def g(p: (Int => Boolean) @InlineParam): Boolean
scala> 
scala> g(i => false) 
Exception in thread "main" java.lang.AssertionError: assertion failed
	at dotty.DottyPredef$.assertFail(DottyPredef.scala:35)
	at dotty.tools.dotc.transform.Splicer$.$anonfun$4(Splicer.scala:61)
	at scala.collection.immutable.List.map(List.scala:283)
	at dotty.tools.dotc.transform.Splicer$.liftArgs$1(Splicer.scala:64)
	at dotty.tools.dotc.transform.Splicer$.getLiftedArgs(Splicer.scala:72)
	at dotty.tools.dotc.transform.Splicer$.splice(Splicer.scala:36)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:568)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1204)
	at dotty.tools.dotc.transform.MacroTransform$Transformer.transform(MacroTransform.scala:54)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:86)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.traverse$1(MacroTransformWithImplicits.scala:53)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transformStats(MacroTransformWithImplicits.scala:60)
	at dotty.tools.dotc.transform.MacroTransform$Transformer.transform(MacroTransform.scala:60)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:86)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1211)
	at dotty.tools.dotc.transform.MacroTransform$Transformer.transform(MacroTransform.scala:54)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:86)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform$$anonfun$2(Trees.scala:1231)
	at scala.collection.immutable.List.mapConserve(List.scala:176)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1231)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transformStats(Trees.scala:1229)
	at dotty.tools.dotc.ast.Trees$Instance$TreeMap.transform(Trees.scala:1217)
	at dotty.tools.dotc.transform.MacroTransform$Transformer.transform(MacroTransform.scala:54)
	at dotty.tools.dotc.transform.MacroTransformWithImplicits$ImplicitsTransformer.transform(MacroTransformWithImplicits.scala:86)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.mapOverTree$1(ReifyQuotes.scala:539)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.op1$1(ReifyQuotes.scala:606)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.transform(ReifyQuotes.scala:537)
	at dotty.tools.dotc.transform.MacroTransform.run(MacroTransform.scala:22)
	at dotty.tools.dotc.transform.ReifyQuotes.run(ReifyQuotes.scala:104)
	at dotty.tools.dotc.core.Phases$Phase.runOn$$anonfun$1(Phases.scala:295)
	at scala.collection.immutable.List.map(List.scala:283)
	at dotty.tools.dotc.core.Phases$Phase.runOn(Phases.scala:297)
	at dotty.tools.dotc.Run.runPhases$4$$anonfun$4(Run.scala:171)
	at scala.compat.java8.JProcedure1.apply(JProcedure1.java:18)
	at scala.compat.java8.JProcedure1.apply(JProcedure1.java:10)
	at scala.collection.IndexedSeqOptimized.foreach(IndexedSeqOptimized.scala:32)
	at scala.collection.IndexedSeqOptimized.foreach$(IndexedSeqOptimized.scala:29)
	at scala.collection.mutable.ArrayOps$ofRef.foreach(ArrayOps.scala:191)
	at dotty.tools.dotc.Run.runPhases$5(Run.scala:186)
	at dotty.tools.dotc.Run.compileUnits$$anonfun$1(Run.scala:194)
	at scala.compat.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
	at dotty.tools.dotc.util.Stats$.maybeMonitored(Stats.scala:88)
	at dotty.tools.dotc.Run.compileUnits(Run.scala:201)
	at dotty.tools.dotc.Run.compileUnits(Run.scala:141)
	at dotty.tools.repl.ReplCompiler.runCompilationUnit(ReplCompiler.scala:188)
	at dotty.tools.repl.ReplCompiler.compile(ReplCompiler.scala:197)
	at dotty.tools.repl.ReplDriver.compile(ReplDriver.scala:229)
	at dotty.tools.repl.ReplDriver.interpret(ReplDriver.scala:202)
	at dotty.tools.repl.ReplDriver.loop$1(ReplDriver.scala:150)
	at dotty.tools.repl.ReplDriver.runUntilQuit$$anonfun$1(ReplDriver.scala:154)
	at dotty.tools.repl.ReplDriver.withRedirectedOutput$$anonfun$2$$anonfun$1(ReplDriver.scala:163)
	at scala.util.DynamicVariable.withValue(DynamicVariable.scala:58)
	at scala.Console$.withErr(Console.scala:192)
	at dotty.tools.repl.ReplDriver.withRedirectedOutput$$anonfun$1(ReplDriver.scala:163)
	at scala.util.DynamicVariable.withValue(DynamicVariable.scala:58)
	at scala.Console$.withOut(Console.scala:163)
	at dotty.tools.repl.ReplDriver.withRedirectedOutput(ReplDriver.scala:163)
	at dotty.tools.repl.ReplDriver.runUntilQuit(ReplDriver.scala:154)
	at dotty.tools.repl.Main$.main(Main.scala:6)
	at dotty.tools.repl.Main.main(Main.scala)

@TomasMikula
Copy link
Contributor Author

TomasMikula commented May 3, 2018

The issue here is that we do not support inline functions as they have no value we can use. In this stage, they are just code.

But my example doesn't need the value of the function at compile time, only its code. Also, the documentation states that

Quotes and splices are duals of each other. For arbitrary expressions e and types T we have:

~’(e) = e

so ~ '(f(42)) from my example should be equivalent to f(42).


One solution would be to simply disallow the inline keyword on method types.

Code using inline functions is part of the documentation:

inline def foreach(inline op: Int => Unit): Unit = {
  var i = from
  while (i < end) {
    op(i)
    i += 1
  }
}

I don't think this issue is the same as #4433, where the value of an inline function is actually needed at compile time.

@nicolasstucki
Copy link
Contributor

True. The issue is that we may not be able to support inline functions in general an may need to disallow them. You could just write inline def h(f: Int => String): String = f(42) to achieve the same, which is much simpler. Or in more complex cases there is also you can quote the function and then use (~f).apply(~x) or just (~f)(~x) which will also inline the function (this apply is available in Expr).

@TomasMikula
Copy link
Contributor Author

I suppose you meant ~{('(f))('(x))}, in order for it to be the apply you linked to.
Yeah, that does compile:

scala> inline def h(inline f: Int => String): String = ~{('(f))('(42))} 
def h(f: (Int => String) @InlineParam): String
scala>  

but I don't see why that should be different than ~{'(f(42))}.

The issue is that we may not be able to support inline functions in general an may need to disallow them.

I suppose this comment applies to #4433 only, where the function is actually run at compile time. Or do you mean that the foreach example from the docs which uses an inline function might be disallowed as well?

@nicolasstucki
Copy link
Contributor

Both. At compile time in a inline def foo = ~{...}, all inline parameters become runtime parameters and functions do not have a runtime value at this stage (only after compilation). This is why should disallow the inline functions altogether in macros. There where some discussions about disallowing it for the foreach example, but this might not be needed.

@TomasMikula
Copy link
Contributor Author

At compile time in a inline def foo = ~{...}, all inline parameters become runtime parameters

Unless they are quoted, no? Which is the case in this issue. After all, the example from my previous comment compiled.

@nicolasstucki
Copy link
Contributor

Yes. And all non inlined parameters must be quoted. Quoting an inline parameter is possible but not really useful. Your previous example will break at use site.

@TomasMikula
Copy link
Contributor Author

Heh, you're right that it breaks at runtime, which is even worse than a compiler crash.

Don't you agree that #4433 is a different issue, since there the inline parameter is not quoted? That's the use case I actually care about: moving some computation to compile time.

@nicolasstucki
Copy link
Contributor

nicolasstucki commented May 3, 2018

For me there are two distinct manifestations of the same root issue. Disallowing it would ensure that the programs generated do not crash.

@Blaisorblade
Copy link
Contributor

@nicolasstucki it seems to me that

inline def h(inline f: Int => String): String = ~ '(f(42)); h(x => x + 1)

should work and produce something like ((x: Int) => x + 1)(42) or 42 + 1 (or 43 if there's constant-folding later). That's just beta-reduction, you don't need function values both their trees. And it's the same beta-reduction one uses to "run" (ahem, expand) h at the call-site in the first place.

@TomasMikula is that what you'd expect?

However, one does need to decide whether this should beta-reduce or not, because that matters if the argument isn't a value.

inline def h(inline f: Int => String): String = ~ '(println("start of h's generated code"); f { val x = 42; println(x); x })

// how many println's in the output? how are they ordered relative to the println in `h`?
h(x => x + x) 
h(x => 0) // ditto

The SIP-28 cited by inline does propose answers to such questions, where function arguments are reduced first, but I'm not sure those answers are complete (do they apply to inline arguments, for instance?) or up-to-date.

But I guess @biboudis is the expert here, and the foundations he's investigating (will) offer principled answer to those questions.

@nicolasstucki
Copy link
Contributor

@TomasMikula do you want to execute a function in the macro that is defined in the call site of that macro?

@TomasMikula
Copy link
Contributor Author

@Blaisorblade Yup, that would be an acceptable outcome.

@nicolasstucki

For me there are two distinct manifestations of the same root issue.

If the issue is that inline functions are allowed, then yes. But I'm not convinced you want to disallow inline functions altogether.

do you want to execute a function in the macro that is defined in the call site of that macro?

In #4433, yes, and I don't find it too crazy (with some restrictions).
But even before going that far, I would at least like to execute at compile time a previously compiled function, but passed to the macro as an argument.
(For such function parameter the keyword inline might not be quite fitting, because what we want is to be able to execute the function argument at compile time. It doesn't matter whether it's by interpreting the source code, or a reflective call to a previously compiled function.)

@nicolasstucki
Copy link
Contributor

We already tried interpretation of source code mixed with reflective calls to evaluate the contents of the ~. This ended up being extremely hard to support as the interpreter needs to know how to interpret the full language and know how it is compiled down to JVM bytecode. We changed to a compiled version in #4155. This is not a path we will go down again.

There is another way to evaluate functions that are passed as argument. We could JIT compile them and run them as long as they do not contain any free variables and only refer to static method that are already compiled. Currently, this is broken but I will make it work at some point, I will create some other issue with the details. The code might look something like this:

inline def g(p: Int => Boolean): Boolean = ~{
  val f = ('(p)).run
  if(f(5)) '(true)
  else '(false)
}

@Blaisorblade
Copy link
Contributor

@nicolasstucki I see why #4433 requires actual evaluation, but the code in this issue should only require inlining, just as it would be if the body of h were f(42). But for that, the calls to ~ and to need to cancel out.

@nicolasstucki
Copy link
Contributor

Maybe we can support it. But then the semantics of inline function would not be the same as the other inline parameters. It is cleaner to not do so and use the normal mechanism quoting an splicing to achive the same with consistent semantics.

@nicolasstucki
Copy link
Contributor

When I referred to the semantics I meant that the values of inline parameters are available at stage -1 (i.e. under the top level ~ in the inline method). These may need some more documentation, this is a consecuence of the last bullet point in relationship-with-inline-and-macros.

@nicolasstucki
Copy link
Contributor

What we are going to do is to disallow inline functions in macros to avoid any compiler crashes and improve compiler stability. The compiler stability comes always first.

If in the future we have compelling usecases we can start supporting it then. Taking into acount that it is possible to support the same using quotes and splices.

@Blaisorblade
Copy link
Contributor

Blaisorblade commented May 4, 2018

I might still be missing something and I'm unconvinced (other than on short-term implementation restrictions for compiler stability).

When I referred to the semantics I meant that the values of inline parameters are available at stage -1 (i.e. under the top level ~ in the inline method). These may need some more documentation, this is a consecuence of the last bullet point in relationship-with-inline-and-macros.

That's interesting, but I think that's at fault.

It also means, again, that the true problem is not with this issue but with #4433, and it is relevant because inline params can be also evaluated per the bullet point you quote, IIUC that is:

If x is an inline value (or an inline parameter of an inline function), it can be accessed in all contexts where the number of splices minus the number of quotes between use and definition is either 0 or 1.

But I think that property should be restricted depending on the type (arguably with a typeclass), since it's a form of cross-stage persistence "in reverse", since it requires evaluating at compile-time program expressions. That's easy for integers (the example), and for strings, and for other literals. But that's hard for functions, class instances and so on. So it seems that rule should not apply consistently. It's really just a non-principled way to provide a limited form of inspection—matching on the content of an expression, finding a literal, extracting its content.

The compiler stability comes always first.

As a short-term measure, fine.

Taking into account that it is possible to support the same using quotes and splices.

That would be convincing, but how would that work? I think the proper way to do it is by writing h2, which still uses an inline function argument. I'm not even sure why h1 compiles, unless of course it just quotes a reference to f instead of its content.

import scala.quoted._
def hMacro(f: Expr[Int => String]): Expr[String] = f('(42))
inline def h1(f: Int => String): String = ~hMacro('(f))
inline def h2(inline f: Int => String): String = ~hMacro('(f))
val fArg: Int => String = _.toString
scala> h1(_.toString)
1 |h1(_.toString)
  |^^^^^^^^^^^^^^
  |Could not find interpreted class rs$line$5 in classpath
1 |
  |                                                    ^
  |                               access to value f from wrong staging level:
  |                                - the definition is at level 0,
  |                                - but the access is at level 1.
cala> h1(fArg)
1 |h1(fArg)
  |^^^^^^^^
  |Could not find interpreted class rs$line$4 in classpath
scala> h2(_.toString)
Exception in thread "main" java.lang.AssertionError: assertion failed
	at scala.Predef$.assert(Predef.scala:204)
	at dotty.tools.dotc.transform.Splicer$.$anonfun$getLiftedArgs$2(Splicer.scala:61)
	at scala.collection.immutable.List.map(List.scala:283)
[...]
scala> h2(fArg)
Exception in thread "main" java.lang.AssertionError: assertion failed
	at scala.Predef$.assert(Predef.scala:204)
	at dotty.tools.dotc.transform.Splicer$.$anonfun$getLiftedArgs$2(Splicer.scala:61)
	at scala.collection.immutable.List.map(List.scala:283)
	at dotty.tools.dotc.transform.Splicer$.liftArgs$1(Splicer.scala:58)
	at dotty.tools.dotc.transform.Splicer$.getLiftedArgs(Splicer.scala:72)
	at dotty.tools.dotc.transform.Splicer$.splice(Splicer.scala:36)
	at dotty.tools.dotc.transform.ReifyQuotes$Reifier.$anonfun$transform$2(ReifyQuotes.scala:562)
	at dotty.tools.dotc.reporting.trace$.op1$3(trace.scala:32)
[...]

@nicolasstucki
Copy link
Contributor

In h1 the '(f) is a reference to the definition of f in the call site.

We could only implement it if we can somehow enforce that the closure passed to the inline function argument has no free variables and is fully inlined (not always the cases with inline it is a best effort). These two constraints can not be guaranteed by inline hence it will probably be impossible to do.

We used inline for this cross-stage persistence "in reverse" because of it worked out to give the guarantees we needed for literal values. It is not the case for functions. @OlivierBlanvillain has suggested to use another name for these parameters which would reflect better the meaning of these parameters.

@nicolasstucki
Copy link
Contributor

I created a PR where the semantics are modified to allow inline functions as parameters.

@TomasMikula
Copy link
Contributor Author

@nicolasstucki

We already tried interpretation of source code mixed with reflective calls to evaluate the contents of the ~. This ended up being extremely hard to support as the interpreter needs to know how to interpret the full language and know how it is compiled down to JVM bytecode.

Why would the interpreter need to know how the language is compiled to JVM bytecode?

There is another way to evaluate functions that are passed as argument. We could JIT compile them and run them as long as they do not contain any free variables and only refer to static method that are already compiled.

Limiting to only static methods seems too restrictive. Also, it would be nice to at least also be able to refer to methods that themselves have this property, i.e.

  • "as long as they have property P, where P means not contain any free variables and only refer to (static) methods that are already compiled or have property P."

@OlivierBlanvillain has suggested to use another name for these parameters which would reflect better the meaning of these parameters.

Just throwing in a suggestion: static.


@Blaisorblade

If x is an inline value (or an inline parameter of an inline function), it can be accessed in all contexts where the number of splices minus the number of quotes between use and definition is either 0 or 1.

But I think that property should be restricted depending on the type (arguably with a typeclass), since it's a form of cross-stage persistence "in reverse", since it requires evaluating at compile-time program expressions.

I'm afraid it has to be restricted based on the actual value passed in for an inline (static per the above suggestion) argument.

@nicolasstucki
Copy link
Contributor

See #4460

@Blaisorblade
Copy link
Contributor

I'm afraid it has to be restricted based on the actual value passed in for an inline (static per the above suggestion) argument.

That's already required to be a "constant expression".

FWIW, once we're adding inspection, extracting the literal could be supported by something like the Scala 2 macro idiom

val Literal(Constant(v)) = t.tree

or something more complicated on tasty trees. (Probably it'd need to be a simplifier for all constant expressions). But not sure what's preferable there.

@TomasMikula
Copy link
Contributor Author

What does constant expression mean, exactly? Is a function literal a constant expression?

@nicolasstucki
Copy link
Contributor

Contant expression there are basically primitive value types Int, Boolean,...., String and a couple of singleton types like Unit and Null. Function literals are not constant expressions.

@Blaisorblade
Copy link
Contributor

Since our docs and errors talk about constant expressions, can we ensure we mean it in the sense of the spec (if that's not already the case?) and/or that we describe deviations?

The basic definition is in SLS 6.24; SIP-23 essentially associates a singleton type with each constant expression.

In fact, among platform-dependent constant expressions we also (seem to) have those computations that are constant-folded by the typer to obtain their singleton types — see the definition of constant value definitions in SLS 4.1. That also works for inline functions:

scala> final val x = 1 + 2
val x: Int(3) = 3
scala> inline def f(inline w: Int) = w
def f(w: Int @InlineParam): Int
scala> f(x+2)
val res9: Int = 5
scala> val z = 1
val z: Int = 1
scala> f(z+2)
1 |f(z+2)
  |  ^^^
  |  argument to inline parameter must be a constant expression or a function

nicolasstucki added a commit that referenced this issue May 14, 2018
Fix #4431: Change semantics of inline params in macro
@Blaisorblade
Copy link
Contributor

Blaisorblade commented May 14, 2018

Since our docs and errors talk about constant expressions, can we ensure we mean it in the sense of the spec (if that's not already the case?) and/or that we describe deviations?

I later confirmed this is the case; the error checking's done by Checking.checkInlineConformant(tree, isFinal = false, "argument to inline parameter"), and checkInlineConformant uses ConstantType which characterizes constant expressions. We should refine the docs. EDIT: on it.

Blaisorblade added a commit to dotty-staging/dotty that referenced this issue May 14, 2018
Clarification following scala#4431.

The error checking's done by `Checking.checkInlineConformant(tree, isFinal =
false, "argument to inline parameter")`, and `checkInlineConformant` uses
`ConstantType` which characterizes constant expressions.
Blaisorblade added a commit to dotty-staging/dotty that referenced this issue May 14, 2018
Clarification following scala#4431.

The error checking's done by `Checking.checkInlineConformant(tree, isFinal =
false, "argument to inline parameter")`, and `checkInlineConformant` uses
`ConstantType` which characterizes constant expressions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants