Skip to content

Commit 69bb6f2

Browse files
committed
[GR-62402] Introduce source options
PullRequest: graalpython/4311
2 parents bfdd4a7 + fbdbb61 commit 69bb6f2

9 files changed

Lines changed: 252 additions & 120 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
This changelog summarizes major changes between GraalVM versions of the Python
44
language runtime. The main focus is on user-observable behavior of the engine.
55

6+
## Version 25.2.0
7+
* Add support for [Truffle source options](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/source/Source.SourceBuilder.html#option(java.lang.String,java.lang.String)):
8+
* The `python.Optimize` option can be used to specify the optimization level, like the `-O` (level 1) and `-OO` (level 2) commandline options.
9+
* The `python.NewGlobals` option can be used to run a source with a fresh globals dictionary instead of the main module globals, which is useful for embeddings that want isolated top-level execution.
10+
611
## Version 25.1.0
712
* Intern string literals in source files
813
* Allocation reporting via Truffle has been removed. Python object sizes were never reported correctly, so the data was misleading and there was a non-neglible overhead for object allocations even when reporting was inactive.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public void before() {
8282
Builder newBuilder = Context.newBuilder();
8383
newBuilder.allowExperimentalOptions(true);
8484
newBuilder.allowAllAccess(true);
85+
newBuilder.option("engine.WarnInterpreterOnly", "false");
8586
PythonTests.closeContext();
8687
tester = new DebuggerTester(newBuilder);
8788
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.test.interop;
42+
43+
import static org.junit.Assert.assertEquals;
44+
45+
import java.io.IOException;
46+
47+
import org.graalvm.polyglot.Context;
48+
import org.graalvm.polyglot.Source;
49+
import org.junit.After;
50+
import org.junit.Before;
51+
import org.junit.Test;
52+
53+
import com.oracle.graal.python.test.PythonTests;
54+
55+
public class SourceOptionsTests extends PythonTests {
56+
private Context context;
57+
58+
@Before
59+
public void setUpTest() {
60+
Context.Builder builder = Context.newBuilder();
61+
builder.allowExperimentalOptions(true);
62+
builder.allowAllAccess(true);
63+
context = builder.build();
64+
}
65+
66+
@After
67+
public void tearDown() {
68+
context.close();
69+
}
70+
71+
@Test
72+
public void testDefaultUsesMainModuleGlobals() {
73+
context.eval("python", "x = 41");
74+
assertEquals(42, context.eval("python", "x + 1").asInt());
75+
}
76+
77+
@Test
78+
public void testNewGlobalsIsolatedFromMainModule() throws IOException {
79+
context.eval("python", "x = 41");
80+
81+
Source source = Source.newBuilder("python", "x = 100\nx + 1", "new-globals.py").option("python.NewGlobals", "true").build();
82+
83+
assertEquals(101, context.eval(source).asInt());
84+
assertEquals(42, context.eval("python", "x + 1").asInt());
85+
}
86+
87+
@Test
88+
public void testSeparateNewGlobalsExecutionsDoNotShareState() throws IOException {
89+
Source writeSource = Source.newBuilder("python", "x = 100", "new-globals-write.py").option("python.NewGlobals", "true").build();
90+
Source readSource = Source.newBuilder("python", "x = globals().get('x', 0)\nx + 1", "new-globals-read.py").option("python.NewGlobals", "true").build();
91+
92+
context.eval(writeSource);
93+
assertEquals(1, context.eval(readSource).asInt());
94+
}
95+
}

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

Lines changed: 50 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,9 @@
3939
import java.lang.invoke.VarHandle;
4040
import java.nio.charset.StandardCharsets;
4141
import java.nio.file.InvalidPathException;
42-
import java.util.ArrayList;
4342
import java.util.Arrays;
4443
import java.util.Collections;
4544
import java.util.EnumSet;
46-
import java.util.HashSet;
4745
import java.util.List;
4846
import java.util.Map;
4947
import java.util.WeakHashMap;
@@ -108,6 +106,7 @@
108106
import com.oracle.graal.python.runtime.PythonContext.PythonThreadState;
109107
import com.oracle.graal.python.runtime.PythonImageBuildOptions;
110108
import com.oracle.graal.python.runtime.PythonOptions;
109+
import com.oracle.graal.python.runtime.PythonSourceOptions;
111110
import com.oracle.graal.python.runtime.exception.PException;
112111
import com.oracle.graal.python.runtime.object.PFactory;
113112
import com.oracle.graal.python.util.Function;
@@ -155,12 +154,7 @@
155154
sandbox = SandboxPolicy.UNTRUSTED, //
156155
implementationName = PythonLanguage.IMPLEMENTATION_NAME, //
157156
version = PythonLanguage.VERSION, //
158-
characterMimeTypes = {PythonLanguage.MIME_TYPE,
159-
"text/x-python-\0\u0000-eval", "text/x-python-\0\u0000-compile", "text/x-python-\1\u0000-eval", "text/x-python-\1\u0000-compile", "text/x-python-\2\u0000-eval",
160-
"text/x-python-\2\u0000-compile", "text/x-python-\0\u0100-eval", "text/x-python-\0\u0100-compile", "text/x-python-\1\u0100-eval", "text/x-python-\1\u0100-compile",
161-
"text/x-python-\2\u0100-eval", "text/x-python-\2\u0100-compile", "text/x-python-\0\u0040-eval", "text/x-python-\0\u0040-compile", "text/x-python-\1\u0040-eval",
162-
"text/x-python-\1\u0040-compile", "text/x-python-\2\u0040-eval", "text/x-python-\2\u0040-compile", "text/x-python-\0\u0140-eval", "text/x-python-\0\u0140-compile",
163-
"text/x-python-\1\u0140-eval", "text/x-python-\1\u0140-compile", "text/x-python-\2\u0140-eval", "text/x-python-\2\u0140-compile"}, //
157+
characterMimeTypes = {PythonLanguage.MIME_TYPE}, //
164158
defaultMimeType = PythonLanguage.MIME_TYPE, //
165159
dependentLanguages = {"nfi", "llvm"}, //
166160
interactive = true, internal = false, //
@@ -274,48 +268,6 @@ public final class PythonLanguage extends TruffleLanguage<PythonContext> {
274268

275269
public static final String MIME_TYPE = "text/x-python";
276270

277-
// the syntax for mime types is as follows
278-
// <mime> ::= "text/x-python-" <optlevel> <flags> "-" kind
279-
// <kind> ::= "compile" | "eval"
280-
// <optlevel> ::= "\0" | "\1" | "\2"
281-
// <flags> ::= "\u0040" | "\u0100" | "\u0140" | "\u0000"
282-
// where 0100 implies annotations, and 0040 implies barry_as_flufl
283-
static final String MIME_PREFIX = MIME_TYPE + "-";
284-
static final int OPT_FLAGS_LEN = 2; // 1 char is optlevel, 1 char is flags
285-
static final String MIME_KIND_COMPILE = "compile";
286-
static final String MIME_KIND_EVAL = "eval";
287-
// Since flags are greater than the highest unicode codepoint, we shift them into more
288-
// reasonable values in the mime type. 4 hex digits
289-
static final int MIME_FLAG_SHIFTBY = 4 * 4;
290-
// a dash follows after the opt flag pair
291-
static final int MIME_KIND_START = MIME_PREFIX.length() + OPT_FLAGS_LEN + 1;
292-
293-
private static boolean mimeTypesComplete(ArrayList<String> mimeJavaStrings) {
294-
ArrayList<String> mimeTypes = new ArrayList<>();
295-
FutureFeature[] all = FutureFeature.values();
296-
for (int flagset = 0; flagset < (1 << all.length); ++flagset) {
297-
int flags = 0;
298-
for (int i = 0; i < all.length; ++i) {
299-
if ((flagset & (1 << i)) != 0) {
300-
flags |= all[i].flagValue;
301-
}
302-
}
303-
for (int opt = 0; opt <= 2; opt++) {
304-
for (String typ : new String[]{MIME_KIND_EVAL, MIME_KIND_COMPILE}) {
305-
mimeTypes.add(MIME_PREFIX + optFlagsToMime(opt, flags) + "-" + typ);
306-
mimeJavaStrings.add(String.format("\"%s\\%d\\u%04x-%s\"", MIME_PREFIX, opt, flags >> MIME_FLAG_SHIFTBY, typ));
307-
}
308-
}
309-
}
310-
HashSet<String> currentMimeTypes = new HashSet<>(List.of(PythonLanguage.class.getAnnotation(Registration.class).characterMimeTypes()));
311-
return currentMimeTypes.containsAll(mimeTypes);
312-
}
313-
314-
static {
315-
ArrayList<String> mimeJavaStrings = new ArrayList<>();
316-
assert mimeTypesComplete(mimeJavaStrings) : "Expected all of {" + String.join(", ", mimeJavaStrings) + "} in the PythonLanguage characterMimeTypes";
317-
}
318-
319271
public static final TruffleString[] T_DEFAULT_PYTHON_EXTENSIONS = new TruffleString[]{T_PY_EXTENSION, tsLiteral(".pyc")};
320272

321273
public static final TruffleLogger LOGGER = TruffleLogger.getLogger(ID, PythonLanguage.class);
@@ -515,6 +467,11 @@ protected OptionDescriptors getOptionDescriptors() {
515467
return PythonOptions.DESCRIPTORS;
516468
}
517469

470+
@Override
471+
protected OptionDescriptors getSourceOptionDescriptors() {
472+
return PythonSourceOptions.DESCRIPTORS;
473+
}
474+
518475
@Override
519476
protected void initializeContext(PythonContext context) {
520477
if (!isLanguageInitialized) {
@@ -539,64 +496,56 @@ private synchronized void initializeLanguage() {
539496
}
540497
}
541498

542-
private static String optFlagsToMime(int optimize, int flags) {
543-
if (optimize < 0) {
544-
optimize = 0;
545-
} else if (optimize > 2) {
546-
optimize = 2;
547-
}
548-
String optField = new String(new byte[]{(byte) optimize});
549-
String flagField = new String(new int[]{(flags & FutureFeature.ALL_FLAGS) >> MIME_FLAG_SHIFTBY}, 0, 1);
550-
assert flagField.length() == 1 : "flags in mime type ended up a surrogate";
551-
return optField + flagField;
552-
}
553-
554-
public static String getCompileMimeType(int optimize, int flags) {
555-
String optFlags = optFlagsToMime(optimize, flags);
556-
return MIME_PREFIX + optFlags + "-compile";
557-
}
558-
559-
public static String getEvalMimeType(int optimize, int flags) {
560-
String optFlags = optFlagsToMime(optimize, flags);
561-
return MIME_PREFIX + optFlags + "-eval";
499+
public static SourceBuilder setPythonOptions(SourceBuilder sourceBuilder, InputType kind, int optimize, int flags) {
500+
String sourceKind = switch (kind) {
501+
case FILE -> "file";
502+
case EVAL -> "eval";
503+
case SINGLE -> "single";
504+
default -> throw CompilerDirectives.shouldNotReachHere("unsupported source kind: " + kind);
505+
};
506+
return sourceBuilder.mimeType(PythonLanguage.MIME_TYPE) //
507+
.option("python.Optimize", Integer.toString(optimize)) //
508+
.option("python.Flags", Integer.toString(flags & FutureFeature.ALL_FLAGS)) //
509+
.option("python.Kind", sourceKind);
562510
}
563511

564512
@Override
565513
protected CallTarget parse(ParsingRequest request) {
566514
PythonContext context = PythonContext.get(null);
567515
Source source = request.getSource();
568-
if (source.getMimeType() == null || MIME_TYPE.equals(source.getMimeType())) {
569-
if (!request.getArgumentNames().isEmpty() && source.isInteractive()) {
570-
throw new IllegalStateException("parse with arguments not allowed for interactive sources");
571-
}
572-
InputType inputType = source.isInteractive() ? InputType.SINGLE : InputType.FILE;
573-
return parse(context, source, inputType, true, 0, source.isInteractive(), request.getArgumentNames(), EnumSet.noneOf(FutureFeature.class));
516+
if (!request.getArgumentNames().isEmpty() && source.isInteractive()) {
517+
throw new IllegalStateException("parse with arguments not allowed for interactive sources");
574518
}
575-
if (!request.getArgumentNames().isEmpty()) {
576-
throw new IllegalStateException("parse with arguments is only allowed for " + MIME_TYPE + " mime type");
577-
}
578-
579-
String mime = source.getMimeType();
580-
String prefix = mime.substring(0, MIME_PREFIX.length());
581-
if (!prefix.equals(MIME_PREFIX)) {
582-
throw CompilerDirectives.shouldNotReachHere("unknown mime type: " + mime);
583-
}
584-
String kind = mime.substring(MIME_KIND_START);
585-
InputType type;
586-
if (kind.equals(MIME_KIND_COMPILE)) {
587-
type = InputType.FILE;
588-
} else if (kind.equals(MIME_KIND_EVAL)) {
589-
type = InputType.EVAL;
519+
InputType inputType;
520+
int optimize;
521+
EnumSet<FutureFeature> futureFeatures;
522+
boolean topLevel;
523+
List<String> argumentNames;
524+
boolean interactiveTerminal;
525+
if (source.isInteractive()) {
526+
inputType = InputType.SINGLE;
527+
optimize = 0;
528+
futureFeatures = EnumSet.noneOf(FutureFeature.class);
529+
topLevel = true;
530+
argumentNames = request.getArgumentNames();
531+
interactiveTerminal = true;
590532
} else {
591-
throw CompilerDirectives.shouldNotReachHere("unknown compilation kind: " + kind + " from mime type: " + mime);
592-
}
593-
int optimize = mime.codePointAt(MIME_PREFIX.length());
594-
int flags = mime.codePointAt(MIME_PREFIX.length() + 1) << MIME_FLAG_SHIFTBY;
595-
if (0 > optimize || optimize > 2 || (flags & ~FutureFeature.ALL_FLAGS) != 0) {
596-
throw CompilerDirectives.shouldNotReachHere("Invalid value for optlevel or flags: " + optimize + "," + flags + " from mime type: " + mime);
533+
var sourceOptions = source.getOptions(this);
534+
String kind = sourceOptions.get(PythonSourceOptions.Kind);
535+
topLevel = kind.isEmpty();
536+
inputType = switch (kind) {
537+
case "", "file" -> InputType.FILE;
538+
case "eval" -> InputType.EVAL;
539+
case "single" -> InputType.SINGLE;
540+
default -> throw CompilerDirectives.shouldNotReachHere("unknown compilation kind: " + kind);
541+
};
542+
optimize = sourceOptions.get(PythonSourceOptions.Optimize);
543+
int flags = sourceOptions.get(PythonSourceOptions.Flags);
544+
futureFeatures = FutureFeature.fromFlags(flags);
545+
argumentNames = request.getArgumentNames().isEmpty() ? null : request.getArgumentNames();
546+
interactiveTerminal = false;
597547
}
598-
assert !source.isInteractive();
599-
return parse(context, source, type, false, optimize, false, null, FutureFeature.fromFlags(flags));
548+
return parse(context, source, inputType, topLevel, optimize, interactiveTerminal, argumentNames, futureFeatures);
600549
}
601550

602551
public static RootCallTarget callTargetFromBytecode(PythonContext context, Source source, CodeUnit code) {
@@ -952,7 +901,7 @@ public static TruffleLogger getCompatibilityLogger(Class<?> clazz) {
952901
return TruffleLogger.getLogger(ID, "compatibility." + clazz.getName());
953902
}
954903

955-
public static Source newSource(PythonContext ctxt, TruffleString tsrc, TruffleString name, boolean mayBeFile, String mime) {
904+
public static Source newSource(PythonContext ctxt, TruffleString tsrc, TruffleString name, boolean mayBeFile, InputType inputType, int optimize, int flags) {
956905
try {
957906
SourceBuilder sourceBuilder = null;
958907
String src = tsrc.toJavaStringUncached();
@@ -977,9 +926,7 @@ public static Source newSource(PythonContext ctxt, TruffleString tsrc, TruffleSt
977926
if (sourceBuilder == null) {
978927
sourceBuilder = Source.newBuilder(ID, src, name.toJavaStringUncached());
979928
}
980-
if (mime != null) {
981-
sourceBuilder.mimeType(mime);
982-
}
929+
sourceBuilder = PythonLanguage.setPythonOptions(sourceBuilder, inputType, optimize, flags);
983930
return newSource(ctxt, sourceBuilder);
984931
} catch (IOException e) {
985932
throw new IllegalStateException(e);

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ Object compile(TruffleString expression, TruffleString filename, TruffleString m
10301030
}
10311031
}
10321032
if ((flags & PyCF_ONLY_AST) != 0) {
1033-
Source source = PythonLanguage.newSource(context, code, filename, mayBeFromFile, PythonLanguage.MIME_TYPE);
1033+
Source source = PythonLanguage.newSource(context, code, filename, mayBeFromFile, InputType.FILE, optimize, flags);
10341034
ParserCallbacksImpl parserCb = new ParserCallbacksImpl(source, PythonOptions.isPExceptionWithJavaStacktrace(getLanguage()));
10351035

10361036
EnumSet<AbstractParser.Flags> compilerFlags = EnumSet.noneOf(AbstractParser.Flags.class);
@@ -1054,14 +1054,10 @@ Object compile(TruffleString expression, TruffleString filename, TruffleString m
10541054
CallTarget ct;
10551055
TruffleString finalCode = code;
10561056
Supplier<CallTarget> createCode = () -> {
1057-
if (type == InputType.FILE) {
1058-
Source source = PythonLanguage.newSource(context, finalCode, filename, mayBeFromFile, PythonLanguage.getCompileMimeType(optimize, flags));
1059-
return context.getEnv().parsePublic(source);
1060-
} else if (type == InputType.EVAL) {
1061-
Source source = PythonLanguage.newSource(context, finalCode, filename, mayBeFromFile, PythonLanguage.getEvalMimeType(optimize, flags));
1057+
Source source = PythonLanguage.newSource(context, finalCode, filename, mayBeFromFile, type, optimize, flags);
1058+
if (type != InputType.SINGLE) {
10621059
return context.getEnv().parsePublic(source);
10631060
} else {
1064-
Source source = PythonLanguage.newSource(context, finalCode, filename, mayBeFromFile, PythonLanguage.MIME_TYPE);
10651061
boolean allowIncomplete = (flags & PyCF_ALLOW_INCOMPLETE_INPUT) != 0;
10661062
return context.getLanguage().parse(context, source, InputType.SINGLE, false, optimize, false, allowIncomplete, null, FutureFeature.fromFlags(flags));
10671063
}
@@ -1211,7 +1207,7 @@ private TruffleString doDecodeSource(Object source, TruffleString filename, byte
12111207
private RuntimeException raiseInvalidSyntax(TruffleString filename, String format, Object... args) {
12121208
PythonContext context = getContext();
12131209
// Create non-empty source to avoid overwriting the message with "unexpected EOF"
1214-
Source source = PythonLanguage.newSource(context, T_SPACE, filename, mayBeFromFile, null);
1210+
Source source = PythonLanguage.newSource(context, T_SPACE, filename, mayBeFromFile, InputType.FILE, 0, 0);
12151211
SourceRange sourceRange = new SourceRange(1, 0, 1, 0);
12161212
TruffleString message = toTruffleStringUncached(String.format(format, args));
12171213
throw raiseSyntaxError(ParserCallbacks.ErrorType.Syntax, sourceRange, message, source, PythonOptions.isPExceptionWithJavaStacktrace(context.getLanguage()));

0 commit comments

Comments
 (0)