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