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