1 /*
  2  * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package jdk.incubator.code.interpreter;
 27 
 28 import java.lang.invoke.*;
 29 import java.lang.reflect.Array;
 30 import java.lang.reflect.Proxy;
 31 import jdk.incubator.code.*;
 32 import jdk.incubator.code.op.CoreOp;
 33 import jdk.incubator.code.type.ArrayType;
 34 import jdk.incubator.code.type.FieldRef;
 35 import jdk.incubator.code.type.MethodRef;
 36 import jdk.incubator.code.type.FunctionType;
 37 import jdk.incubator.code.type.JavaType;
 38 import jdk.incubator.code.TypeElement;
 39 import jdk.incubator.code.type.VarType;
 40 import java.util.*;
 41 import java.util.concurrent.locks.ReentrantLock;
 42 import java.util.stream.Collectors;
 43 import java.util.stream.Stream;
 44 
 45 import static java.util.stream.Collectors.toMap;
 46 
 47 public final class Interpreter {
 48     private Interpreter() {
 49     }
 50 
 51     /**
 52      * Invokes an invokable operation by interpreting the code elements within
 53      * the operations body.
 54      * <p>
 55      * The sequence of arguments must consists of objects corresponding, in order,
 56      * to the invokable operation's {@link Op.Invokable#parameters() parameters}.
 57      * If the invokable operation {@link Op.Invokable#capturedValues() captures values}
 58      * then the sequence of arguments must be appended with objects corresponding,
 59      * in order, to the captured values.
 60      *
 61      * @param l the lookup to use for interpreting reflective operations.
 62      * @param op the invokeable operation to interpret.
 63      * @param args the invokeable's arguments appended with captured arguments, if any.
 64      * @return the interpreter result of invokable operation.
 65      * @param <T> the type of Invokable.
 66      * @throws InterpreterException if there is a failure to interpret
 67      * @throws Throwable if interpretation results in the throwing of an uncaught exception
 68      */
 69     public static <T extends Op & Op.Invokable>
 70     Object invoke(MethodHandles.Lookup l, T op,
 71                   Object... args) {
 72         // Arguments can contain null values so we cannot use List.of
 73         return invoke(l, op, Arrays.asList(args));
 74     }
 75 
 76     /**
 77      * Invokes an invokable operation by interpreting the code elements within
 78      * the operations body.
 79      * <p>
 80      * The list of arguments must consists of objects corresponding, in order,
 81      * to the invokable operation's {@link Op.Invokable#parameters() parameters}.
 82      * If the invokable operation {@link Op.Invokable#capturedValues() captures values}
 83      * then the list of arguments must be appended with objects corresponding,
 84      * in order, to the captured values.
 85      *
 86      * @param l the lookup to use for interpreting reflective operations.
 87      * @param op the invokeable operation to interpret.
 88      * @param args the invokeable's arguments appended with captured arguments, if any.
 89      * @return the interpreter result of invokable operation.
 90      * @param <T> the type of Invokable.
 91      * @throws InterpreterException if there is a failure to interpret
 92      * @throws Throwable if interpretation results in the throwing of an uncaught exception
 93      */
 94     public static <T extends Op & Op.Invokable>
 95     Object invoke(MethodHandles.Lookup l, T op,
 96                   List<Object> args) {
 97         List<Block.Parameter> parameters = op.parameters();
 98         List<Value> capturedValues = op.capturedValues();
 99         if (parameters.size() + capturedValues.size() != args.size()) {
100             throw interpreterException(new IllegalArgumentException(
101                     String.format("Actual #arguments (%d) differs from #parameters (%d) plus #captured arguments (%d)",
102                             args.size(), parameters.size(), capturedValues.size())));
103         }
104 
105         // Map symbolic parameters to runtime arguments
106         Map<Value, Object> valuesAndArguments = new HashMap<>();
107         for (int i = 0; i < parameters.size(); i++) {
108             valuesAndArguments.put(parameters.get(i), args.get(i));
109         }
110         // Map symbolic captured values to the additional runtime arguments
111         for (int i = 0; i < capturedValues.size(); i++) {
112             valuesAndArguments.put(capturedValues.get(i), args.get(parameters.size() + i));
113         }
114 
115         return interpretEntryBlock(l, op.body().entryBlock(), new OpContext(), valuesAndArguments);
116     }
117 
118 
119     @SuppressWarnings("serial")
120     public static final class InterpreterException extends RuntimeException {
121         private InterpreterException(Throwable cause) {
122             super(cause);
123         }
124     }
125 
126     static InterpreterException interpreterException(Throwable cause) {
127         return new InterpreterException(cause);
128     }
129 
130     record BlockContext(Block b, Map<Value, Object> valuesAndArguments) {
131     }
132 
133     static final class OpContext {
134         final Map<Object, ReentrantLock> locks = new HashMap<>();
135         final Deque<BlockContext> stack = new ArrayDeque<>();
136         final Deque<ExceptionRegionRecord> erStack = new ArrayDeque<>();
137 
138         Object getValue(Value v) {
139             // @@@ Only dominating values are accessible
140             BlockContext bc = findContext(v);
141             if (bc != null) {
142                 return bc.valuesAndArguments.get(v);
143             } else {
144                 throw interpreterException(new IllegalArgumentException("Undefined value: " + v));
145             }
146         }
147 
148         Object setValue(Value v, Object o) {
149             BlockContext bc = findContext(v);
150             if (bc != null) {
151                 throw interpreterException(new IllegalArgumentException("Value already defined: " + v));
152             }
153             stack.peek().valuesAndArguments.put(v, o);
154             return o;
155         }
156 
157         BlockContext findContext(Value v) {
158             Optional<BlockContext> ob = stack.stream().filter(b -> b.valuesAndArguments.containsKey(v)).findFirst();
159             return ob.orElse(null);
160         }
161 
162         boolean contains(Block.Reference s) {
163             Block sb = s.targetBlock();
164             return stack.stream().anyMatch(bc -> bc.b.equals(sb));
165         }
166 
167         void successor(Block.Reference sb) {
168             List<Object> sbValues = sb.arguments().stream().map(this::getValue).toList();
169 
170             Block b = sb.targetBlock();
171             Map<Value, Object> bValues = new HashMap<>();
172             for (int i = 0; i < sbValues.size(); i++) {
173                 bValues.put(b.parameters().get(i), sbValues.get(i));
174             }
175 
176             if (contains(sb)) {
177                 // if block is already dominating pop back up from the back branch to the block
178                 // before the successor block
179                 while (!stack.peek().b.equals(sb.targetBlock())) {
180                     stack.pop();
181                 }
182                 stack.pop();
183             }
184             stack.push(new BlockContext(b, bValues));
185         }
186 
187         void successor(Block b, Map<Value, Object> bValues) {
188             stack.push(new BlockContext(b, bValues));
189         }
190 
191         void popTo(BlockContext bc) {
192             while (!stack.peek().equals(bc)) {
193                 stack.pop();
194             }
195         }
196 
197         void pushExceptionRegion(ExceptionRegionRecord erb) {
198             erStack.push(erb);
199         }
200 
201         void popExceptionRegion(CoreOp.ExceptionRegionExit ere) {
202             ere.catchBlocks().forEach(catchBlock -> {
203                 if (erStack.peek().catchBlock != catchBlock.targetBlock()) {
204                     // @@@ Use internal exception type
205                     throw interpreterException(new IllegalStateException("Mismatched exception regions"));
206                 }
207                 erStack.pop();
208             });
209         }
210 
211         Block exception(MethodHandles.Lookup l, Throwable e) {
212             // Find the first matching exception region
213             // with a catch block whose argument type is assignable-compatible to the throwable
214             ExceptionRegionRecord er;
215             Block cb = null;
216             while ((er = erStack.poll()) != null &&
217                     (cb = er.match(l, e)) == null) {
218             }
219 
220             if (er == null) {
221                 return null;
222             }
223 
224             // Pop the block context to the block defining the start of the exception region
225             popTo(er.mark);
226             while (erStack.size() > er.erStackDepth()) {
227                 erStack.pop();
228             }
229             return cb;
230         }
231     }
232 
233     static final class VarBox
234             implements CoreOp.Var<Object> {
235         Object value;
236 
237         public Object value() {
238             return value;
239         }
240 
241         VarBox(Object value) {
242             this.value = value;
243         }
244 
245         static final Object UINITIALIZED = new Object();
246     }
247 
248     record ClosureRecord(CoreOp.ClosureOp op,
249                          List<Object> capturedArguments) {
250     }
251 
252     record TupleRecord(List<Object> components) {
253         Object getComponent(int index) {
254             return components.get(index);
255         }
256 
257         TupleRecord with(int index, Object value) {
258             List<Object> copy = new ArrayList<>(components);
259             copy.set(index, value);
260             return new TupleRecord(copy);
261         }
262     }
263 
264     record ExceptionRegionRecord(BlockContext mark, int erStackDepth, Block catchBlock) {
265         Block match(MethodHandles.Lookup l, Throwable e) {
266             List<Block.Parameter> args = catchBlock.parameters();
267             if (args.size() != 1) {
268                 throw interpreterException(new IllegalStateException("Catch block must have one argument"));
269             }
270             TypeElement et = args.get(0).type();
271             if (et instanceof VarType vt) {
272                 et = vt.valueType();
273             }
274             if (resolveToClass(l, et).isInstance(e)) {
275                 return catchBlock;
276             }
277             return null;
278         }
279     }
280 
281     static Object interpretBody(MethodHandles.Lookup l, Body body,
282                                 OpContext oc,
283                                 List<Object> args) {
284         List<Block.Parameter> parameters = body.entryBlock().parameters();
285         if (parameters.size() != args.size()) {
286             throw interpreterException(new IllegalArgumentException(
287                     "Incorrect number of arguments arguments"));
288         }
289 
290         // Map symbolic parameters to runtime arguments
291         Map<Value, Object> arguments = new HashMap<>();
292         for (int i = 0; i < parameters.size(); i++) {
293             arguments.put(parameters.get(i), args.get(i));
294         }
295 
296         return interpretEntryBlock(l, body.entryBlock(), oc, arguments);
297     }
298 
299     static Object interpretEntryBlock(MethodHandles.Lookup l, Block entry,
300                                       OpContext oc,
301                                       Map<Value, Object> valuesAndArguments) {
302         assert entry.isEntryBlock();
303 
304         // If the stack is not empty it means we are interpreting
305         // an entry block with a parent body whose nearest ancestor body
306         // is the current context block's parent body
307         BlockContext yieldContext = oc.stack.peek();
308         assert yieldContext == null ||
309                 yieldContext.b().parentBody() == entry.parentBody().parentOp().ancestorBody();
310 
311         // Note that first block cannot have any successors so the queue will have at least one entry
312         oc.stack.push(new BlockContext(entry, valuesAndArguments));
313         while (true) {
314             BlockContext bc = oc.stack.peek();
315 
316             // Execute all but the terminating operation
317             int nops = bc.b.ops().size();
318             try {
319                 for (int i = 0; i < nops - 1; i++) {
320                     Op op = bc.b.ops().get(i);
321                     assert !(op instanceof Op.Terminating) : op.opName();
322 
323                     Object result = interpretOp(l, oc, op);
324                     oc.setValue(op.result(), result);
325                 }
326             } catch (InterpreterException e) {
327                 throw e;
328             } catch (Throwable t) {
329                 processThrowable(oc, l, t);
330                 continue;
331             }
332 
333             // Execute the terminating operation
334             Op to = bc.b.terminatingOp();
335             if (to instanceof CoreOp.ConditionalBranchOp cb) {
336                 boolean p;
337                 Object bop = oc.getValue(cb.predicate());
338                 if (bop instanceof Boolean bp) {
339                     p = bp;
340                 } else if (bop instanceof Integer ip) {
341                     // @@@ This is required when lifting up from bytecode, since boolean values
342                     // are erased to int values, abd the bytecode lifting implementation is not currently
343                     // sophisticated enough to recover the type information
344                     p = ip != 0;
345                 } else {
346                     throw interpreterException(
347                             new UnsupportedOperationException("Unsupported type input to operation: " + cb));
348                 }
349                 Block.Reference sb = p ? cb.trueBranch() : cb.falseBranch();
350                 oc.successor(sb);
351             } else if (to instanceof CoreOp.BranchOp b) {
352                 Block.Reference sb = b.branch();
353 
354                 oc.successor(sb);
355             } else if (to instanceof CoreOp.ThrowOp _throw) {
356                 Throwable t = (Throwable) oc.getValue(_throw.argument());
357                 processThrowable(oc, l, t);
358             } else if (to instanceof CoreOp.ReturnOp ret) {
359                 Value rv = ret.returnValue();
360                 return rv == null ? null : oc.getValue(rv);
361             } else if (to instanceof CoreOp.YieldOp yop) {
362                 if (yieldContext == null) {
363                     throw interpreterException(
364                             new IllegalStateException("Yielding to no parent body"));
365                 }
366                 Value yv = yop.yieldValue();
367                 Object yr = yv == null ? null : oc.getValue(yv);
368                 oc.popTo(yieldContext);
369                 return yr;
370             } else if (to instanceof CoreOp.ExceptionRegionEnter ers) {
371                 int erStackDepth = oc.erStack.size();
372                 ers.catchBlocks().forEach(catchBlock -> {
373                     var er = new ExceptionRegionRecord(oc.stack.peek(), erStackDepth, catchBlock.targetBlock());
374                     oc.pushExceptionRegion(er);
375                 });
376 
377                 oc.successor(ers.start());
378             } else if (to instanceof CoreOp.ExceptionRegionExit ere) {
379                 oc.popExceptionRegion(ere);
380 
381                 oc.successor(ere.end());
382             } else {
383                 throw interpreterException(
384                         new UnsupportedOperationException("Unsupported terminating operation: " + to.opName()));
385             }
386         }
387     }
388 
389     static void processThrowable(OpContext oc, MethodHandles.Lookup l, Throwable t) {
390         // Find a matching catch block
391         Block cb = oc.exception(l, t);
392         if (cb == null) {
393             // If there is no matching catch bock then rethrow back to the caller
394             eraseAndThrow(t);
395             throw new InternalError("should not reach here");
396         }
397 
398         // Add a new block context to the catch block with the exception as the argument
399         Map<Value, Object> bValues = new HashMap<>();
400         Block.Parameter eArg = cb.parameters().get(0);
401         if (eArg.type() instanceof VarType) {
402             bValues.put(eArg, new VarBox(t));
403         } else {
404             bValues.put(eArg, t);
405         }
406         oc.successor(cb, bValues);
407     }
408 
409 
410 
411     @SuppressWarnings("unchecked")
412     public static <E extends Throwable> void eraseAndThrow(Throwable e) throws E {
413         throw (E) e;
414     }
415 
416     static Object interpretOp(MethodHandles.Lookup l, OpContext oc, Op o) {
417         if (o instanceof CoreOp.ConstantOp co) {
418             if (co.resultType().equals(JavaType.J_L_CLASS)) {
419                 return resolveToClass(l, (JavaType) co.value());
420             } else {
421                 return co.value();
422             }
423         } else if (o instanceof CoreOp.FuncCallOp fco) {
424             String name = fco.funcName();
425 
426             // Find top-level op
427             Op top = fco;
428             while (top.ancestorBody() != null) {
429                 top = top.ancestorBody().parentOp();
430             }
431 
432             // Ensure top-level op is a module and function name
433             // is in the module's function table
434             if (top instanceof CoreOp.ModuleOp mop) {
435                 CoreOp.FuncOp funcOp = mop.functionTable().get(name);
436                 if (funcOp == null) {
437                     throw interpreterException(
438                             new IllegalStateException
439                                     ("Function " + name + " cannot be resolved: not in module's function table"));
440                 }
441 
442                 List<Object> values = o.operands().stream().map(oc::getValue).toList();
443                 return Interpreter.invoke(l, funcOp, values);
444             } else {
445                 throw interpreterException(
446                         new IllegalStateException(
447                                 "Function " + name + " cannot be resolved: top level op is not a module"));
448             }
449         } else if (o instanceof CoreOp.InvokeOp co) {
450             MethodType target = resolveToMethodType(l, o.opType());
451             MethodHandles.Lookup il = switch (co.invokeKind()) {
452                 case STATIC, INSTANCE -> l;
453                 case SUPER -> l.in(target.parameterType(0));
454             };
455             MethodHandle mh = resolveToMethodHandle(il, co.invokeDescriptor(), co.invokeKind());
456 
457             mh = mh.asType(target).asFixedArity();
458             Object[] values = o.operands().stream().map(oc::getValue).toArray();
459             return invoke(mh, values);
460         } else if (o instanceof CoreOp.NewOp no) {
461             Object[] values = o.operands().stream().map(oc::getValue).toArray();
462             JavaType nType = (JavaType) no.constructorType().returnType();
463             if (nType instanceof ArrayType at) {
464                 if (values.length > at.dimensions()) {
465                     throw interpreterException(new IllegalArgumentException("Bad constructor NewOp: " + no));
466                 }
467                 int[] lengths = Stream.of(values).mapToInt(v -> (int) v).toArray();
468                 for (int length : lengths) {
469                     nType = ((ArrayType)nType).componentType();
470                 }
471                 return Array.newInstance(resolveToClass(l, nType), lengths);
472             } else {
473                 MethodHandle mh = constructorHandle(l, no.constructorType());
474                 return invoke(mh, values);
475             }
476         } else if (o instanceof CoreOp.QuotedOp qo) {
477             SequencedMap<Value, Object> capturedValues = qo.capturedValues().stream()
478                     .collect(toMap(v -> v, oc::getValue, (v, _) -> v, LinkedHashMap::new));
479             return new Quoted(qo.quotedOp(), capturedValues);
480         } else if (o instanceof CoreOp.LambdaOp lo) {
481             SequencedMap<Value, Object> capturedValuesAndArguments = lo.capturedValues().stream()
482                     .collect(toMap(v -> v, oc::getValue, (v, _) -> v, LinkedHashMap::new));
483             Class<?> fi = resolveToClass(l, lo.functionalInterface());
484 
485             Object[] capturedArguments = capturedValuesAndArguments.sequencedValues().toArray(Object[]::new);
486             MethodHandle fProxy = INVOKE_LAMBDA_MH.bindTo(l).bindTo(lo).bindTo(capturedArguments)
487                     .asCollector(Object[].class, lo.parameters().size());
488             Object fiInstance = MethodHandleProxies.asInterfaceInstance(fi, fProxy);
489 
490             // If a quotable lambda proxy again to implement Quotable
491             if (Quotable.class.isAssignableFrom(fi)) {
492                 return Proxy.newProxyInstance(l.lookupClass().getClassLoader(), new Class<?>[]{fi},
493                         (_, method, args) -> {
494                             if (method.getDeclaringClass() == Quotable.class) {
495                                 // Implement Quotable::quoted
496                                 return new Quoted(lo, capturedValuesAndArguments);
497                             } else {
498                                 // Delegate to FI instance
499                                 return method.invoke(fiInstance, args);
500                             }
501                         });
502             } else {
503                 return fiInstance;
504             }
505         } else if (o instanceof CoreOp.ClosureOp co) {
506             List<Object> capturedArguments = co.capturedValues().stream()
507                     .map(oc::getValue).toList();
508             return new ClosureRecord(co, capturedArguments);
509         } else if (o instanceof CoreOp.ClosureCallOp cco) {
510             List<Object> values = o.operands().stream().map(oc::getValue).toList();
511             ClosureRecord cr = (ClosureRecord) values.get(0);
512 
513             List<Object> arguments = new ArrayList<>(values.subList(1, values.size()));
514             arguments.addAll(cr.capturedArguments);
515             return Interpreter.invoke(l, cr.op(), arguments);
516         } else if (o instanceof CoreOp.VarOp vo) {
517             Object v = vo.isUninitialized()
518                     ? VarBox.UINITIALIZED
519                     : oc.getValue(o.operands().get(0));
520             return new VarBox(v);
521         } else if (o instanceof CoreOp.VarAccessOp.VarLoadOp vlo) {
522             // Cast to CoreOp.Var, since the instance may have originated as an external instance
523             // via a captured value map
524             CoreOp.Var<?> vb = (CoreOp.Var<?>) oc.getValue(o.operands().get(0));
525             Object value = vb.value();
526             if (value == VarBox.UINITIALIZED) {
527                 throw interpreterException(new IllegalStateException("Loading from uninitialized variable"));
528             }
529             return value;
530         } else if (o instanceof CoreOp.VarAccessOp.VarStoreOp vso) {
531             VarBox vb = (VarBox) oc.getValue(o.operands().get(0));
532             vb.value = oc.getValue(o.operands().get(1));
533             return null;
534         } else if (o instanceof CoreOp.TupleOp to) {
535             List<Object> values = o.operands().stream().map(oc::getValue).toList();
536             return new TupleRecord(values);
537         } else if (o instanceof CoreOp.TupleLoadOp tlo) {
538             TupleRecord tb = (TupleRecord) oc.getValue(o.operands().get(0));
539             return tb.getComponent(tlo.index());
540         } else if (o instanceof CoreOp.TupleWithOp two) {
541             TupleRecord tb = (TupleRecord) oc.getValue(o.operands().get(0));
542             return tb.with(two.index(), oc.getValue(o.operands().get(1)));
543         } else if (o instanceof CoreOp.FieldAccessOp.FieldLoadOp fo) {
544             if (fo.operands().isEmpty()) {
545                 VarHandle vh = fieldStaticHandle(l, fo.fieldDescriptor());
546                 return vh.get();
547             } else {
548                 Object v = oc.getValue(o.operands().get(0));
549                 VarHandle vh = fieldHandle(l, fo.fieldDescriptor());
550                 return vh.get(v);
551             }
552         } else if (o instanceof CoreOp.FieldAccessOp.FieldStoreOp fo) {
553             if (fo.operands().size() == 1) {
554                 Object v = oc.getValue(o.operands().get(0));
555                 VarHandle vh = fieldStaticHandle(l, fo.fieldDescriptor());
556                 vh.set(v);
557             } else {
558                 Object r = oc.getValue(o.operands().get(0));
559                 Object v = oc.getValue(o.operands().get(1));
560                 VarHandle vh = fieldHandle(l, fo.fieldDescriptor());
561                 vh.set(r, v);
562             }
563             return null;
564         } else if (o instanceof CoreOp.InstanceOfOp io) {
565             Object v = oc.getValue(o.operands().get(0));
566             return isInstance(l, io.type(), v);
567         } else if (o instanceof CoreOp.CastOp co) {
568             Object v = oc.getValue(o.operands().get(0));
569             return cast(l, co.type(), v);
570         } else if (o instanceof CoreOp.ArrayLengthOp) {
571             Object a = oc.getValue(o.operands().get(0));
572             return Array.getLength(a);
573         } else if (o instanceof CoreOp.ArrayAccessOp.ArrayLoadOp) {
574             Object a = oc.getValue(o.operands().get(0));
575             Object index = oc.getValue(o.operands().get(1));
576             return Array.get(a, (int) index);
577         } else if (o instanceof CoreOp.ArrayAccessOp.ArrayStoreOp) {
578             Object a = oc.getValue(o.operands().get(0));
579             Object index = oc.getValue(o.operands().get(1));
580             Object v = oc.getValue(o.operands().get(2));
581             Array.set(a, (int) index, v);
582             return null;
583         } else if (o instanceof CoreOp.ArithmeticOperation || o instanceof CoreOp.TestOperation) {
584             MethodHandle mh = opHandle(l, o.opName(), o.opType());
585             Object[] values = o.operands().stream().map(oc::getValue).toArray();
586             return invoke(mh, values);
587         } else if (o instanceof CoreOp.ConvOp) {
588             MethodHandle mh = opHandle(l, o.opName() + "_" + o.opType().returnType(), o.opType());
589             Object[] values = o.operands().stream().map(oc::getValue).toArray();
590             return invoke(mh, values);
591         } else if (o instanceof CoreOp.AssertOp _assert) {
592             Body testBody = _assert.bodies.get(0);
593             boolean testResult = (boolean) interpretBody(l, testBody, oc, List.of());
594             if (!testResult) {
595                 if (_assert.bodies.size() > 1) {
596                     Body messageBody = _assert.bodies.get(1);
597                     String message = String.valueOf(interpretBody(l, messageBody, oc, List.of()));
598                     throw new AssertionError(message);
599                 } else {
600                     throw new AssertionError();
601                 }
602             }
603             return null;
604         } else if (o instanceof CoreOp.ConcatOp) {
605             return o.operands().stream()
606                     .map(oc::getValue)
607                     .map(String::valueOf)
608                     .collect(Collectors.joining());
609         } else if (o instanceof CoreOp.MonitorOp.MonitorEnterOp) {
610             Object monitorTarget = oc.getValue(o.operands().get(0));
611             if (monitorTarget == null) {
612                 throw new NullPointerException();
613             }
614             ReentrantLock lock = oc.locks.computeIfAbsent(monitorTarget, _ -> new ReentrantLock());
615             lock.lock();
616             return null;
617         } else if (o instanceof CoreOp.MonitorOp.MonitorExitOp) {
618             Object monitorTarget = oc.getValue(o.operands().get(0));
619             if (monitorTarget == null) {
620                 throw new NullPointerException();
621             }
622             ReentrantLock lock = oc.locks.get(monitorTarget);
623             if (lock == null) {
624                 throw new IllegalMonitorStateException();
625             }
626             lock.unlock();
627             return null;
628         } else {
629             throw interpreterException(
630                     new UnsupportedOperationException("Unsupported operation: " + o.opName()));
631         }
632     }
633 
634     static final MethodHandle INVOKE_LAMBDA_MH;
635     static {
636         try {
637             INVOKE_LAMBDA_MH = MethodHandles.lookup().findStatic(Interpreter.class, "invokeLambda",
638                     MethodType.methodType(Object.class, MethodHandles.Lookup.class,
639                             CoreOp.LambdaOp.class, Object[].class, Object[].class));
640         } catch (Throwable t) {
641             throw new InternalError(t);
642         }
643     }
644 
645     static Object invokeLambda(MethodHandles.Lookup l, CoreOp.LambdaOp op, Object[] capturedArgs, Object[] args) {
646         List<Object> arguments = new ArrayList<>(Arrays.asList(args));
647         arguments.addAll(Arrays.asList(capturedArgs));
648         return invoke(l, op, arguments);
649     }
650 
651     static MethodHandle opHandle(MethodHandles.Lookup l, String opName, FunctionType ft) {
652         MethodType mt = resolveToMethodType(l, ft).erase();
653         try {
654             return MethodHandles.lookup().findStatic(InvokableLeafOps.class, opName, mt);
655         } catch (NoSuchMethodException | IllegalAccessException e) {
656             throw interpreterException(e);
657         }
658     }
659 
660     static MethodHandle constructorHandle(MethodHandles.Lookup l, FunctionType ft) {
661         MethodType mt = resolveToMethodType(l, ft);
662 
663         if (mt.returnType().isArray()) {
664             if (mt.parameterCount() != 1 || mt.parameterType(0) != int.class) {
665                 throw interpreterException(new IllegalArgumentException("Bad constructor descriptor: " + ft));
666             }
667             return MethodHandles.arrayConstructor(mt.returnType());
668         } else {
669             try {
670                 return l.findConstructor(mt.returnType(), mt.changeReturnType(void.class));
671             } catch (NoSuchMethodException | IllegalAccessException e) {
672                 throw interpreterException(e);
673             }
674         }
675     }
676 
677     static VarHandle fieldStaticHandle(MethodHandles.Lookup l, FieldRef d) {
678         return resolveToVarHandle(l, d);
679     }
680 
681     static VarHandle fieldHandle(MethodHandles.Lookup l, FieldRef d) {
682         return resolveToVarHandle(l, d);
683     }
684 
685     static Object isInstance(MethodHandles.Lookup l, TypeElement d, Object v) {
686         Class<?> c = resolveToClass(l, d);
687         return c.isInstance(v);
688     }
689 
690     static Object cast(MethodHandles.Lookup l, TypeElement d, Object v) {
691         Class<?> c = resolveToClass(l, d);
692         return c.cast(v);
693     }
694 
695     static MethodHandle resolveToMethodHandle(MethodHandles.Lookup l, MethodRef d, CoreOp.InvokeOp.InvokeKind kind) {
696         try {
697             return d.resolveToHandle(l, kind);
698         } catch (ReflectiveOperationException e) {
699             throw interpreterException(e);
700         }
701     }
702 
703     static VarHandle resolveToVarHandle(MethodHandles.Lookup l, FieldRef d) {
704         try {
705             return d.resolveToHandle(l);
706         } catch (ReflectiveOperationException e) {
707             throw interpreterException(e);
708         }
709     }
710 
711     public static MethodType resolveToMethodType(MethodHandles.Lookup l, FunctionType ft) {
712         try {
713             return MethodRef.toNominalDescriptor(ft).resolveConstantDesc(l);
714         } catch (ReflectiveOperationException e) {
715             throw interpreterException(e);
716         }
717     }
718 
719     public static Class<?> resolveToClass(MethodHandles.Lookup l, TypeElement d) {
720         try {
721             if (d instanceof JavaType jt) {
722                 return (Class<?>)jt.erasure().resolve(l);
723             } else {
724                 throw new ReflectiveOperationException();
725             }
726         } catch (ReflectiveOperationException e) {
727             throw interpreterException(e);
728         }
729     }
730 
731     static Object invoke(MethodHandle m, Object... args) {
732         try {
733             return m.invokeWithArguments(args);
734         } catch (RuntimeException | Error e) {
735             throw e;
736         } catch (Throwable e) {
737             eraseAndThrow(e);
738             throw new InternalError("should not reach here");
739         }
740     }
741 }