Skip to content

Commit 00ead38

Browse files
committed
[GR-26607] Simplify custom conversions in argument clinic
PullRequest: graalpython/1320
2 parents b655a42 + 603f73d commit 00ead38

File tree

20 files changed

+665
-403
lines changed

20 files changed

+665
-403
lines changed

graalpython/com.oracle.graal.python.annotations/src/com/oracle/graal/python/annotations/ArgumentClinic.java

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,30 +58,12 @@
5858
* configuration for the conversion routine. Note that not all routines support all the
5959
* configuration options.
6060
*
61-
* Conversion routines are implemented in {@code ArgumentClinicModel#BuiltinConvertor}. It
62-
* creates Java code snippets that instantiate the actual cast nodes, which should implement
61+
* Conversion routines are implemented in {@code ConverterFactory}. It creates Java code
62+
* snippets that instantiate the actual cast nodes, which should implement
6363
* {@code ArgumentCastNode}.
6464
*/
6565
ClinicConversion conversion() default ClinicConversion.None;
6666

67-
/**
68-
* Overrides the {@link #conversion()} value. Name of a method of the builtin class that should
69-
* be used as a factory to create the conversion node. When this value is set, then other
70-
* conversion options are ignored.
71-
*/
72-
String customConversion() default "";
73-
74-
/**
75-
* The boxing optimized execute method variants will not attempt to cast the listed primitive
76-
* types and will just pass them directly to the specializations. This does not apply to
77-
* primitive values that are already boxed: those are always passed to the convertor.
78-
*
79-
* It is not necessary to set this when using a builtin conversion. Built-in convertors provide
80-
* their own list of short circuit types, which is applied if this field is set to its default
81-
* value.
82-
*/
83-
PrimitiveType[] shortCircuitPrimitive() default {};
84-
8567
/**
8668
* The string should contain valid Java constant value expression, for example, {@code true}, or
8769
* {@code \"some string\"}. You may have to update the annotation processor to include import of
@@ -95,6 +77,18 @@
9577
*/
9678
boolean useDefaultForNone() default false;
9779

80+
/**
81+
* Specifies the name of the conversion node class, which must include a static factory method
82+
* annotated with {@link ClinicConverterFactory}.
83+
*/
84+
Class<?> conversionClass() default void.class;
85+
86+
/**
87+
* Specifies arguments to the factory method. String literals must be explicitly quoted:
88+
* {@code args = "\"abc\""}
89+
*/
90+
String[] args() default {};
91+
9892
enum PrimitiveType {
9993
Boolean,
10094
Int,
@@ -104,36 +98,41 @@ enum PrimitiveType {
10498

10599
enum ClinicConversion {
106100
/**
107-
* No builtin convertor will be used.
101+
* No builtin converter will be used.
108102
*/
109103
None,
110104
/**
111-
* Corresponds to CPython's {@code bool} convertor. Supports {@link #defaultValue()}.
105+
* Corresponds to CPython's {@code bool} converter. Supports {@link #defaultValue()}.
112106
* {@code PNone.NONE} is, for now, always converted to {@code false}.
113107
*/
114108
Boolean,
115109
/**
116-
* GraalPython specific convertor that narrows any String representation to Java String.
110+
* GraalPython specific converter that narrows any String representation to Java String.
117111
* Supports {@link #defaultValue()}, and {@link #useDefaultForNone()}.
118112
*/
119113
String,
120114
/**
121-
* Corresponds to CPython's {@code int} convertor. Supports {@link #defaultValue()}, and
115+
* Corresponds to CPython's {@code int} converter. Supports {@link #defaultValue()}, and
122116
* {@link #useDefaultForNone()}.
123117
*/
124118
Int,
125119
/**
126-
* Corresponds to CPython's {@code Py_ssize_t} convertor. Supports {@link #defaultValue()},
120+
* Corresponds to CPython's {@code Py_ssize_t} converter. Supports {@link #defaultValue()},
127121
* and {@link #useDefaultForNone()}.
128122
*/
129123
Index,
130124
/**
131-
* Corresponds to CPython's {@code int(accept={str})} convertor. Supports
125+
* Corresponds to CPython's {@code slice_index} converter. Supports {@link #defaultValue()},
126+
* and {@link #useDefaultForNone()}.
127+
*/
128+
SliceIndex,
129+
/**
130+
* Corresponds to CPython's {@code int(accept={str})} converter. Supports
132131
* {@link #defaultValue()}, and {@link #useDefaultForNone()}.
133132
*/
134133
CodePoint,
135134
/**
136-
* Corresponds to CPython's {@code Py_buffer} convertor.
135+
* Corresponds to CPython's {@code Py_buffer} converter.
137136
*/
138137
Buffer,
139138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2020, 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.annotations;
42+
43+
import java.lang.annotation.ElementType;
44+
import java.lang.annotation.Target;
45+
46+
/**
47+
* Annotates the factory method (which must be static) in the class specified by
48+
* {@link ArgumentClinic#conversionClass()}.
49+
*/
50+
@Target(ElementType.METHOD)
51+
public @interface ClinicConverterFactory {
52+
53+
/**
54+
* The boxing optimized execute method variants will not attempt to cast the listed primitive
55+
* types and will just pass them directly to the specializations. This does not apply to
56+
* primitive values that are already boxed: those are always passed to the converter.
57+
*/
58+
ArgumentClinic.PrimitiveType[] shortCircuitPrimitive() default {};
59+
60+
/**
61+
* Annotates parameter of the factory method which will receive the default value
62+
* {@link ArgumentClinic#defaultValue()}.
63+
*/
64+
@Target(ElementType.PARAMETER)
65+
@interface DefaultValue {
66+
}
67+
68+
/**
69+
* Annotates parameter of the factory method which will receive the value of
70+
* {@link ArgumentClinic#useDefaultForNone()}.
71+
*/
72+
@Target(ElementType.PARAMETER)
73+
@interface UseDefaultForNone {
74+
}
75+
76+
/**
77+
* Annotates parameter of the factory method which will receive the name of the builtin
78+
* function.
79+
*/
80+
@Target(ElementType.PARAMETER)
81+
@interface BuiltinName {
82+
}
83+
84+
/**
85+
* Annotates parameter of the factory method which will receive the index of the argument of the
86+
* builtin functions.
87+
*/
88+
@Target(ElementType.PARAMETER)
89+
@interface ArgumentIndex {
90+
}
91+
92+
/**
93+
* Annotates parameter of the factory method which will receive the name of the argument of the
94+
* builtin functions.
95+
*/
96+
@Target(ElementType.PARAMETER)
97+
@interface ArgumentName {
98+
}
99+
}

graalpython/com.oracle.graal.python.processor/src/com/oracle/graal/python/processor/ArgumentClinicModel.java

Lines changed: 54 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@
4040
*/
4141
package com.oracle.graal.python.processor;
4242

43-
import static java.lang.String.format;
44-
4543
import java.util.Arrays;
44+
import java.util.Collections;
4645
import java.util.HashSet;
4746
import java.util.List;
4847
import java.util.Set;
@@ -55,93 +54,12 @@
5554
import com.oracle.graal.python.annotations.ArgumentClinic.PrimitiveType;
5655

5756
public class ArgumentClinicModel {
58-
static final class BuiltinConvertor {
59-
public static final String CLINIC_PACKAGE = "com.oracle.graal.python.nodes.function.builtins.clinic";
60-
61-
public static String getCodeSnippet(ArgumentClinic annotation, BuiltinAnnotation builtin) {
62-
switch (annotation.conversion()) {
63-
case Boolean:
64-
return format("JavaBooleanConvertorNodeGen.create(%s)", annotation.defaultValue());
65-
case String:
66-
String defaultVal = annotation.defaultValue();
67-
if (defaultVal.isEmpty()) {
68-
return format("JavaStringConvertorNodeGen.create(\"%s\")", builtin.name);
69-
} else {
70-
return format("JavaStringConvertorWithDefaultValueNodeGen.create(\"%s\", %s, %s)", builtin.name, defaultVal, annotation.useDefaultForNone());
71-
}
72-
case Int:
73-
return format("JavaIntConversionNodeGen.create(%s, %s)", annotation.defaultValue(), annotation.useDefaultForNone());
74-
case CodePoint:
75-
return format("CodePointConversionNodeGen.create(\"%s\", %s, %s)", builtin.name, annotation.defaultValue(), annotation.useDefaultForNone());
76-
case Buffer:
77-
return "BufferConversionNodeGen.create()";
78-
case Index:
79-
return format("IndexConversionNodeGen.create(%s, %s)", annotation.defaultValue(), annotation.useDefaultForNone());
80-
case None:
81-
return format("DefaultValueNode.create(%s, %s)", annotation.defaultValue(), annotation.useDefaultForNone());
82-
default:
83-
throw new IllegalArgumentException(annotation.conversion().toString());
84-
}
85-
}
86-
87-
public static void addImports(ArgumentClinic annotation, Set<String> imports) {
88-
// We may add imports for some other prominent constants
89-
// Another possibility is to introduce something like ArgumentClinicImportStatic
90-
// annotation, or piggy-back on Truffle DSL's ImportStatic annotation, but then we can
91-
// also use fully qualified names in such rare cases
92-
if (annotation.defaultValue().startsWith("PNone.")) {
93-
imports.add("com.oracle.graal.python.builtins.objects.PNone");
94-
}
95-
if (annotation.conversion() != ClinicConversion.None || (annotation.customConversion().isEmpty() && !annotation.defaultValue().isEmpty())) {
96-
imports.add(CLINIC_PACKAGE + '.' + getConvertorImport(annotation));
97-
}
98-
}
99-
100-
private static String getConvertorImport(ArgumentClinic annotation) {
101-
switch (annotation.conversion()) {
102-
case Boolean:
103-
return "JavaBooleanConvertorNodeGen";
104-
case String:
105-
return annotation.defaultValue().isEmpty() ? "JavaStringConvertorNodeGen" : "JavaStringConvertorWithDefaultValueNodeGen";
106-
case Int:
107-
return "JavaIntConversionNodeGen";
108-
case Index:
109-
return "IndexConversionNodeGen";
110-
case CodePoint:
111-
return "CodePointConversionNodeGen";
112-
case Buffer:
113-
return "BufferConversionNodeGen";
114-
case None:
115-
return "DefaultValueNode";
116-
default:
117-
throw new IllegalArgumentException(annotation.conversion().toString());
118-
}
119-
}
120-
121-
public static PrimitiveType[] getAcceptedPrimitiveTypes(ArgumentClinic annotation) {
122-
switch (annotation.conversion()) {
123-
case Boolean:
124-
return new PrimitiveType[]{PrimitiveType.Boolean};
125-
case String:
126-
case CodePoint:
127-
case Buffer:
128-
return new PrimitiveType[0];
129-
case Int:
130-
case Index:
131-
return new PrimitiveType[]{PrimitiveType.Int};
132-
case None:
133-
return new PrimitiveType[]{PrimitiveType.Boolean, PrimitiveType.Int, PrimitiveType.Long, PrimitiveType.Double};
134-
default:
135-
throw new IllegalArgumentException(annotation.conversion().toString());
136-
}
137-
}
138-
}
13957

14058
/**
14159
* Mirrors the data of the {@code Builtin} annotation, which cannot be in the "annotations"
14260
* project because of its dependence on other GraalPython runtime classes.
14361
*/
144-
static final class BuiltinAnnotation {
62+
public static final class BuiltinAnnotation {
14563
public final String name;
14664
public final String[] argumentNames;
14765

@@ -188,38 +106,73 @@ static final class ArgumentClinicData {
188106
public final int index;
189107
public final Set<PrimitiveType> acceptedPrimitiveTypes;
190108
public final String castNodeFactory;
109+
public final Set<String> imports;
191110

192-
public ArgumentClinicData(ArgumentClinic annotation, int index, Set<PrimitiveType> acceptedPrimitiveTypes, String castNodeFactory) {
111+
private ArgumentClinicData(ArgumentClinic annotation, int index, Set<PrimitiveType> acceptedPrimitiveTypes, String castNodeFactory, Set<String> imports) {
193112
this.annotation = annotation;
194113
this.index = index;
195114
this.acceptedPrimitiveTypes = acceptedPrimitiveTypes;
196115
this.castNodeFactory = castNodeFactory;
116+
this.imports = imports;
197117
}
198118

199-
public boolean isClinicArgument() {
200-
return annotation != null;
119+
private static ConverterFactory getFactory(ArgumentClinic annotation, TypeElement type, ConverterFactory factory) throws ProcessingError {
120+
if (factory == null && annotation.args().length != 0) {
121+
throw new ProcessingError(type, "No conversionClass specified but arguments were provided");
122+
}
123+
if (factory != null) {
124+
return factory;
125+
}
126+
if (annotation.conversion() == ClinicConversion.None && annotation.defaultValue().isEmpty()) {
127+
throw new ProcessingError(type, "ArgumentClinic annotation must declare either builtin conversion or custom conversion.");
128+
}
129+
return ConverterFactory.getBuiltin(annotation);
201130
}
202131

203-
public static ArgumentClinicData create(ArgumentClinic annotation, TypeElement type, BuiltinAnnotation builtinAnnotation, int index) throws ProcessingError {
132+
public static ArgumentClinicData create(ArgumentClinic annotation, TypeElement type, BuiltinAnnotation builtinAnnotation, int index, ConverterFactory annotationFactory)
133+
throws ProcessingError {
204134
if (annotation == null) {
205-
return new ArgumentClinicData(null, index, new HashSet<>(Arrays.asList(PrimitiveType.values())), null);
135+
return new ArgumentClinicData(null, index, new HashSet<>(Arrays.asList(PrimitiveType.values())), null, Collections.emptySet());
136+
}
137+
ConverterFactory factory = getFactory(annotation, type, annotationFactory);
138+
if (annotation.args().length != factory.extraParamCount) {
139+
throw new ProcessingError(type, "Conversion %s.%s expects %d arguments", factory.fullClassName, factory.methodName, factory.extraParamCount);
206140
}
207141

208-
PrimitiveType[] acceptedPrimitives = new PrimitiveType[0];
209-
String castNodeFactory;
210-
if (annotation.customConversion().isEmpty()) {
211-
if (annotation.conversion() == ClinicConversion.None && annotation.defaultValue().isEmpty()) {
212-
throw new ProcessingError(type, "ArgumentClinic annotation must declare either builtin conversion or custom conversion.");
142+
String[] args = new String[factory.params.length];
143+
int extraParamIndex = 0;
144+
for (int i = 0; i < args.length; ++i) {
145+
switch (factory.params[i]) {
146+
case BuiltinName:
147+
args[i] = String.format("\"%s\"", builtinAnnotation.name);
148+
break;
149+
case ArgumentIndex:
150+
args[i] = String.valueOf(index);
151+
break;
152+
case ArgumentName:
153+
args[i] = String.format("\"%s\"", builtinAnnotation.argumentNames[index]);
154+
break;
155+
case DefaultValue:
156+
args[i] = annotation.defaultValue();
157+
break;
158+
case UseDefaultForNone:
159+
args[i] = String.valueOf(annotation.useDefaultForNone());
160+
break;
161+
case Extra:
162+
args[i] = annotation.args()[extraParamIndex++];
163+
break;
164+
default:
165+
throw new IllegalStateException("Unsupported ClinicArgument: " + factory.params[i]);
213166
}
214-
castNodeFactory = BuiltinConvertor.getCodeSnippet(annotation, builtinAnnotation);
215-
acceptedPrimitives = BuiltinConvertor.getAcceptedPrimitiveTypes(annotation);
216-
} else {
217-
castNodeFactory = type.getQualifiedName().toString() + '.' + annotation.customConversion() + "()";
218167
}
219-
if (annotation.shortCircuitPrimitive().length > 0) {
220-
acceptedPrimitives = annotation.shortCircuitPrimitive();
168+
String castNodeFactory = String.format("%s.%s(%s)", factory.className, factory.methodName, String.join(", ", args));
169+
Set<String> imports = new HashSet<>();
170+
imports.add(factory.fullClassName);
171+
if (annotation.defaultValue().startsWith("PNone.")) {
172+
imports.add("com.oracle.graal.python.builtins.objects.PNone");
221173
}
222-
return new ArgumentClinicData(annotation, index, new HashSet<>(Arrays.asList(acceptedPrimitives)), castNodeFactory);
174+
175+
return new ArgumentClinicData(annotation, index, new HashSet<>(Arrays.asList(factory.acceptedPrimitiveTypes)), castNodeFactory, imports);
223176
}
224177
}
225178
}

0 commit comments

Comments
 (0)