-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_scalar_module_variable_lowering.py
More file actions
357 lines (304 loc) · 14 KB
/
Copy pathtest_scalar_module_variable_lowering.py
File metadata and controls
357 lines (304 loc) · 14 KB
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
"""Scalar module-variable planning and lowering tests."""
from __future__ import annotations
from dataclasses import replace
from unittest.mock import Mock
import pytest
from tests.fortran._support.ownership_policy import parse_pyi_text
from prik.parsers.fortran.parser import parse_fortran_project
from prik.pipeline.build import _apply_source_python_exports, _merge_wrapper_modules
from prik.semantics.fortran2ir import fortran_project_to_semantic_modules
from prik.policy.ownership import AssignmentMode, SetterAction
from prik.policy.completion import complete_semantic_policies
from prik.policy.models import ModuleGetterAction
from prik.pipeline.wrapper import WrapperGenerator
from prik.planning import WrapperPlanner
from prik.codegen.c.binding import CBindingGenerator
from prik.codegen.fortran.bridge import FortranBridgeGenerator
SCALAR_MODULE_CONTRACT = """
limit: Final[Int32] = 12
counter: Int32 = 3
target_scale: Annotated[Float64, Aliased]
optional_scale: Allocatable[Float64]
selected_scale: Pointer[Float64]
def summarize() -> Int32: ...
"""
def _plan():
module = parse_pyi_text(SCALAR_MODULE_CONTRACT, module_name="scalar_state")
complete_semantic_policies(module)
return WrapperPlanner().build(module)
def _computed_constant_plan():
parsed = parse_fortran_project(
{
"computed_constants.f90": """
module computed_constants
integer, parameter :: computed = kind(1.0) * 2
character*1, parameter :: prefix = 'D'
end module computed_constants
"""
}
)
modules = fortran_project_to_semantic_modules(parsed)
_apply_source_python_exports(modules)
module = _merge_wrapper_modules(modules, name="computed_constants_wrapper")
complete_semantic_policies(module)
return WrapperPlanner().build(module)
def _parameter_array_plan():
parsed = parse_fortran_project(
{
"parameter_array.f90": """
module parameter_array
use iso_fortran_env, only: real64
real(real64), parameter :: dpmpar(3) = [epsilon(1.0_real64), tiny(1.0_real64), huge(1.0_real64)]
end module parameter_array
"""
}
)
modules = fortran_project_to_semantic_modules(parsed)
_apply_source_python_exports(modules)
module = _merge_wrapper_modules(modules, name="parameter_array_wrapper")
complete_semantic_policies(module)
return WrapperPlanner().build(module)
def _source(artifacts, suffix: str) -> str:
return next(item.text for item in artifacts.sources if item.path.name.endswith(suffix))
def _replace_variable(plan, python_name: str, edit):
root = plan.namespaces[0]
variables = tuple(
edit(variable) if variable.binding.python_names == (python_name,) else variable for variable in root.variables
)
return replace(plan, namespaces=(replace(root, variables=variables), *plan.namespaces[1:]))
def test_module_variable_plan_contains_only_completed_dispatch_facts():
plan = _plan()
variables = {variable.binding.python_names[0]: variable for variable in plan.namespaces[0].variables}
assert variables["limit"].binding.getter_action is ModuleGetterAction.CONSTANT_VALUE
assert variables["limit"].binding.setter_action is SetterAction.OMIT
assert variables["limit"].bridge.native_assignment is AssignmentMode.NONE
assert variables["limit"].binding.constant_value == 12
assert variables["counter"].binding.getter_action is ModuleGetterAction.DIRECT_VALUE
assert variables["counter"].binding.setter_action is SetterAction.WRITE_THROUGH
assert variables["counter"].bridge.native_assignment is AssignmentMode.VALUE_COPY
assert variables["counter"].binding.initializer == 3
assert variables["target_scale"].bridge.native_assignment is AssignmentMode.VALUE_COPY
assert variables["optional_scale"].binding.getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT
assert variables["optional_scale"].entrypoint.descriptor_kind == "allocatable"
assert variables["optional_scale"].binding.setter_action is SetterAction.REJECT_REPLACEMENT
assert variables["optional_scale"].bridge.native_assignment is AssignmentMode.NONE
assert variables["selected_scale"].entrypoint.descriptor_kind == "pointer"
assert variables["selected_scale"].bridge.native_assignment is AssignmentMode.NONE
def test_symbolic_source_parameter_reuses_scalar_bridge_getter_for_module_initialization():
plan = _computed_constant_plan()
variables = {variable.binding.python_names[0]: variable for variable in plan.namespaces[1].variables}
computed = variables["computed"]
assert computed.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_VALUE
assert computed.binding.constant_value is None
assert computed.entrypoint.getter_role == "computed_constants.computed:getter"
assert computed.binding.setter_action is SetterAction.OMIT
artifacts = WrapperGenerator().generate(plan)
c_source = _source(artifacts, ".c")
fortran_source = _source(artifacts, ".f90")
assert "int32_t bind_c_get_computed(void);" in c_source
assert "int32_t constant_computed_value_0 = bind_c_get_computed();" in c_source
assert 'PyUnicode_FromString("D")' in c_source
assert "native_computed => computed" in fortran_source
assert "function bind_c_get_computed()" in fortran_source
assert "result = native_computed" in fortran_source
assert "bind_c_set_computed" not in c_source
assert "bind_c_set_computed" not in fortran_source
def test_parameter_array_uses_one_immutable_python_owned_import_snapshot():
plan = _parameter_array_plan()
variable = next(
variable
for namespace in plan.namespaces
for variable in namespace.variables
if variable.binding.python_names == ("dpmpar",)
)
assert variable.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE
assert variable.binding.setter_action is SetterAction.OMIT
assert variable.binding.constant_value is None
assert variable.array is not None
assert variable.array.shape == ("3",)
artifacts = WrapperGenerator().generate(plan)
c_source = _source(artifacts, ".c")
fortran_source = _source(artifacts, ".f90")
assert "void * bind_c_get_dpmpar(int64_t * extent_0);" in c_source
assert "PyArray_EMPTY(1, constant_dpmpar_value_0_dimensions, NPY_FLOAT64, 1)" in c_source
assert "memcpy(PyArray_DATA((PyArrayObject *)constant_dpmpar_object_0)" in c_source
assert "PyArray_CLEARFLAGS((PyArrayObject *)constant_dpmpar_object_0, NPY_ARRAY_WRITEABLE)" in c_source
assert 'PyModule_AddObject(namespace_parameter_array, "dpmpar", constant_dpmpar_object_0)' in c_source
assert "real(c_double), allocatable, target, save, dimension(:) :: parameter_snapshot" in fortran_source
assert "parameter_snapshot = native_dpmpar" in fortran_source
assert "result = c_loc(parameter_snapshot)" in fortran_source
def test_module_variable_visitors_consume_their_backend_owned_actions():
plan = _plan()
counter = next(
variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",)
)
split_actions = replace(
counter,
binding=replace(
counter.binding,
getter_action=ModuleGetterAction.CONSTANT_VALUE,
setter_action=SetterAction.OMIT,
),
bridge=replace(
counter.bridge,
native_getter_action=ModuleGetterAction.DIRECT_VALUE,
native_assignment=AssignmentMode.VALUE_COPY,
),
)
assert CBindingGenerator().visit(split_actions) == ()
bridge = FortranBridgeGenerator()
bridge.visit(plan)
assert [procedure.name for procedure in bridge.visit(split_actions)] == [
"bind_c_get_counter",
"bind_c_set_counter",
]
def test_fortran_module_setter_rejects_unsupported_bridge_assignment():
plan = _plan()
counter = next(
variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",)
)
invalid = replace(counter, bridge=replace(counter.bridge, native_assignment=AssignmentMode.ALIAS))
bridge = FortranBridgeGenerator()
bridge.visit(plan)
with pytest.raises(ValueError, match="Unsupported Fortran module setter assignment"):
bridge.visit(invalid)
@pytest.mark.parametrize(
("python_name", "assignment"),
[
("counter", AssignmentMode.NONE),
("counter", AssignmentMode.ALIAS),
("optional_scale", AssignmentMode.VALUE_COPY),
("optional_scale", AssignmentMode.ALIAS),
("limit", AssignmentMode.VALUE_COPY),
],
)
def test_module_setter_assignment_mismatch_fails_before_backend_preflight_or_lowering(
python_name,
assignment,
):
invalid = _replace_variable(
_plan(),
python_name,
lambda variable: replace(
variable,
bridge=replace(variable.bridge, native_assignment=assignment),
),
)
c_generator = Mock(spec=CBindingGenerator)
fortran_generator = Mock(spec=FortranBridgeGenerator)
c_printer = Mock()
fortran_printer = Mock()
generator = WrapperGenerator(
c_generator=c_generator,
fortran_generator=fortran_generator,
c_printer=c_printer,
fortran_printer=fortran_printer,
)
with pytest.raises(ValueError, match="invalid-module-native-assignment") as error:
generator.generate(invalid)
assert "Unsupported Fortran module setter assignment" not in str(error.value)
c_generator.require_supported.assert_not_called()
fortran_generator.require_supported.assert_not_called()
c_generator.visit.assert_not_called()
fortran_generator.visit.assert_not_called()
c_generator.requires_native_support.assert_not_called()
c_printer.doprint.assert_not_called()
fortran_printer.doprint.assert_not_called()
def test_module_variable_generators_dispatch_get_set_and_rejection_from_plan():
artifacts = WrapperGenerator().generate(_plan())
c_source = _source(artifacts, ".c")
fortran_source = _source(artifacts, ".f90")
assert "scalar_state_root_module_property_setup_getattro" in c_source
assert "scalar_state_root_module_property_setup_setattro" in c_source
assert 'PyModule_AddObject(mod, "limit"' in c_source
assert "bind_c_set_counter(3);" in c_source
assert "return bind_c_get_counter();" not in c_source
assert "bind_c_get_counter()" in c_source
assert "bind_c_set_counter(value)" in c_source
assert "module variable optional_scale is read-only" in c_source
assert "module variable selected_scale is read-only" in c_source
assert 'getenv("PRIK_WRAPPER_FAIL_ALLOC")' in c_source
assert "result = native_counter" in fortran_source
assert "native_counter = value" in fortran_source
assert "allocated(native_optional_scale)" in fortran_source
assert "associated(native_selected_scale)" in fortran_source
assert "optional_scale = value" not in fortran_source
assert "selected_scale = value" not in fortran_source
def test_generated_support_procedure_symbol_is_shared_by_both_boundary_lowerers():
plan = _plan()
procedure = next(
item
for item in plan.entrypoint.support_procedures
if item.owner_path == "scalar_state.counter" and item.role == "module:set"
)
renamed = replace(procedure, symbol_name="planned_counter_assignment")
edited = replace(
plan,
entrypoint=replace(
plan.entrypoint,
support_procedures=tuple(
renamed if item is procedure else item for item in plan.entrypoint.support_procedures
),
),
)
artifacts = WrapperGenerator().generate(edited)
c_source = _source(artifacts, ".c")
fortran_source = _source(artifacts, ".f90")
assert "void planned_counter_assignment(int32_t value);" in c_source
assert "planned_counter_assignment(value);" in c_source
assert "subroutine planned_counter_assignment(value)" in fortran_source
assert 'bind(c, name="planned_counter_assignment")' in fortran_source
def test_missing_generated_support_procedure_fails_before_lowering():
plan = _plan()
edited = replace(
plan,
entrypoint=replace(
plan.entrypoint,
support_procedures=tuple(
item
for item in plan.entrypoint.support_procedures
if not (item.owner_path == "scalar_state.counter" and item.role == "module:set")
),
),
)
with pytest.raises(ValueError, match="incomplete-auxiliary-entrypoint-inventory"):
WrapperGenerator().generate(edited)
def test_bridge_local_module_target_edit_does_not_change_the_c_boundary():
plan = _plan()
baseline = _source(WrapperGenerator().generate(plan), ".c")
counter = next(
variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",)
)
edited_counter = replace(
counter,
bridge=replace(counter.bridge, native_name="counter_alternate"),
)
root = replace(
plan.namespaces[0],
variables=tuple(
edited_counter if variable is counter else variable for variable in plan.namespaces[0].variables
),
)
edited = replace(plan, namespaces=(root, *plan.namespaces[1:]))
artifacts = WrapperGenerator().generate(edited)
assert _source(artifacts, ".c") == baseline
assert "native_counter => counter_alternate" in _source(artifacts, ".f90")
def test_generator_rejects_python_module_setter_without_bridge_handoff():
plan = _plan()
counter = next(
variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",)
)
invalid_counter = replace(counter, entrypoint=replace(counter.entrypoint, setter_role=None))
invalid = replace(
plan,
namespaces=(
replace(
plan.namespaces[0],
variables=tuple(
invalid_counter if variable is counter else variable for variable in plan.namespaces[0].variables
),
),
),
)
with pytest.raises(ValueError, match="missing-module-setter-role"):
WrapperGenerator().generate(invalid)