Skip to content

Commit 72c2401

Browse files
committed
Recognise iterable and sequence type
Make sequence operation on PyUnicode work (some of them) by introducing quick tests for the presence of the relevant special methods.
1 parent f813238 commit 72c2401

File tree

16 files changed

+557
-271
lines changed

16 files changed

+557
-271
lines changed

rt4core/src/kernelTest/java/uk/co/farowl/vsj4/runtime/kernel/TypeFactoryTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import java.lang.invoke.MethodHandles;
1111
import java.lang.invoke.MethodHandles.Lookup;
12+
import java.util.List;
1213
import java.util.Map;
1314
import java.util.function.Function;
1415

@@ -61,6 +62,11 @@ public void exposeMethods(Class<?> definingClass) {}
6162
@Override
6263
public void populate(Map<? super String, Object> dict,
6364
Lookup lookup) {}
65+
66+
@Override
67+
public Iterable<Entry> entries(Lookup lookup) {
68+
return List.of();
69+
}
6470
};
6571

6672
/**

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/Abstract.java

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c)2024 Jython Developers.
1+
// Copyright (c)2025 Jython Developers.
22
// Licensed to PSF under a contributor agreement.
33
package uk.co.farowl.vsj4.runtime;
44

@@ -135,10 +135,9 @@ public static <T> T tojava(Object o, Class<T> c) {
135135
* @throws Throwable on errors within {@code __hash__}
136136
*/
137137
public static int hash(Object v) throws PyBaseException, Throwable {
138+
Representation rep = PyType.getRepresentation(v);
138139
try {
139-
Representation rep = PyType.getRepresentation(v);
140-
MethodHandle mh = rep.op_hash();
141-
return (int)mh.invokeExact(v);
140+
return (int)rep.op_hash().invokeExact(v);
142141
} catch (EmptyException e) {
143142
throw typeError("unhashable type: %s", v);
144143
}
@@ -365,8 +364,8 @@ public static Object lookupAttr(Object o, Object name)
365364
public static void setAttr(Object o, String name, Object value)
366365
throws PyAttributeError, Throwable {
367366
// Decisions are based on type of o (that of name is known)
367+
Representation rep = PyType.getRepresentation(o);
368368
try {
369-
Representation rep = PyType.getRepresentation(o);
370369
rep.op_setattr().invokeExact(o, name, value);
371370
} catch (EmptyException e) {
372371
throw attributeAccessError(o, name,
@@ -408,8 +407,8 @@ public static void setAttr(Object o, Object name, Object value)
408407
public static void delAttr(Object o, String name)
409408
throws PyAttributeError, Throwable {
410409
// Decisions are based on type of o (that of name is known)
410+
Representation rep = PyType.getRepresentation(o);
411411
try {
412-
Representation rep = PyType.getRepresentation(o);
413412
rep.op_delattr().invokeExact(o, name);
414413
} catch (EmptyException e) {
415414
throw attributeAccessError(o, name,
@@ -731,9 +730,9 @@ private static Object lookupSpecial(Object self, String name)
731730
return null;
732731
} else {
733732
// res might be a descriptor
733+
Representation rep = PyType.getRepresentation(res);
734734
try {
735735
// invoke the descriptor's __get__
736-
Representation rep = PyType.getRepresentation(res);
737736
MethodHandle f = rep.op_get();
738737
res = f.invokeExact(res, self, selfType);
739738
} catch (EmptyException e) {}
@@ -777,20 +776,26 @@ static Object getIterator(Object o)
777776
// Compare CPython PyObject_GetIter in abstract.c
778777
static <E extends PyBaseException> Object getIterator(Object o,
779778
Supplier<E> exc) throws PyBaseException, Throwable {
780-
Representation rep = PyType.getRepresentation(o);
781-
if (SpecialMethod.op_iter.isDefinedFor(rep)) {
782-
// o defines __iter__, call it.
783-
Object r = rep.op_iter().invokeExact(o);
784-
// Did that return an iterator? Check r defines __next__.
785-
if (SpecialMethod.op_next
786-
.isDefinedFor(PyType.getRepresentation(r))) {
787-
return r;
779+
780+
Representation orep = PyType.getRepresentation(o);
781+
PyType otype = orep.pythonType(o);
782+
783+
try {
784+
// Call o.__iter__, which may be empty.
785+
Object i = orep.op_iter().invokeExact(o);
786+
// Did that return an iterator? Check i defines __next__.
787+
Representation irep = PyType.getRepresentation(i);
788+
if (irep.pythonType(i).isIterator()) {
789+
return i;
788790
} else if (exc == null) {
789-
throw returnTypeError("iter", "iterator", r);
791+
throw returnTypeError("iter", "iterator", i);
792+
}
793+
} catch (EmptyException e) {
794+
// otype does not define __iter__: try __getitem__
795+
if (otype.isSequence()) {
796+
// o defines __getitem__: make a (Python) iterator.
797+
return new PyIterator(o);
790798
}
791-
} else if (SpecialMethod.op_getitem.isDefinedFor(rep)) {
792-
// o defines __getitem__: make a (Python) iterator.
793-
return new PyIterator(o);
794799
}
795800

796801
// Out of possibilities: throw caller-defined exception
@@ -809,8 +814,7 @@ static <E extends PyBaseException> Object getIterator(Object o,
809814
* @return true if {@code o} supports the iterator protocol
810815
*/
811816
static boolean iterableCheck(Object o) {
812-
return SpecialMethod.op_iter
813-
.isDefinedFor(PyType.getRepresentation(o));
817+
return PyType.of(o).isIterable();
814818
}
815819

816820
/**
@@ -822,8 +826,7 @@ static boolean iterableCheck(Object o) {
822826
*/
823827
// Compare CPython PyIter_Check in abstract.c
824828
static boolean iteratorCheck(Object o) {
825-
return SpecialMethod.op_next
826-
.isDefinedFor(PyType.getRepresentation(o));
829+
return PyType.of(o).isIterator();
827830
}
828831

829832
/**
@@ -855,15 +858,14 @@ static Object next(Object iter) throws Throwable {
855858
*
856859
* @param o object accessed
857860
* @param name of attribute
858-
* @param slot operation
861+
* @param sm operation
859862
* @return an error to throw
860863
*/
861864
private static PyBaseException attributeAccessError(Object o,
862-
String name, SpecialMethod slot) {
863-
String mode, kind,
864-
fmt = "'%.100s' object has %s attributes (%s.%.50s)";
865+
String name, SpecialMethod sm) {
866+
String mode, kind;
865867
// What were we trying to do?
866-
switch (slot) {
868+
switch (sm) {
867869
case op_delattr:
868870
mode = "delete ";
869871
break;
@@ -874,16 +876,17 @@ private static PyBaseException attributeAccessError(Object o,
874876
mode = "";
875877
break;
876878
}
877-
// Can we even read this object's attributes?
879+
// Can we even read the attribute?
878880
Representation rep = PyType.getRepresentation(o);
879881
String typeName = rep.pythonType(o).getName();
880882
boolean readable =
881883
SpecialMethod.op_getattribute.isDefinedFor(rep)
882884
|| SpecialMethod.op_getattr.isDefinedFor(rep);
883885
kind = readable ? "only read-only" : "no";
884886
// Now we know what to say
885-
return PyErr.format(PyExc.TypeError, fmt, typeName, kind, mode,
886-
name);
887+
return PyErr.format(PyExc.TypeError,
888+
"'%.100s' object has %s attributes (%s '%.50s')",
889+
typeName, kind, mode, name);
887890
}
888891

889892
// Convenience functions constructing errors --------------------

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/Feature.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
// Copyright (c)2025 Jython Developers.
2+
// Licensed to PSF under a contributor agreement.
13
package uk.co.farowl.vsj4.runtime;
24

3-
import uk.co.farowl.vsj4.runtime.kernel.TypeFlag;
4-
55
/**
66
* Enumeration of the features of a type that may be specified in a
77
* {@link TypeSpec}.
@@ -46,26 +46,21 @@ public enum Feature {
4646

4747
/**
4848
* Instances of the type object are treated as sequences for pattern
49-
* matching.
49+
* matching. This does not affect whether Python treats instances as
50+
* a sequence functionally, which depends instead on whether a type
51+
* defines the required special methods.
5052
*/
5153
// Compare CPython Py_TPFLAGS_SEQUENCE
5254
SEQUENCE(TypeFlag.SEQUENCE),
5355
/**
5456
* Instances of the type object are treated as mappings for pattern
55-
* matching
57+
* matching. This does not affect whether Python treats instances as
58+
* a mapping functionally, which depends instead on whether a type
59+
* defines the required special methods.
5660
*/
5761
// Compare CPython Py_TPFLAGS_MAPPING
5862
MAPPING(TypeFlag.MAPPING),
5963

60-
/**
61-
* This flag is used to give certain built-ins a pattern-matching
62-
* behaviour that allows a single positional sub-pattern to match
63-
* against the subject itself (rather than a mapped attribute on
64-
* it).
65-
*/
66-
// Compare CPython _Py_TPFLAGS_MATCH_SELF
67-
MATCH_SELF(TypeFlag.MATCH_SELF),
68-
6964
/**
7065
* An instance of this type is a method descriptor, that is, it
7166
* supports an optimised call pattern where a {@code self} argument
@@ -85,7 +80,7 @@ public enum Feature {
8580
* equivalent to {@code func(*args, **kwds)}.</li>
8681
* </ul>
8782
*/
88-
METHOD_DESCR(TypeFlag.IS_METHOD_DESCR);
83+
METHOD_DESCR(TypeFlag.METHOD_DESCR);
8984

9085
/** Navigate from feature to corresponding type flag. */
9186
public final TypeFlag flag;

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/PyNameError.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class PyNameError extends PyBaseException {
1717
.add(Feature.REPLACEABLE, Feature.IMMUTABLE)
1818
.doc("Name not found globally."));
1919

20-
/** {@code UnboundLocalError} extends {@link NameError}. */
20+
/** {@code UnboundLocalError} extends {@code NameError}. */
2121
protected static PyType UnboundLocalError =
2222
extendsException(TYPE, "UnboundLocalError",
2323
"Local name referenced but not bound to a value.");

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/PyNumber.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c)2024 Jython Developers.
1+
// Copyright (c)2025 Jython Developers.
22
// Licensed to PSF under a contributor agreement.
33
package uk.co.farowl.vsj4.runtime;
44

@@ -235,20 +235,22 @@ protected static boolean indexCheck(Object obj) {
235235
}
236236

237237
/**
238-
* Return a Python {@code int} (or subclass) from the object
239-
* {@code o}. Raise {@code TypeError} if the result is not a Python
240-
* {@code int} subclass, or if the object {@code o} cannot be
241-
* interpreted as an index (it does not fill
242-
* {@link SpecialMethod#op_index}). This method makes no guarantee
243-
* about the <i>range</i> of the result.
238+
* Interpret the argument {@code o} as an integer, returning a
239+
* Python {@code int} (or subclass), by means of a call to
240+
* the lossless conversion method
241+
* {@code __index__}. Raise {@code TypeError} if the result is not a
242+
* Python {@code int} subclass, or if the object {@code o} cannot be
243+
* interpreted as an index (it does not define {@code __index__}.
244+
* This method makes no guarantee about the <i>range</i> of the
245+
* result.
244246
*
245247
* @param o operand
246248
* @return {@code o} coerced to a Python {@code int}
247249
* @throws PyBaseException (TypeError) if {@code o} cannot be
248250
* interpreted as an {@code int}
249251
* @throws Throwable otherwise from invoked implementations
250252
*/
251-
// Compare with CPython abstract.c :: PyNumber_Index
253+
// Compare with CPython abstract.c :: _PyNumber_Index
252254
static Object index(Object o) throws PyBaseException, Throwable {
253255

254256
Representation rep = PyType.getRepresentation(o);

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/PySequence.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
import uk.co.farowl.vsj4.runtime.PySlice.Indices;
1717
import uk.co.farowl.vsj4.runtime.PyUtil.NoConversion;
18+
import uk.co.farowl.vsj4.runtime.kernel.KernelTypeFlag;
1819
import uk.co.farowl.vsj4.runtime.kernel.Representation;
19-
import uk.co.farowl.vsj4.runtime.kernel.SpecialMethod;
2020
import uk.co.farowl.vsj4.support.internal.EmptyException;
2121

2222
/**
@@ -261,15 +261,17 @@ public static PyList list(Object o)
261261
fastNewList(Object o, Supplier<E> exc) throws E, Throwable {
262262
List<Object> list = new ArrayList<>();
263263
Representation rep = PyType.getRepresentation(o);
264+
PyType type = rep.pythonType(o);
264265

265-
if (SpecialMethod.op_iter.isDefinedFor(rep)) {
266+
if (type.hasFeature(KernelTypeFlag.HAS_ITER)) {
266267
// Go via the iterator on o
267268
Object iter = rep.op_iter().invokeExact(o);
268269
// Check iter is an iterator (defines __next__).
269-
Representation iterOps = PyType.getRepresentation(iter);
270-
if (SpecialMethod.op_next.isDefinedFor(iterOps)) {
270+
Representation iterRep = PyType.getRepresentation(iter);
271+
PyType iterType = iterRep.pythonType(iter);
272+
if (iterType.hasFeature(KernelTypeFlag.HAS_NEXT)) {
271273
// Create a handle on __next__
272-
MethodHandle next = iterOps.op_next().bindTo(iter);
274+
MethodHandle next = iterRep.op_next().bindTo(iter);
273275
// Iterate o into a list
274276
try {
275277
for (;;) { list.add(next.invokeExact()); }
@@ -279,7 +281,7 @@ public static PyList list(Object o)
279281
return list;
280282
} // else fall out at throw exc
281283

282-
} else if (SpecialMethod.op_getitem.isDefinedFor(rep)) {
284+
} else if ((type.hasFeature(KernelTypeFlag.HAS_GETITEM))) {
283285
// o defines __getitem__
284286
MethodHandle getitem = rep.op_getitem().bindTo(o);
285287
try {

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/TypeExposerImplementation.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.lang.invoke.MethodType;
99
import java.lang.invoke.WrongMethodTypeException;
1010
import java.lang.reflect.Method;
11+
import java.util.Iterator;
1112
import java.util.LinkedList;
1213
import java.util.List;
1314
import java.util.Map;
@@ -96,6 +97,47 @@ public void populate(Map<? super String, Object> dict,
9697
}
9798
}
9899

100+
101+
@Override
102+
public Iterable<Entry> entries(Lookup lookup) {
103+
if (type == null)
104+
// type may only properly be null during certain tests
105+
throw new InterpreterError(
106+
"Cannot generate entries for type 'null'");
107+
logger.atDebug().addArgument(type.getName())
108+
.log("Populating type '{}'");
109+
110+
// The returned object will stream name-attr pairs
111+
return new Iterable<Entry>() {
112+
113+
@Override
114+
public Iterator<Entry> iterator() {
115+
return new Iterator<Entry>() {
116+
// Entries derive from the specs table
117+
Iterator<Spec> specIter = specs.values().iterator();
118+
119+
@Override
120+
public boolean hasNext() {
121+
// ... if there are specs left to process
122+
return specIter.hasNext();
123+
}
124+
125+
@Override
126+
public Entry next() {
127+
Spec spec = specIter.next();
128+
logger.atTrace().addArgument(type.getName())
129+
.addArgument(spec.name)
130+
.log("- Add {}.{}");
131+
spec.checkFormation();
132+
// Create attribute according to spec type
133+
Object attr = spec.asAttribute(type, lookup);
134+
return new Entry(spec.name, attr);
135+
}
136+
};
137+
}
138+
};
139+
}
140+
99141
/**
100142
* Add to {@link #specs}, definitions based on methods found in the
101143
* given class and either annotated for exposure or having the name

0 commit comments

Comments
 (0)