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