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