Interface Methods


public interface Methods
Utilities for invoking, declaring, and defining methods

Method invocation requires a bit more kruft than we would like, it that kruft does have its benefits. (For an example of method definition, see Emitter.)

Consider the example where we'd like to invoke Integer.compare(int, int):

 var mdescIntegerCompare = MthDesc.derive(Integer::compare)
                .check(MthDesc::returns, Types.T_INT)
                .check(MthDesc::param, Types.T_INT)
                .check(MthDesc::param, Types.T_INT)
                .check(MthDesc::build);
 em
                .emit(Op::iload, params.a)
                .emit(Op::ldc__i, 20)
                .emit(Op::invokestatic, Types.refOf(Integer.class), "compare", mdescIntegerCompare,
                        false)
                .step(Inv::takeArg)
                .step(Inv::takeArg)
                .step(Inv::ret)
                .emit(Op::ireturn, retReq);
 

The first requirement (as in the case of defining a method) is to obtain the descriptor of the target method. There are a few ways to generate method descriptors, but the safest when calling a compiled or library method is to derive it from a reference to that method. There is no such thing as a "method literal" in Java, but there are method references, and we can match the types of those references. One wrinkle to this, however, is that we cannot distinguish auto-boxed parameters from those that genuinely accept the boxed type, i.e., both void f(int a) and void g(Integer b) can be made into references of type Consumer<Integer>, because <int> is not a legal type signature. Thus, we require the user to call Methods.MthDescCheckedBuilderP.check(BiFunction, Object) with, e.g., Methods.MthDesc.param(MthDescCheckedBuilderP, TInt) to specify that an Integer is actually an int. However, we cannot check that the user did this correctly until runtime. Still, we are able to prevent a Hippo parameter from receiving an int, and that is much better than nothing.

Once we have the method descriptor, we can use it in invocation operators, e.g., Op.invokestatic(Emitter, TRef, String, MthDesc, boolean). In an of itself, this operator does not consume nor push anything to the stack. Instead, it returns an Methods.Inv object, which facilitates the popping and checking of each parameter, followed by the pushing of the returned value, if non-void. It is not obvious to us (if such a technique even exists) to pop an arbitrary number of entries from <N> in a single method. Instead, we have to treat Java's type checker as a sort of automaton that we can step, one pop at a time, by invoking a method. This method is Methods.Inv.takeArg(Inv), which for chaining purposes, is most easily invoked using the aptly-named Methods.Inv.step(Function) instance method. We would rather not have to do it this way, as it is unnecessary kruft that may also have a run-time cost. One benefit, however, is that if the arguments on the stack do not match the parameters required by the descriptor, the first mismatched takeArg line (corresponding to the right-most mismatched parameter) will fail to compile, and so we know which argument is incorrect. Finally, we must do one last step to to push the return value, e.g., Methods.Inv.ret(Inv). This will check that all parameters have been popped, and then push a value of the descriptor's return type. It returns the resulting emitter. For a void method, use Methods.Inv.retVoid(Inv) to avert the push. Unfortunately, ret is still permitted, but at least downstream operators are likely to fail, since nothing should consume Types.TVoid.