diff --git a/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/loader/Python3Loader.java b/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/loader/Python3Loader.java index 91448ecf6..b89bea9a6 100644 --- a/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/loader/Python3Loader.java +++ b/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/loader/Python3Loader.java @@ -38,6 +38,7 @@ import org.python.core.PyObject; import org.python.core.PySyntaxError; import org.python.core.PyUnicode; +import org.python.util.PythonInterpreter; public class Python3Loader extends PythonLoader { @@ -102,6 +103,15 @@ public ConstantFoldingRewriter createCAstRewriter(CAst ast) { @Override protected Object eval(CAstOperator op, Object lhs, Object rhs) { String s = lhs + " " + op.getValue() + " " + rhs; + + PythonInterpreter ip = Python3Interpreter.getInterp(); + if (ip == null) { + // Jython init failed (memoized in Python3Interpreter). Skip constant folding + // for this expression; analysis remains correct, just less precise. Don't log + // an "Evaluating:" entry — nothing is actually evaluated, and the underlying + // init failure was already announced from getInterp(). + return null; + } logger.info(() -> "Evaluating: " + s); // Use the Python interpreter to evaluate the expression. @@ -109,7 +119,7 @@ protected Object eval(CAstOperator op, Object lhs, Object rhs) { PyObject x; try { - x = Python3Interpreter.getInterp().eval(unicode); + x = ip.eval(unicode); } catch (PySyntaxError e) { // Handle syntax errors gracefully. logger.log(WARNING, e, () -> "Syntax error in expression: " + unicode); diff --git a/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/util/Python3Interpreter.java b/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/util/Python3Interpreter.java index f29be6276..d007a4e79 100644 --- a/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/util/Python3Interpreter.java +++ b/jython/com.ibm.wala.cast.python.jython3/source/com/ibm/wala/cast/python/util/Python3Interpreter.java @@ -1,5 +1,6 @@ package com.ibm.wala.cast.python.util; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import org.python.core.PyException; @@ -13,17 +14,86 @@ public class Python3Interpreter extends com.ibm.wala.cast.python.util.PythonInte private static PythonInterpreter interp; - public static PythonInterpreter getInterp() { + /** + * Memoizes a failed Jython init so subsequent {@link #getInterp()} calls return {@code null} + * cheaply instead of re-running {@code new PythonInterpreter()} (which can be expensive when it + * fails — site-import walks the Jython resource path on every attempt). When a single failure has + * occurred, callers receive {@code null} and can degrade their behavior (e.g., {@link + * com.ibm.wala.cast.python.loader.Python3Loader} skips constant folding). + */ + private static volatile boolean initFailed = false; + + /** + * Memoizes whether the "interpreter unavailable" warning has already been emitted from {@link + * #evalAsInteger(String)}. Without this, callers like {@code interpretAsInt} (invoked many times + * during shape inference) would flood logs with one WARNING per call after the first init + * failure. The first failure is already announced by the catch block in {@link #getInterp()}; + * subsequent calls log at FINE level only. + * + * @implNote Uses {@link AtomicBoolean#compareAndSet} so the check-and-set is atomic. Under + * concurrent call graph construction, multiple threads can race into the {@code if (ip == + * null)} branch simultaneously; a non-atomic {@code volatile boolean} flag would let several + * of them all pass the check before any sets it, defeating the "log once" intent. + */ + private static final AtomicBoolean unavailableWarned = new AtomicBoolean(false); + + public static synchronized PythonInterpreter getInterp() { + if (initFailed) return null; if (interp == null) { - PySystemState.initialize(); - interp = new PythonInterpreter(); + try { + PySystemState.initialize(); + interp = new PythonInterpreter(); + } catch (Exception t) { + // Jython init can fail when bootstrap resources (e.g., the embedded + // _frozen_importlib bytecode used by org.python.core.imp) aren't reachable from the + // current classloader/working directory. This is environment-dependent (e.g., happens + // under Tycho-OSGi but not under plain Maven-surefire). Treat as a recoverable failure: + // log once, memoize, and let callers degrade gracefully. + // + // We catch {@link Exception} (not {@link Throwable}) so that {@link Error} types + // (OOM, stack overflow, linkage errors) keep propagating to the caller — those signal + // serious VM problems we don't want to silently swallow and continue past. + initFailed = true; + LOGGER.log( + Level.WARNING, + t, + () -> + "Jython interpreter init failed; all interpreter-based evaluation will be disabled" + + " for this run (constant folding in Python3Loader, shape-argument" + + " evaluation via interpretAsInt/evalAsInteger, etc.)."); + return null; + } } return interp; } public Integer evalAsInteger(String expr) { + PythonInterpreter ip = getInterp(); + if (ip == null) { + // Return {@code null} (the same "cannot evaluate" signal used elsewhere in this method's + // contract) rather than throwing, so callers like + // {@code com.ibm.wala.cast.python.util.PythonInterpreter#interpretAsInt} — which expect a + // nullable {@link Integer} and don't catch checked or runtime exceptions — degrade + // gracefully in the same OSGi-classloader environments that triggered the {@link + // #getInterp()} init failure in the first place. + // + // Log the first such call at WARNING (so operators see that some shape inference is being + // skipped because of the earlier init failure); subsequent calls log at FINE only, since + // the underlying init failure has already been announced from {@link #getInterp()}. + if (unavailableWarned.compareAndSet(false, true)) { + LOGGER.log( + Level.WARNING, + () -> + "Jython interpreter unavailable (init failed earlier); evalAsInteger will return" + + " null for this and any subsequent calls. First skipped expression: " + + expr); + } else { + LOGGER.log(Level.FINE, () -> "evalAsInteger returning null (interp unavailable): " + expr); + } + return null; + } try { - PyObject val = getInterp().eval(expr); + PyObject val = ip.eval(expr); if (val.isInteger()) { return val.asInt(); } else