Please edit the codebase to address the following task:
Static type inference currently rejects keyword arguments when instantiating PlutusData dataclasses. The call handler only allows keywords for user-defined functions, so constructor calls such as A(x=a, y=b, z=c) trigger an assertion even though positional calls succeed. This prevents keyword-style instantiation of on-chain data classes.
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(x=a, y=b, z=c).x
"""
eval_uplc_value(source_code, 1, 2, 3)
PY`
Keyword arguments supplied to a PlutusData dataclass constructor should be accepted, allowing the validator to return the requested field value.
The call raises opshin.util.CompilerError: AssertionError('Keyword arguments can only be used with user defined functions'), preventing the contract from compiling.
8c9b34b1cd4ee29763e619396c89c2b0d803d4b3.Let me explore the codebase to understand the issue and find the relevant code.
Let me explore the codebase to understand the issue and find the relevant code.
| pattern | Keyword arguments can only be used |
|---|---|
| output_mode | content |
opshin/type_inference.py:780: ), "Keyword arguments can only be used with user defined functions"
opshin/type_inference.py:780: ), "Keyword arguments can only be used with user defined functions"
| file_path | /testbed/opshin/type_inference.py |
|---|---|
| offset | 755 |
| limit | 60 |
755→ assert (
756→ ts.slice.upper.typ == IntegerInstanceType
757→ ), "upper slice indices for bytes must be integers"
758→ elif isinstance(ts.value.typ.typ, DictType):
759→ if not isinstance(ts.slice, Slice):
760→ ts.slice = self.visit(node.slice)
761→ assert (
762→ ts.slice.typ == ts.value.typ.typ.key_typ
763→ ), f"Dict subscript must have dict key type {ts.value.typ.typ.key_typ} but has type {ts.slice.typ}"
764→ ts.typ = ts.value.typ.typ.value_typ
765→ else:
766→ raise TypeInferenceError(
767→ f"Could not infer type of subscript of dict with a slice."
768→ )
769→ else:
770→ raise TypeInferenceError(
771→ f"Could not infer type of subscript of typ {ts.value.typ.__class__}"
772→ )
773→ return ts
774→
775→ def visit_Call(self, node: Call) -> TypedCall:
776→ tc = copy(node)
777→ if node.keywords:
778→ assert (
779→ node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
780→ ), "Keyword arguments can only be used with user defined functions"
781→ keywords = copy(node.keywords)
782→ reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
783→ args = []
784→ for i, a in enumerate(reg_args):
785→ if len(node.args) > i:
786→ args.append(self.visit(node.args[i]))
787→ else:
788→ candidates = [
789→ (idx, keyword)
790→ for idx, keyword in enumerate(keywords)
791→ if keyword.arg == a.orig_arg
792→ ]
793→ assert (
794→ len(candidates) == 1
795→ ), f"There should be one keyword or positional argument for the arg {a.orig_arg} but found {len(candidates)}"
796→ args.append(self.visit(candidates[0][1].value))
797→ keywords.pop(candidates[0][0])
798→ assert (
799→ len(keywords) == 0
800→ ), f"Could not match the keywords {[keyword.arg for keyword in keywords]} to any argument"
801→ tc.args = args
802→ tc.keywords = []
803→ else:
804→ tc.args = [self.visit(a) for a in node.args]
805→
806→ # might be isinstance
807→ if isinstance(tc.func, Name) and tc.func.orig_id == "isinstance":
808→ target_class = tc.args[1].typ
809→ if (
810→ isinstance(tc.args[0].typ, InstanceType)
811→ and isinstance(tc.args[0].typ.typ, AnyType)
812→ and not self.allow_isinstance_anything
813→ ):
814→ raise AssertionError(
755→ assert (
756→ ts.slice.upper.typ == IntegerInstanceType
757→ ), "upper slice indices for bytes must be integers"
758→ elif isinstance(ts.value.typ.typ, DictType):
759→ if not isinstance(ts.slice, Slice):
760→ ts.slice = self.visit(node.slice)
761→ assert (
762→ ts.slice.typ == ts.value.typ.typ.key_typ
763→ ), f"Dict subscript must have dict key type {ts.value.typ.typ.key_typ} but has type {ts.slice.typ}"
764→ ts.typ = ts.value.typ.typ.value_typ
765→ else:
766→ raise TypeInferenceError(
767→ f"Could not infer type of subscript of dict with a slice."
768→ )
769→ else:
770→ raise TypeInferenceError(
771→ f"Could not infer type of subscript of typ {ts.value.typ.__class__}"
772→ )
773→ return ts
774→
775→ def visit_Call(self, node: Call) -> TypedCall:
776→ tc = copy(node)
777→ if node.keywords:
778→ assert (
779→ node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
780→ ), "Keyword arguments can only be used with user defined functions"
781→ keywords = copy(node.keywords)
782→ reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
783→ args = []
784→ for i, a in enumerate(reg_args):
785→ if len(node.args) > i:
786→ args.append(self.visit(node.args[i]))
787→ else:
788→ candidates = [
789→ (idx, keyword)
790→ for idx, keyword in enumerate(keywords)
791→ if keyword.arg == a.orig_arg
792→ ]
793→ assert (
794→ len(candidates) == 1
795→ ), f"There should be one keyword or positional argument for the arg {a.orig_arg} but found {len(candidates)}"
796→ args.append(self.visit(candidates[0][1].value))
797→ keywords.pop(candidates[0][0])
798→ assert (
799→ len(keywords) == 0
800→ ), f"Could not match the keywords {[keyword.arg for keyword in keywords]} to any argument"
801→ tc.args = args
802→ tc.keywords = []
803→ else:
804→ tc.args = [self.visit(a) for a in node.args]
805→
806→ # might be isinstance
807→ if isinstance(tc.func, Name) and tc.func.orig_id == "isinstance":
808→ target_class = tc.args[1].typ
809→ if (
810→ isinstance(tc.args[0].typ, InstanceType)
811→ and isinstance(tc.args[0].typ.typ, AnyType)
812→ and not self.allow_isinstance_anything
813→ ):
814→ raise AssertionError(
Now I need to understand how PlutusData dataclasses are handled. The issue is that keyword arguments are only allowed for functions in FUNCTION_ARGUMENT_REGISTRY, but PlutusData dataclasses need to support them too.
Let me look at how the registry is built and how dataclasses work in this codebase.
Now I need to understand how PlutusData dataclasses are handled. The issue is that keyword arguments are only allowed for functions in FUNCTION_ARGUMENT_REGISTRY, but PlutusData dataclasses need to support them too.
Let me look at how the registry is built and how dataclasses work in this codebase.
| pattern | FUNCTION_ARGUMENT_REGISTRY |
|---|---|
| output_mode | content |
opshin/type_inference.py:246: self.FUNCTION_ARGUMENT_REGISTRY = {}
opshin/type_inference.py:364: self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [
opshin/type_inference.py:611: self.FUNCTION_ARGUMENT_REGISTRY[node.name] = node.args.args
opshin/type_inference.py:779: node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
opshin/type_inference.py:782: reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]opshin/type_inference.py:246: self.FUNCTION_ARGUMENT_REGISTRY = {}
opshin/type_inference.py:364: self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [
opshin/type_inference.py:611: self.FUNCTION_ARGUMENT_REGISTRY[node.name] = node.args.args
opshin/type_inference.py:779: node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
opshin/type_inference.py:782: reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]| file_path | /testbed/opshin/type_inference.py |
|---|---|
| offset | 350 |
| limit | 30 |
350→ # if an assert is amng the statements apply the isinstance cast 351→ if isinstance(stmt, Assert): 352→ typchecks, _ = TypeCheckVisitor(self.allow_isinstance_anything).visit( 353→ stmt.test 354→ ) 355→ # for the time after this assert, the variable has the specialized type 356→ prevtyps.update(self.implement_typechecks(typchecks)) 357→ self.implement_typechecks(prevtyps) 358→ return stmts 359→ 360→ def visit_ClassDef(self, node: ClassDef) -> TypedClassDef: 361→ class_record = RecordReader.extract(node, self) 362→ typ = RecordType(class_record) 363→ self.set_variable_type(node.name, typ) 364→ self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ 365→ typedarg(arg=field, typ=field_typ, orig_arg=field) 366→ for field, field_typ in class_record.fields 367→ ] 368→ typed_node = copy(node) 369→ typed_node.class_typ = typ 370→ return typed_node 371→ 372→ def visit_Constant(self, node: Constant) -> TypedConstant: 373→ tc = copy(node) 374→ assert type(node.value) not in [ 375→ float, 376→ complex, 377→ type(...), 378→ ], "Float, complex numbers and ellipsis currently not supported" 379→ tc.typ = constant_type(node.value)
350→ # if an assert is amng the statements apply the isinstance cast 351→ if isinstance(stmt, Assert): 352→ typchecks, _ = TypeCheckVisitor(self.allow_isinstance_anything).visit( 353→ stmt.test 354→ ) 355→ # for the time after this assert, the variable has the specialized type 356→ prevtyps.update(self.implement_typechecks(typchecks)) 357→ self.implement_typechecks(prevtyps) 358→ return stmts 359→ 360→ def visit_ClassDef(self, node: ClassDef) -> TypedClassDef: 361→ class_record = RecordReader.extract(node, self) 362→ typ = RecordType(class_record) 363→ self.set_variable_type(node.name, typ) 364→ self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ 365→ typedarg(arg=field, typ=field_typ, orig_arg=field) 366→ for field, field_typ in class_record.fields 367→ ] 368→ typed_node = copy(node) 369→ typed_node.class_typ = typ 370→ return typed_node 371→ 372→ def visit_Constant(self, node: Constant) -> TypedConstant: 373→ tc = copy(node) 374→ assert type(node.value) not in [ 375→ float, 376→ complex, 377→ type(...), 378→ ], "Float, complex numbers and ellipsis currently not supported" 379→ tc.typ = constant_type(node.value)
The visit_ClassDef method already registers the class in FUNCTION_ARGUMENT_REGISTRY. The issue is that the keyword arguments check in visit_Call uses node.func.id, but for class instantiation, node.func might not have an id attribute, or the condition is checking something else.
Let me look at the actual assertion more carefully:
if node.keywords:
assert (
node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
), "Keyword arguments can only be used with user defined functions"
The issue is that node.func.id might not exist for class instantiation - let me check. Actually, since A(x=a, y=b, z=c) is a call where func is Name('A'), node.func.id should be 'A'.
But the registry does include 'A' from visit_ClassDef. So the real problem is that when registering class fields, the code stores them with orig_arg=field, but when looking up keyword arguments in the call, it's checking against a.orig_arg - I need to trace through exactly how the typedarg objects are being created and compared.
Looking at the registration code, visit_ClassDef creates typedarg objects with the field name, while visit_FunctionDef stores the raw argument nodes directly. The assertion failing at line 778-780 means the function name isn't in the registry at all, so the issue is that the keyword argument lookup is happening before the function gets registered, or the registration itself is broken.
The visit_ClassDef method already registers the class in FUNCTION_ARGUMENT_REGISTRY. The issue is that the keyword arguments check in visit_Call uses node.func.id, but for class instantiation, node.func might not have an id attribute, or the condition is checking something else.
Let me look at the actual assertion more carefully:
if node.keywords:
assert (
node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
), "Keyword arguments can only be used with user defined functions"
The issue is that node.func.id might not exist for class instantiation - let me check. Actually, since A(x=a, y=b, z=c) is a call where func is Name('A'), node.func.id should be 'A'.
But the registry does include 'A' from visit_ClassDef. So the real problem is that when registering class fields, the code stores them with orig_arg=field, but when looking up keyword arguments in the call, it's checking against a.orig_arg - I need to trace through exactly how the typedarg objects are being created and compared.
Looking at the registration code, visit_ClassDef creates typedarg objects with the field name, while visit_FunctionDef stores the raw argument nodes directly. The assertion failing at line 778-780 means the function name isn't in the registry at all, so the issue is that the keyword argument lookup is happening before the function gets registered, or the registration itself is broken.
Let me check the visit_ClassDef more carefully and look at what happens with PlutusData subclasses:
| pattern | PlutusData |
|---|---|
| path | /testbed/opshin/type_inference.py |
| output_mode | content |
17:from pycardano import PlutusData
46:def record_from_plutusdata(c: PlutusData):
84: if isinstance(c, PlutusData):
108: ), f"Union must combine multiple PlutusData classes but found {e2.__class__.__name__}"
114: ), "Union must combine PlutusData classes with unique constructors"
164: assert isinstance(target_class, RecordType), "Can only cast to PlutusData"
177: ), "Can only cast instances of Union types of PlutusData or cast the same class. If you know what you are doing, enable the flag '--allow-isinstance-anything'"
1040: ), f"PlutusData attribute {node.target.id} may not have a default value"17:from pycardano import PlutusData
46:def record_from_plutusdata(c: PlutusData):
84: if isinstance(c, PlutusData):
108: ), f"Union must combine multiple PlutusData classes but found {e2.__class__.__name__}"
114: ), "Union must combine PlutusData classes with unique constructors"
164: assert isinstance(target_class, RecordType), "Can only cast to PlutusData"
177: ), "Can only cast instances of Union types of PlutusData or cast the same class. If you know what you are doing, enable the flag '--allow-isinstance-anything'"
1040: ), f"PlutusData attribute {node.target.id} may not have a default value"| pattern | node\.func\.id |
|---|---|
| path | /testbed/opshin/type_inference.py |
| output_mode | content |
779: node.func.id in self.FUNCTION_ARGUMENT_REGISTRY 782: reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
779: node.func.id in self.FUNCTION_ARGUMENT_REGISTRY 782: reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
The issue is that node.func might not have id attribute if node.func is not a Name node. But for A(x=a, y=b, z=c), node.func should be a Name node with id = 'A'.
Wait, let me re-read the issue - it says the current state is "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3". Maybe the visit_ClassDef adding to FUNCTION_ARGUMENT_REGISTRY was added recently in the latest commits.
Let me check the git history to see when this behavior was introduced.
The issue is that node.func might not have id attribute if node.func is not a Name node. But for A(x=a, y=b, z=c), node.func should be a Name node with id = 'A'.
Wait, let me re-read the issue - it says the current state is "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3". Maybe the visit_ClassDef adding to FUNCTION_ARGUMENT_REGISTRY was added recently in the latest commits.
Let me check the git history to see when this behavior was introduced.
| command | git log --oneline -10 |
|---|---|
| description | Show recent git commits |
1452acb Merge pull request #386 from SCMusson/feat_allow_keywords daa5af6 Remove superfluous line in tests c595159 fix test bug 66f2b7b Formatting 9ce7b5a AggressiveTypeInference class variables to instance variables 8fd3b53 Scope unit test 9cd6cc7 Merge branch 'feat_allow_keywords' of https://github.com/SCMusson/opshin into feat_allow_keywords 5ca65e3 Tests and bug fix e3a12fc run black f835522 Merge branch 'dev' into feat_allow_keywords
1452acb Merge pull request #386 from SCMusson/feat_allow_keywords daa5af6 Remove superfluous line in tests c595159 fix test bug 66f2b7b Formatting 9ce7b5a AggressiveTypeInference class variables to instance variables 8fd3b53 Scope unit test 9cd6cc7 Merge branch 'feat_allow_keywords' of https://github.com/SCMusson/opshin into feat_allow_keywords 5ca65e3 Tests and bug fix e3a12fc run black f835522 Merge branch 'dev' into feat_allow_keywords
| command | python - <<'PY'
from opshin.tests.utils import eval_uplc_value
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(x=a, y=b, z=c).x
"""
eval_uplc_value(source_code, 1, 2, 3)
PY
|
|---|---|
| description | Reproduce the bug |
(Bash completed with no output)
(Bash completed with no output)
| command | python - <<'PY' 2>&1
from opshin.tests.utils import eval_uplc_value
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(x=a, y=b, z=c).x
"""
result = eval_uplc_value(source_code, 1, 2, 3)
print("Result:", result)
PY
|
|---|---|
| description | Reproduce the bug with error output |
Result: 1
Result: 1
Hmm, the test passes? Let me check more carefully what the issue is. Maybe the issue is specifically about when a PlutusData class is defined and the visit_ClassDef isn't registering them. But looking at the code, visit_ClassDef does register classes in the FUNCTION_ARGUMENT_REGISTRY.
Wait, but the issue says "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" which might be a very old commit. Let me check.
Hmm, the test passes? Let me check more carefully what the issue is. Maybe the issue is specifically about when a PlutusData class is defined and the visit_ClassDef isn't registering them. But looking at the code, visit_ClassDef does register classes in the FUNCTION_ARGUMENT_REGISTRY.
Wait, but the issue says "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" which might be a very old commit. Let me check.
Interesting - the test passes on the current HEAD. Let me check whether the issue is with a specific scenario not captured by this test:
| command | git log --oneline 8c9b34b 2>&1 | head -5 |
|---|---|
| description | Check if the specific commit exists |
8c9b34b Merge pull request #386 from SCMusson/feat_allow_keywords daa5af6 Remove superfluous line in tests c595159 fix test bug 66f2b7b Formatting 9ce7b5a AggressiveTypeInference class variables to instance variables
8c9b34b Merge pull request #386 from SCMusson/feat_allow_keywords daa5af6 Remove superfluous line in tests c595159 fix test bug 66f2b7b Formatting 9ce7b5a AggressiveTypeInference class variables to instance variables
That's the same commit as the latest merge. Wait, the issue mentions "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" and the current HEAD is 1452acb Merge pull request #386. Let me check if these are related. It looks like 8c9b34b is the same as 1452acb (both are the merge of feat_allow_keywords).
Actually I think the test actually passes, but maybe there's a variant that doesn't work. Let me look at what types of constructors might not be in the registry.
The test returned 1, which is correct, so the basic case works. The issue might be with edge cases where node.func isn't a simple Name node—like when it's an attribute access instead. For A(x=a, y=b, z=c), the func is Name('A'), but I'm wondering if AggressiveTypeInference is missing something in how it handles certain constructor patterns.
That's the same commit as the latest merge. Wait, the issue mentions "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" and the current HEAD is 1452acb Merge pull request #386. Let me check if these are related. It looks like 8c9b34b is the same as 1452acb (both are the merge of feat_allow_keywords).
Actually I think the test actually passes, but maybe there's a variant that doesn't work. Let me look at what types of constructors might not be in the registry.
The test returned 1, which is correct, so the basic case works. The issue might be with edge cases where node.func isn't a simple Name node—like when it's an attribute access instead. For A(x=a, y=b, z=c), the func is Name('A'), but I'm wondering if AggressiveTypeInference is missing something in how it handles certain constructor patterns.
| pattern | AggressiveTypeInference |
|---|---|
| path | /testbed/opshin/type_inference.py |
| output_mode | content |
241:class AggressiveTypeInferencer(CompilingNodeTransformer): 1013: _type_inferencer: AggressiveTypeInferencer 1015: def __init__(self, type_inferencer: AggressiveTypeInferencer): 1021: def extract(cls, c: ClassDef, type_inferencer: AggressiveTypeInferencer) -> Record: 1095: return AggressiveTypeInferencer().visit(ast)
241:class AggressiveTypeInferencer(CompilingNodeTransformer): 1013: _type_inferencer: AggressiveTypeInferencer 1015: def __init__(self, type_inferencer: AggressiveTypeInferencer): 1021: def extract(cls, c: ClassDef, type_inferencer: AggressiveTypeInferencer) -> Record: 1095: return AggressiveTypeInferencer().visit(ast)
| file_path | /testbed/opshin/type_inference.py |
|---|---|
| offset | 241 |
| limit | 30 |
241→class AggressiveTypeInferencer(CompilingNodeTransformer):
242→ step = "Static Type Inference"
243→
244→ def __init__(self, allow_isinstance_anything=False):
245→ self.allow_isinstance_anything = allow_isinstance_anything
246→ self.FUNCTION_ARGUMENT_REGISTRY = {}
247→
248→ # A stack of dictionaries for storing scoped knowledge of variable types
249→ self.scopes = [INITIAL_SCOPE]
250→
251→ # Obtain the type of a variable name in the current scope
252→ def variable_type(self, name: str) -> Type:
253→ name = name
254→ for scope in reversed(self.scopes):
255→ if name in scope:
256→ return scope[name]
257→ raise TypeInferenceError(
258→ f"Variable {map_to_orig_name(name)} not initialized at access"
259→ )
260→
261→ def enter_scope(self):
262→ self.scopes.append({})
263→
264→ def exit_scope(self):
265→ self.scopes.pop()
266→
267→ def set_variable_type(self, name: str, typ: Type, force=False):
268→ if not force and name in self.scopes[-1] and self.scopes[-1][name] != typ:
269→ if self.scopes[-1][name] >= typ:
270→ # the specified type is broader, we pass on this
241→class AggressiveTypeInferencer(CompilingNodeTransformer):
242→ step = "Static Type Inference"
243→
244→ def __init__(self, allow_isinstance_anything=False):
245→ self.allow_isinstance_anything = allow_isinstance_anything
246→ self.FUNCTION_ARGUMENT_REGISTRY = {}
247→
248→ # A stack of dictionaries for storing scoped knowledge of variable types
249→ self.scopes = [INITIAL_SCOPE]
250→
251→ # Obtain the type of a variable name in the current scope
252→ def variable_type(self, name: str) -> Type:
253→ name = name
254→ for scope in reversed(self.scopes):
255→ if name in scope:
256→ return scope[name]
257→ raise TypeInferenceError(
258→ f"Variable {map_to_orig_name(name)} not initialized at access"
259→ )
260→
261→ def enter_scope(self):
262→ self.scopes.append({})
263→
264→ def exit_scope(self):
265→ self.scopes.pop()
266→
267→ def set_variable_type(self, name: str, typ: Type, force=False):
268→ if not force and name in self.scopes[-1] and self.scopes[-1][name] != typ:
269→ if self.scopes[-1][name] >= typ:
270→ # the specified type is broader, we pass on this
Let me look at the issue more carefully. The test I ran actually passed. But maybe there's some test that exists in the test suite that's failing, or maybe the issue relates to when node.func doesn't have an id attribute.
Let me look at the test files to see what tests exist for keyword arguments.
Let me look at the issue more carefully. The test I ran actually passed. But maybe there's some test that exists in the test suite that's failing, or maybe the issue relates to when node.func doesn't have an id attribute.
Let me look at the test files to see what tests exist for keyword arguments.
| pattern | keyword |
|---|---|
| path | /testbed/opshin/tests |
| output_mode | content |
| glob | *.py |
| context | 2 |
opshin/tests/test_keywords.py-13-class Keyword_tests(unittest.TestCase): /testbed/opshin/tests/test_keywords.py-14- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:15: def test_all_keywords(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-16- source_code = """ opshin/tests/test_keywords.py-17-def simple_example(x: int, y: int, z: int) -> int: -- /testbed/opshin/tests/test_keywords.py-25- /testbed/opshin/tests/test_keywords.py-26- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:27: def test_mixture_args_and_keywords(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-28- source_code = """ opshin/tests/test_keywords.py-29-def simple_example(x: int, y: int, z: int) -> int: -- /testbed/opshin/tests/test_keywords.py-37- /testbed/opshin/tests/test_keywords.py-38- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:39: def test_keyword_position_independence(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-40- source_code = """ opshin/tests/test_keywords.py-41-def simple_example(x: int, y: int, z:int) -> int: -- /testbed/opshin/tests/test_keywords.py-49- /testbed/opshin/tests/test_keywords.py-50- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:51: def test_arg_after_keyword_failure(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-52- source_code = """ opshin/tests/test_keywords.py-53-def simple_example(x: int, y: int, z:int) -> int: -- /testbed/opshin/tests/test_keywords.py-61- /testbed/opshin/tests/test_keywords.py-62- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:63: def test_too_many_keywords_failure(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-64- source_code = """ opshin/tests/test_keywords.py-65-def simple_example(x: int, y: int) -> int: -- /testbed/opshin/tests/test_keywords.py-73- /testbed/opshin/tests/test_keywords.py-74- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:75: def test_incorrect_keywords_failure(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-76- source_code = """ opshin/tests/test_keywords.py-77-def simple_example(x: int, y: int, z: int) -> int:
opshin/tests/test_keywords.py-13-class Keyword_tests(unittest.TestCase): /testbed/opshin/tests/test_keywords.py-14- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:15: def test_all_keywords(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-16- source_code = """ opshin/tests/test_keywords.py-17-def simple_example(x: int, y: int, z: int) -> int: -- /testbed/opshin/tests/test_keywords.py-25- /testbed/opshin/tests/test_keywords.py-26- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:27: def test_mixture_args_and_keywords(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-28- source_code = """ opshin/tests/test_keywords.py-29-def simple_example(x: int, y: int, z: int) -> int: -- /testbed/opshin/tests/test_keywords.py-37- /testbed/opshin/tests/test_keywords.py-38- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:39: def test_keyword_position_independence(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-40- source_code = """ opshin/tests/test_keywords.py-41-def simple_example(x: int, y: int, z:int) -> int: -- /testbed/opshin/tests/test_keywords.py-49- /testbed/opshin/tests/test_keywords.py-50- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:51: def test_arg_after_keyword_failure(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-52- source_code = """ opshin/tests/test_keywords.py-53-def simple_example(x: int, y: int, z:int) -> int: -- /testbed/opshin/tests/test_keywords.py-61- /testbed/opshin/tests/test_keywords.py-62- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:63: def test_too_many_keywords_failure(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-64- source_code = """ opshin/tests/test_keywords.py-65-def simple_example(x: int, y: int) -> int: -- /testbed/opshin/tests/test_keywords.py-73- /testbed/opshin/tests/test_keywords.py-74- @given(x=st.integers(), y=st.integers(), z=st.integers()) opshin/tests/test_keywords.py:75: def test_incorrect_keywords_failure(self, x: int, y: int, z: int): /testbed/opshin/tests/test_keywords.py-76- source_code = """ opshin/tests/test_keywords.py-77-def simple_example(x: int, y: int, z: int) -> int:
| file_path | /testbed/opshin/tests/test_keywords.py |
|---|
1→import unittest
2→
3→import hypothesis
4→from hypothesis import given
5→from hypothesis import strategies as st
6→from .utils import eval_uplc_value
7→from . import PLUTUS_VM_PROFILE
8→
9→
10→hypothesis.settings.load_profile(PLUTUS_VM_PROFILE)
11→
12→
13→class Keyword_tests(unittest.TestCase):
14→ @given(x=st.integers(), y=st.integers(), z=st.integers())
15→ def test_all_keywords(self, x: int, y: int, z: int):
16→ source_code = """
17→def simple_example(x: int, y: int, z: int) -> int:
18→ return (x-y)*z
19→
20→def validator(a: int, b: int, c: int) -> int:
21→ return simple_example(x=a, y=b, z=c)
22→"""
23→ ret = eval_uplc_value(source_code, x, y, z)
24→ self.assertEqual(ret, (x - y) * z)
25→
26→ @given(x=st.integers(), y=st.integers(), z=st.integers())
27→ def test_mixture_args_and_keywords(self, x: int, y: int, z: int):
28→ source_code = """
29→def simple_example(x: int, y: int, z: int) -> int:
30→ return (x-y)*z
31→
32→def validator(a: int, b: int, c: int) -> int:
33→ return simple_example(a, b, z=c)
34→"""
35→ ret = eval_uplc_value(source_code, x, y, z)
36→ self.assertEqual(ret, (x - y) * z)
37→
38→ @given(x=st.integers(), y=st.integers(), z=st.integers())
39→ def test_keyword_position_independence(self, x: int, y: int, z: int):
40→ source_code = """
41→def simple_example(x: int, y: int, z:int) -> int:
42→ return (x-y)*z
43→
44→def validator(a: int, b: int, c: int) -> int:
45→ return simple_example(z=c, x=a, y=b)
46→"""
47→ ret = eval_uplc_value(source_code, x, y, z)
48→ self.assertEqual(ret, (x - y) * z)
49→
50→ @given(x=st.integers(), y=st.integers(), z=st.integers())
51→ def test_arg_after_keyword_failure(self, x: int, y: int, z: int):
52→ source_code = """
53→def simple_example(x: int, y: int, z:int) -> int:
54→ return (x-y)*z
55→
56→def validator(a: int, b: int, c: int) -> int:
57→ return simple_example(x=a, y=b, c)
58→"""
59→ with self.assertRaises(Exception):
60→ ret = eval_uplc_value(source_code, x, y, z)
61→
62→ @given(x=st.integers(), y=st.integers(), z=st.integers())
63→ def test_too_many_keywords_failure(self, x: int, y: int, z: int):
64→ source_code = """
65→def simple_example(x: int, y: int) -> int:
66→ return x-y
67→
68→def validator(a: int, b: int, c: int) -> int:
69→ return simple_example(x=a, y=b, z=c)
70→"""
71→ with self.assertRaises(Exception):
72→ ret = eval_uplc_value(source_code, x, y, z)
73→
74→ @given(x=st.integers(), y=st.integers(), z=st.integers())
75→ def test_incorrect_keywords_failure(self, x: int, y: int, z: int):
76→ source_code = """
77→def simple_example(x: int, y: int, z: int) -> int:
78→ return (x-y)*z
79→
80→def validator(a: int, b: int, c: int) -> int:
81→ return simple_example(x=a, y=b, k=c)
82→"""
83→ with self.assertRaises(Exception):
84→ ret = eval_uplc_value(source_code, x, y, z)
85→
86→ @given(x=st.integers(), y=st.integers(), z=st.integers())
87→ def test_correct_scope(self, x: int, y: int, z: int):
88→ source_code = """
89→def simple_example(x: int, y: int, z: int) -> int:
90→ def simple_example(new_x: int, new_z: int) -> int:
91→ return new_x-new_z
92→ return simple_example(new_x = x, new_z = z) * y
93→
94→def validator(a: int, b: int, c: int) -> int:
95→ return simple_example(x=a, y=b, z=c)
96→"""
97→ ret = eval_uplc_value(source_code, x, y, z)
98→ self.assertEqual(ret, (x - z) * y)
99→
1→import unittest
2→
3→import hypothesis
4→from hypothesis import given
5→from hypothesis import strategies as st
6→from .utils import eval_uplc_value
7→from . import PLUTUS_VM_PROFILE
8→
9→
10→hypothesis.settings.load_profile(PLUTUS_VM_PROFILE)
11→
12→
13→class Keyword_tests(unittest.TestCase):
14→ @given(x=st.integers(), y=st.integers(), z=st.integers())
15→ def test_all_keywords(self, x: int, y: int, z: int):
16→ source_code = """
17→def simple_example(x: int, y: int, z: int) -> int:
18→ return (x-y)*z
19→
20→def validator(a: int, b: int, c: int) -> int:
21→ return simple_example(x=a, y=b, z=c)
22→"""
23→ ret = eval_uplc_value(source_code, x, y, z)
24→ self.assertEqual(ret, (x - y) * z)
25→
26→ @given(x=st.integers(), y=st.integers(), z=st.integers())
27→ def test_mixture_args_and_keywords(self, x: int, y: int, z: int):
28→ source_code = """
29→def simple_example(x: int, y: int, z: int) -> int:
30→ return (x-y)*z
31→
32→def validator(a: int, b: int, c: int) -> int:
33→ return simple_example(a, b, z=c)
34→"""
35→ ret = eval_uplc_value(source_code, x, y, z)
36→ self.assertEqual(ret, (x - y) * z)
37→
38→ @given(x=st.integers(), y=st.integers(), z=st.integers())
39→ def test_keyword_position_independence(self, x: int, y: int, z: int):
40→ source_code = """
41→def simple_example(x: int, y: int, z:int) -> int:
42→ return (x-y)*z
43→
44→def validator(a: int, b: int, c: int) -> int:
45→ return simple_example(z=c, x=a, y=b)
46→"""
47→ ret = eval_uplc_value(source_code, x, y, z)
48→ self.assertEqual(ret, (x - y) * z)
49→
50→ @given(x=st.integers(), y=st.integers(), z=st.integers())
51→ def test_arg_after_keyword_failure(self, x: int, y: int, z: int):
52→ source_code = """
53→def simple_example(x: int, y: int, z:int) -> int:
54→ return (x-y)*z
55→
56→def validator(a: int, b: int, c: int) -> int:
57→ return simple_example(x=a, y=b, c)
58→"""
59→ with self.assertRaises(Exception):
60→ ret = eval_uplc_value(source_code, x, y, z)
61→
62→ @given(x=st.integers(), y=st.integers(), z=st.integers())
63→ def test_too_many_keywords_failure(self, x: int, y: int, z: int):
64→ source_code = """
65→def simple_example(x: int, y: int) -> int:
66→ return x-y
67→
68→def validator(a: int, b: int, c: int) -> int:
69→ return simple_example(x=a, y=b, z=c)
70→"""
71→ with self.assertRaises(Exception):
72→ ret = eval_uplc_value(source_code, x, y, z)
73→
74→ @given(x=st.integers(), y=st.integers(), z=st.integers())
75→ def test_incorrect_keywords_failure(self, x: int, y: int, z: int):
76→ source_code = """
77→def simple_example(x: int, y: int, z: int) -> int:
78→ return (x-y)*z
79→
80→def validator(a: int, b: int, c: int) -> int:
81→ return simple_example(x=a, y=b, k=c)
82→"""
83→ with self.assertRaises(Exception):
84→ ret = eval_uplc_value(source_code, x, y, z)
85→
86→ @given(x=st.integers(), y=st.integers(), z=st.integers())
87→ def test_correct_scope(self, x: int, y: int, z: int):
88→ source_code = """
89→def simple_example(x: int, y: int, z: int) -> int:
90→ def simple_example(new_x: int, new_z: int) -> int:
91→ return new_x-new_z
92→ return simple_example(new_x = x, new_z = z) * y
93→
94→def validator(a: int, b: int, c: int) -> int:
95→ return simple_example(x=a, y=b, z=c)
96→"""
97→ ret = eval_uplc_value(source_code, x, y, z)
98→ self.assertEqual(ret, (x - z) * y)
99→
I see. There are no tests for keyword arguments with PlutusData dataclass constructors. Let me look at the issue more carefully.
The visit_ClassDef at line 360 registers user-defined classes (@dataclass class A(PlutusData)). So when A(x=a, y=b, z=c) is called, A should be in the registry.
But wait - visit_ClassDef is called for each class defined in the user code. But when is this called relative to when the visit_Call tries to look up the name? Let me think...
Actually, I'm noticing the registry stores typedarg objects with orig_arg set to the field name, but I need to verify whether that field name matches what's actually passed as a keyword argument in the call. Let me trace through how RecordReader.extract processes these fields to see if there's a mismatch between the registered names and what's being looked up.
I see. There are no tests for keyword arguments with PlutusData dataclass constructors. Let me look at the issue more carefully.
The visit_ClassDef at line 360 registers user-defined classes (@dataclass class A(PlutusData)). So when A(x=a, y=b, z=c) is called, A should be in the registry.
But wait - visit_ClassDef is called for each class defined in the user code. But when is this called relative to when the visit_Call tries to look up the name? Let me think...
Actually, I'm noticing the registry stores typedarg objects with orig_arg set to the field name, but I need to verify whether that field name matches what's actually passed as a keyword argument in the call. Let me trace through how RecordReader.extract processes these fields to see if there's a mismatch between the registered names and what's being looked up.
| file_path | /testbed/opshin/type_inference.py |
|---|---|
| offset | 1013 |
| limit | 100 |
1013→ _type_inferencer: AggressiveTypeInferencer
1014→
1015→ def __init__(self, type_inferencer: AggressiveTypeInferencer):
1016→ self.constructor = None
1017→ self.attributes = []
1018→ self._type_inferencer = type_inferencer
1019→
1020→ @classmethod
1021→ def extract(cls, c: ClassDef, type_inferencer: AggressiveTypeInferencer) -> Record:
1022→ f = cls(type_inferencer)
1023→ f.visit(c)
1024→ if f.constructor is None:
1025→ det_string = RecordType(
1026→ Record(f.name, f.orig_name, 0, frozenlist(f.attributes))
1027→ ).id_map(skip_constructor=True)
1028→ det_hash = sha256(str(det_string).encode("utf8")).hexdigest()
1029→ f.constructor = int(det_hash, 16) % 2**32
1030→ return Record(f.name, f.orig_name, f.constructor, frozenlist(f.attributes))
1031→
1032→ def visit_AnnAssign(self, node: AnnAssign) -> None:
1033→ assert isinstance(
1034→ node.target, Name
1035→ ), "Record elements must have named attributes"
1036→ typ = self._type_inferencer.type_from_annotation(node.annotation)
1037→ if node.target.id != "CONSTR_ID":
1038→ assert (
1039→ node.value is None
1040→ ), f"PlutusData attribute {node.target.id} may not have a default value"
1041→ assert not isinstance(
1042→ typ, TupleType
1043→ ), "Records can currently not hold tuples"
1044→ self.attributes.append(
1045→ (
1046→ node.target.id,
1047→ InstanceType(typ),
1048→ )
1049→ )
1050→ return
1051→ assert typ == IntegerType, "CONSTR_ID must be assigned an integer"
1052→ assert isinstance(
1053→ node.value, Constant
1054→ ), "CONSTR_ID must be assigned a constant integer"
1055→ assert isinstance(
1056→ node.value.value, int
1057→ ), "CONSTR_ID must be assigned an integer"
1058→ self.constructor = node.value.value
1059→
1060→ def visit_ClassDef(self, node: ClassDef) -> None:
1061→ self.name = node.name
1062→ self.orig_name = node.orig_name
1063→ for s in node.body:
1064→ self.visit(s)
1065→
1066→ def visit_Pass(self, node: Pass) -> None:
1067→ pass
1068→
1069→ def visit_Assign(self, node: Assign) -> None:
1070→ assert len(node.targets) == 1, "Record elements must be assigned one by one"
1071→ target = node.targets[0]
1072→ assert isinstance(target, Name), "Record elements must have named attributes"
1073→ assert (
1074→ target.id == "CONSTR_ID"
1075→ ), "Type annotations may only be omitted for CONSTR_ID"
1076→ assert isinstance(
1077→ node.value, Constant
1078→ ), "CONSTR_ID must be assigned a constant integer"
1079→ assert isinstance(
1080→ node.value.value, int
1081→ ), "CONSTR_ID must be assigned an integer"
1082→ self.constructor = node.value.value
1083→
1084→ def visit_Expr(self, node: Expr) -> None:
1085→ assert isinstance(
1086→ node.value, Constant
1087→ ), "Only comments are allowed inside classes"
1088→ return None
1089→
1090→ def generic_visit(self, node: AST) -> None:
1091→ raise NotImplementedError(f"Can not compile {ast.dump(node)} inside of a class")
1092→
1093→
1094→def typed_ast(ast: AST):
1095→ return AggressiveTypeInferencer().visit(ast)
1096→
1097→
1098→def map_to_orig_name(name: str):
1099→ return re.sub(r"_\d+$", "", name)
1100→
1101→
1102→class ReturnExtractor(TypedNodeVisitor):
1103→ """
1104→ Utility to check that all paths end in Return statements with the proper type
1105→
1106→ Returns whether there is no remaining path
1107→ """
1108→
1109→ def __init__(self, func_rettyp: Type):
1110→ self.func_rettyp = func_rettyp
1111→
1112→ def visit_sequence(self, nodes: typing.List[TypedAST]) -> bool:
1013→ _type_inferencer: AggressiveTypeInferencer
1014→
1015→ def __init__(self, type_inferencer: AggressiveTypeInferencer):
1016→ self.constructor = None
1017→ self.attributes = []
1018→ self._type_inferencer = type_inferencer
1019→
1020→ @classmethod
1021→ def extract(cls, c: ClassDef, type_inferencer: AggressiveTypeInferencer) -> Record:
1022→ f = cls(type_inferencer)
1023→ f.visit(c)
1024→ if f.constructor is None:
1025→ det_string = RecordType(
1026→ Record(f.name, f.orig_name, 0, frozenlist(f.attributes))
1027→ ).id_map(skip_constructor=True)
1028→ det_hash = sha256(str(det_string).encode("utf8")).hexdigest()
1029→ f.constructor = int(det_hash, 16) % 2**32
1030→ return Record(f.name, f.orig_name, f.constructor, frozenlist(f.attributes))
1031→
1032→ def visit_AnnAssign(self, node: AnnAssign) -> None:
1033→ assert isinstance(
1034→ node.target, Name
1035→ ), "Record elements must have named attributes"
1036→ typ = self._type_inferencer.type_from_annotation(node.annotation)
1037→ if node.target.id != "CONSTR_ID":
1038→ assert (
1039→ node.value is None
1040→ ), f"PlutusData attribute {node.target.id} may not have a default value"
1041→ assert not isinstance(
1042→ typ, TupleType
1043→ ), "Records can currently not hold tuples"
1044→ self.attributes.append(
1045→ (
1046→ node.target.id,
1047→ InstanceType(typ),
1048→ )
1049→ )
1050→ return
1051→ assert typ == IntegerType, "CONSTR_ID must be assigned an integer"
1052→ assert isinstance(
1053→ node.value, Constant
1054→ ), "CONSTR_ID must be assigned a constant integer"
1055→ assert isinstance(
1056→ node.value.value, int
1057→ ), "CONSTR_ID must be assigned an integer"
1058→ self.constructor = node.value.value
1059→
1060→ def visit_ClassDef(self, node: ClassDef) -> None:
1061→ self.name = node.name
1062→ self.orig_name = node.orig_name
1063→ for s in node.body:
1064→ self.visit(s)
1065→
1066→ def visit_Pass(self, node: Pass) -> None:
1067→ pass
1068→
1069→ def visit_Assign(self, node: Assign) -> None:
1070→ assert len(node.targets) == 1, "Record elements must be assigned one by one"
1071→ target = node.targets[0]
1072→ assert isinstance(target, Name), "Record elements must have named attributes"
1073→ assert (
1074→ target.id == "CONSTR_ID"
1075→ ), "Type annotations may only be omitted for CONSTR_ID"
1076→ assert isinstance(
1077→ node.value, Constant
1078→ ), "CONSTR_ID must be assigned a constant integer"
1079→ assert isinstance(
1080→ node.value.value, int
1081→ ), "CONSTR_ID must be assigned an integer"
1082→ self.constructor = node.value.value
1083→
1084→ def visit_Expr(self, node: Expr) -> None:
1085→ assert isinstance(
1086→ node.value, Constant
1087→ ), "Only comments are allowed inside classes"
1088→ return None
1089→
1090→ def generic_visit(self, node: AST) -> None:
1091→ raise NotImplementedError(f"Can not compile {ast.dump(node)} inside of a class")
1092→
1093→
1094→def typed_ast(ast: AST):
1095→ return AggressiveTypeInferencer().visit(ast)
1096→
1097→
1098→def map_to_orig_name(name: str):
1099→ return re.sub(r"_\d+$", "", name)
1100→
1101→
1102→class ReturnExtractor(TypedNodeVisitor):
1103→ """
1104→ Utility to check that all paths end in Return statements with the proper type
1105→
1106→ Returns whether there is no remaining path
1107→ """
1108→
1109→ def __init__(self, func_rettyp: Type):
1110→ self.func_rettyp = func_rettyp
1111→
1112→ def visit_sequence(self, nodes: typing.List[TypedAST]) -> bool:
Now let me understand the issue better. Looking at visit_AnnAssign in the RecordReader:
self.attributes.append((node.target.id, InstanceType(typ),)) - so field is node.target.idAnd in visit_ClassDef:
self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [
typedarg(arg=field, typ=field_typ, orig_arg=field)
for field, field_typ in class_record.fields
]
field is the field name (like x, y, z). So the registry should be using orig_arg=field.
I'm wondering if name mangling is happening somewhere that would change these field names in the typed AST. Let me trace through the test case to see what's actually being registered versus what's expected.
The commit hashes seem to be pointing to the same merge, so I need to verify whether they're actually identical or if there's a discrepancy in how they're being referenced.
Now let me understand the issue better. Looking at visit_AnnAssign in the RecordReader:
self.attributes.append((node.target.id, InstanceType(typ),)) - so field is node.target.idAnd in visit_ClassDef:
self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [
typedarg(arg=field, typ=field_typ, orig_arg=field)
for field, field_typ in class_record.fields
]
field is the field name (like x, y, z). So the registry should be using orig_arg=field.
I'm wondering if name mangling is happening somewhere that would change these field names in the typed AST. Let me trace through the test case to see what's actually being registered versus what's expected.
The commit hashes seem to be pointing to the same merge, so I need to verify whether they're actually identical or if there's a discrepancy in how they're being referenced.
| command | git show 8c9b34b --oneline | head -1 && git show 1452acb --oneline | head -1 |
|---|---|
| description | Check if commits are the same |
8c9b34b Merge pull request #386 from SCMusson/feat_allow_keywords 1452acb Merge pull request #386 from SCMusson/feat_allow_keywords
8c9b34b Merge pull request #386 from SCMusson/feat_allow_keywords 1452acb Merge pull request #386 from SCMusson/feat_allow_keywords
These are the same PR but different commits. Let me check the actual diff between them:
These are the same PR but different commits. Let me check the actual diff between them:
| command | git diff 8c9b34b 1452acb 2>&1 | head -50 |
|---|---|
| description | Check diff between the two commits |
diff --git a/opshin/type_inference.py b/opshin/type_inference.py
index 5a10d06..51b5cc7 100644
--- a/opshin/type_inference.py
+++ b/opshin/type_inference.py
@@ -361,6 +361,10 @@ class AggressiveTypeInferencer(CompilingNodeTransformer):
class_record = RecordReader.extract(node, self)
typ = RecordType(class_record)
self.set_variable_type(node.name, typ)
+ self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [
+ typedarg(arg=field, typ=field_typ, orig_arg=field)
+ for field, field_typ in class_record.fields
+ ]
typed_node = copy(node)
typed_node.class_typ = typ
return typed_nodediff --git a/opshin/type_inference.py b/opshin/type_inference.py
index 5a10d06..51b5cc7 100644
--- a/opshin/type_inference.py
+++ b/opshin/type_inference.py
@@ -361,6 +361,10 @@ class AggressiveTypeInferencer(CompilingNodeTransformer):
class_record = RecordReader.extract(node, self)
typ = RecordType(class_record)
self.set_variable_type(node.name, typ)
+ self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [
+ typedarg(arg=field, typ=field_typ, orig_arg=field)
+ for field, field_typ in class_record.fields
+ ]
typed_node = copy(node)
typed_node.class_typ = typ
return typed_nodeSo the current HEAD has the fix for PlutusData dataclass keywords. The issue description says "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" which would be the pre-fix state where the registry entries for class constructors didn't exist.
But my test above ran successfully! That means the fix at 1452acb already addresses the issue. But wait - the task says to "edit the codebase to address the following task." Let me re-read the task.
The task says to run a test and verify. Let me actually run the test with the exact code from the issue description.
The test I ran returned the correct result, so it seems the current codebase at 1452acb already has a partial fix that handles class constructor fields, even though the problematic commit 8c9b34b didn't have the visit_ClassDef registry fix in place.
But I'm wondering if there's still an edge case lurking. The visit_ClassDef method registers fields using the internal field name, while visit_AnnAssign uses the original field name from the AST node. I need to verify whether these names actually match or if there's name mangling happening that could cause a mismatch between what gets registered and what the visitor encounters.
So the current HEAD has the fix for PlutusData dataclass keywords. The issue description says "commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" which would be the pre-fix state where the registry entries for class constructors didn't exist.
But my test above ran successfully! That means the fix at 1452acb already addresses the issue. But wait - the task says to "edit the codebase to address the following task." Let me re-read the task.
The task says to run a test and verify. Let me actually run the test with the exact code from the issue description.
The test I ran returned the correct result, so it seems the current codebase at 1452acb already has a partial fix that handles class constructor fields, even though the problematic commit 8c9b34b didn't have the visit_ClassDef registry fix in place.
But I'm wondering if there's still an edge case lurking. The visit_ClassDef method registers fields using the internal field name, while visit_AnnAssign uses the original field name from the AST node. I need to verify whether these names actually match or if there's name mangling happening that could cause a mismatch between what gets registered and what the visitor encounters.
Let me look at how typedarg is defined and how orig_arg is used:
| pattern | typedarg |
|---|---|
| path | /testbed/opshin/type_inference.py |
| output_mode | content |
| context | 2 |
363- self.set_variable_type(node.name, typ) 364- self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ 365: typedarg(arg=field, typ=field_typ, orig_arg=field) 366- for field, field_typ in class_record.fields 367- ] -- 559- return typed_cmp 560- 561: def visit_arg(self, node: arg) -> typedarg: 562- ta = copy(node) 563- ta.typ = InstanceType(self.type_from_annotation(node.annotation)) -- 565- return ta 566- 567: def visit_arguments(self, node: arguments) -> typedarguments: 568- if node.kw_defaults or node.kwarg or node.kwonlyargs or node.defaults: 569- raise NotImplementedError(
363- self.set_variable_type(node.name, typ) 364- self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ 365: typedarg(arg=field, typ=field_typ, orig_arg=field) 366- for field, field_typ in class_record.fields 367- ] -- 559- return typed_cmp 560- 561: def visit_arg(self, node: arg) -> typedarg: 562- ta = copy(node) 563- ta.typ = InstanceType(self.type_from_annotation(node.annotation)) -- 565- return ta 566- 567: def visit_arguments(self, node: arguments) -> typedarguments: 568- if node.kw_defaults or node.kwarg or node.kwonlyargs or node.defaults: 569- raise NotImplementedError(
| pattern | class typedarg|typedarg =|typedarg\b |
|---|---|
| path | /testbed/opshin |
| output_mode | content |
| glob | *.py |
| context | 1 |
/testbed/opshin/type_inference.py-364- self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ opshin/type_inference.py:365: typedarg(arg=field, typ=field_typ, orig_arg=field) /testbed/opshin/type_inference.py-366- for field, field_typ in class_record.fields -- /testbed/opshin/type_inference.py-560- opshin/type_inference.py:561: def visit_arg(self, node: arg) -> typedarg: /testbed/opshin/type_inference.py-562- ta = copy(node) -- /testbed/opshin/typed_ast.py-18- opshin/typed_ast.py:19:class typedarg(TypedAST, arg): /testbed/opshin/typed_ast.py-20- pass -- /testbed/opshin/typed_ast.py-22- opshin/typed_ast.py:23:class typedarguments(TypedAST, arguments): opshin/typed_ast.py:24: args: typing.List[typedarg] opshin/typed_ast.py:25: vararg: typing.Union[typedarg, None] opshin/typed_ast.py:26: kwonlyargs: typing.List[typedarg] opshin/typed_ast.py-27- kw_defaults: typing.List[typing.Union[typedexpr, None]] opshin/typed_ast.py:28: kwarg: typing.Union[typedarg, None] opshin/typed_ast.py-29- defaults: typing.List[typedexpr]
/testbed/opshin/type_inference.py-364- self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ opshin/type_inference.py:365: typedarg(arg=field, typ=field_typ, orig_arg=field) /testbed/opshin/type_inference.py-366- for field, field_typ in class_record.fields -- /testbed/opshin/type_inference.py-560- opshin/type_inference.py:561: def visit_arg(self, node: arg) -> typedarg: /testbed/opshin/type_inference.py-562- ta = copy(node) -- /testbed/opshin/typed_ast.py-18- opshin/typed_ast.py:19:class typedarg(TypedAST, arg): /testbed/opshin/typed_ast.py-20- pass -- /testbed/opshin/typed_ast.py-22- opshin/typed_ast.py:23:class typedarguments(TypedAST, arguments): opshin/typed_ast.py:24: args: typing.List[typedarg] opshin/typed_ast.py:25: vararg: typing.Union[typedarg, None] opshin/typed_ast.py:26: kwonlyargs: typing.List[typedarg] opshin/typed_ast.py-27- kw_defaults: typing.List[typing.Union[typedexpr, None]] opshin/typed_ast.py:28: kwarg: typing.Union[typedarg, None] opshin/typed_ast.py-29- defaults: typing.List[typedexpr]
| file_path | /testbed/opshin/typed_ast.py |
|---|---|
| offset | 1 |
| limit | 30 |
1→from .type_impls import *
2→
3→
4→class TypedAST(AST):
5→ typ: Type
6→
7→
8→class typedexpr(TypedAST, expr):
9→ def typechecks(self) -> typing.Dict[str, Type]:
10→ """Successful typechecks if this expression evaluates to True"""
11→ return {}
12→
13→
14→class typedstmt(TypedAST, stmt):
15→ # Statements always have type None
16→ typ = NoneInstanceType
17→
18→
19→class typedarg(TypedAST, arg):
20→ pass
21→
22→
23→class typedarguments(TypedAST, arguments):
24→ args: typing.List[typedarg]
25→ vararg: typing.Union[typedarg, None]
26→ kwonlyargs: typing.List[typedarg]
27→ kw_defaults: typing.List[typing.Union[typedexpr, None]]
28→ kwarg: typing.Union[typedarg, None]
29→ defaults: typing.List[typedexpr]
30→
1→from .type_impls import *
2→
3→
4→class TypedAST(AST):
5→ typ: Type
6→
7→
8→class typedexpr(TypedAST, expr):
9→ def typechecks(self) -> typing.Dict[str, Type]:
10→ """Successful typechecks if this expression evaluates to True"""
11→ return {}
12→
13→
14→class typedstmt(TypedAST, stmt):
15→ # Statements always have type None
16→ typ = NoneInstanceType
17→
18→
19→class typedarg(TypedAST, arg):
20→ pass
21→
22→
23→class typedarguments(TypedAST, arguments):
24→ args: typing.List[typedarg]
25→ vararg: typing.Union[typedarg, None]
26→ kwonlyargs: typing.List[typedarg]
27→ kw_defaults: typing.List[typing.Union[typedexpr, None]]
28→ kwarg: typing.Union[typedarg, None]
29→ defaults: typing.List[typedexpr]
30→
The typedarg extends arg which has arg (the argument name) and orig_arg seems to be a custom field. Let me check what orig_arg is.
The typedarg extends arg which has arg (the argument name) and orig_arg seems to be a custom field. Let me check what orig_arg is.
| pattern | orig_arg |
|---|---|
| path | /testbed/opshin |
| output_mode | content |
| glob | *.py |
opshin/type_inference.py:365: typedarg(arg=field, typ=field_typ, orig_arg=field)
opshin/type_inference.py:791: if keyword.arg == a.orig_arg
opshin/type_inference.py:795: ), f"There should be one keyword or positional argument for the arg {a.orig_arg} but found {len(candidates)}"
opshin/rewrite/rewrite_orig_name.py:34: a_cp.orig_arg = a.argopshin/type_inference.py:365: typedarg(arg=field, typ=field_typ, orig_arg=field)
opshin/type_inference.py:791: if keyword.arg == a.orig_arg
opshin/type_inference.py:795: ), f"There should be one keyword or positional argument for the arg {a.orig_arg} but found {len(candidates)}"
opshin/rewrite/rewrite_orig_name.py:34: a_cp.orig_arg = a.arg| file_path | /testbed/opshin/rewrite/rewrite_orig_name.py |
|---|
1→from ast import *
2→from copy import copy
3→from ..util import CompilingNodeTransformer
4→
5→"""
6→Rewrites all occurrences of names to contain a pointer to the original name for good
7→"""
8→
9→
10→class RewriteOrigName(CompilingNodeTransformer):
11→ step = "Assigning the orig_id/orig_name field with the variable name"
12→
13→ def visit_Name(self, node: Name) -> Name:
14→ nc = copy(node)
15→ nc.orig_id = node.id
16→ return nc
17→
18→ def visit_ClassDef(self, node: ClassDef) -> ClassDef:
19→ node_cp = copy(node)
20→ node_cp.orig_name = node.name
21→ node_cp.body = [self.visit(n) for n in node.body]
22→ return node_cp
23→
24→ def visit_NoneType(self, node: None) -> None:
25→ return None
26→
27→ def visit_FunctionDef(self, node: FunctionDef) -> FunctionDef:
28→ node_cp = copy(node)
29→ node_cp.orig_name = node.name
30→ node_cp.args = copy(node.args)
31→ node_cp.args.args = []
32→ for a in node.args.args:
33→ a_cp = self.visit(a)
34→ a_cp.orig_arg = a.arg
35→ node_cp.args.args.append(a_cp)
36→ node_cp.returns = self.visit(node.returns)
37→ node_cp.decorator_list = [self.visit(l) for l in node.decorator_list]
38→ node_cp.body = [self.visit(s) for s in node.body]
39→ return node_cp
40→
1→from ast import *
2→from copy import copy
3→from ..util import CompilingNodeTransformer
4→
5→"""
6→Rewrites all occurrences of names to contain a pointer to the original name for good
7→"""
8→
9→
10→class RewriteOrigName(CompilingNodeTransformer):
11→ step = "Assigning the orig_id/orig_name field with the variable name"
12→
13→ def visit_Name(self, node: Name) -> Name:
14→ nc = copy(node)
15→ nc.orig_id = node.id
16→ return nc
17→
18→ def visit_ClassDef(self, node: ClassDef) -> ClassDef:
19→ node_cp = copy(node)
20→ node_cp.orig_name = node.name
21→ node_cp.body = [self.visit(n) for n in node.body]
22→ return node_cp
23→
24→ def visit_NoneType(self, node: None) -> None:
25→ return None
26→
27→ def visit_FunctionDef(self, node: FunctionDef) -> FunctionDef:
28→ node_cp = copy(node)
29→ node_cp.orig_name = node.name
30→ node_cp.args = copy(node.args)
31→ node_cp.args.args = []
32→ for a in node.args.args:
33→ a_cp = self.visit(a)
34→ a_cp.orig_arg = a.arg
35→ node_cp.args.args.append(a_cp)
36→ node_cp.returns = self.visit(node.returns)
37→ node_cp.decorator_list = [self.visit(l) for l in node.decorator_list]
38→ node_cp.body = [self.visit(s) for s in node.body]
39→ return node_cp
40→
Now I understand the full picture. The orig_arg attribute in typedarg stores the original (pre-mangled) argument name. In RewriteOrigName, for function args: a_cp.orig_arg = a.arg - the original arg name before mangling.
In visit_ClassDef when building the registry for class constructors:
self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ typedarg(arg=field, typ=field_typ, orig_arg=field) for field, field_typ in class_record.fields]
The field names here might already be mangled versions since name mangling happens during the rewrites. I need to trace through the pipeline to figure out when exactly visit_ClassDef gets called relative to the name-mangling passes.
Now I understand the full picture. The orig_arg attribute in typedarg stores the original (pre-mangled) argument name. In RewriteOrigName, for function args: a_cp.orig_arg = a.arg - the original arg name before mangling.
In visit_ClassDef when building the registry for class constructors:
self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ typedarg(arg=field, typ=field_typ, orig_arg=field) for field, field_typ in class_record.fields]
The field names here might already be mangled versions since name mangling happens during the rewrites. I need to trace through the pipeline to figure out when exactly visit_ClassDef gets called relative to the name-mangling passes.
| pattern | rewrite_mangle\|RewriteMangle\|mangle |
|---|---|
| path | /testbed/opshin |
| output_mode | content |
| glob | *.py |
No matches found
No matches found
| pattern | _\d+$\|orig_id\|map_to_orig |
|---|---|
| path | /testbed/opshin |
| output_mode | content |
| glob | *.py |
No matches found
No matches found
| command | grep -r "orig_id\|map_to_orig" /testbed/opshin --include="*.py" | head -20 |
|---|---|
| description | Search for name mapping patterns |
/testbed/opshin/type_inference.py: if isinstance(node.func, Name) and node.func.orig_id == SPECIAL_BOOL:
/testbed/opshin/type_inference.py: if not (isinstance(node.func, Name) and node.func.orig_id == "isinstance"):
/testbed/opshin/type_inference.py: f"Variable {map_to_orig_name(name)} not initialized at access"
/testbed/opshin/type_inference.py: f"Type {self.scopes[-1][name]} of variable {map_to_orig_name(name)} in local scope does not match inferred type {typ}"
/testbed/opshin/type_inference.py: f"Class name {ann.orig_id} not initialized before annotating variable"
/testbed/opshin/type_inference.py: if ann.value.orig_id == "Union":
/testbed/opshin/type_inference.py: if ann.value.orig_id == "List":
/testbed/opshin/type_inference.py: if ann.value.orig_id == "Dict":
/testbed/opshin/type_inference.py: if ann.value.orig_id == "Tuple":
/testbed/opshin/type_inference.py: isinstance(o, Name) and o.orig_id == "wraps_builtin"
/testbed/opshin/type_inference.py: if isinstance(ts.value, Name) and ts.value.orig_id in [
/testbed/opshin/type_inference.py: ts.slice.upper.func.orig_id = "len"
/testbed/opshin/type_inference.py: ts.slice.upper.func.orig_id = "len"
/testbed/opshin/type_inference.py: if isinstance(tc.func, Name) and tc.func.orig_id == "isinstance":
/testbed/opshin/type_inference.py:def map_to_orig_name(name: str):
/testbed/opshin/rewrite/rewrite_orig_name.py: step = "Assigning the orig_id/orig_name field with the variable name"
/testbed/opshin/rewrite/rewrite_orig_name.py: nc.orig_id = node.id
/testbed/opshin/compiler.py: plt.TraceError(f"NameError: {map_to_orig_name(x)}")
/testbed/opshin/compiler.py: plt.Delay(plt.TraceError(f"NameError: {map_to_orig_name(x)}")),/testbed/opshin/type_inference.py: if isinstance(node.func, Name) and node.func.orig_id == SPECIAL_BOOL:
/testbed/opshin/type_inference.py: if not (isinstance(node.func, Name) and node.func.orig_id == "isinstance"):
/testbed/opshin/type_inference.py: f"Variable {map_to_orig_name(name)} not initialized at access"
/testbed/opshin/type_inference.py: f"Type {self.scopes[-1][name]} of variable {map_to_orig_name(name)} in local scope does not match inferred type {typ}"
/testbed/opshin/type_inference.py: f"Class name {ann.orig_id} not initialized before annotating variable"
/testbed/opshin/type_inference.py: if ann.value.orig_id == "Union":
/testbed/opshin/type_inference.py: if ann.value.orig_id == "List":
/testbed/opshin/type_inference.py: if ann.value.orig_id == "Dict":
/testbed/opshin/type_inference.py: if ann.value.orig_id == "Tuple":
/testbed/opshin/type_inference.py: isinstance(o, Name) and o.orig_id == "wraps_builtin"
/testbed/opshin/type_inference.py: if isinstance(ts.value, Name) and ts.value.orig_id in [
/testbed/opshin/type_inference.py: ts.slice.upper.func.orig_id = "len"
/testbed/opshin/type_inference.py: ts.slice.upper.func.orig_id = "len"
/testbed/opshin/type_inference.py: if isinstance(tc.func, Name) and tc.func.orig_id == "isinstance":
/testbed/opshin/type_inference.py:def map_to_orig_name(name: str):
/testbed/opshin/rewrite/rewrite_orig_name.py: step = "Assigning the orig_id/orig_name field with the variable name"
/testbed/opshin/rewrite/rewrite_orig_name.py: nc.orig_id = node.id
/testbed/opshin/compiler.py: plt.TraceError(f"NameError: {map_to_orig_name(x)}")
/testbed/opshin/compiler.py: plt.Delay(plt.TraceError(f"NameError: {map_to_orig_name(x)}")),Now let me look at how the pipeline processes the AST to understand when name mangling happens relative to the type inference step.
Now let me look at how the pipeline processes the AST to understand when name mangling happens relative to the type inference step.
| pattern | RewriteOrigName\|typed_ast\|AggressiveTypeInferencer |
|---|---|
| path | /testbed/opshin |
| output_mode | content |
| glob | *.py |
No matches found
No matches found
| command | grep -rn "RewriteOrigName\|typed_ast\|AggressiveTypeInferencer" /testbed/opshin --include="*.py" | grep -v "test_\|\.pyc" |
|---|---|
| description | Find pipeline usage |
/testbed/opshin/type_inference.py:19:from .typed_ast import * /testbed/opshin/type_inference.py:241:class AggressiveTypeInferencer(CompilingNodeTransformer): /testbed/opshin/type_inference.py:1013: _type_inferencer: AggressiveTypeInferencer /testbed/opshin/type_inference.py:1015: def __init__(self, type_inferencer: AggressiveTypeInferencer): /testbed/opshin/type_inference.py:1021: def extract(cls, c: ClassDef, type_inferencer: AggressiveTypeInferencer) -> Record: /testbed/opshin/type_inference.py:1094:def typed_ast(ast: AST): /testbed/opshin/type_inference.py:1095: return AggressiveTypeInferencer().visit(ast) /testbed/opshin/rewrite/rewrite_import_hashlib.py:5:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_import_integrity_check.py:8:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_remove_type_stuff.py:3:from ..typed_ast import ( /testbed/opshin/rewrite/rewrite_orig_name.py:10:class RewriteOrigName(CompilingNodeTransformer): /testbed/opshin/rewrite/rewrite_inject_builtins.py:3:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_import_uplc_builtins.py:7:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_empty_dicts.py:7:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_empty_lists.py:7:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_inject_builtin_constr.py:3:from ..typed_ast import * /testbed/opshin/fun_impls.py:3:from .typed_ast import * /testbed/opshin/optimize/optimize_remove_deadvars.py:9:from ..typed_ast import TypedAnnAssign, TypedFunctionDef, TypedClassDef, TypedName /testbed/opshin/compiler.py:24:from .rewrite.rewrite_orig_name import RewriteOrigName /testbed/opshin/compiler.py:36:from .typed_ast import ( /testbed/opshin/compiler.py:1052: RewriteOrigName(), /testbed/opshin/compiler.py:1055: AggressiveTypeInferencer(config.allow_isinstance_anything),
/testbed/opshin/type_inference.py:19:from .typed_ast import * /testbed/opshin/type_inference.py:241:class AggressiveTypeInferencer(CompilingNodeTransformer): /testbed/opshin/type_inference.py:1013: _type_inferencer: AggressiveTypeInferencer /testbed/opshin/type_inference.py:1015: def __init__(self, type_inferencer: AggressiveTypeInferencer): /testbed/opshin/type_inference.py:1021: def extract(cls, c: ClassDef, type_inferencer: AggressiveTypeInferencer) -> Record: /testbed/opshin/type_inference.py:1094:def typed_ast(ast: AST): /testbed/opshin/type_inference.py:1095: return AggressiveTypeInferencer().visit(ast) /testbed/opshin/rewrite/rewrite_import_hashlib.py:5:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_import_integrity_check.py:8:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_remove_type_stuff.py:3:from ..typed_ast import ( /testbed/opshin/rewrite/rewrite_orig_name.py:10:class RewriteOrigName(CompilingNodeTransformer): /testbed/opshin/rewrite/rewrite_inject_builtins.py:3:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_import_uplc_builtins.py:7:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_empty_dicts.py:7:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_empty_lists.py:7:from ..typed_ast import * /testbed/opshin/rewrite/rewrite_inject_builtin_constr.py:3:from ..typed_ast import * /testbed/opshin/fun_impls.py:3:from .typed_ast import * /testbed/opshin/optimize/optimize_remove_deadvars.py:9:from ..typed_ast import TypedAnnAssign, TypedFunctionDef, TypedClassDef, TypedName /testbed/opshin/compiler.py:24:from .rewrite.rewrite_orig_name import RewriteOrigName /testbed/opshin/compiler.py:36:from .typed_ast import ( /testbed/opshin/compiler.py:1052: RewriteOrigName(), /testbed/opshin/compiler.py:1055: AggressiveTypeInferencer(config.allow_isinstance_anything),
| file_path | /testbed/opshin/compiler.py |
|---|---|
| offset | 1045 |
| limit | 40 |
1045→ RewriteImportHashlib(), 1046→ RewriteImportTyping(), 1047→ RewriteForbiddenOverwrites(), 1048→ RewriteImportDataclasses(), 1049→ RewriteInjectBuiltins(), 1050→ RewriteConditions(), 1051→ # Save the original names of variables 1052→ RewriteOrigName(), 1053→ RewriteScoping(), 1054→ # The type inference needs to be run after complex python operations were rewritten 1055→ AggressiveTypeInferencer(config.allow_isinstance_anything), 1056→ # Rewrites that circumvent the type inference or use its results 1057→ RewriteEmptyLists(), 1058→ RewriteEmptyDicts(), 1059→ RewriteImportUPLCBuiltins(), 1060→ RewriteInjectBuiltinsConstr(), 1061→ RewriteRemoveTypeStuff(), 1062→ # Apply optimizations 1063→ OptimizeRemoveDeadvars() if config.remove_dead_code else NoOp(), 1064→ OptimizeRemoveDeadconstants() if config.remove_dead_code else NoOp(), 1065→ OptimizeRemovePass(), 1066→ ] 1067→ for s in compile_pipeline: 1068→ prog = s.visit(prog) 1069→ prog = custom_fix_missing_locations(prog) 1070→ 1071→ # the compiler runs last 1072→ s = PlutoCompiler( 1073→ force_three_params=config.force_three_params, 1074→ validator_function_name=validator_function_name, 1075→ ) 1076→ prog = s.visit(prog) 1077→ 1078→ return prog 1079→
1045→ RewriteImportHashlib(), 1046→ RewriteImportTyping(), 1047→ RewriteForbiddenOverwrites(), 1048→ RewriteImportDataclasses(), 1049→ RewriteInjectBuiltins(), 1050→ RewriteConditions(), 1051→ # Save the original names of variables 1052→ RewriteOrigName(), 1053→ RewriteScoping(), 1054→ # The type inference needs to be run after complex python operations were rewritten 1055→ AggressiveTypeInferencer(config.allow_isinstance_anything), 1056→ # Rewrites that circumvent the type inference or use its results 1057→ RewriteEmptyLists(), 1058→ RewriteEmptyDicts(), 1059→ RewriteImportUPLCBuiltins(), 1060→ RewriteInjectBuiltinsConstr(), 1061→ RewriteRemoveTypeStuff(), 1062→ # Apply optimizations 1063→ OptimizeRemoveDeadvars() if config.remove_dead_code else NoOp(), 1064→ OptimizeRemoveDeadconstants() if config.remove_dead_code else NoOp(), 1065→ OptimizeRemovePass(), 1066→ ] 1067→ for s in compile_pipeline: 1068→ prog = s.visit(prog) 1069→ prog = custom_fix_missing_locations(prog) 1070→ 1071→ # the compiler runs last 1072→ s = PlutoCompiler( 1073→ force_three_params=config.force_three_params, 1074→ validator_function_name=validator_function_name, 1075→ ) 1076→ prog = s.visit(prog) 1077→ 1078→ return prog 1079→
| pattern | RewriteScoping |
|---|---|
| path | /testbed/opshin |
| output_mode | files_with_matches |
Found 2 files opshin/rewrite/rewrite_scoping.py opshin/compiler.py
Found 2 files opshin/rewrite/rewrite_scoping.py opshin/compiler.py
| file_path | /testbed/opshin/rewrite/rewrite_scoping.py |
|---|
1→import typing
2→from ast import *
3→from copy import copy
4→
5→from ordered_set import OrderedSet
6→
7→from .rewrite_forbidden_overwrites import FORBIDDEN_NAMES
8→from ..type_inference import INITIAL_SCOPE
9→from ..util import CompilingNodeTransformer, CompilingNodeVisitor
10→
11→"""
12→Rewrites all variable names to point to the definition in the nearest enclosing scope
13→"""
14→
15→
16→class ShallowNameDefCollector(CompilingNodeVisitor):
17→ step = "Collecting defined variable names"
18→
19→ def __init__(self):
20→ self.vars = OrderedSet()
21→
22→ def visit_Name(self, node: Name) -> None:
23→ if isinstance(node.ctx, Store):
24→ self.vars.add(node.id)
25→
26→ def visit_ClassDef(self, node: ClassDef):
27→ self.vars.add(node.name)
28→ # ignore the content (i.e. attribute names) of class definitions
29→
30→ def visit_FunctionDef(self, node: FunctionDef):
31→ self.vars.add(node.name)
32→ # ignore the recursive stuff
33→
34→
35→class RewriteScoping(CompilingNodeTransformer):
36→ step = "Rewrite all variables to inambiguously point to the definition in the nearest enclosing scope"
37→ latest_scope_id: int
38→ scopes: typing.List[typing.Tuple[OrderedSet, int]]
39→
40→ def variable_scope_id(self, name: str) -> int:
41→ """find the id of the scope in which this variable is defined (closest to its usage)"""
42→ name = name
43→ for scope, scope_id in reversed(self.scopes):
44→ if name in scope:
45→ return scope_id
46→ raise NameError(
47→ f"free variable '{name}' referenced before assignment in enclosing scope"
48→ )
49→
50→ def enter_scope(self):
51→ self.scopes.append((OrderedSet(), self.latest_scope_id))
52→ self.latest_scope_id += 1
53→
54→ def exit_scope(self):
55→ self.scopes.pop()
56→
57→ def set_variable_scope(self, name: str):
58→ self.scopes[-1][0].add(name)
59→
60→ def map_name(self, name: str):
61→ scope_id = self.variable_scope_id(name)
62→ if scope_id == -1:
63→ # do not rewrite Dict, Union, etc
64→ return name
65→ return f"{name}_{scope_id}"
66→
67→ def visit_Module(self, node: Module) -> Module:
68→ self.latest_scope_id = 0
69→ self.scopes = [(OrderedSet(INITIAL_SCOPE.keys() | FORBIDDEN_NAMES), -1)]
70→ node_cp = copy(node)
71→ self.enter_scope()
72→ # vars defined in this scope
73→ shallow_node_def_collector = ShallowNameDefCollector()
74→ for s in node.body:
75→ shallow_node_def_collector.visit(s)
76→ vars_def = shallow_node_def_collector.vars
77→ for var_name in vars_def:
78→ self.set_variable_scope(var_name)
79→ node_cp.body = [self.visit(s) for s in node.body]
80→ return node_cp
81→
82→ def visit_Name(self, node: Name) -> Name:
83→ nc = copy(node)
84→ # setting is handled in either enclosing module or function
85→ nc.id = self.map_name(node.id)
86→ return nc
87→
88→ def visit_ClassDef(self, node: ClassDef) -> ClassDef:
89→ return RecordScoper.scope(node, self)
90→
91→ def visit_FunctionDef(self, node: FunctionDef) -> FunctionDef:
92→ node_cp = copy(node)
93→ # setting is handled in either enclosing module or function
94→ node_cp.name = self.map_name(node.name)
95→ self.enter_scope()
96→ node_cp.args = copy(node.args)
97→ node_cp.args.args = []
98→ # args are defined in this scope
99→ for a in node.args.args:
100→ a_cp = copy(a)
101→ self.set_variable_scope(a.arg)
102→ a_cp.arg = self.map_name(a.arg)
103→ a_cp.annotation = self.visit(a.annotation)
104→ node_cp.args.args.append(a_cp)
105→ node_cp.returns = self.visit(node.returns)
106→ # vars defined in this scope
107→ shallow_node_def_collector = ShallowNameDefCollector()
108→ for s in node.body:
109→ shallow_node_def_collector.visit(s)
110→ vars_def = shallow_node_def_collector.vars
111→ for var_name in vars_def:
112→ self.set_variable_scope(var_name)
113→ # map all vars and recurse
114→ node_cp.body = [self.visit(s) for s in node.body]
115→ self.exit_scope()
116→ return node_cp
117→
118→ def visit_NoneType(self, node: None) -> None:
119→ return node
120→
121→
122→class RecordScoper(NodeTransformer):
123→ _scoper: RewriteScoping
124→
125→ def __init__(self, scoper: RewriteScoping):
126→ self._scoper = scoper
127→
128→ @classmethod
129→ def scope(cls, c: ClassDef, scoper: RewriteScoping) -> ClassDef:
130→ f = cls(scoper)
131→ return f.visit(c)
132→
133→ def visit_ClassDef(self, c: ClassDef) -> ClassDef:
134→ node_cp = copy(c)
135→ node_cp.name = self._scoper.map_name(node_cp.name)
136→ return self.generic_visit(node_cp)
137→
138→ def visit_AnnAssign(self, node: AnnAssign) -> AnnAssign:
139→ assert isinstance(
140→ node.target, Name
141→ ), "Record elements must have named attributes"
142→ node_cp = copy(node)
143→ node_cp.annotation = self._scoper.visit(node_cp.annotation)
144→ return node_cp
145→
1→import typing
2→from ast import *
3→from copy import copy
4→
5→from ordered_set import OrderedSet
6→
7→from .rewrite_forbidden_overwrites import FORBIDDEN_NAMES
8→from ..type_inference import INITIAL_SCOPE
9→from ..util import CompilingNodeTransformer, CompilingNodeVisitor
10→
11→"""
12→Rewrites all variable names to point to the definition in the nearest enclosing scope
13→"""
14→
15→
16→class ShallowNameDefCollector(CompilingNodeVisitor):
17→ step = "Collecting defined variable names"
18→
19→ def __init__(self):
20→ self.vars = OrderedSet()
21→
22→ def visit_Name(self, node: Name) -> None:
23→ if isinstance(node.ctx, Store):
24→ self.vars.add(node.id)
25→
26→ def visit_ClassDef(self, node: ClassDef):
27→ self.vars.add(node.name)
28→ # ignore the content (i.e. attribute names) of class definitions
29→
30→ def visit_FunctionDef(self, node: FunctionDef):
31→ self.vars.add(node.name)
32→ # ignore the recursive stuff
33→
34→
35→class RewriteScoping(CompilingNodeTransformer):
36→ step = "Rewrite all variables to inambiguously point to the definition in the nearest enclosing scope"
37→ latest_scope_id: int
38→ scopes: typing.List[typing.Tuple[OrderedSet, int]]
39→
40→ def variable_scope_id(self, name: str) -> int:
41→ """find the id of the scope in which this variable is defined (closest to its usage)"""
42→ name = name
43→ for scope, scope_id in reversed(self.scopes):
44→ if name in scope:
45→ return scope_id
46→ raise NameError(
47→ f"free variable '{name}' referenced before assignment in enclosing scope"
48→ )
49→
50→ def enter_scope(self):
51→ self.scopes.append((OrderedSet(), self.latest_scope_id))
52→ self.latest_scope_id += 1
53→
54→ def exit_scope(self):
55→ self.scopes.pop()
56→
57→ def set_variable_scope(self, name: str):
58→ self.scopes[-1][0].add(name)
59→
60→ def map_name(self, name: str):
61→ scope_id = self.variable_scope_id(name)
62→ if scope_id == -1:
63→ # do not rewrite Dict, Union, etc
64→ return name
65→ return f"{name}_{scope_id}"
66→
67→ def visit_Module(self, node: Module) -> Module:
68→ self.latest_scope_id = 0
69→ self.scopes = [(OrderedSet(INITIAL_SCOPE.keys() | FORBIDDEN_NAMES), -1)]
70→ node_cp = copy(node)
71→ self.enter_scope()
72→ # vars defined in this scope
73→ shallow_node_def_collector = ShallowNameDefCollector()
74→ for s in node.body:
75→ shallow_node_def_collector.visit(s)
76→ vars_def = shallow_node_def_collector.vars
77→ for var_name in vars_def:
78→ self.set_variable_scope(var_name)
79→ node_cp.body = [self.visit(s) for s in node.body]
80→ return node_cp
81→
82→ def visit_Name(self, node: Name) -> Name:
83→ nc = copy(node)
84→ # setting is handled in either enclosing module or function
85→ nc.id = self.map_name(node.id)
86→ return nc
87→
88→ def visit_ClassDef(self, node: ClassDef) -> ClassDef:
89→ return RecordScoper.scope(node, self)
90→
91→ def visit_FunctionDef(self, node: FunctionDef) -> FunctionDef:
92→ node_cp = copy(node)
93→ # setting is handled in either enclosing module or function
94→ node_cp.name = self.map_name(node.name)
95→ self.enter_scope()
96→ node_cp.args = copy(node.args)
97→ node_cp.args.args = []
98→ # args are defined in this scope
99→ for a in node.args.args:
100→ a_cp = copy(a)
101→ self.set_variable_scope(a.arg)
102→ a_cp.arg = self.map_name(a.arg)
103→ a_cp.annotation = self.visit(a.annotation)
104→ node_cp.args.args.append(a_cp)
105→ node_cp.returns = self.visit(node.returns)
106→ # vars defined in this scope
107→ shallow_node_def_collector = ShallowNameDefCollector()
108→ for s in node.body:
109→ shallow_node_def_collector.visit(s)
110→ vars_def = shallow_node_def_collector.vars
111→ for var_name in vars_def:
112→ self.set_variable_scope(var_name)
113→ # map all vars and recurse
114→ node_cp.body = [self.visit(s) for s in node.body]
115→ self.exit_scope()
116→ return node_cp
117→
118→ def visit_NoneType(self, node: None) -> None:
119→ return node
120→
121→
122→class RecordScoper(NodeTransformer):
123→ _scoper: RewriteScoping
124→
125→ def __init__(self, scoper: RewriteScoping):
126→ self._scoper = scoper
127→
128→ @classmethod
129→ def scope(cls, c: ClassDef, scoper: RewriteScoping) -> ClassDef:
130→ f = cls(scoper)
131→ return f.visit(c)
132→
133→ def visit_ClassDef(self, c: ClassDef) -> ClassDef:
134→ node_cp = copy(c)
135→ node_cp.name = self._scoper.map_name(node_cp.name)
136→ return self.generic_visit(node_cp)
137→
138→ def visit_AnnAssign(self, node: AnnAssign) -> AnnAssign:
139→ assert isinstance(
140→ node.target, Name
141→ ), "Record elements must have named attributes"
142→ node_cp = copy(node)
143→ node_cp.annotation = self._scoper.visit(node_cp.annotation)
144→ return node_cp
145→
Now I understand the pipeline:
RewriteOrigName - saves original names (sets orig_arg, orig_id, orig_name)RewriteScoping - mangles names to be scope-unique (e.g., x → x_0, A → A_0)AggressiveTypeInferencer - does type inferenceSo when AggressiveTypeInferencer.visit_ClassDef is called, the class name is already mangled to A_0 (stored in node.name), and the field names in class_record.fields are the original field names (since RecordScoper.visit_AnnAssign doesn't mangle the field names).
The call A(x=a, y=b, z=c) gets rewritten to A_0(x=a_0, y=b_0, z=c_0) by RewriteScoping, but the keyword argument names themselves (x, y, z) stay unmunged since they're not variable references. Meanwhile, class_record.fields contains the original field names like [('x', ...), ('y', ...), ('z', ...)] because the field definitions were never mangled.
When matching keywords in visit_Call, I'm checking if keyword.arg (which is 'x', 'y', 'z') matches a.orig_arg (also 'x', 'y', 'z'), and this comparison works correctly. The key insight is that for function arguments, orig_arg stores the original unmangled name while arg holds the mangled version after scoping rewrite, so I need to be careful about which one I'm comparing against.
For class fields in the registry, the orig_arg is set directly from the field name stored in class_record.fields, which comes from the target identifier in annotated assignments. Since field names aren't mangled during the scoping pass, class_record.fields contains the original field names like 'x', 'y', 'z'.
When registering the class in FUNCTION_ARGUMENT_REGISTRY, each field becomes a typedarg with matching arg and orig_arg values. During a call, the mangled class name like 'A_0' is used to look up the registry, and keyword arguments are matched by comparing keyword.arg against a.orig_arg — so 'x' matches 'x'. This all checks out, and my test confirms it works.
Now I need to figure out what the actual task is asking me to fix by re-reading the requirements.
The task mentions running a specific test to verify behavior, and notes that the same code works with positional arguments but fails with keyword arguments, throwing "Keyword arguments can only be used with user defined functions." Looking at the git history, I'm seeing multiple merge commits for the keyword arguments feature, so I need to trace through the actual commits more carefully to understand what changed. I'm seeing that both 8c9b34b and 1452acb claim to be merge commits for PR #386, but when I compare them with a diff, 1452acb contains the registry fix for class constructors while 8c9b34b doesn't—yet my test passed on the current HEAD. This suggests I might need to add a test that specifically validates this fix.
Let me reconsider whether the current HEAD fully addresses the issue across all cases, like when PlutosData is defined at different scope levels or when node.func lacks an id attribute (such as in self.A(x=a)).
Looking at the assertion that checks node.func.id against the registry, I realize the real problem: if node.func isn't a Name node but an Attribute node instead, accessing .id would throw an AttributeError rather than trigger the assertion. The issue's error message shows an AssertionError, which means node.func.id exists but isn't registered—so the bug might be that the code doesn't handle attribute access patterns or that the registry isn't being populated correctly at certain scope levels.
The fix at commit 1452acb adds classes to the registry, which should resolve this. My test passed with the current HEAD, but I notice the existing test suite only covers user-defined functions, not PlutusData constructors with keyword arguments. I should probably add a test case to verify the fix handles dataclass constructors properly.
Looking more closely at the code, I see the issue: node.func.id is being accessed directly on the raw AST node before it's been visited and transformed, which could cause problems if node.func isn't a simple identifier. Let me trace through the full visit_Call method to understand the order of operations.
Now I understand the pipeline:
RewriteOrigName - saves original names (sets orig_arg, orig_id, orig_name)RewriteScoping - mangles names to be scope-unique (e.g., x → x_0, A → A_0)AggressiveTypeInferencer - does type inferenceSo when AggressiveTypeInferencer.visit_ClassDef is called, the class name is already mangled to A_0 (stored in node.name), and the field names in class_record.fields are the original field names (since RecordScoper.visit_AnnAssign doesn't mangle the field names).
The call A(x=a, y=b, z=c) gets rewritten to A_0(x=a_0, y=b_0, z=c_0) by RewriteScoping, but the keyword argument names themselves (x, y, z) stay unmunged since they're not variable references. Meanwhile, class_record.fields contains the original field names like [('x', ...), ('y', ...), ('z', ...)] because the field definitions were never mangled.
When matching keywords in visit_Call, I'm checking if keyword.arg (which is 'x', 'y', 'z') matches a.orig_arg (also 'x', 'y', 'z'), and this comparison works correctly. The key insight is that for function arguments, orig_arg stores the original unmangled name while arg holds the mangled version after scoping rewrite, so I need to be careful about which one I'm comparing against.
For class fields in the registry, the orig_arg is set directly from the field name stored in class_record.fields, which comes from the target identifier in annotated assignments. Since field names aren't mangled during the scoping pass, class_record.fields contains the original field names like 'x', 'y', 'z'.
When registering the class in FUNCTION_ARGUMENT_REGISTRY, each field becomes a typedarg with matching arg and orig_arg values. During a call, the mangled class name like 'A_0' is used to look up the registry, and keyword arguments are matched by comparing keyword.arg against a.orig_arg — so 'x' matches 'x'. This all checks out, and my test confirms it works.
Now I need to figure out what the actual task is asking me to fix by re-reading the requirements.
The task mentions running a specific test to verify behavior, and notes that the same code works with positional arguments but fails with keyword arguments, throwing "Keyword arguments can only be used with user defined functions." Looking at the git history, I'm seeing multiple merge commits for the keyword arguments feature, so I need to trace through the actual commits more carefully to understand what changed. I'm seeing that both 8c9b34b and 1452acb claim to be merge commits for PR #386, but when I compare them with a diff, 1452acb contains the registry fix for class constructors while 8c9b34b doesn't—yet my test passed on the current HEAD. This suggests I might need to add a test that specifically validates this fix.
Let me reconsider whether the current HEAD fully addresses the issue across all cases, like when PlutosData is defined at different scope levels or when node.func lacks an id attribute (such as in self.A(x=a)).
Looking at the assertion that checks node.func.id against the registry, I realize the real problem: if node.func isn't a Name node but an Attribute node instead, accessing .id would throw an AttributeError rather than trigger the assertion. The issue's error message shows an AssertionError, which means node.func.id exists but isn't registered—so the bug might be that the code doesn't handle attribute access patterns or that the registry isn't being populated correctly at certain scope levels.
The fix at commit 1452acb adds classes to the registry, which should resolve this. My test passed with the current HEAD, but I notice the existing test suite only covers user-defined functions, not PlutusData constructors with keyword arguments. I should probably add a test case to verify the fix handles dataclass constructors properly.
Looking more closely at the code, I see the issue: node.func.id is being accessed directly on the raw AST node before it's been visited and transformed, which could cause problems if node.func isn't a simple identifier. Let me trace through the full visit_Call method to understand the order of operations.
Let me look at the full visit_Call code more carefully:
| file_path | /testbed/opshin/type_inference.py |
|---|---|
| offset | 775 |
| limit | 100 |
775→ def visit_Call(self, node: Call) -> TypedCall:
776→ tc = copy(node)
777→ if node.keywords:
778→ assert (
779→ node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
780→ ), "Keyword arguments can only be used with user defined functions"
781→ keywords = copy(node.keywords)
782→ reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
783→ args = []
784→ for i, a in enumerate(reg_args):
785→ if len(node.args) > i:
786→ args.append(self.visit(node.args[i]))
787→ else:
788→ candidates = [
789→ (idx, keyword)
790→ for idx, keyword in enumerate(keywords)
791→ if keyword.arg == a.orig_arg
792→ ]
793→ assert (
794→ len(candidates) == 1
795→ ), f"There should be one keyword or positional argument for the arg {a.orig_arg} but found {len(candidates)}"
796→ args.append(self.visit(candidates[0][1].value))
797→ keywords.pop(candidates[0][0])
798→ assert (
799→ len(keywords) == 0
800→ ), f"Could not match the keywords {[keyword.arg for keyword in keywords]} to any argument"
801→ tc.args = args
802→ tc.keywords = []
803→ else:
804→ tc.args = [self.visit(a) for a in node.args]
805→
806→ # might be isinstance
807→ if isinstance(tc.func, Name) and tc.func.orig_id == "isinstance":
808→ target_class = tc.args[1].typ
809→ if (
810→ isinstance(tc.args[0].typ, InstanceType)
811→ and isinstance(tc.args[0].typ.typ, AnyType)
812→ and not self.allow_isinstance_anything
813→ ):
814→ raise AssertionError(
815→ "OpShin does not permit checking the instance of raw Anything/Datum objects as this only checks the equality of the constructor id and nothing more. "
816→ "If you are certain of what you are doing, please use the flag '--allow-isinstance-anything'."
817→ )
818→ ntc = Compare(
819→ left=Attribute(tc.args[0], "CONSTR_ID"),
820→ ops=[Eq()],
821→ comparators=[Constant(target_class.record.constructor)],
822→ )
823→ custom_fix_missing_locations(ntc, node)
824→ ntc = self.visit(ntc)
825→ ntc.typ = BoolInstanceType
826→ ntc.typechecks = TypeCheckVisitor(self.allow_isinstance_anything).visit(tc)
827→ return ntc
828→ tc.func = self.visit(node.func)
829→ # might be a class
830→ if isinstance(tc.func.typ, ClassType):
831→ tc.func.typ = tc.func.typ.constr_type()
832→ # type might only turn out after the initialization (note the constr could be polymorphic)
833→ if isinstance(tc.func.typ, InstanceType) and isinstance(
834→ tc.func.typ.typ, PolymorphicFunctionType
835→ ):
836→ tc.func.typ = PolymorphicFunctionInstanceType(
837→ tc.func.typ.typ.polymorphic_function.type_from_args(
838→ [a.typ for a in tc.args]
839→ ),
840→ tc.func.typ.typ.polymorphic_function,
841→ )
842→ if isinstance(tc.func.typ, InstanceType) and isinstance(
843→ tc.func.typ.typ, FunctionType
844→ ):
845→ functyp = tc.func.typ.typ
846→ assert len(tc.args) == len(
847→ functyp.argtyps
848→ ), f"Signature of function does not match number of arguments. Expected {len(functyp.argtyps)} arguments with these types: {functyp.argtyps} but got {len(tc.args)} arguments."
849→ # all arguments need to be subtypes of the parameter type
850→ for i, (a, ap) in enumerate(zip(tc.args, functyp.argtyps)):
851→ assert (
852→ ap >= a.typ
853→ ), f"Signature of function does not match arguments in argument {i}. Expected this type: {ap} but got {a.typ}."
854→ tc.typ = functyp.rettyp
855→ return tc
856→ raise TypeInferenceError("Could not infer type of call")
857→
858→ def visit_Pass(self, node: Pass) -> TypedPass:
859→ tp = copy(node)
860→ return tp
861→
862→ def visit_Return(self, node: Return) -> TypedReturn:
863→ tp = copy(node)
864→ tp.value = self.visit(node.value)
865→ tp.typ = tp.value.typ
866→ return tp
867→
868→ def visit_Attribute(self, node: Attribute) -> TypedAttribute:
869→ tp = copy(node)
870→ tp.value = self.visit(node.value)
871→ owner = tp.value.typ
872→ # accesses to field
873→ tp.typ = owner.attribute_type(node.attr)
874→ return tp
775→ def visit_Call(self, node: Call) -> TypedCall:
776→ tc = copy(node)
777→ if node.keywords:
778→ assert (
779→ node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
780→ ), "Keyword arguments can only be used with user defined functions"
781→ keywords = copy(node.keywords)
782→ reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
783→ args = []
784→ for i, a in enumerate(reg_args):
785→ if len(node.args) > i:
786→ args.append(self.visit(node.args[i]))
787→ else:
788→ candidates = [
789→ (idx, keyword)
790→ for idx, keyword in enumerate(keywords)
791→ if keyword.arg == a.orig_arg
792→ ]
793→ assert (
794→ len(candidates) == 1
795→ ), f"There should be one keyword or positional argument for the arg {a.orig_arg} but found {len(candidates)}"
796→ args.append(self.visit(candidates[0][1].value))
797→ keywords.pop(candidates[0][0])
798→ assert (
799→ len(keywords) == 0
800→ ), f"Could not match the keywords {[keyword.arg for keyword in keywords]} to any argument"
801→ tc.args = args
802→ tc.keywords = []
803→ else:
804→ tc.args = [self.visit(a) for a in node.args]
805→
806→ # might be isinstance
807→ if isinstance(tc.func, Name) and tc.func.orig_id == "isinstance":
808→ target_class = tc.args[1].typ
809→ if (
810→ isinstance(tc.args[0].typ, InstanceType)
811→ and isinstance(tc.args[0].typ.typ, AnyType)
812→ and not self.allow_isinstance_anything
813→ ):
814→ raise AssertionError(
815→ "OpShin does not permit checking the instance of raw Anything/Datum objects as this only checks the equality of the constructor id and nothing more. "
816→ "If you are certain of what you are doing, please use the flag '--allow-isinstance-anything'."
817→ )
818→ ntc = Compare(
819→ left=Attribute(tc.args[0], "CONSTR_ID"),
820→ ops=[Eq()],
821→ comparators=[Constant(target_class.record.constructor)],
822→ )
823→ custom_fix_missing_locations(ntc, node)
824→ ntc = self.visit(ntc)
825→ ntc.typ = BoolInstanceType
826→ ntc.typechecks = TypeCheckVisitor(self.allow_isinstance_anything).visit(tc)
827→ return ntc
828→ tc.func = self.visit(node.func)
829→ # might be a class
830→ if isinstance(tc.func.typ, ClassType):
831→ tc.func.typ = tc.func.typ.constr_type()
832→ # type might only turn out after the initialization (note the constr could be polymorphic)
833→ if isinstance(tc.func.typ, InstanceType) and isinstance(
834→ tc.func.typ.typ, PolymorphicFunctionType
835→ ):
836→ tc.func.typ = PolymorphicFunctionInstanceType(
837→ tc.func.typ.typ.polymorphic_function.type_from_args(
838→ [a.typ for a in tc.args]
839→ ),
840→ tc.func.typ.typ.polymorphic_function,
841→ )
842→ if isinstance(tc.func.typ, InstanceType) and isinstance(
843→ tc.func.typ.typ, FunctionType
844→ ):
845→ functyp = tc.func.typ.typ
846→ assert len(tc.args) == len(
847→ functyp.argtyps
848→ ), f"Signature of function does not match number of arguments. Expected {len(functyp.argtyps)} arguments with these types: {functyp.argtyps} but got {len(tc.args)} arguments."
849→ # all arguments need to be subtypes of the parameter type
850→ for i, (a, ap) in enumerate(zip(tc.args, functyp.argtyps)):
851→ assert (
852→ ap >= a.typ
853→ ), f"Signature of function does not match arguments in argument {i}. Expected this type: {ap} but got {a.typ}."
854→ tc.typ = functyp.rettyp
855→ return tc
856→ raise TypeInferenceError("Could not infer type of call")
857→
858→ def visit_Pass(self, node: Pass) -> TypedPass:
859→ tp = copy(node)
860→ return tp
861→
862→ def visit_Return(self, node: Return) -> TypedReturn:
863→ tp = copy(node)
864→ tp.value = self.visit(node.value)
865→ tp.typ = tp.value.typ
866→ return tp
867→
868→ def visit_Attribute(self, node: Attribute) -> TypedAttribute:
869→ tp = copy(node)
870→ tp.value = self.visit(node.value)
871→ owner = tp.value.typ
872→ # accesses to field
873→ tp.typ = owner.attribute_type(node.attr)
874→ return tp
Now I see the issue clearly. In visit_Call, when there are keywords, it does:
assert (
node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
), "Keyword arguments can only be used with user defined functions"
This directly accesses node.func.id. node.func is the pre-processed node. node.func should be a Name node with id = 'A_0' (the mangled class name). And the registry has 'A_0' as a key.
But wait - what if node.func is not a Name node? The assertion would fail with AttributeError, not AssertionError.
So the error message tells me that node.func.id exists but isn't in the registry - meaning the class name was never added to the registry, which was the bug in the earlier commit before the fix was applied.
Let me verify which commit the testbed is actually on right now.
Now I see the issue clearly. In visit_Call, when there are keywords, it does:
assert (
node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
), "Keyword arguments can only be used with user defined functions"
This directly accesses node.func.id. node.func is the pre-processed node. node.func should be a Name node with id = 'A_0' (the mangled class name). And the registry has 'A_0' as a key.
But wait - what if node.func is not a Name node? The assertion would fail with AttributeError, not AssertionError.
So the error message tells me that node.func.id exists but isn't in the registry - meaning the class name was never added to the registry, which was the bug in the earlier commit before the fix was applied.
Let me verify which commit the testbed is actually on right now.
| command | git rev-parse HEAD && git log --oneline -3 |
|---|---|
| description | Check current HEAD |
1452acb931a6e494a315099697115945f504e3cf 1452acb Merge pull request #386 from SCMusson/feat_allow_keywords daa5af6 Remove superfluous line in tests c595159 fix test bug
1452acb931a6e494a315099697115945f504e3cf 1452acb Merge pull request #386 from SCMusson/feat_allow_keywords daa5af6 Remove superfluous line in tests c595159 fix test bug
So the current HEAD is 1452acb which is DIFFERENT from 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3 as specified in the task. Looking at the git diff earlier, the current HEAD (1452acb) added the registry entry for class constructors to fix the issue.
But wait - the git log shows 8c9b34b Merge pull request #386... but the full hash of 8c9b34b is 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3.
The current testbed is at 1452acb, which is the later commit that already includes the fix for the registry entry. The task specifies the bug is at 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3, and while my test showed the code works, I should probably add a test case to test_keywords.py to ensure this scenario is properly covered.
Looking at the diff between those commits, 1452acb adds the FUNCTION_ARGUMENT_REGISTRY[node.name] entry in visit_ClassDef, which is the fix for the original issue where keywords with PlutusData class constructors would raise a CompilerError. The current codebase already has this fix applied, but I should verify if there are any remaining edge cases or if a test is needed to prevent regression.
Now examining how the registry is populated — it's creating typed arguments from the class fields, mapping each field name to its type. I'm checking what the typedarg function expects and how these arguments are being constructed...
Looking at the visit_Call method, it's comparing keyword.arg (the actual keyword argument passed in) against a.orig_arg (the original field name), and these should match for fields like 'x', 'y', 'z'. But I need to verify whether field names could be mangled somewhere in the process...
Checking the visit_AnnAssign method...
Continuing to examine the typedarg constructor...
When creating a typedarg instance with arg=field, typ=field_typ, orig_arg=field, I need to verify that the orig_arg attribute is actually being set correctly. Since typedarg extends arg and orig_arg isn't a declared field in the base AST class, Python's AST machinery might not handle it as expected—it could be silently ignored or cause issues when the node is processed. I'm verifying that AST constructors in Python 3.8+ do accept extra keyword arguments and set them as attributes, which the test confirms works. So the real task here is probably to add regression test coverage for keyword arguments with PlutusData constructors to test_keywords.py.
Let me check out the repository at the commit mentioned in the issue to see if the bug still exists there.
So the current HEAD is 1452acb which is DIFFERENT from 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3 as specified in the task. Looking at the git diff earlier, the current HEAD (1452acb) added the registry entry for class constructors to fix the issue.
But wait - the git log shows 8c9b34b Merge pull request #386... but the full hash of 8c9b34b is 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3.
The current testbed is at 1452acb, which is the later commit that already includes the fix for the registry entry. The task specifies the bug is at 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3, and while my test showed the code works, I should probably add a test case to test_keywords.py to ensure this scenario is properly covered.
Looking at the diff between those commits, 1452acb adds the FUNCTION_ARGUMENT_REGISTRY[node.name] entry in visit_ClassDef, which is the fix for the original issue where keywords with PlutusData class constructors would raise a CompilerError. The current codebase already has this fix applied, but I should verify if there are any remaining edge cases or if a test is needed to prevent regression.
Now examining how the registry is populated — it's creating typed arguments from the class fields, mapping each field name to its type. I'm checking what the typedarg function expects and how these arguments are being constructed...
Looking at the visit_Call method, it's comparing keyword.arg (the actual keyword argument passed in) against a.orig_arg (the original field name), and these should match for fields like 'x', 'y', 'z'. But I need to verify whether field names could be mangled somewhere in the process...
Checking the visit_AnnAssign method...
Continuing to examine the typedarg constructor...
When creating a typedarg instance with arg=field, typ=field_typ, orig_arg=field, I need to verify that the orig_arg attribute is actually being set correctly. Since typedarg extends arg and orig_arg isn't a declared field in the base AST class, Python's AST machinery might not handle it as expected—it could be silently ignored or cause issues when the node is processed. I'm verifying that AST constructors in Python 3.8+ do accept extra keyword arguments and set them as attributes, which the test confirms works. So the real task here is probably to add regression test coverage for keyword arguments with PlutusData constructors to test_keywords.py.
Let me check out the repository at the commit mentioned in the issue to see if the bug still exists there.
Let me check the state at commit 8c9b34b to understand what's missing:
| command | git show 8c9b34b:opshin/type_inference.py | grep -n "FUNCTION_ARGUMENT_REGISTRY\|visit_ClassDef" | head -20 |
|---|---|
| description | Check registry in the reported commit |
246: self.FUNCTION_ARGUMENT_REGISTRY = {}
360: def visit_ClassDef(self, node: ClassDef) -> TypedClassDef:
607: self.FUNCTION_ARGUMENT_REGISTRY[node.name] = node.args.args
775: node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
778: reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
1056: def visit_ClassDef(self, node: ClassDef) -> None:246: self.FUNCTION_ARGUMENT_REGISTRY = {}
360: def visit_ClassDef(self, node: ClassDef) -> TypedClassDef:
607: self.FUNCTION_ARGUMENT_REGISTRY[node.name] = node.args.args
775: node.func.id in self.FUNCTION_ARGUMENT_REGISTRY
778: reg_args = self.FUNCTION_ARGUMENT_REGISTRY[node.func.id]
1056: def visit_ClassDef(self, node: ClassDef) -> None:In commit 8c9b34b, there's no FUNCTION_ARGUMENT_REGISTRY entry for class constructors. But in the current HEAD 1452acb, there is. So the fix has already been applied.
But the task says to "edit the codebase to address the following task." If the fix is already there... maybe there's still something missing?
Let me look at the test file to see if there's a test for this:
In commit 8c9b34b, there's no FUNCTION_ARGUMENT_REGISTRY entry for class constructors. But in the current HEAD 1452acb, there is. So the fix has already been applied.
But the task says to "edit the codebase to address the following task." If the fix is already there... maybe there's still something missing?
Let me look at the test file to see if there's a test for this:
| command | python -m pytest opshin/tests/test_keywords.py -v 2>&1 | tail -20 |
|---|---|
| description | Run the keywords tests |
rootdir: /testbed
configfile: pyproject.toml
plugins: hypothesis-6.151.9, typeguard-4.5.1
collecting ... collected 7 items
opshin/tests/test_keywords.py::Keyword_tests::test_all_keywords PASSED [ 14%]
opshin/tests/test_keywords.py::Keyword_tests::test_arg_after_keyword_failure PASSED [ 28%]
opshin/tests/test_keywords.py::Keyword_tests::test_correct_scope PASSED [ 42%]
opshin/tests/test_keywords.py::Keyword_tests::test_incorrect_keywords_failure PASSED [ 57%]
opshin/tests/test_keywords.py::Keyword_tests::test_keyword_position_independence PASSED [ 71%]
opshin/tests/test_keywords.py::Keyword_tests::test_mixture_args_and_keywords PASSED [ 85%]
opshin/tests/test_keywords.py::Keyword_tests::test_too_many_keywords_failure PASSED [100%]
=============================== warnings summary ===============================
../usr/local/lib/python3.11/site-packages/blockfrost/config.py:2
/usr/local/lib/python3.11/site-packages/blockfrost/config.py:2: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
import pkg_resources
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================= 7 passed, 1 warning in 3.31s =========================rootdir: /testbed
configfile: pyproject.toml
plugins: hypothesis-6.151.9, typeguard-4.5.1
collecting ... collected 7 items
opshin/tests/test_keywords.py::Keyword_tests::test_all_keywords PASSED [ 14%]
opshin/tests/test_keywords.py::Keyword_tests::test_arg_after_keyword_failure PASSED [ 28%]
opshin/tests/test_keywords.py::Keyword_tests::test_correct_scope PASSED [ 42%]
opshin/tests/test_keywords.py::Keyword_tests::test_incorrect_keywords_failure PASSED [ 57%]
opshin/tests/test_keywords.py::Keyword_tests::test_keyword_position_independence PASSED [ 71%]
opshin/tests/test_keywords.py::Keyword_tests::test_mixture_args_and_keywords PASSED [ 85%]
opshin/tests/test_keywords.py::Keyword_tests::test_too_many_keywords_failure PASSED [100%]
=============================== warnings summary ===============================
../usr/local/lib/python3.11/site-packages/blockfrost/config.py:2
/usr/local/lib/python3.11/site-packages/blockfrost/config.py:2: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
import pkg_resources
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================= 7 passed, 1 warning in 3.31s =========================All 7 existing tests pass. But there's NO test for keyword arguments with PlutusData dataclass constructors. The task says the fix has been applied (the test I manually ran with eval_uplc_value passed), but there's no automated test for this.
The task asks me to "edit the codebase to address the following task." I think the task is asking me to:
PlutusData constructorsBut wait, the task says the bug exists and needs to be fixed. Maybe the fix is not completely in place. Let me look at this more carefully.
Actually, re-reading the task: "Repository state: commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3." This means the task was reported based on the state at that commit. But the diff I showed earlier shows that the current HEAD adds the registry fix. So the fix is already there. Let me check the diff output more carefully to understand what changed. I'm looking at a git diff and trying to understand the commit history. The diff shows that commit 1452acb adds lines to the type inference code, but I'm confused about the timeline—if 8c9b34b is the current state where the bug exists, then 1452acb should be a later fix, yet the diff suggests 1452acb has more code than 8c9b34b. Let me reconsider what the repository state actually is. the fix that was added later. So the issue description is pointing to a commit that's missing the PlutusData registry entries, while the current HEAD already has them partially implemented. I need to figure out what's still missing or broken in the current version that's causing the test failures.
All 7 existing tests pass. But there's NO test for keyword arguments with PlutusData dataclass constructors. The task says the fix has been applied (the test I manually ran with eval_uplc_value passed), but there's no automated test for this.
The task asks me to "edit the codebase to address the following task." I think the task is asking me to:
PlutusData constructorsBut wait, the task says the bug exists and needs to be fixed. Maybe the fix is not completely in place. Let me look at this more carefully.
Actually, re-reading the task: "Repository state: commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3." This means the task was reported based on the state at that commit. But the diff I showed earlier shows that the current HEAD adds the registry fix. So the fix is already there. Let me check the diff output more carefully to understand what changed. I'm looking at a git diff and trying to understand the commit history. The diff shows that commit 1452acb adds lines to the type inference code, but I'm confused about the timeline—if 8c9b34b is the current state where the bug exists, then 1452acb should be a later fix, yet the diff suggests 1452acb has more code than 8c9b34b. Let me reconsider what the repository state actually is. the fix that was added later. So the issue description is pointing to a commit that's missing the PlutusData registry entries, while the current HEAD already has them partially implemented. I need to figure out what's still missing or broken in the current version that's causing the test failures.
Let me verify the current state of the code at line 360-370:
| file_path | /testbed/opshin/type_inference.py |
|---|---|
| offset | 360 |
| limit | 15 |
360→ def visit_ClassDef(self, node: ClassDef) -> TypedClassDef: 361→ class_record = RecordReader.extract(node, self) 362→ typ = RecordType(class_record) 363→ self.set_variable_type(node.name, typ) 364→ self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ 365→ typedarg(arg=field, typ=field_typ, orig_arg=field) 366→ for field, field_typ in class_record.fields 367→ ] 368→ typed_node = copy(node) 369→ typed_node.class_typ = typ 370→ return typed_node 371→ 372→ def visit_Constant(self, node: Constant) -> TypedConstant: 373→ tc = copy(node) 374→ assert type(node.value) not in [
360→ def visit_ClassDef(self, node: ClassDef) -> TypedClassDef: 361→ class_record = RecordReader.extract(node, self) 362→ typ = RecordType(class_record) 363→ self.set_variable_type(node.name, typ) 364→ self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [ 365→ typedarg(arg=field, typ=field_typ, orig_arg=field) 366→ for field, field_typ in class_record.fields 367→ ] 368→ typed_node = copy(node) 369→ typed_node.class_typ = typ 370→ return typed_node 371→ 372→ def visit_Constant(self, node: Constant) -> TypedConstant: 373→ tc = copy(node) 374→ assert type(node.value) not in [
The registry fix IS in the current codebase. But the git log shows this was added in 1452acb and not in 8c9b34b. The task says the bug is at 8c9b34b. So the current codebase has the fix already applied.
But wait - what if the issue is actually that the testbed was set up at a state BEFORE 1452acb was created? Let me check what "Repository state: commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" means in context. Both 8c9b34b and 1452acb are "Merge pull request #386 from SCMusson/feat_allow_keywords".
I'm realizing the confusion now - 1452acb is the current HEAD, but 8c9b34b is an earlier commit that's not showing up in my recent log output, even though git show confirms it exists with the same merge message.
The real question is whether the current HEAD actually contains the fix, and from reading the code, it does - though it might be incomplete since the visit_Call method is checking if the function is registered in the argument registry...
Still writing validation logic... I need to verify whether the actual reproduction case from the issue passes or fails with the current codebase state, since the git status shows deleted files that might indicate the testbed is at a different point than expected.
The registry fix IS in the current codebase. But the git log shows this was added in 1452acb and not in 8c9b34b. The task says the bug is at 8c9b34b. So the current codebase has the fix already applied.
But wait - what if the issue is actually that the testbed was set up at a state BEFORE 1452acb was created? Let me check what "Repository state: commit 8c9b34b1cd4ee29763e619396c89c2b0d803d4b3" means in context. Both 8c9b34b and 1452acb are "Merge pull request #386 from SCMusson/feat_allow_keywords".
I'm realizing the confusion now - 1452acb is the current HEAD, but 8c9b34b is an earlier commit that's not showing up in my recent log output, even though git show confirms it exists with the same merge message.
The real question is whether the current HEAD actually contains the fix, and from reading the code, it does - though it might be incomplete since the visit_Call method is checking if the function is registered in the argument registry...
Still writing validation logic... I need to verify whether the actual reproduction case from the issue passes or fails with the current codebase state, since the git status shows deleted files that might indicate the testbed is at a different point than expected.
Let me run the exact reproduction script from the issue and also check if there's a test gap:
| command | python - 2>&1 <<'PY'
from opshin.tests.utils import eval_uplc_value
import traceback
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(x=a, y=b, z=c).x
"""
try:
result = eval_uplc_value(source_code, 1, 2, 3)
print("Result:", result)
except Exception as e:
traceback.print_exc()
PY
|
|---|---|
| description | Run exact reproduction script |
Result: 1
Result: 1
The reproduction script passes on the current HEAD. The fix is already applied. The task just needs a test to be added to cover this case.
But wait - let me re-read the task again: "Please edit the codebase to address the following task." It says there's a bug. The description says the bug is at commit 8c9b34b. The current HEAD has a fix but...
Hmm, actually, I wonder if the task is asking me to do one of these:
Let me think about potential edge cases:1.
Now I'm checking whether the mangled name in visit_Call using node.func.id matches the registry key from visit_ClassDef which also uses the mangled name, so those should align. But I should also test what happens when someone calls a PlutusData constructor with keyword arguments from inside a method.
The reproduction script passes on the current HEAD. The fix is already applied. The task just needs a test to be added to cover this case.
But wait - let me re-read the task again: "Please edit the codebase to address the following task." It says there's a bug. The description says the bug is at commit 8c9b34b. The current HEAD has a fix but...
Hmm, actually, I wonder if the task is asking me to do one of these:
Let me think about potential edge cases:1.
Now I'm checking whether the mangled name in visit_Call using node.func.id matches the registry key from visit_ClassDef which also uses the mangled name, so those should align. But I should also test what happens when someone calls a PlutusData constructor with keyword arguments from inside a method.
| command | python - 2>&1 <<'PY'
from opshin.tests.utils import eval_uplc_value
import traceback
# Test with reordered keywords
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(z=c, x=a, y=b).z
"""
try:
result = eval_uplc_value(source_code, 1, 2, 3)
print("Reordered result:", result)
except Exception as e:
traceback.print_exc()
# Test with wrong keyword
source_code2 = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(x=a, y=b, w=c).x
"""
try:
result = eval_uplc_value(source_code2, 1, 2, 3)
print("Wrong keyword result:", result)
except Exception as e:
print("Wrong keyword correctly fails:", type(e).__name__, str(e)[:100])
PY
|
|---|---|
| description | Test edge cases for PlutusData keywords |
Reordered result: 3
Wrong keyword correctly fails: CompilerError (AssertionError('There should be one keyword or positional argument for the arg z but found 0'), <asReordered result: 3
Wrong keyword correctly fails: CompilerError (AssertionError('There should be one keyword or positional argument for the arg z but found 0'), <asOK so the fix is working correctly. The current codebase already has the fix. The task is asking me to add a test to cover keyword arguments with PlutusData constructors.
Actually, wait - let me reconsider. The task is "Please edit the codebase to address the following task." The "task" describes a bug. Maybe the task is about ensuring that the testbed (which is supposedly at 8c9b34b) gets the fix, but the testbed is actually at a later commit that has the fix.
OR, maybe the task was generated based on the state at 8c9b34b, and then the testbed was set up fresh with the current codebase.
Since the fix is already in place, I'll add a test case to test_keywords.py that validates keyword arguments work correctly with PlutusData constructors, including scenarios with reordered keywords, mixed positional and keyword arguments, and proper error handling for invalid keywords.
OK so the fix is working correctly. The current codebase already has the fix. The task is asking me to add a test to cover keyword arguments with PlutusData constructors.
Actually, wait - let me reconsider. The task is "Please edit the codebase to address the following task." The "task" describes a bug. Maybe the task is about ensuring that the testbed (which is supposedly at 8c9b34b) gets the fix, but the testbed is actually at a later commit that has the fix.
OR, maybe the task was generated based on the state at 8c9b34b, and then the testbed was set up fresh with the current codebase.
Since the fix is already in place, I'll add a test case to test_keywords.py that validates keyword arguments work correctly with PlutusData constructors, including scenarios with reordered keywords, mixed positional and keyword arguments, and proper error handling for invalid keywords.
The fix is already in the codebase (visit_ClassDef registering class fields). What's missing is test coverage for PlutusData constructor keywords. Let me add tests:
| file_path | /testbed/opshin/tests/test_keywords.py |
|---|
1→import unittest
2→
3→import hypothesis
4→from hypothesis import given
5→from hypothesis import strategies as st
6→from .utils import eval_uplc_value
7→from . import PLUTUS_VM_PROFILE
8→
9→
10→hypothesis.settings.load_profile(PLUTUS_VM_PROFILE)
11→
12→
13→class Keyword_tests(unittest.TestCase):
14→ @given(x=st.integers(), y=st.integers(), z=st.integers())
15→ def test_all_keywords(self, x: int, y: int, z: int):
16→ source_code = """
17→def simple_example(x: int, y: int, z: int) -> int:
18→ return (x-y)*z
19→
20→def validator(a: int, b: int, c: int) -> int:
21→ return simple_example(x=a, y=b, z=c)
22→"""
23→ ret = eval_uplc_value(source_code, x, y, z)
24→ self.assertEqual(ret, (x - y) * z)
25→
26→ @given(x=st.integers(), y=st.integers(), z=st.integers())
27→ def test_mixture_args_and_keywords(self, x: int, y: int, z: int):
28→ source_code = """
29→def simple_example(x: int, y: int, z: int) -> int:
30→ return (x-y)*z
31→
32→def validator(a: int, b: int, c: int) -> int:
33→ return simple_example(a, b, z=c)
34→"""
35→ ret = eval_uplc_value(source_code, x, y, z)
36→ self.assertEqual(ret, (x - y) * z)
37→
38→ @given(x=st.integers(), y=st.integers(), z=st.integers())
39→ def test_keyword_position_independence(self, x: int, y: int, z: int):
40→ source_code = """
41→def simple_example(x: int, y: int, z:int) -> int:
42→ return (x-y)*z
43→
44→def validator(a: int, b: int, c: int) -> int:
45→ return simple_example(z=c, x=a, y=b)
46→"""
47→ ret = eval_uplc_value(source_code, x, y, z)
48→ self.assertEqual(ret, (x - y) * z)
49→
50→ @given(x=st.integers(), y=st.integers(), z=st.integers())
51→ def test_arg_after_keyword_failure(self, x: int, y: int, z: int):
52→ source_code = """
53→def simple_example(x: int, y: int, z:int) -> int:
54→ return (x-y)*z
55→
56→def validator(a: int, b: int, c: int) -> int:
57→ return simple_example(x=a, y=b, c)
58→"""
59→ with self.assertRaises(Exception):
60→ ret = eval_uplc_value(source_code, x, y, z)
61→
62→ @given(x=st.integers(), y=st.integers(), z=st.integers())
63→ def test_too_many_keywords_failure(self, x: int, y: int, z: int):
64→ source_code = """
65→def simple_example(x: int, y: int) -> int:
66→ return x-y
67→
68→def validator(a: int, b: int, c: int) -> int:
69→ return simple_example(x=a, y=b, z=c)
70→"""
71→ with self.assertRaises(Exception):
72→ ret = eval_uplc_value(source_code, x, y, z)
73→
74→ @given(x=st.integers(), y=st.integers(), z=st.integers())
75→ def test_incorrect_keywords_failure(self, x: int, y: int, z: int):
76→ source_code = """
77→def simple_example(x: int, y: int, z: int) -> int:
78→ return (x-y)*z
79→
80→def validator(a: int, b: int, c: int) -> int:
81→ return simple_example(x=a, y=b, k=c)
82→"""
83→ with self.assertRaises(Exception):
84→ ret = eval_uplc_value(source_code, x, y, z)
85→
86→ @given(x=st.integers(), y=st.integers(), z=st.integers())
87→ def test_correct_scope(self, x: int, y: int, z: int):
88→ source_code = """
89→def simple_example(x: int, y: int, z: int) -> int:
90→ def simple_example(new_x: int, new_z: int) -> int:
91→ return new_x-new_z
92→ return simple_example(new_x = x, new_z = z) * y
93→
94→def validator(a: int, b: int, c: int) -> int:
95→ return simple_example(x=a, y=b, z=c)
96→"""
97→ ret = eval_uplc_value(source_code, x, y, z)
98→ self.assertEqual(ret, (x - z) * y)
99→
1→import unittest
2→
3→import hypothesis
4→from hypothesis import given
5→from hypothesis import strategies as st
6→from .utils import eval_uplc_value
7→from . import PLUTUS_VM_PROFILE
8→
9→
10→hypothesis.settings.load_profile(PLUTUS_VM_PROFILE)
11→
12→
13→class Keyword_tests(unittest.TestCase):
14→ @given(x=st.integers(), y=st.integers(), z=st.integers())
15→ def test_all_keywords(self, x: int, y: int, z: int):
16→ source_code = """
17→def simple_example(x: int, y: int, z: int) -> int:
18→ return (x-y)*z
19→
20→def validator(a: int, b: int, c: int) -> int:
21→ return simple_example(x=a, y=b, z=c)
22→"""
23→ ret = eval_uplc_value(source_code, x, y, z)
24→ self.assertEqual(ret, (x - y) * z)
25→
26→ @given(x=st.integers(), y=st.integers(), z=st.integers())
27→ def test_mixture_args_and_keywords(self, x: int, y: int, z: int):
28→ source_code = """
29→def simple_example(x: int, y: int, z: int) -> int:
30→ return (x-y)*z
31→
32→def validator(a: int, b: int, c: int) -> int:
33→ return simple_example(a, b, z=c)
34→"""
35→ ret = eval_uplc_value(source_code, x, y, z)
36→ self.assertEqual(ret, (x - y) * z)
37→
38→ @given(x=st.integers(), y=st.integers(), z=st.integers())
39→ def test_keyword_position_independence(self, x: int, y: int, z: int):
40→ source_code = """
41→def simple_example(x: int, y: int, z:int) -> int:
42→ return (x-y)*z
43→
44→def validator(a: int, b: int, c: int) -> int:
45→ return simple_example(z=c, x=a, y=b)
46→"""
47→ ret = eval_uplc_value(source_code, x, y, z)
48→ self.assertEqual(ret, (x - y) * z)
49→
50→ @given(x=st.integers(), y=st.integers(), z=st.integers())
51→ def test_arg_after_keyword_failure(self, x: int, y: int, z: int):
52→ source_code = """
53→def simple_example(x: int, y: int, z:int) -> int:
54→ return (x-y)*z
55→
56→def validator(a: int, b: int, c: int) -> int:
57→ return simple_example(x=a, y=b, c)
58→"""
59→ with self.assertRaises(Exception):
60→ ret = eval_uplc_value(source_code, x, y, z)
61→
62→ @given(x=st.integers(), y=st.integers(), z=st.integers())
63→ def test_too_many_keywords_failure(self, x: int, y: int, z: int):
64→ source_code = """
65→def simple_example(x: int, y: int) -> int:
66→ return x-y
67→
68→def validator(a: int, b: int, c: int) -> int:
69→ return simple_example(x=a, y=b, z=c)
70→"""
71→ with self.assertRaises(Exception):
72→ ret = eval_uplc_value(source_code, x, y, z)
73→
74→ @given(x=st.integers(), y=st.integers(), z=st.integers())
75→ def test_incorrect_keywords_failure(self, x: int, y: int, z: int):
76→ source_code = """
77→def simple_example(x: int, y: int, z: int) -> int:
78→ return (x-y)*z
79→
80→def validator(a: int, b: int, c: int) -> int:
81→ return simple_example(x=a, y=b, k=c)
82→"""
83→ with self.assertRaises(Exception):
84→ ret = eval_uplc_value(source_code, x, y, z)
85→
86→ @given(x=st.integers(), y=st.integers(), z=st.integers())
87→ def test_correct_scope(self, x: int, y: int, z: int):
88→ source_code = """
89→def simple_example(x: int, y: int, z: int) -> int:
90→ def simple_example(new_x: int, new_z: int) -> int:
91→ return new_x-new_z
92→ return simple_example(new_x = x, new_z = z) * y
93→
94→def validator(a: int, b: int, c: int) -> int:
95→ return simple_example(x=a, y=b, z=c)
96→"""
97→ ret = eval_uplc_value(source_code, x, y, z)
98→ self.assertEqual(ret, (x - z) * y)
99→
| replace_all | false |
|---|---|
| file_path | /testbed/opshin/tests/test_keywords.py |
| old_string | @given(x=st.integers(), y=st.integers(), z=st.integers())
def test_correct_scope(self, x: int, y: int, z: int):
source_code = """
def simple_example(x: int, y: int, z: int) -> int:
def simple_example(new_x: int, new_z: int) -> int:
return new_x-new_z
return simple_example(new_x = x, new_z = z) * y
def validator(a: int, b: int, c: int) -> int:
return simple_example(x=a, y=b, z=c)
"""
ret = eval_uplc_value(source_code, x, y, z)
self.assertEqual(ret, (x - z) * y) |
| new_string | @given(x=st.integers(), y=st.integers(), z=st.integers())
def test_correct_scope(self, x: int, y: int, z: int):
source_code = """
def simple_example(x: int, y: int, z: int) -> int:
def simple_example(new_x: int, new_z: int) -> int:
return new_x-new_z
return simple_example(new_x = x, new_z = z) * y
def validator(a: int, b: int, c: int) -> int:
return simple_example(x=a, y=b, z=c)
"""
ret = eval_uplc_value(source_code, x, y, z)
self.assertEqual(ret, (x - z) * y)
@given(x=st.integers(), y=st.integers(), z=st.integers())
def test_plutusdata_all_keywords(self, x: int, y: int, z: int):
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(x=a, y=b, z=c).x
"""
ret = eval_uplc_value(source_code, x, y, z)
self.assertEqual(ret, x)
@given(x=st.integers(), y=st.integers(), z=st.integers())
def test_plutusdata_keyword_position_independence(self, x: int, y: int, z: int):
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(z=c, x=a, y=b).z
"""
ret = eval_uplc_value(source_code, x, y, z)
self.assertEqual(ret, z)
@given(x=st.integers(), y=st.integers(), z=st.integers())
def test_plutusdata_incorrect_keywords_failure(self, x: int, y: int, z: int):
source_code = """
from opshin.prelude import *
@dataclass
class A(PlutusData):
x: int
y: int
z: int
def validator(a: int, b: int, c: int) -> int:
return A(x=a, y=b, w=c).x
"""
with self.assertRaises(Exception):
eval_uplc_value(source_code, x, y, z) |
The file /testbed/opshin/tests/test_keywords.py has been updated successfully.
The file /testbed/opshin/tests/test_keywords.py has been updated successfully.
| command | python -m pytest opshin/tests/test_keywords.py -v 2>&1 | tail -25 |
|---|---|
| description | Run updated keywords tests |
cachedir: .pytest_cache
hypothesis profile 'default'
rootdir: /testbed
configfile: pyproject.toml
plugins: hypothesis-6.151.9, typeguard-4.5.1
collecting ... collected 10 items
opshin/tests/test_keywords.py::Keyword_tests::test_all_keywords PASSED [ 10%]
opshin/tests/test_keywords.py::Keyword_tests::test_arg_after_keyword_failure PASSED [ 20%]
opshin/tests/test_keywords.py::Keyword_tests::test_correct_scope PASSED [ 30%]
opshin/tests/test_keywords.py::Keyword_tests::test_incorrect_keywords_failure PASSED [ 40%]
opshin/tests/test_keywords.py::Keyword_tests::test_keyword_position_independence PASSED [ 50%]
opshin/tests/test_keywords.py::Keyword_tests::test_mixture_args_and_keywords PASSED [ 60%]
opshin/tests/test_keywords.py::Keyword_tests::test_plutusdata_all_keywords PASSED [ 70%]
opshin/tests/test_keywords.py::Keyword_tests::test_plutusdata_incorrect_keywords_failure PASSED [ 80%]
opshin/tests/test_keywords.py::Keyword_tests::test_plutusdata_keyword_position_independence PASSED [ 90%]
opshin/tests/test_keywords.py::Keyword_tests::test_too_many_keywords_failure PASSED [100%]
=============================== warnings summary ===============================
../usr/local/lib/python3.11/site-packages/blockfrost/config.py:2
/usr/local/lib/python3.11/site-packages/blockfrost/config.py:2: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
import pkg_resources
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================== 10 passed, 1 warning in 10.52s ========================cachedir: .pytest_cache
hypothesis profile 'default'
rootdir: /testbed
configfile: pyproject.toml
plugins: hypothesis-6.151.9, typeguard-4.5.1
collecting ... collected 10 items
opshin/tests/test_keywords.py::Keyword_tests::test_all_keywords PASSED [ 10%]
opshin/tests/test_keywords.py::Keyword_tests::test_arg_after_keyword_failure PASSED [ 20%]
opshin/tests/test_keywords.py::Keyword_tests::test_correct_scope PASSED [ 30%]
opshin/tests/test_keywords.py::Keyword_tests::test_incorrect_keywords_failure PASSED [ 40%]
opshin/tests/test_keywords.py::Keyword_tests::test_keyword_position_independence PASSED [ 50%]
opshin/tests/test_keywords.py::Keyword_tests::test_mixture_args_and_keywords PASSED [ 60%]
opshin/tests/test_keywords.py::Keyword_tests::test_plutusdata_all_keywords PASSED [ 70%]
opshin/tests/test_keywords.py::Keyword_tests::test_plutusdata_incorrect_keywords_failure PASSED [ 80%]
opshin/tests/test_keywords.py::Keyword_tests::test_plutusdata_keyword_position_independence PASSED [ 90%]
opshin/tests/test_keywords.py::Keyword_tests::test_too_many_keywords_failure PASSED [100%]
=============================== warnings summary ===============================
../usr/local/lib/python3.11/site-packages/blockfrost/config.py:2
/usr/local/lib/python3.11/site-packages/blockfrost/config.py:2: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
import pkg_resources
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================== 10 passed, 1 warning in 10.52s ========================All 10 tests pass. Here's what I found and did:
Root cause: visit_ClassDef in AggressiveTypeInferencer wasn't registering PlutusData dataclass fields in FUNCTION_ARGUMENT_REGISTRY. The keyword-argument handling in visit_Call only allowed keywords when the callee was in that registry, so A(x=a, y=b, z=c) raised AssertionError('Keyword arguments can only be used with user defined functions').
Fix (already applied at HEAD 1452acb): visit_ClassDef now registers each class in the registry with its field names:
self.FUNCTION_ARGUMENT_REGISTRY[node.name] = [
typedarg(arg=field, typ=field_typ, orig_arg=field)
for field, field_typ in class_record.fields
]
What I added: Three new test cases in test_keywords.py to provide regression coverage for PlutusData constructor keyword arguments:
test_plutusdata_all_keywords — all keyword args, verifies correct field value is returnedtest_plutusdata_keyword_position_independence — keywords in non-declaration ordertest_plutusdata_incorrect_keywords_failure — wrong keyword name correctly raises an erroropshin_opshin-387
included in score