Skip to content

Commit

Permalink
Merge pull request #126 from OpShin/feat/allow_upcasting
Browse files Browse the repository at this point in the history
Fix upcasting by using annotated assignment
  • Loading branch information
nielstron committed Apr 11, 2023
2 parents 137444d + 61f5799 commit 4ce0914
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 10 deletions.
6 changes: 6 additions & 0 deletions opshin/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,12 @@ def visit_AnnAssign(self, node: AnnAssign) -> plt.AST:
# we need to map this as it will originate from PlutusData
# AnyType is the only type other than the builtin itself that can be cast to builtin values
val = transform_ext_params_map(node.target.typ)(val)
if isinstance(node.target.typ, InstanceType) and isinstance(
node.target.typ.typ, AnyType
):
# we need to map this back as it will be treated as PlutusData
# AnyType is the only type other than the builtin itself that can be cast to from builtin values
val = transform_output_map(node.value.typ)(val)
return plt.Lambda(
[STATEMONAD],
extend_statemonad(
Expand Down
30 changes: 28 additions & 2 deletions opshin/prelude.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ def resolve_spent_utxo(txins: List[TxInInfo], p: Spending) -> TxOut:
return [txi.resolved for txi in txins if txi.out_ref == p.tx_out_ref][0]


def resolve_datum(txout: TxOut, tx_info: TxInfo) -> BuiltinData:
"""Returns the datum attached to a given transaction output, independent of whether it was inlined or embedded."""
def resolve_datum_unsafe(txout: TxOut, tx_info: TxInfo) -> BuiltinData:
"""
Returns the datum attached to a given transaction output, independent of whether it was inlined or embedded.
Raises an exception if no datum was attached.
"""
attached_datum = txout.datum
if isinstance(attached_datum, SomeOutputDatumHash):
# TODO can we raise a KeyError here if not found?
Expand All @@ -77,3 +80,26 @@ def resolve_datum(txout: TxOut, tx_info: TxInfo) -> BuiltinData:
# no datum attached
assert False, "No datum was attached to the given transaction output"
return res


def resolve_datum(
txout: TxOut, tx_info: TxInfo
) -> Union[SomeOutputDatum, NoOutputDatum]:
"""
Returns SomeOutputDatum with the datum attached to a given transaction output,
independent of whether it was inlined or embedded, if there was an attached datum.
Otherwise it returns NoOutputDatum.
"""
attached_datum = txout.datum
if isinstance(attached_datum, SomeOutputDatumHash):
if attached_datum.datum_hash in tx_info.data.keys():
res: Union[SomeOutputDatum, NoOutputDatum] = SomeOutputDatum(
tx_info.data.get(attached_datum.datum_hash, Nothing())
)
else:
assert (
False
), "Could not resolve attached datum from datum hash, because it was not embedded in the transaction"
else:
res: Union[SomeOutputDatum, NoOutputDatum] = attached_datum
return res
3 changes: 3 additions & 0 deletions opshin/rewrite/rewrite_import_hashlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def attribute(self, attr) -> plt.AST:
return plt.Lambda(["self"], plt.Var("self"))
raise NotImplementedError("HashType only has attribute 'digest'")

def __ge__(self, other):
return isinstance(other, HashType)


HashInstanceType = InstanceType(HashType())

Expand Down
43 changes: 39 additions & 4 deletions opshin/tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,14 +703,49 @@ def validator(x: Anything) -> int:
res = uplc_eval(uplc.Apply(code, uplc.PlutusInteger(0))).value
self.assertEqual(res, 0)

@unittest.expectedFailure
def test_typecast_int_anything(self):
# this should not compile, we can not upcast with this notation
# up to discussion whether this should be allowed, but i.g. it should never be necessary or useful
# this should compile, it happens implicitly anyways when calling a function with Any parameters
source_code = """
def validator(x: int) -> Anything:
b: Anything = x
return x
return b
"""
ast = compiler.parse(source_code)
code = compiler.compile(ast).compile()
res = uplc_eval(uplc.Apply(code, uplc.PlutusInteger(0))).value
self.assertEqual(res, 0)

def test_typecast_int_anything_int(self):
source_code = """
def validator(x: int) -> Anything:
b: Anything = x
c: int = b
return c + 1
"""
ast = compiler.parse(source_code)
code = compiler.compile(ast).compile()
res = uplc_eval(uplc.Apply(code, uplc.PlutusInteger(0))).value
self.assertEqual(res, 1)

def test_typecast_anything_int_anything(self):
source_code = """
def validator(x: Anything) -> Anything:
b: int = x
c: Anything = b + 1
return c
"""
ast = compiler.parse(source_code)
code = compiler.compile(ast).compile()
res = uplc_eval(uplc.Apply(code, uplc.PlutusInteger(0))).value
self.assertEqual(res, 1)

@unittest.expectedFailure
def test_typecast_int_str(self):
# this should compile, the two types are unrelated and there is no meaningful way to cast them either direction
source_code = """
def validator(x: int) -> str:
b: str = x
return b
"""
ast = compiler.parse(source_code)
code = compiler.compile(ast)
Expand Down
12 changes: 8 additions & 4 deletions opshin/type_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ def exit_scope(self):
self.scopes.pop()

def set_variable_type(self, name: str, typ: Type, force=False):
if not force and name in self.scopes[-1] and typ != self.scopes[-1][name]:
if not force and name in self.scopes[-1] and self.scopes[-1][name] != typ:
if self.scopes[-1][name] >= typ:
# the specified type is broader, we pass on this
return
raise TypeInferenceError(
f"Type {self.scopes[-1][name]} of variable {name} in local scope does not match inferred type {typ}"
)
Expand Down Expand Up @@ -209,9 +212,10 @@ def visit_AnnAssign(self, node: AnnAssign) -> TypedAnnAssign:
node.target.id, InstanceType(typed_ass.annotation), force=True
)
typed_ass.target = self.visit(node.target)
assert typed_ass.value.typ >= InstanceType(
typed_ass.annotation
), "Can only downcast to a specialized type"
assert (
typed_ass.value.typ >= InstanceType(typed_ass.annotation)
or InstanceType(typed_ass.annotation) >= typed_ass.value.typ
), "Can only cast between related types"
return typed_ass

def visit_If(self, node: If) -> TypedIf:
Expand Down

0 comments on commit 4ce0914

Please sign in to comment.