From 1526e1bb0082f110ed82277f98d400e5cf5c27c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 3 Mar 2024 02:45:51 +0100 Subject: [PATCH 1/6] Use unified opshin logger everywhere --- examples/datum_cast.py | 3 ++- opshin/__main__.py | 12 +++++++++++- opshin/compiler.py | 8 +++----- opshin/optimize/optimize_const_folding.py | 8 ++++---- opshin/type_inference.py | 2 -- opshin/typed_ast.py | 2 -- opshin/types.py | 4 +--- opshin/util.py | 5 +++++ 8 files changed, 26 insertions(+), 18 deletions(-) diff --git a/examples/datum_cast.py b/examples/datum_cast.py index 370931e5..0ca3498a 100644 --- a/examples/datum_cast.py +++ b/examples/datum_cast.py @@ -22,8 +22,9 @@ def validator(d: Anything, r: Anything) -> bytes: e: BatchOrder = d # this casts the input to bytes - in the type system! In the contract this is a no-op r2: bytes = r + r3: bytes = b"1234" c = e.sender.payment_credential # this actually checks that c is of the type PubKeyCredential if isinstance(c, PubKeyCredential): res = c.credential_hash - return res + r2 + return res + r2 + r3 diff --git a/opshin/__main__.py b/opshin/__main__.py index 2a0e4b3f..7a1c6986 100644 --- a/opshin/__main__.py +++ b/opshin/__main__.py @@ -1,6 +1,7 @@ import inspect import argparse +import logging import tempfile import cbor2 @@ -28,7 +29,7 @@ Purpose, PlutusContract, ) -from .util import CompilerError, data_from_json +from .util import CompilerError, data_from_json, OPSHIN_LOG_HANDLER from .prelude import ScriptContext @@ -227,6 +228,8 @@ def check_params( def perform_command(args): + if args.verbose: + logging.basicConfig(level=logging.DEBUG) command = Command(args.command) purpose = Purpose(args.purpose) input_file = args.input_file if args.input_file != "-" else sys.stdin @@ -466,6 +469,12 @@ def parse_args(): action="version", version=f"opshin {__version__} {__copyright__}", ) + a.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose logging.", + ) a.add_argument( "--recursion-limit", default=sys.getrecursionlimit(), @@ -481,6 +490,7 @@ def main(): if Command(args.command) != Command.lint: perform_command(args) else: + OPSHIN_LOG_HANDLER.stream = sys.stdout try: perform_command(args) except Exception as e: diff --git a/opshin/compiler.py b/opshin/compiler.py index 7b78c915..03aa12e0 100644 --- a/opshin/compiler.py +++ b/opshin/compiler.py @@ -37,8 +37,6 @@ RawPlutoExpr, ) -_LOGGER = logging.getLogger(__name__) - BoolOpMap = { And: plt.And, @@ -230,7 +228,7 @@ def visit_Module(self, node: TypedModule) -> plt.AST: else: possible_types = [second_last_arg.typ] if any(isinstance(t, UnitType) for t in possible_types): - _LOGGER.warning( + OPSHIN_LOGGER.warning( "The redeemer is annotated to be 'None'. This value is usually encoded in PlutusData with constructor id 0 and no fields. If you want the script to double function as minting and spending script, annotate the second argument with 'NoRedeemer'." ) enable_double_func_mint_spend = not any( @@ -239,7 +237,7 @@ def visit_Module(self, node: TypedModule) -> plt.AST: for t in possible_types ) if not enable_double_func_mint_spend: - _LOGGER.warning( + OPSHIN_LOGGER.warning( "The second argument to the validator function potentially has constructor id 0. The validator will not be able to double function as minting script and spending script." ) @@ -331,7 +329,7 @@ def visit_Constant(self, node: TypedConstant) -> plt.AST: except ValueError: pass else: - _LOGGER.warning( + OPSHIN_LOGGER.warning( f"The string {node.value} looks like it is supposed to be a hex-encoded bytestring but is actually utf8-encoded. Try using `bytes.fromhex('{node.value.decode()}')` instead." ) plt_val = plt.UPLCConstant(rec_constant_map(node.value)) diff --git a/opshin/optimize/optimize_const_folding.py b/opshin/optimize/optimize_const_folding.py index d04c85ef..bc8862c4 100644 --- a/opshin/optimize/optimize_const_folding.py +++ b/opshin/optimize/optimize_const_folding.py @@ -19,7 +19,7 @@ Pre-evaluates constant statements """ -_LOGGER = logging.getLogger(__name__) +OPSHIN_LOGGER = logging.getLogger(__name__) ACCEPTED_ATOMIC_TYPES = [ int, @@ -227,7 +227,7 @@ def update_constants(self, node): try: exec(unparse(node), g, l) except Exception as e: - _LOGGER.debug(e) + OPSHIN_LOGGER.debug(e) else: # the class is defined and added to the globals self.scopes_constants[-1].update(l) @@ -259,7 +259,7 @@ def visit_FunctionDef(self, node: FunctionDef) -> FunctionDef: # we need to pass the global dict as local dict here to make closures possible (rec functions) exec(unparse(node), g, g) except Exception as e: - _LOGGER.debug(e) + OPSHIN_LOGGER.debug(e) else: # the class is defined and added to the globals self.scopes_constants[-1][node.name] = g[node.name] @@ -331,7 +331,7 @@ def generic_visit(self, node: AST): l = self._constant_vars() node_eval = eval(node_source, g, l) except Exception as e: - _LOGGER.debug(e) + OPSHIN_LOGGER.debug(e) return node if any( diff --git a/opshin/type_inference.py b/opshin/type_inference.py index 63f7688c..00db62ed 100644 --- a/opshin/type_inference.py +++ b/opshin/type_inference.py @@ -23,8 +23,6 @@ # from frozendict import frozendict -_LOGGER = logging.getLogger(__name__) - INITIAL_SCOPE = { # class annotations diff --git a/opshin/typed_ast.py b/opshin/typed_ast.py index 134b349c..ae2a81de 100644 --- a/opshin/typed_ast.py +++ b/opshin/typed_ast.py @@ -1,7 +1,5 @@ from .types import * -_LOGGER = logging.getLogger(__name__) - class TypedAST(AST): typ: Type diff --git a/opshin/types.py b/opshin/types.py index 1ceda4d4..e970773c 100644 --- a/opshin/types.py +++ b/opshin/types.py @@ -11,8 +11,6 @@ from .util import * -_LOGGER = logging.getLogger(__name__) - class TypeInferenceError(AssertionError): pass @@ -244,7 +242,7 @@ def cmp(self, op: cmpop, o: "Type") -> plt.AST: return super().cmp(op, o) def stringify(self, recursive: bool = False) -> plt.AST: - _LOGGER.warning( + OPSHIN_LOGGER.warning( "Serializing AnyType will result in RawPlutusData (CBOR representation) to be printed without additional type information. Annotate types where possible to avoid this warning." ) return OLambda( diff --git a/opshin/util.py b/opshin/util.py index 218dc45c..bde231d5 100644 --- a/opshin/util.py +++ b/opshin/util.py @@ -1,6 +1,7 @@ from _ast import Name, Store, ClassDef, FunctionDef, Load from collections import defaultdict from copy import copy, deepcopy +import logging import typing @@ -15,6 +16,10 @@ import pluthon as plt from hashlib import sha256 +OPSHIN_LOGGER = logging.getLogger("opshin") +OPSHIN_LOG_HANDLER = logging.StreamHandler() +OPSHIN_LOGGER.addHandler(OPSHIN_LOG_HANDLER) + def distinct(xs: list): """Returns true iff the list consists of distinct elements""" From 0a1bbadf4336007e472dc79406c4f62d64cfee62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 3 Mar 2024 03:03:52 +0100 Subject: [PATCH 2/6] Properly log warnings with the node annotation --- opshin/__main__.py | 19 +++++++++++++++++++ opshin/util.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/opshin/__main__.py b/opshin/__main__.py index 7a1c6986..2a210343 100644 --- a/opshin/__main__.py +++ b/opshin/__main__.py @@ -488,9 +488,28 @@ def main(): args = parse_args() sys.setrecursionlimit(args.recursion_limit) if Command(args.command) != Command.lint: + OPSHIN_LOG_HANDLER.setFormatter( + logging.Formatter( + f"%(levelname)s for {args.input_file}:%(lineno)d %(message)s" + ) + ) perform_command(args) else: OPSHIN_LOG_HANDLER.stream = sys.stdout + if args.output_format_json: + OPSHIN_LOG_HANDLER.setFormatter( + logging.Formatter( + '{"line":%(lineno)d,"column":%(col_offset)d,"error_class":"%(levelname)s","message":"%(message)s"}' + ) + ) + else: + OPSHIN_LOG_HANDLER.setFormatter( + logging.Formatter( + args.input_file + + ":%(lineno)d:%(col_offset)d:%(levelname)s: %(message)s" + ) + ) + try: perform_command(args) except Exception as e: diff --git a/opshin/util.py b/opshin/util.py index bde231d5..70dedb5f 100644 --- a/opshin/util.py +++ b/opshin/util.py @@ -21,6 +21,36 @@ OPSHIN_LOGGER.addHandler(OPSHIN_LOG_HANDLER) +class FileContextFilter(logging.Filter): + """ + This is a filter which injects contextual information into the log. + + The information is about the currently inspected AST node. + The information needs to be updated inside the NodeTransformer and NodeVisitor classes. + """ + + file_name = "unknown" + node: ast.AST = None + + def filter(self, record): + + if self.node is None: + record.lineno = 1 + record.col_offset = 0 + record.end_lineno = 1 + record.end_col_offset = 0 + else: + record.lineno = self.node.lineno + record.col_offset = self.node.col_offset + record.end_lineno = self.node.end_lineno + record.end_col_offset = self.node.end_col_offset + return True + + +OPSHIN_LOG_CONTEXT_FILTER = FileContextFilter() +OPSHIN_LOG_HANDLER.addFilter(OPSHIN_LOG_CONTEXT_FILTER) + + def distinct(xs: list): """Returns true iff the list consists of distinct elements""" return len(xs) == len(set(xs)) @@ -29,6 +59,7 @@ def distinct(xs: list): class TypedNodeTransformer(ast.NodeTransformer): def visit(self, node): """Visit a node.""" + OPSHIN_LOG_CONTEXT_FILTER.node = node node_class_name = node.__class__.__name__ if node_class_name.startswith("Typed"): node_class_name = node_class_name[len("Typed") :] @@ -40,6 +71,7 @@ def visit(self, node): class TypedNodeVisitor(ast.NodeVisitor): def visit(self, node): """Visit a node.""" + OPSHIN_LOG_CONTEXT_FILTER.node = node node_class_name = node.__class__.__name__ if node_class_name.startswith("Typed"): node_class_name = node_class_name[len("Typed") :] @@ -59,6 +91,7 @@ class CompilingNodeTransformer(TypedNodeTransformer): step = "Node transformation" def visit(self, node): + OPSHIN_LOG_CONTEXT_FILTER.node = node try: return super().visit(node) except Exception as e: From 295a98ecb6d23939a200d63e893955553dbd1539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 3 Mar 2024 03:05:44 +0100 Subject: [PATCH 3/6] Fix logger usage in const folding --- opshin/optimize/optimize_const_folding.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/opshin/optimize/optimize_const_folding.py b/opshin/optimize/optimize_const_folding.py index bc8862c4..d6ae6e5a 100644 --- a/opshin/optimize/optimize_const_folding.py +++ b/opshin/optimize/optimize_const_folding.py @@ -19,8 +19,6 @@ Pre-evaluates constant statements """ -OPSHIN_LOGGER = logging.getLogger(__name__) - ACCEPTED_ATOMIC_TYPES = [ int, str, From 4e893248e5a19b24614e91d664591a08d7f7c06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 3 Mar 2024 03:19:32 +0100 Subject: [PATCH 4/6] Fix --- opshin/optimize/optimize_const_folding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opshin/optimize/optimize_const_folding.py b/opshin/optimize/optimize_const_folding.py index d6ae6e5a..0f1ecfcf 100644 --- a/opshin/optimize/optimize_const_folding.py +++ b/opshin/optimize/optimize_const_folding.py @@ -12,7 +12,7 @@ except NameError: from astunparse import unparse -from ..util import CompilingNodeTransformer, CompilingNodeVisitor +from ..util import CompilingNodeTransformer, CompilingNodeVisitor, OPSHIN_LOGGER from ..type_inference import INITIAL_SCOPE """ From 92b87e077dd662f17b9dce2e4915b47f5a15be8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 3 Mar 2024 18:18:18 +0100 Subject: [PATCH 5/6] Restore original functioning of datum cast --- examples/datum_cast.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/datum_cast.py b/examples/datum_cast.py index 0ca3498a..370931e5 100644 --- a/examples/datum_cast.py +++ b/examples/datum_cast.py @@ -22,9 +22,8 @@ def validator(d: Anything, r: Anything) -> bytes: e: BatchOrder = d # this casts the input to bytes - in the type system! In the contract this is a no-op r2: bytes = r - r3: bytes = b"1234" c = e.sender.payment_credential # this actually checks that c is of the type PubKeyCredential if isinstance(c, PubKeyCredential): res = c.credential_hash - return res + r2 + r3 + return res + r2 From 09ed8ef9c982fb4d8aa38b10a2d20a7e406308f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 3 Mar 2024 18:36:06 +0100 Subject: [PATCH 6/6] Fix test case for linting --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0467ed1a..9492ad6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,7 +43,9 @@ script: - > coverage run -a --source=opshin -m opshin build spending examples/smart_contracts/wrapped_token.py '{"bytes": "ae810731b5d21c0d182d89c60a1eff7095dffd1c0dce8707a8611099"}' '{"bytes": "4d494c4b"}' '{"int": 1000000}' --force-three-params - > - test ! -n "$(coverage run -a --source=opshin -m opshin lint any examples/smart_contracts/wrapped_token.py)" + test ! -n "$(coverage run -a --source=opshin -m opshin lint any examples/smart_contracts/always_true.py)" +- > + test -n "$(coverage run -a --source=opshin -m opshin lint any examples/smart_contracts/wrapped_token.py)" - > test -n "$(coverage run -a --source=opshin -m opshin lint any examples/broken.py)" - >