Skip to content

Commit ba2aa32

Browse files
committed
[GR-73863] Support lazy loading of source texts on DSL interpreter
PullRequest: graalpython/4302
2 parents 8dc91b3 + f57b8d8 commit ba2aa32

16 files changed

Lines changed: 270 additions & 251 deletions

File tree

graalpython/AGENTS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,37 @@ Main implementation tree: Java (Truffle interpreter), C (CPython C-API compatibi
1919
- This subtree contains both “source of truth” code and vendored/upstream-ish imports; keep patches minimal in `lib-python/` and large C module imports.
2020
- Some large headers/databases (unicode tables) are generated; avoid editing them by hand unless you also update the generator pipeline.
2121

22+
## RUNNING
23+
24+
- Python code can be executed with GraalPy using `mx python`, invoked just like normal `python` command. The project
25+
*must* be first built with `mx python-jvm` or you will execute stale code. Note that `mx python-jvm` just builds, it
26+
doesn't take arguments nor execute code.
27+
28+
## TESTING
29+
30+
- There are multiple kinds of tests:
31+
- GraalPy Python tests
32+
- Our own tests in `com.oracle.graal.python.test/src/tests/`
33+
- Executed with `mx graalpytest test_file_name`
34+
- New test should normally be added here, unless they need to be in Java
35+
- CPython tests, also called tagged tests
36+
- Tests copied from upstream CPython in `lib-python/3/tests`. Should not be modified unless specifically
37+
requested. If modified, modifications should be marked with a `# GraalPy change` comment above the changed
38+
part.
39+
- Executed with `mx graalpytest --tagged test_file_name`
40+
- Uses a "tagging" system where only a subset of tests specified in tag files is normally executed. The `--all`
41+
flag makes it ignore the tags and execute all tests.
42+
- JUnit tests
43+
- In `com.oracle.graal.python.test/src` and `com.oracle.graal.python.test.integration/src`
44+
- Used primarily for testing features exposed to Java, such as embedding, instrumentation or interop.
45+
- The tests need to be built with `mx build` prior to execution. The `mx unittest com.example.TestName` command
46+
can be used to run individual tests.
47+
- The `mx graalpytest` command accepts pytest‑style test selectors (e.g., `test_mod.py::TestClass::test_method`) but is
48+
**not** a full pytest implementation. Standard pytest command‑line flags such as `-k`, `-m`, `-v`, `--maxfail` are not
49+
supported.
50+
- Important: The test commands don't automatically rebuild the project. It is your reponsibility to rebuild the project
51+
using `mx python-jvm` after making changes prior to running tests otherwise the tests will run stale code.
52+
2253
## ANTI-PATTERNS
2354
- Don’t use `mxbuild/**` outputs to understand behavior; always navigate `.../src/...` trees.
2455
- C-API: never mix `PyMem_*` / `PyObject_*` allocators with platform `malloc` family.

graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,15 @@ public void testInspectJavaArray() throws Throwable {
550550

551551
@Test
552552
public void testSourceFileURI() throws Throwable {
553+
testSourceFileURIImpl(false);
554+
}
555+
556+
@Test
557+
public void testSourceFileURIBytecode() throws Throwable {
558+
testSourceFileURIImpl(true);
559+
}
560+
561+
private void testSourceFileURIImpl(boolean runFromBytecode) throws Throwable {
553562
if (System.getProperty("os.name").toLowerCase().contains("mac")) {
554563
// on the mac machines we run with symlinked directories and such, and it's annoying to
555564
// cater for that
@@ -565,6 +574,11 @@ public void testSourceFileURI() throws Throwable {
565574
"sys.path.insert(0, '" + tempDir.toString() + "')\n" +
566575
"import imported\n" +
567576
"imported.sum(2, 3)\n").getBytes());
577+
578+
if (runFromBytecode) {
579+
compileToBytecode(importedFile, importingFile);
580+
}
581+
568582
Source source = Source.newBuilder("python", importingFile.toFile()).build();
569583
try (DebuggerSession session = tester.startSession()) {
570584
Breakpoint breakpoint = Breakpoint.newBuilder(importingFile.toUri()).lineIs(4).build();
@@ -602,6 +616,16 @@ public void testSourceFileURI() throws Throwable {
602616
}
603617
}
604618

619+
private void compileToBytecode(Path... files) {
620+
StringBuilder sourceCode = new StringBuilder("import py_compile\n");
621+
for (Path file : files) {
622+
sourceCode.append("py_compile.compile(r\"").append(file).append("\")\n");
623+
}
624+
Source compileSource = Source.newBuilder("python", sourceCode.toString(), "compile_source_uri.py").buildLiteral();
625+
tester.startEval(compileSource);
626+
tester.expectDone();
627+
}
628+
605629
@Test
606630
public void testInlineEvaluationBreakpointBuiltin() throws Throwable {
607631
final Source source = Source.newBuilder("python", """

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
import static com.oracle.graal.python.annotations.PythonOS.PLATFORM_WIN32;
2929
import static com.oracle.graal.python.nodes.BuiltinNames.T__SIGNAL;
30-
import static com.oracle.graal.python.nodes.StringLiterals.J_PY_EXTENSION;
3130
import static com.oracle.graal.python.nodes.StringLiterals.T_PY_EXTENSION;
3231
import static com.oracle.graal.python.nodes.truffle.TruffleStringMigrationHelpers.isJavaString;
3332
import static com.oracle.graal.python.util.PythonUtils.ARRAY_ACCESSOR;
@@ -110,7 +109,6 @@
110109
import com.oracle.graal.python.runtime.exception.PException;
111110
import com.oracle.graal.python.runtime.object.PFactory;
112111
import com.oracle.graal.python.util.Function;
113-
import com.oracle.graal.python.util.LazySource;
114112
import com.oracle.graal.python.util.PythonUtils;
115113
import com.oracle.graal.python.util.Supplier;
116114
import com.oracle.truffle.api.Assumption;
@@ -367,6 +365,12 @@ public boolean isSingleContext() {
367365
*/
368366
private final ConcurrentHashMap<Object, Source> sourceCache = new ConcurrentHashMap<>();
369367

368+
/*
369+
* A map from sources without content to either a Source object with content or a TruffleFile
370+
* that can be used to construct such object.
371+
*/
372+
private final WeakHashMap<Source, Object> originalSources = new WeakHashMap<>();
373+
370374
@Idempotent
371375
public static PythonLanguage get(Node node) {
372376
return REFERENCE.get(node);
@@ -548,54 +552,17 @@ protected CallTarget parse(ParsingRequest request) {
548552
return parse(context, source, inputType, topLevel, optimize, interactiveTerminal, argumentNames, futureFeatures);
549553
}
550554

551-
public static RootCallTarget callTargetFromBytecode(PythonContext context, Source source, CodeUnit code) {
552-
boolean internal = shouldMarkSourceInternal(context);
553-
SourceBuilder builder = null;
554-
// The original file path should be passed as the name
555-
String name = source.getName();
556-
if (name != null && !name.isEmpty()) {
557-
builder = sourceForOriginalFile(context, code, internal, name);
558-
if (builder == null) {
559-
if (name.startsWith(FROZEN_FILENAME_PREFIX) && name.endsWith(FROZEN_FILENAME_SUFFIX)) {
560-
String id = name.substring(FROZEN_FILENAME_PREFIX.length(), name.length() - FROZEN_FILENAME_SUFFIX.length());
561-
String fs = context.getEnv().getFileNameSeparator();
562-
String path = context.getStdlibHome() + fs + id.replace(".", fs) + J_PY_EXTENSION;
563-
builder = sourceForOriginalFile(context, code, internal, path);
564-
if (builder == null) {
565-
path = context.getStdlibHome() + fs + id.replace(".", fs) + fs + "__init__.py";
566-
builder = sourceForOriginalFile(context, code, internal, path);
567-
}
568-
}
569-
}
570-
}
571-
if (builder == null) {
572-
builder = Source.newBuilder(source).internal(internal).content(Source.CONTENT_NONE);
573-
}
555+
public RootCallTarget callTargetFromBytecode(Source source, CodeUnit code) {
574556
RootNode rootNode;
575-
LazySource lazySource = new LazySource(builder);
576-
577557
if (PythonOptions.ENABLE_BYTECODE_DSL_INTERPRETER) {
578-
// TODO lazily load source in bytecode DSL interpreter too
579-
rootNode = ((BytecodeDSLCodeUnit) code).createRootNode(context, lazySource.getSource());
558+
rootNode = ((BytecodeDSLCodeUnit) code).createRootNode(this, source);
580559
} else {
581-
rootNode = PBytecodeRootNode.create(context.getLanguage(), (BytecodeCodeUnit) code, lazySource, internal);
560+
rootNode = PBytecodeRootNode.create(this, (BytecodeCodeUnit) code, source, source.isInternal());
582561
}
583562

584563
return PythonUtils.getOrCreateCallTarget(rootNode);
585564
}
586565

587-
private static SourceBuilder sourceForOriginalFile(PythonContext context, CodeUnit code, boolean internal, String path) {
588-
try {
589-
TruffleFile file = context.getEnv().getPublicTruffleFile(path);
590-
if (!file.isReadable()) {
591-
return null;
592-
}
593-
return Source.newBuilder(PythonLanguage.ID, file).name(code.name.toJavaStringUncached()).internal(internal);
594-
} catch (SecurityException | UnsupportedOperationException | InvalidPathException e) {
595-
return null;
596-
}
597-
}
598-
599566
public RootCallTarget parse(PythonContext context, Source source, InputType type, boolean topLevel, int optimize, boolean interactiveTerminal, List<String> argumentNames,
600567
EnumSet<FutureFeature> futureFeatures) {
601568
return parse(context, source, type, topLevel, optimize, interactiveTerminal, false, argumentNames, futureFeatures);
@@ -677,7 +644,7 @@ private RootNode compileForBytecodeInterpreter(ModTy mod, Source source, int opt
677644
Compiler compiler = new Compiler(parserCallbacks);
678645
CompilationUnit cu = compiler.compile(mod, EnumSet.noneOf(Compiler.Flags.class), optimize, futureFeatures);
679646
BytecodeCodeUnit co = cu.assemble();
680-
return PBytecodeRootNode.create(this, co, new LazySource(source), source.isInternal(), parserCallbacks);
647+
return PBytecodeRootNode.create(this, co, source, source.isInternal(), parserCallbacks);
681648
}
682649

683650
private RootNode compileForBytecodeDSLInterpreter(ModTy mod, Source source, int optimize,
@@ -957,7 +924,7 @@ private static Source newSource(PythonContext context, SourceBuilder srcBuilder)
957924
return srcBuilder.build();
958925
}
959926

960-
private static boolean shouldMarkSourceInternal(PythonContext ctxt) {
927+
public static boolean shouldMarkSourceInternal(PythonContext ctxt) {
961928
return !ctxt.isCoreInitialized() && !ctxt.getLanguage().getEngineOption(PythonOptions.ExposeInternalSources);
962929
}
963930

@@ -1259,6 +1226,36 @@ public Source getOrCreateSource(Function<Object, Source> rootNodeFunction, Objec
12591226
return sourceCache.computeIfAbsent(key, rootNodeFunction);
12601227
}
12611228

1229+
public Source getOrCreateSourceWithContent(Source sourceWithoutContent) {
1230+
if (sourceWithoutContent.hasCharacters()) {
1231+
return sourceWithoutContent;
1232+
}
1233+
synchronized (originalSources) {
1234+
Object original = originalSources.get(sourceWithoutContent);
1235+
if (original instanceof Source originalSource) {
1236+
return originalSource;
1237+
}
1238+
if (original instanceof TruffleFile originalFile) {
1239+
Source source;
1240+
try {
1241+
source = Source.newBuilder(ID, originalFile).name(sourceWithoutContent.getName()).internal(sourceWithoutContent.isInternal()).mimeType(MIME_TYPE).build();
1242+
} catch (IOException | SecurityException | UnsupportedOperationException | IllegalArgumentException e) {
1243+
source = sourceWithoutContent;
1244+
}
1245+
originalSources.put(sourceWithoutContent, source);
1246+
return source;
1247+
}
1248+
assert original == null;
1249+
return sourceWithoutContent;
1250+
}
1251+
}
1252+
1253+
public void registerOriginalFile(Source sourceWithoutContent, TruffleFile originalFile) {
1254+
synchronized (originalSources) {
1255+
originalSources.put(sourceWithoutContent, originalFile);
1256+
}
1257+
}
1258+
12621259
public static PythonOS getPythonOS() {
12631260
if (PythonOS.internalCurrent == PythonOS.PLATFORM_ANY) {
12641261
if (ImageInfo.inImageBuildtimeCode()) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GraalPythonModuleBuiltins.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ private static Object doLoadBytecodeFile(Object bytecodePath, Object sourcePath,
507507
try {
508508
// get_data
509509
TruffleString strBytecodePath = PyObjectStrAsTruffleStringNode.executeUncached(bytecodePath);
510-
TruffleFile bytecodeFile = context.getEnv().getPublicTruffleFile(strBytecodePath.toJavaStringUncached());
510+
TruffleFile bytecodeFile = context.getPublicTruffleFileRelaxed(strBytecodePath);
511511
byte[] bytes = bytecodeFile.readAllBytes();
512512
// _classify_pyc
513513
if (bytes.length < 16 || !Arrays.equals(bytes, 0, 4, MAGIC_NUMBER_BYTES, 0, 4)) {
@@ -560,7 +560,16 @@ private static Object doLoadBytecodeFile(Object bytecodePath, Object sourcePath,
560560
Object message = PyObjectCallMethodObjArgs.executeUncached(MESSAGE, T_FORMAT, bytecodePath, sourcePath);
561561
CallNode.executeUncached(context.lookupBuiltinModule(T__BOOTSTRAP).getAttribute(T__VERBOSE_MESSAGE), message);
562562
}
563-
return MarshalModuleBuiltins.fromBytecodeFile(context, bytecodeFile, bytes, 16, bytes.length - 16, cacheKey);
563+
TruffleFile sourceFile = null;
564+
if (sourcePath != PNone.NONE) {
565+
try {
566+
TruffleString strSourcePath = PyObjectStrAsTruffleStringNode.executeUncached(sourcePath);
567+
sourceFile = context.getPublicTruffleFileRelaxed(strSourcePath);
568+
} catch (SecurityException | UnsupportedOperationException | IllegalArgumentException ignored) {
569+
// Fall back to Marshal's empty source.
570+
}
571+
}
572+
return MarshalModuleBuiltins.fromBytecodeFile(context.getLanguage(), bytecodeFile, sourceFile, bytes, 16, bytes.length - 16, cacheKey);
564573
} catch (MarshalModuleBuiltins.Marshal.MarshalError me) {
565574
throw PRaiseNode.raiseStatic(inliningTarget, me.type, me.message, me.arguments);
566575
} catch (IOException | SecurityException | UnsupportedOperationException | IllegalArgumentException e) {

0 commit comments

Comments
 (0)