File: test_specialization.py

package info (click to toggle)
nanobind 2.11.0-3
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 3,300 kB
  • sloc: cpp: 12,232; python: 6,315; ansic: 4,813; makefile: 22; sh: 15
file content (103 lines) | stat: -rw-r--r-- 3,030 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import sys
import sysconfig
import dis
import pytest

# Note: these tests verify that CPython's adaptive specializing interpreter can
# optimize various expressions involving nanobind types. They are expected to
# be somewhat fragile across Python versions as the bytecode and specialization
# opcodes may change.

# Skip tests on PyPy and free-threaded Python
skip_tests = sys.implementation.name == "pypy" or \
    sysconfig.get_config_var("Py_GIL_DISABLED")

import test_classes_ext as t
def disasm(func):
    """Extract specialized opcode names from a function"""
    instructions = list(dis.get_instructions(func, adaptive=True))
    return [(instr.opname, instr.argval) for instr in instructions]

def warmup(fn):
    # Call the function a few times to ensure that it is specialized
    for _ in range(8):
        fn()

def count_op(ops, expected):
    hits = 0
    for opname, _ in ops:
        if opname == expected:
            hits += 1
    return hits

@pytest.mark.skipif(
    sys.version_info < (3, 14) or skip_tests,
    reason="Static attribute specialization requires CPython 3.14+")
def test_static_attribute_specialization():
    s = t.Struct
    def fn():
        return s.static_test

    ops = disasm(fn)
    print(ops)
    op_base = count_op(ops, "LOAD_ATTR")
    op_opt = (
        count_op(ops, "LOAD_ATTR_ADAPTIVE") +
        count_op(ops, "LOAD_ATTR_CLASS"))
    assert op_base == 1 and op_opt == 0

    warmup(fn)
    ops = disasm(fn)
    print(ops)

    op_base = count_op(ops, "LOAD_ATTR")
    op_opt = (
        count_op(ops, "LOAD_ATTR_ADAPTIVE") +
        count_op(ops, "LOAD_ATTR_CLASS"))
    assert op_base == 0 and op_opt == 1

@pytest.mark.skipif(
    sys.version_info < (3, 11) or skip_tests,
    reason="Method call specialization requires CPython 3.14+")
def test_method_call_specialization():
    s = t.Struct()
    def fn():
        return s.value()

    ops = disasm(fn)
    op_base = (
        count_op(ops, "LOAD_METHOD") +
        count_op(ops, "LOAD_ATTR"))
    op_opt = (
        count_op(ops, "LOAD_ATTR_METHOD_NO_DICT") +
        count_op(ops, "CALL_ADAPTIVE"))
    print(ops)
    assert op_base == 1 and op_opt == 0

    warmup(fn)
    ops = disasm(fn)
    print(ops)
    op_base = (
        count_op(ops, "LOAD_METHOD") +
        count_op(ops, "LOAD_ATTR"))
    op_opt = (
        count_op(ops, "LOAD_ATTR_METHOD_NO_DICT") +
        count_op(ops, "CALL_ADAPTIVE"))
    assert op_base == 0 and op_opt == 1


@pytest.mark.skipif(sys.version_info < (3, 11) or skip_tests,
    reason="Immutability requires Python 3.11+")
def test_immutability():
    # Test nb_method immutability
    method = t.Struct.value
    method_type = type(method)
    assert method_type.__name__ == "nb_method"
    with pytest.raises(TypeError, match="immutable"):
        method_type.test_attr = 123

    # Test metaclass immutability
    metaclass = type(t.Struct)
    assert metaclass.__name__.startswith("nb_type")
    with pytest.raises(TypeError, match="immutable"):
        metaclass.test_attr = 123