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