-
-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unions of PlutusData and BuiltinData #397
Conversation
bind_self = node.func.typ.typ.bind_self | ||
bound_vs = sorted(list(node.func.typ.typ.bound_vars.keys())) | ||
args = [] | ||
for a, t in zip(node.args, node.func.typ.typ.argtyps): | ||
for i, (a, t) in enumerate(zip(node.args, node.func.typ.typ.argtyps)): | ||
# now impl_from_args has been chosen, skip type arg | ||
if ( | ||
hasattr(node.func, "orig_id") | ||
and node.func.orig_id == "isinstance" | ||
and i == 1 | ||
): | ||
continue | ||
assert isinstance(t, InstanceType) | ||
# pass in all arguments evaluated with the statemonad | ||
a_int = self.visit(a) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of assert isinstance(t, InstanceType)
for type checking is not ideal for production code. If the assertion fails, it will raise an AssertionError
, which may not provide sufficient context for debugging.
Recommendation: Replace the assert
statement with a proper type check and raise a more informative exception if the type check fails. This will improve error handling and maintainability.
def impl_from_args(self, args: typing.List[Type]) -> plt.AST: | ||
if isinstance(args[1], IntegerType): | ||
return OLambda( | ||
["x"], | ||
plt.ChooseData( | ||
OVar("x"), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(True), | ||
plt.Bool(False), | ||
), | ||
) | ||
elif isinstance(args[1], ByteStringType): | ||
return OLambda( | ||
["x"], | ||
plt.ChooseData( | ||
OVar("x"), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(True), | ||
), | ||
) | ||
elif isinstance(args[1], RecordType): | ||
return OLambda( | ||
["x"], | ||
plt.ChooseData( | ||
OVar("x"), | ||
plt.Bool(True), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
), | ||
) | ||
elif isinstance(args[1], ListType): | ||
return OLambda( | ||
["x"], | ||
plt.ChooseData( | ||
OVar("x"), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(True), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
), | ||
) | ||
elif isinstance(args[1], DictType): | ||
return OLambda( | ||
["x"], | ||
plt.ChooseData( | ||
OVar("x"), | ||
plt.Bool(False), | ||
plt.Bool(True), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
plt.Bool(False), | ||
), | ||
) | ||
else: | ||
raise NotImplementedError( | ||
f"Only isinstance for byte, int, Plutus Dataclass types are supported" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The impl_from_args
method in the IsinstanceImpl
class has a significant issue with maintainability and extensibility. The method uses a series of elif
statements to handle different types, which can become cumbersome as more types are added. This approach is not scalable and makes the code harder to maintain.
Recommended Solution:
Refactor the method to use a dictionary mapping types to their corresponding OLambda
implementations. This will make the code more modular and easier to extend in the future.
n.skip_next = True | ||
return self.visit(BoolOp(And(), [n, ntc])) | ||
else: | ||
return ntc | ||
try: | ||
tc.func = self.visit(node.func) | ||
except Exception as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The try
block at the end of the fragment catches a generic Exception
, which can obscure the root cause of errors and make debugging difficult. Catching all exceptions is generally not recommended unless you re-raise the exception or handle it in a very specific way.
Recommended Solution: Catch more specific exceptions that you expect might be raised in this block. If you must catch all exceptions, consider logging the exception details before re-raising it.
I think it would be best to only allow joining Dict[Any, Any] and List[Any] in these unions, ideally making dict and list synonyms of this type. If a user really knows what they are doing maybe an unsafe cast from dict to Dict[int, int] can then be allowed? Also it's important that str is not allowed in these unions but this seems to be the case already. What happens when a function has parameter type Union[A, int] and is passed an int/A (where the type of that variable was just int / A before)? Need to wrap in PlutusData in those cases |
["x"], | ||
plt.ChooseData( | ||
OVar("x"), | ||
plt.Bool(True), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the take on isinstance as a polymorphic impl.
Is this enough though to ensure some x is the correct record type and not just any record type? isinstance(x, B) should not always return true for x of type Union[A, B]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are both builtin and plutusData types in a union, then when checking plutusData it first checks its a record type and then invokes the old functionality of checking CONSTR_ID:
opshin/opshin/type_inference.py
Lines 962 to 981 in e0e5d25
ntc = Compare( | |
left=Attribute(tc.args[0], "CONSTR_ID"), | |
ops=[Eq()], | |
comparators=[Constant(target_class.record.constructor)], | |
) | |
custom_fix_missing_locations(ntc, node) | |
ntc = self.visit(ntc) | |
ntc.typ = BoolInstanceType | |
ntc.typechecks = TypeCheckVisitor(self.allow_isinstance_anything).visit(tc) | |
if isinstance(tc.args[0].typ.typ, UnionType) and any( | |
[ | |
isinstance(a, (IntegerType, ByteStringType, ListType, DictType)) | |
for a in tc.args[0].typ.typ.typs | |
] | |
): | |
n = copy(node) | |
n.skip_next = True | |
return self.visit(BoolOp(And(), [n, ntc])) | |
else: | |
return ntc |
bound_vars={ | ||
v: self.variable_type(v) | ||
for v in externally_bound_vars(node) | ||
if not v in ["List", "Dict"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where does this come from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither List or Dict appear in self.scopes
(so self.variable_type will fail) but end up in externally_bound_vars
. I'm not sure what the most 'correct' solution here is. I'm guessing there would be problems if someone tried to define there own class with the name Test
and Dict
.
I've just realised too that
So would the following be what you are looking for:
Alternatively working towards using Let me know what you think. |
assert ( | ||
isinstance(elt.slice, Name) | ||
and elt.slice.orig_id == "Anything" | ||
), f"Only List[Anything] is supported in Unions. Received List[{elt.slice.orig_id}]." | ||
if isinstance(elt, Subscript) and elt.value.id == "Dict": | ||
assert all( | ||
isinstance(e, Name) and e.orig_id == "Anything" | ||
for e in elt.slice.elts | ||
), f"Only Dict[Anything, Anything] or Dict is supported in Unions. Received Dict[{elt.slice.elts[0].orig_id}, {elt.slice.elts[1].orig_id}]." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of assert
statements for type checking and validation can be problematic in production code. Assertions can be disabled with the -O
(optimize) flag when running Python, which means these checks might not be executed, potentially leading to unexpected behavior or security issues.
Recommended Solution: Replace assert
statements with explicit exception handling, such as raising TypeError
or ValueError
with appropriate error messages.
Nice catch! If isinstance(x, Dict) is valid python that should be fine, otherwise isinstance(x, dict) would be required. |
assert ( | ||
func.returns is None or func.returns.id != n.name | ||
), "Invalid Python, class name is undefined at this stage" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of assert
statements for type checking and validation can be problematic in production code. Assertions can be disabled with the -O
(optimize) flag when running Python, which means these checks might not be executed, potentially leading to unexpected behavior or security issues.
Recommended Solution: Replace assert
statements with explicit exception handling, such as raising TypeError
or ValueError
with appropriate error messages.
except Exception: | ||
# if this fails raise original error | ||
raise e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The try
block catches a generic Exception
, which can obscure the root cause of errors and make debugging difficult. Catching all exceptions is generally not recommended unless you re-raise the exception or handle it in a very specific way.
Recommended Solution: Catch more specific exceptions that you expect might be raised in this block. If you must catch all exceptions, consider logging the exception details before re-raising it.
I've had some free time so I've come back to this.
Only List[Anything] and Dict[Anything, Anything] are accepted in a Union now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great now. Just some small comments.
opshin/tests/test_Unions.py
Outdated
return 6 | ||
elif isinstance(x, bytes): | ||
return 7 | ||
elif isinstance(x, list): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to make really sure that Python and OpShin map to the same operaton here (i.e. that List is allowed for isinstance)
elif isinstance(x, list): | |
elif isinstance(x, List): |
opshin/tests/test_Unions.py
Outdated
return 7 | ||
elif isinstance(x, list): | ||
return 8 | ||
elif isinstance(x, dict): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
elif isinstance(x, dict): | |
elif isinstance(x, Dict): |
Implemented those requested changes. |
As we have discovered a few missing edge cases here (see #403) I will not be able to pay out the full bug bounty for this PR. If @SCMusson is willing to supply the additional fixes I would simply offer additional time to resolve this and claim the full reward, otherwise we will have to decide a reasonable split with another developer. |
I plan to return to this problem however I will be short of available time for the next week after which I will be able to devote some time to fixing this. |
I think this is now fixed in #403 |
Targeting issue #367
The following would now compile
I think it can't currently distinguish between
Dict[int, int]
andDict[int, bytes]
for example. I'll work on that if you think this is heading in the correct direction. I also need to sit down and think of what edge cases might break this and write some more tests.