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