Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
under the base name, and its signature as public metadata (`inspect.signature`,
`python_multiplatform.describe`; contract in `KotlinSurface.kt`'s KDoc) - and, beside each Kotlin
name, its Pythonic alias (U-12).
An `int` reaches an integral parameter only inside that Kotlin type's range, and when an overload
set still ties, the candidate whose integral parameters are each argument's Kotlin literal type
(`Int` if it fits in 32 bits, else `Long`) is called, as `kotlinc` resolves `Color(0xFFFFFFFF)` to
`Color(Long)` and `Color(0x11223344)` to `Color(Int)` (#146). `Status: implemented`,
`PM/commonTest/.../pythonx/PythonxAdapterTest.kt` `anIntArgumentPicksTheOverloadItsKotlinLiteralWould`.
`describe(module, name)` describes any bound name, a named constant (`STATIC_GETTER`) included,
without evaluating it (#36). A module's `dir()` lists its direct child packages/objects and reading
one as an attribute imports it (#35) - Kotlin names only (the U-12 aliases are served, not listed).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,43 @@ object PythonxAdapter {
_LONG_MIN = -(1 << 63)
_LONG_MAX = (1 << 63) - 1

# Issue #146: the range of each Kotlin integral type an INT slot can declare. A slot whose
# declared type is not listed (a value class over `Int`, say) is not range-checked here.
_INTEGRAL_RANGES = {
'kotlin.Byte': (-(1 << 7), (1 << 7) - 1),
'kotlin.Short': (-(1 << 15), (1 << 15) - 1),
'kotlin.Int': (_INT_MIN, _INT_MAX),
'kotlin.Long': (_LONG_MIN, _LONG_MAX),
}


def _literal_misfits(decl, args, kwargs):
'''How many int arguments land in an integral slot of another width than their Kotlin literal.

Kotlin types an integer literal as `Int` when it fits in 32 bits and as `Long` otherwise,
and resolves overloads on that type: `Color(0x11223344)` is `Color(Int)`,
`Color(0xFFFFFFFF)` is `Color(Long)`. `_Overloads` prefers the candidates with the fewest
misfits among those still tied, so a call picks the overload Kotlin would (#146).
'''
if decl.composer_index >= 0 or not decl.param_type_names:
return 0
pairs = list(enumerate(args))
for key, value in kwargs.items():
index = decl.keyword_slot(key)
if index >= 0:
pairs.append((index, value))
misfits = 0
for index, value in pairs:
if index >= len(decl.param_type_names) or isinstance(value, bool) or not isinstance(value, int):
continue
declared = decl.param_type_names[index]
if declared not in _INTEGRAL_RANGES:
continue
literal = 'kotlin.Int' if _INT_MIN <= value <= _INT_MAX else 'kotlin.Long'
if declared != literal:
misfits += 1
return misfits

# What `_coerce` puts in a scalar's slot during a *trial* bind, for `_CALLABLE_PENDING`'s
# reason: boxing crosses into Kotlin and roots the box, which a candidate that then loses
# has no business doing. The winner is re-bound strictly before the call.
Expand Down Expand Up @@ -1639,6 +1676,16 @@ object PythonxAdapter {
': it is a value class and not on the raw-primitive allowlist, so ' + repr(value) +
' would be reinterpreted rather than converted',
)
if tag == 'INT' and isinstance(value, int):
# Issue #146: an integral slot takes an int only inside its Kotlin type's range,
# as `kotlinc` would refuse the literal. Without this an `Int` slot also matched
# `0xFFFFFFFF`, and `Color(0xFFFFFFFF)` tied with `Color(Long)`.
bounds = _INTEGRAL_RANGES.get(type_name)
if bounds is not None and not (bounds[0] <= value <= bounds[1]):
return _refuse(
strict,
repr(value) + ' does not fit in ' + _simple_name(type_name),
)
return float(value) if tag == 'FLOAT' else int(value)
if tag == 'BOOLEAN':
if not isinstance(value, bool):
Expand Down Expand Up @@ -1959,6 +2006,12 @@ object PythonxAdapter {
if matched:
fewest = min(entry[2] for entry in matched)
matched = [entry for entry in matched if entry[2] == fewest]
if len(matched) > 1:
# Still tied: prefer the overload whose integral slots match each int's Kotlin
# literal type (#146). Candidates that agree on that stay tied and are refused.
scored = [(_literal_misfits(entry[0], args, kwargs), entry) for entry in matched]
least = min(score for score, _ in scored)
matched = [entry for score, entry in scored if score == least]
if len(matched) == 1:
decl = matched[0][0]
# Re-bound strictly, and the trial binding above is discarded. A trial bind is a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import python.multiplatform.reflection.TypeTag
* | `paddingFromBaseline__TextUnit` | the value-class reject list: a packed wrapper must refuse a raw number |
* | `emptyModifier` | where a chain starts. Compose has no bound declaration for this; see [EMPTY_MODIFIER] |
* | `Arrangement.Start` / `.End` | `kind = STATIC_GETTER`: a value behind a name, read as an attribute and not called |
* | `Color__Int` / `Color__Long` | one integral parameter at two widths: an int picks the overload its Kotlin literal would (#146) |
*/
object ComposeShapedFragment : FunctionTableFragment {

Expand Down Expand Up @@ -94,6 +95,32 @@ object ComposeShapedFragment : FunctionTableFragment {
paramTypeNames = listOf(MODIFIER),
returnTypeName = "kotlin.String",
) { args -> (args[0] as StubModifier).describe() },
// `Color(color: Int)` and `Color(color: Long)` (#146): the same TypeTag, two Kotlin widths. Kotlin
// types `0xFFFFFFFF` as a Long literal and `0x11223344` as an Int one, and picks accordingly.
ExposedCallable(
name = "androidx.compose.ui.graphics.Color__Int",
arity = 1,
paramTypes = listOf(TypeTag.INT),
returnType = TypeTag.STRING,
paramNames = listOf("color"),
paramTypeNames = listOf("kotlin.Int"),
returnTypeName = "kotlin.String",
) { args ->
calls += "Color__Int"
"Int:${(args[0] as Number).toLong()}"
},
ExposedCallable(
name = "androidx.compose.ui.graphics.Color__Long",
arity = 1,
paramTypes = listOf(TypeTag.INT),
returnType = TypeTag.STRING,
paramNames = listOf("color"),
paramTypeNames = listOf("kotlin.Long"),
returnTypeName = "kotlin.String",
) { args ->
calls += "Color__Long"
"Long:${(args[0] as Number).toLong()}"
},
// padding, all four overloads, with the receiver in slot 0 exactly as the walker emits it.
extension(
name = "androidx.compose.foundation.layout.padding__Dp",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,45 @@ class PythonxAdapterTest {
)
}

/**
* An int picks the overload its Kotlin literal would (#146, SPEC U-8). `Color(0xFFFFFFFF)` is the
* notebook's own spelling (pythonx-compose `UI.ipynb`); Kotlin types that literal as `Long`, since it
* does not fit in 32 bits, and `0x11223344` as `Int`. An `Int` slot never takes an int outside 32
* bits, even when named explicitly, and an int outside 64 bits fits no overload at all.
*/
@Test
fun anIntArgumentPicksTheOverloadItsKotlinLiteralWould() = withAdapter {
Python3.exec(
"""
from androidx.compose.ui.graphics import Color, Color__Int, Color__Long
_px = {
'long': Color(0xFFFFFFFF),
'int': Color(0x11223344),
'negative': Color(-1),
'wide': Color(1 << 40),
'explicitLong': Color__Long(5),
}
for _key, _call in (('tooWide', lambda: Color(1 << 70)), ('intOverflow', lambda: Color__Int(1 << 31))):
try:
_px[_key] = 'call succeeded: ' + _call()
except TypeError as e:
_px[_key] = 'TypeError'
""".trimIndent(),
)

assertEquals("Long:4294967295", eval("_px['long']"))
assertEquals("Int:287454020", eval("_px['int']"))
assertEquals("Int:-1", eval("_px['negative']"))
assertEquals("Long:1099511627776", eval("_px['wide']"))
assertEquals("Long:5", eval("_px['explicitLong']"))
assertEquals("TypeError", eval("_px['tooWide']"), "an int outside 64 bits reached an integral slot")
assertEquals("TypeError", eval("_px['intOverflow']"), "an Int slot took an int outside 32 bits")
assertEquals(
listOf("Color__Long", "Color__Int", "Color__Int", "Color__Long", "Color__Long"),
ComposeShapedFragment.calls,
)
}

/**
* What the dispatcher does when it cannot decide, and what it does when the caller has already
* decided.
Expand Down
Loading