oracle / graal

GraalVM compiles Java applications into native executables that start instantly, scale fast, and use fewer compute resources 🚀
https://www.graalvm.org
Other
20.15k stars 1.61k forks source link

[truffle-dsl] Shared behaviour / "inheritance" model with Truffle libraries #1836

Open eliasvasylenko opened 4 years ago

eliasvasylenko commented 4 years ago

I think there are some missing pieces that would make using libraries a little easier and require less repetition. For the sake of example, consider an imaginary Lisp dialect where the only primitives are Cons and Symbol, which are both Data, and which have some common behaviour between them (e.g. a consOnto message).

There is a choice when designing libraries between "grouping" and "splitting"; do we create one big DataLibrary with lots of isData, isCons, and isSymbol guard messages for specific types, or do we split them into lots of separate ConsLibrary and SymbolLibrary etc? Either way it seems not to be possible to declare relationships like "every cons cell must also export InteropLibrary", with a default implementation for interop array messages based on traversing the cons structure as a list.

If we group, then we get the benefit of being able to declare which messages require the exporting of other messages using @Abstract, and we only have to inject a single @CachedLibrary to deal with an instance in a specialisation, but there are limited options for specifying fast default behaviour on a per-type basis since A) default message implementations can't specialize and B) exporters of a library can't be conceptually abstract, i.e. they must export all messages of the library.

If we split, we now can write better-performing default exports since we're assuming a more specific type and the availability of certain message implementations, but if there is a hierarchical relationship between libraries we can't express this, we just have to try to remember that every exporter of e.g. IntLibrary must also export ConsLibrary, DataLibrary and InteropLibrary. We also may have to inject a bunch of extra @CachedLibrary side-by-side into our specialisations.

In other words, I'm suggesting to enhance the way in which we can express hierarchy between libraries and share parts of their implementation. The simplest way to address some of this seems to be to allow the @Abstract annotation to apply to types instead of just methods.

Another option is to allow something similar to the following:

@GenerateLibrary(mustExport = {DataLibrary.class, InteropLibrary.class})
public class ConsLibrary {
  // ...
}

With a new method on Library to get instances of our required exports:

@Specialization(guards = "conses.isCons(cons)", limit = "3")
public static Object doDefault(
    Object cons,
    @CachedLibrary(limit = "3") ConsLibrary conses) {
  return conses.as(DataLibrary.class).someCommonOperation(cons);
}

Then every annotation of @ExportLibrary(ConsLibrary.class) must be accompanied by exports of the other libraries.

boris-spas commented 4 years ago

Tracking internally as Issue GR-20936.

chumer commented 4 years ago

Thanks a lot for this suggestion. Sorry for coming back to this so late.

I agree being able to define the mustExport relationship seems useful. Within messages of a library one can express this relationship already with @Abstract(ifExported="otherMessage")

I am not sure about the implementation of: conses.as(DataLibrary.class).someCommonOperation(cons) And the advantages it has over using:

@Specialization(guards = "conses.isCons(cons)", limit = "3")
public static Object doDefault(
    Object cons,
    @CachedLibrary("cons") ConsLibrary conses, 
    @CachedLibrary("cons") DataLibrary data) {
  return data.someCommonOperation(cons);
}

Could you elaborate? I can see one advantage with your suggestion, namely that in this case the library instance used internally to export messages in ConsLibrary could be reused also outside at the call site. As the as method could just return the same instance internally used. I only don't see this happen very often. But if it turns out to happen, we can implement that internally. We can automatically lookup a DataLibrary in ConsLibrary if necessary (we know they are related because their receiver expression is the same).

The reason why we not try to split libraries in many small libraries, but keep one big library for interop is that it is a hard trade-off in terms of footprint. Using many small libraries requires many small instances of cached libraries. While with the one big library approach you only need to use one. Also writing generic operations really becomes a pain with small libraries.

My recommendation of structuring libraries is one library for interop and one library for the language.

eliasvasylenko commented 4 years ago

Not at all, thanks for the response. And sorry for stuffing so much content into a single issue, but I couldn't think of any good way to break it down since it was more of an "exploration of alternatives" than a solid proposal.

I am not sure about the implementation of: conses.as(DataLibrary.class).someCommonOperation(cons) And the advantages it has over using:

Well I just figured that if we statically enforce that an implementation of ConsLibrary must also export DataLibrary then we should also be able to statically furnish the latter with a reference to the former (or indeed have it be the same reference). I thought that it seems a bit redundant to be doubling up on the caching machinary that's all, but ...

Could you elaborate? I can see one advantage with your suggestion, namely that in this case the library instance used internally to export messages in ConsLibrary could be reused also outside at the call site. As the as method could just return the same instance internally used. I only don't see this happen very often. But if it turns out to happen, we can implement that internally. We can automatically lookup a DataLibrary in ConsLibrary if necessary (we know they are related because their receiver expression is the same).

Yes you're right, my little API suggestion is unnecessary since you can just make the same optimisation transparently by recognising that the receiver expressions are the same. Your way is better.

My recommendation of structuring libraries is one library for interop and one library for the language.

Right, the "grouping" option is what I've gone with so far too as it feels like the best compromise.

But if we group then the problem still remains that it's difficult to share fast code. Defaults in the @GenerateLibrary class can't specialize, and @ExportLibrary classes can't be abstract, so there appears to be no way to share message specializations between library exporters.

Say we have some class or interface AbstractCons, which we want to contain some specialized message exports to share between implementations, but which is itself incomplete. It would be nice if we could annotate the class with @Abstract, or omit the @ExportLibrary annotation, then have some other class ConsPair extends AbstractCons inherit those message exports and export any remaining messages.