From 8aa5d15e07762f7a3cc7b0ab76a7a6ffb78dcd30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 5 Mar 2023 18:41:24 +0100 Subject: [PATCH 01/20] Fix eval and eval_uplc --- .travis.yml | 4 ++-- eopsin/__main__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c7714f30..fa3bb33c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,11 @@ script: - coverage run --source=eopsin setup.py test # the samples from the README file - > - coverage run -a --source=eopsin -m eopsin eval examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 0, \"fields\": []}" + coverage run -a --source=eopsin -m eopsin eval examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 6, \"fields\": []}" - > coverage run -a --source=eopsin -m eopsin compile examples/smart_contracts/assert_sum.py > assert_sum.uplc - > - coverage run -a --source=eopsin -m eopsin eval_uplc examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 0, \"fields\": []}" + coverage run -a --source=eopsin -m eopsin eval_uplc examples/smart_contracts/assert_sum.py "{\"int\": 4}" "{\"int\": 38}" "{\"constructor\": 6, \"fields\": []}" - > coverage run -a --source=eopsin -m eopsin compile_pluto examples/smart_contracts/assert_sum.py - > diff --git a/eopsin/__main__.py b/eopsin/__main__.py index 48c2632f..9ba40c6e 100644 --- a/eopsin/__main__.py +++ b/eopsin/__main__.py @@ -212,7 +212,7 @@ def main(): print("------------------") assert isinstance(code, uplc.ast.Program) try: - ret = uplc.dumps(uplc.eval(f)) + ret = uplc.dumps(uplc.eval(code)) except Exception as e: print("An exception was raised") ret = e From 301ab13d3bc1f037022911e99c9bfb8d850628e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 5 Mar 2023 18:41:48 +0100 Subject: [PATCH 02/20] Version bump - optimized deadcode elimination - CLI fix for eval / eval_uplc --- eopsin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eopsin/__init__.py b/eopsin/__init__.py index 8b7c27f5..be4d7fea 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 3) +VERSION = (0, 9, 4) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" From 1eb05ef20ffee558a3c89531f6fef6839440cdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 6 Mar 2023 23:26:18 +0100 Subject: [PATCH 03/20] Add link to eopsin-example --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 4d39a24b..561051b3 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,16 @@ If the program compiles then: ### Getting Started +#### Example repository + +Check out this example repository for a quick start in setting up a development environment +and compiling some sample contracts yours: + +https://github.com/ImperatorLang/eopsin-example + +You can replace the contracts in your local copy of the repository with code from the +`examples` section here to start exploring different contracts. + #### Developer Community and Questions This repository contains a discussions page. @@ -54,6 +64,7 @@ A short non-complete introduction in starting to write smart contracts follows. 2. Make sure your contract is valid python and the types check out. Write simple contracts first and run them using `eopsin eval` to get a feeling for how they work. 3. Make sure your contract is valid eopsin code. Run `eopsin compile` and look at the compiler erros for guidance along what works and doesn't work and why. 4. Dig into the [`examples`](https://github.com/ImperatorLang/eopsin/tree/master/examples) to understand common patterns. Check out the [`prelude`](https://imperatorlang.github.io/eopsin/eopsin/prelude.html) for understanding how the Script Context is structured and how complex datums are defined. +5. Check out the [sample repository](https://github.com/ImperatorLang/eopsin-example) to find a sample setup for developing your own contract. A simple contract called the "Gift Contract" verifies that only specific wallets can withdraw money. They are authenticated by a signature. From 4881daa42783bd09038834d7b7ffbc523173dd54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 00:38:43 +0100 Subject: [PATCH 04/20] Add link to the EUTxO introduction --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 561051b3..11ae5b4c 100644 --- a/README.md +++ b/README.md @@ -60,15 +60,17 @@ python3.8 -m pip install eopsin-lang A short non-complete introduction in starting to write smart contracts follows. -1. Make sure you understand python. Eopsin works like python and uses python. There are tons of tutorials for python, choose what suits you best. -2. Make sure your contract is valid python and the types check out. Write simple contracts first and run them using `eopsin eval` to get a feeling for how they work. -3. Make sure your contract is valid eopsin code. Run `eopsin compile` and look at the compiler erros for guidance along what works and doesn't work and why. -4. Dig into the [`examples`](https://github.com/ImperatorLang/eopsin/tree/master/examples) to understand common patterns. Check out the [`prelude`](https://imperatorlang.github.io/eopsin/eopsin/prelude.html) for understanding how the Script Context is structured and how complex datums are defined. -5. Check out the [sample repository](https://github.com/ImperatorLang/eopsin-example) to find a sample setup for developing your own contract. +1. Make sure you understand EUTxOs, Addresses, Validators etc on Cardano. [There is a wonderful crashcourse by @KtorZ](https://aiken-lang.org/fundamentals/eutxo). The contract will work on these concepts +2. Make sure you understand python. Eopsin works like python and uses python. There are tons of tutorials for python, choose what suits you best. +3. Make sure your contract is valid python and the types check out. Write simple contracts first and run them using `eopsin eval` to get a feeling for how they work. +4. Make sure your contract is valid eopsin code. Run `eopsin compile` and look at the compiler erros for guidance along what works and doesn't work and why. +5. Dig into the [`examples`](https://github.com/ImperatorLang/eopsin/tree/master/examples) to understand common patterns. Check out the [`prelude`](https://imperatorlang.github.io/eopsin/eopsin/prelude.html) for understanding how the Script Context is structured and how complex datums are defined. +6. Check out the [sample repository](https://github.com/ImperatorLang/eopsin-example) to find a sample setup for developing your own contract. A simple contract called the "Gift Contract" verifies that only specific wallets can withdraw money. They are authenticated by a signature. -See the [tutorial by `pycardano`](https://pycardano.readthedocs.io/en/latest/guides/plutus.html) for explanations on what each of the parameters to the validator means +If you don't understand what a pubkeyhash is and how this validates anything, check out [this gentle introduction into Cardanos EUTxO](https://aiken-lang.org/fundamentals/eutxo) +Also see the [tutorial by `pycardano`](https://pycardano.readthedocs.io/en/latest/guides/plutus.html) for explanations on what each of the parameters to the validator means and how to build transactions with the contract. ```python3 From 098b56653e46ec12a074f90a6b66fc9f188ccc25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 00:53:55 +0100 Subject: [PATCH 05/20] Bump version with improved documentation --- README.md | 15 ++++++++++++++- eopsin/__init__.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11ae5b4c..5d30438d 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,25 @@ A short non-complete introduction in starting to write smart contracts follows. 5. Dig into the [`examples`](https://github.com/ImperatorLang/eopsin/tree/master/examples) to understand common patterns. Check out the [`prelude`](https://imperatorlang.github.io/eopsin/eopsin/prelude.html) for understanding how the Script Context is structured and how complex datums are defined. 6. Check out the [sample repository](https://github.com/ImperatorLang/eopsin-example) to find a sample setup for developing your own contract. + +To summarized, a smart contract in eopsin is defined by the function `validator` in your contract file. +The function validates that a specific value can be spent, minted, burned, withdrawn etc, depending +on where it is invoked/used as a credential. +If the function fails (i.e. raises an error of any kind such as a `KeyError` or `AssertionError`) +the validation is denied, and the funds can not be spent, minted, burned etc. + +> There is a subtle difference here in comparison to most other Smart Contract languages. +> In eopsin a validator may return anything (in particular also `False`) - as long as it does not fail, the execution is considered valid. +> This is more similar to how contracts in Solidity always pass, unless they run out of gas or hit an error. +> So make sure to `assert` what you want to ensure to hold for validation! + A simple contract called the "Gift Contract" verifies that only specific wallets can withdraw money. They are authenticated by a signature. -If you don't understand what a pubkeyhash is and how this validates anything, check out [this gentle introduction into Cardanos EUTxO](https://aiken-lang.org/fundamentals/eutxo) +If you don't understand what a pubkeyhash is and how this validates anything, check out [this gentle introduction into Cardanos EUTxO](https://aiken-lang.org/fundamentals/eutxo). Also see the [tutorial by `pycardano`](https://pycardano.readthedocs.io/en/latest/guides/plutus.html) for explanations on what each of the parameters to the validator means and how to build transactions with the contract. + ```python3 from eopsin.prelude import * diff --git a/eopsin/__init__.py b/eopsin/__init__.py index be4d7fea..55e5f927 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 4) +VERSION = (0, 9, 5) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" From ab18fa39e876498a37315b00d20a6829cecb9439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 00:55:15 +0100 Subject: [PATCH 06/20] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d30438d..5871dd78 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ A short non-complete introduction in starting to write smart contracts follows. 6. Check out the [sample repository](https://github.com/ImperatorLang/eopsin-example) to find a sample setup for developing your own contract. -To summarized, a smart contract in eopsin is defined by the function `validator` in your contract file. +In summary, a smart contract in eopsin is defined by the function `validator` in your contract file. The function validates that a specific value can be spent, minted, burned, withdrawn etc, depending on where it is invoked/used as a credential. If the function fails (i.e. raises an error of any kind such as a `KeyError` or `AssertionError`) From f451d75a3786e0df39b782a1478a15ff759c6c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 11:37:19 +0100 Subject: [PATCH 07/20] Fix empty lists of type dict --- eopsin/typed_ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eopsin/typed_ast.py b/eopsin/typed_ast.py index e7dbb96c..5a22c165 100644 --- a/eopsin/typed_ast.py +++ b/eopsin/typed_ast.py @@ -1023,7 +1023,7 @@ def empty_list(p: Type): el = empty_list(p.typ.typ) return plt.EmptyListList(uplc.BuiltinList([], el.sample_value)) if isinstance(p.typ, DictType): - plt.EmptyDataPairList() + return plt.EmptyDataPairList() if isinstance(p.typ, RecordType): return plt.EmptyDataList() raise NotImplementedError(f"Empty lists of type {p} can't be constructed yet") From 685b66e7a3000f2ec401a53d28d4a48a01a85c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 11:37:40 +0100 Subject: [PATCH 08/20] Bump version - fix creation of empty lists of type dict --- eopsin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eopsin/__init__.py b/eopsin/__init__.py index 55e5f927..b3d40938 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 5) +VERSION = (0, 9, 6) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" From 9d5898de4990ea613062761c90e4b497aa0395ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 11:49:21 +0100 Subject: [PATCH 09/20] Bump version - fix parsing json to plutus data --- eopsin/__init__.py | 2 +- eopsin/util.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/eopsin/__init__.py b/eopsin/__init__.py index b3d40938..2599873d 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 6) +VERSION = (0, 9, 7) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" diff --git a/eopsin/util.py b/eopsin/util.py index 9e7a7843..84e94f89 100644 --- a/eopsin/util.py +++ b/eopsin/util.py @@ -552,5 +552,7 @@ def data_from_json(j: typing.Dict[str, typing.Any]) -> uplc.PlutusData: if "map" in j: return uplc.PlutusMap({d["k"]: d["v"] for d in j["map"]}) if "constructor" in j and "fields" in j: - return uplc.PlutusConstr(j["constructor"], j["fields"]) + return uplc.PlutusConstr( + j["constructor"], list(map(data_from_json, j["fields"])) + ) raise NotImplementedError(f"Unknown datum representation {j}") From bc06ac82300cc16828a3836015112a0713efc0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 12:02:54 +0100 Subject: [PATCH 10/20] Fix empty data pair list list construction --- eopsin/__init__.py | 2 +- eopsin/typed_ast.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/eopsin/__init__.py b/eopsin/__init__.py index 2599873d..4949a3e5 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 7) +VERSION = (0, 9, 8) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" diff --git a/eopsin/typed_ast.py b/eopsin/typed_ast.py index 5a22c165..eb42e59f 100644 --- a/eopsin/typed_ast.py +++ b/eopsin/typed_ast.py @@ -1023,7 +1023,15 @@ def empty_list(p: Type): el = empty_list(p.typ.typ) return plt.EmptyListList(uplc.BuiltinList([], el.sample_value)) if isinstance(p.typ, DictType): - return plt.EmptyDataPairList() + return plt.EmptyListList( + uplc.BuiltinList( + [], + uplc.BuiltinPair( + uplc.PlutusConstr(0, FrozenList([])), + uplc.PlutusConstr(0, FrozenList([])), + ), + ) + ) if isinstance(p.typ, RecordType): return plt.EmptyDataList() raise NotImplementedError(f"Empty lists of type {p} can't be constructed yet") From d69ede221a58f2f57ff7dfe9fb4a3fe6bd514ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 15:05:31 +0100 Subject: [PATCH 11/20] Enable empty lists for anytype --- eopsin/typed_ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eopsin/typed_ast.py b/eopsin/typed_ast.py index eb42e59f..c907cf2a 100644 --- a/eopsin/typed_ast.py +++ b/eopsin/typed_ast.py @@ -1032,7 +1032,7 @@ def empty_list(p: Type): ), ) ) - if isinstance(p.typ, RecordType): + if isinstance(p.typ, RecordType) or isinstance(p.typ, AnyType): return plt.EmptyDataList() raise NotImplementedError(f"Empty lists of type {p} can't be constructed yet") From f9f5610ff2a6756f1197c456c95de24821e80436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 07:55:49 +0100 Subject: [PATCH 12/20] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5871dd78..3a941480 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ Others may be able to help you and will also benefit from the previously shared Check out the community [here](https://github.com/ImperatorLang/eopsin/discussions) +Alternatively, feel free to join the welcoming discord community +at the TxPipe server: https://discord.gg/2ETSZnQQH9 + #### Installation Install Python 3.8. Then run From bda917157b644e24b25644ed14e6ee7b2b05086d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 09:19:56 +0100 Subject: [PATCH 13/20] Change comment about casting PlutusData --- eopsin/type_inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eopsin/type_inference.py b/eopsin/type_inference.py index db5cb861..0198c86a 100644 --- a/eopsin/type_inference.py +++ b/eopsin/type_inference.py @@ -242,7 +242,7 @@ def visit_If(self, node: If) -> TypedIf: ), "Can only cast instances, not classes" assert isinstance( target_inst_class.typ, UnionType - ), "Can only cast instances of Union types" + ), "Can only cast instances of Union types of PlutusData" assert isinstance(target_class, RecordType), "Can only cast to PlutusData" assert ( target_class in target_inst_class.typ.typs From 09c4a2c9598f24e2480c530362c847b19187fb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 11:17:29 +0100 Subject: [PATCH 14/20] Fix import system --- eopsin/__main__.py | 14 ++++++---- eopsin/compiler.py | 4 +-- eopsin/rewrite/rewrite_import.py | 47 +++++++++++++++++++++++++++++--- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/eopsin/__main__.py b/eopsin/__main__.py index 9ba40c6e..7d4567c6 100644 --- a/eopsin/__main__.py +++ b/eopsin/__main__.py @@ -93,9 +93,11 @@ def main(): source_code = f.read() if command == Command.eval: - with open("__tmp_eopsin.py", "w") as fp: - fp.write(source_code) - sc = importlib.import_module("__tmp_eopsin") + if args.input_file == "-": + with open("__tmp_eopsin.py", "w") as fp: + fp.write(source_code) + input_file = "__tmp_eopsin.py" + sc = importlib.import_module(input_file) print("Starting execution") print("------------------") try: @@ -110,14 +112,16 @@ def main(): print("------------------") print(ret) - source_ast = compiler.parse(source_code) + source_ast = compiler.parse(source_code, filename=input_file) if command == Command.parse: print("Parsed successfully.") return try: - code = compiler.compile(source_ast, force_three_params=args.force_three_params) + code = compiler.compile( + source_ast, filename=input_file, force_three_params=args.force_three_params + ) except CompilerError as c: # Generate nice error message from compiler error if not isinstance(c.node, ast.Module): diff --git a/eopsin/compiler.py b/eopsin/compiler.py index af651962..3df4bd93 100644 --- a/eopsin/compiler.py +++ b/eopsin/compiler.py @@ -839,10 +839,10 @@ def generic_visit(self, node: AST) -> plt.AST: raise NotImplementedError(f"Can not compile {node}") -def compile(prog: AST, force_three_params=False): +def compile(prog: AST, filename=None, force_three_params=False): rewrite_steps = [ # Important to call this one first - it imports all further files - RewriteImport(), + RewriteImport(filename=filename), # Rewrites that simplify the python code RewriteAugAssign(), RewriteTupleAssign(), diff --git a/eopsin/rewrite/rewrite_import.py b/eopsin/rewrite/rewrite_import.py index 1c991208..1624fd9b 100644 --- a/eopsin/rewrite/rewrite_import.py +++ b/eopsin/rewrite/rewrite_import.py @@ -1,6 +1,8 @@ import importlib +import importlib.util import pathlib import typing +import sys from ast import * from ..util import CompilingNodeTransformer @@ -10,9 +12,41 @@ """ +def import_module(name, package=None): + """An approximate implementation of import.""" + absolute_name = importlib.util.resolve_name(name, package) + try: + return sys.modules[absolute_name] + except KeyError: + pass + + path = None + if "." in absolute_name: + parent_name, _, child_name = absolute_name.rpartition(".") + parent_module = import_module(parent_name) + path = parent_module.__spec__.submodule_search_locations + for finder in sys.meta_path: + spec = finder.find_spec(absolute_name, path) + if spec is not None: + break + else: + msg = f"No module named {absolute_name!r}" + raise ModuleNotFoundError(msg, name=absolute_name) + module = importlib.util.module_from_spec(spec) + sys.modules[absolute_name] = module + spec.loader.exec_module(module) + if path is not None: + setattr(parent_module, child_name, module) + return module + + class RewriteImport(CompilingNodeTransformer): step = "Resolving imports" + def __init__(self, filename=None, package=None): + self.filename = filename + self.package = package + def visit_ImportFrom( self, node: ImportFrom ) -> typing.Union[ImportFrom, typing.List[AST]]: @@ -28,14 +62,19 @@ def visit_ImportFrom( node.names[0].asname == None ), "The import must have the form 'from import *'" # TODO set anchor point according to own package - module_file = pathlib.Path(importlib.import_module(node.module).__file__) + if self.filename: + sys.path.append(str(pathlib.Path(self.filename).parent.absolute())) + module = import_module(node.module, self.package) + if self.filename: + sys.path.pop() + module_file = pathlib.Path(module.__file__) assert ( module_file.suffix == ".py" ), "The import must import a single python file." # visit the imported file again - make sure that recursive imports are resolved accordingly with module_file.open("r") as fp: module_content = fp.read() - recursively_resolved: Module = self.visit( - parse(module_content, filename=module_file.name) - ) + recursively_resolved: Module = RewriteImport( + filename=str(module_file), package=module.__package__ + ).visit(parse(module_content, filename=module_file.name)) return recursively_resolved.body From 7a4fb8654854ffa4031b62cb7db745aacfc30b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 11:33:20 +0100 Subject: [PATCH 15/20] Bump version - fixes in the import system - write to build/ --- .gitignore | 2 +- eopsin/__init__.py | 2 +- eopsin/__main__.py | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 226dad1a..052121c3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ venv .vscode __tmp_eopsin.py test.py -test/ +build/ *.uplc *.cbor *.plutus diff --git a/eopsin/__init__.py b/eopsin/__init__.py index 4949a3e5..b54cbaa1 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 8) +VERSION = (0, 9, 9) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" diff --git a/eopsin/__main__.py b/eopsin/__main__.py index 7d4567c6..e53d14a3 100644 --- a/eopsin/__main__.py +++ b/eopsin/__main__.py @@ -97,7 +97,9 @@ def main(): with open("__tmp_eopsin.py", "w") as fp: fp.write(source_code) input_file = "__tmp_eopsin.py" - sc = importlib.import_module(input_file) + sys.path.append(str(pathlib.Path(input_file).parent.absolute())) + sc = importlib.import_module(pathlib.Path(input_file).stem) + sys.path.pop() print("Starting execution") print("------------------") try: @@ -172,10 +174,10 @@ def main(): "Please supply an output directory if no input file is specified." ) exit(-1) - target_dir = pathlib.Path(pathlib.Path(input_file).stem) + target_dir = pathlib.Path("build") / pathlib.Path(input_file).stem else: target_dir = pathlib.Path(args.output_directory) - target_dir.mkdir(exist_ok=True) + target_dir.mkdir(exist_ok=True, parents=True) uplc_dump = code.dumps() cbor_hex = pyaiken.uplc.flat(uplc_dump) # create cbor file for use with pycardano/lucid From 98cc9aeaa28e8092283a8a688012bb27668c1eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 7 Mar 2023 15:05:31 +0100 Subject: [PATCH 16/20] Enable empty lists for anytype --- eopsin/typed_ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eopsin/typed_ast.py b/eopsin/typed_ast.py index eb42e59f..c907cf2a 100644 --- a/eopsin/typed_ast.py +++ b/eopsin/typed_ast.py @@ -1032,7 +1032,7 @@ def empty_list(p: Type): ), ) ) - if isinstance(p.typ, RecordType): + if isinstance(p.typ, RecordType) or isinstance(p.typ, AnyType): return plt.EmptyDataList() raise NotImplementedError(f"Empty lists of type {p} can't be constructed yet") From c0f1c6d18314906312c93fd47292c78d04887d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 09:19:56 +0100 Subject: [PATCH 17/20] Change comment about casting PlutusData --- eopsin/type_inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eopsin/type_inference.py b/eopsin/type_inference.py index db5cb861..0198c86a 100644 --- a/eopsin/type_inference.py +++ b/eopsin/type_inference.py @@ -242,7 +242,7 @@ def visit_If(self, node: If) -> TypedIf: ), "Can only cast instances, not classes" assert isinstance( target_inst_class.typ, UnionType - ), "Can only cast instances of Union types" + ), "Can only cast instances of Union types of PlutusData" assert isinstance(target_class, RecordType), "Can only cast to PlutusData" assert ( target_class in target_inst_class.typ.typs From 37a358a60ee583f3d5a7d876ad61680e9bf9a5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 11:17:29 +0100 Subject: [PATCH 18/20] Fix import system --- eopsin/__main__.py | 14 ++++++---- eopsin/compiler.py | 4 +-- eopsin/rewrite/rewrite_import.py | 47 +++++++++++++++++++++++++++++--- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/eopsin/__main__.py b/eopsin/__main__.py index 9ba40c6e..7d4567c6 100644 --- a/eopsin/__main__.py +++ b/eopsin/__main__.py @@ -93,9 +93,11 @@ def main(): source_code = f.read() if command == Command.eval: - with open("__tmp_eopsin.py", "w") as fp: - fp.write(source_code) - sc = importlib.import_module("__tmp_eopsin") + if args.input_file == "-": + with open("__tmp_eopsin.py", "w") as fp: + fp.write(source_code) + input_file = "__tmp_eopsin.py" + sc = importlib.import_module(input_file) print("Starting execution") print("------------------") try: @@ -110,14 +112,16 @@ def main(): print("------------------") print(ret) - source_ast = compiler.parse(source_code) + source_ast = compiler.parse(source_code, filename=input_file) if command == Command.parse: print("Parsed successfully.") return try: - code = compiler.compile(source_ast, force_three_params=args.force_three_params) + code = compiler.compile( + source_ast, filename=input_file, force_three_params=args.force_three_params + ) except CompilerError as c: # Generate nice error message from compiler error if not isinstance(c.node, ast.Module): diff --git a/eopsin/compiler.py b/eopsin/compiler.py index af651962..3df4bd93 100644 --- a/eopsin/compiler.py +++ b/eopsin/compiler.py @@ -839,10 +839,10 @@ def generic_visit(self, node: AST) -> plt.AST: raise NotImplementedError(f"Can not compile {node}") -def compile(prog: AST, force_three_params=False): +def compile(prog: AST, filename=None, force_three_params=False): rewrite_steps = [ # Important to call this one first - it imports all further files - RewriteImport(), + RewriteImport(filename=filename), # Rewrites that simplify the python code RewriteAugAssign(), RewriteTupleAssign(), diff --git a/eopsin/rewrite/rewrite_import.py b/eopsin/rewrite/rewrite_import.py index 1c991208..1624fd9b 100644 --- a/eopsin/rewrite/rewrite_import.py +++ b/eopsin/rewrite/rewrite_import.py @@ -1,6 +1,8 @@ import importlib +import importlib.util import pathlib import typing +import sys from ast import * from ..util import CompilingNodeTransformer @@ -10,9 +12,41 @@ """ +def import_module(name, package=None): + """An approximate implementation of import.""" + absolute_name = importlib.util.resolve_name(name, package) + try: + return sys.modules[absolute_name] + except KeyError: + pass + + path = None + if "." in absolute_name: + parent_name, _, child_name = absolute_name.rpartition(".") + parent_module = import_module(parent_name) + path = parent_module.__spec__.submodule_search_locations + for finder in sys.meta_path: + spec = finder.find_spec(absolute_name, path) + if spec is not None: + break + else: + msg = f"No module named {absolute_name!r}" + raise ModuleNotFoundError(msg, name=absolute_name) + module = importlib.util.module_from_spec(spec) + sys.modules[absolute_name] = module + spec.loader.exec_module(module) + if path is not None: + setattr(parent_module, child_name, module) + return module + + class RewriteImport(CompilingNodeTransformer): step = "Resolving imports" + def __init__(self, filename=None, package=None): + self.filename = filename + self.package = package + def visit_ImportFrom( self, node: ImportFrom ) -> typing.Union[ImportFrom, typing.List[AST]]: @@ -28,14 +62,19 @@ def visit_ImportFrom( node.names[0].asname == None ), "The import must have the form 'from import *'" # TODO set anchor point according to own package - module_file = pathlib.Path(importlib.import_module(node.module).__file__) + if self.filename: + sys.path.append(str(pathlib.Path(self.filename).parent.absolute())) + module = import_module(node.module, self.package) + if self.filename: + sys.path.pop() + module_file = pathlib.Path(module.__file__) assert ( module_file.suffix == ".py" ), "The import must import a single python file." # visit the imported file again - make sure that recursive imports are resolved accordingly with module_file.open("r") as fp: module_content = fp.read() - recursively_resolved: Module = self.visit( - parse(module_content, filename=module_file.name) - ) + recursively_resolved: Module = RewriteImport( + filename=str(module_file), package=module.__package__ + ).visit(parse(module_content, filename=module_file.name)) return recursively_resolved.body From fcb9a278ef900e8abafabb888796dbc687613f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 11:33:20 +0100 Subject: [PATCH 19/20] Bump version - fixes in the import system - write to build/ --- .gitignore | 2 +- eopsin/__init__.py | 2 +- eopsin/__main__.py | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 226dad1a..052121c3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ venv .vscode __tmp_eopsin.py test.py -test/ +build/ *.uplc *.cbor *.plutus diff --git a/eopsin/__init__.py b/eopsin/__init__.py index 4949a3e5..b54cbaa1 100644 --- a/eopsin/__init__.py +++ b/eopsin/__init__.py @@ -8,7 +8,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 9, 8) +VERSION = (0, 9, 9) __version__ = ".".join([str(i) for i in VERSION]) __author__ = "nielstron" diff --git a/eopsin/__main__.py b/eopsin/__main__.py index 7d4567c6..e53d14a3 100644 --- a/eopsin/__main__.py +++ b/eopsin/__main__.py @@ -97,7 +97,9 @@ def main(): with open("__tmp_eopsin.py", "w") as fp: fp.write(source_code) input_file = "__tmp_eopsin.py" - sc = importlib.import_module(input_file) + sys.path.append(str(pathlib.Path(input_file).parent.absolute())) + sc = importlib.import_module(pathlib.Path(input_file).stem) + sys.path.pop() print("Starting execution") print("------------------") try: @@ -172,10 +174,10 @@ def main(): "Please supply an output directory if no input file is specified." ) exit(-1) - target_dir = pathlib.Path(pathlib.Path(input_file).stem) + target_dir = pathlib.Path("build") / pathlib.Path(input_file).stem else: target_dir = pathlib.Path(args.output_directory) - target_dir.mkdir(exist_ok=True) + target_dir.mkdir(exist_ok=True, parents=True) uplc_dump = code.dumps() cbor_hex = pyaiken.uplc.flat(uplc_dump) # create cbor file for use with pycardano/lucid From 8ee97ff87cafdc4a502c19bd3e79238461ca7fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 8 Mar 2023 12:13:04 +0100 Subject: [PATCH 20/20] Add inversion.dev to list of supporters --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3a941480..bd5752b7 100644 --- a/README.md +++ b/README.md @@ -182,5 +182,6 @@ Donation in ADA can be submitted to `$imperatorlang` or `addr1qyz3vgd5xxevjy2rvq ### Supporters +