mycelium-compiler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mycelium_compiler-0.1.0/PKG-INFO +118 -0
- mycelium_compiler-0.1.0/README.md +111 -0
- mycelium_compiler-0.1.0/mycelium_compiler/__init__.py +1 -0
- mycelium_compiler-0.1.0/mycelium_compiler/codegen/__init__.py +28 -0
- mycelium_compiler-0.1.0/mycelium_compiler/codegen/core.py +390 -0
- mycelium_compiler-0.1.0/mycelium_compiler/codegen/inferrer.py +533 -0
- mycelium_compiler-0.1.0/mycelium_compiler/codegen/transpiler.py +2010 -0
- mycelium_compiler-0.1.0/mycelium_compiler/codegen/utils.py +218 -0
- mycelium_compiler-0.1.0/mycelium_compiler/main.py +36 -0
- mycelium_compiler-0.1.0/mycelium_compiler/parser.py +189 -0
- mycelium_compiler-0.1.0/mycelium_compiler/types.py +1 -0
- mycelium_compiler-0.1.0/mycelium_compiler/validator.py +100 -0
- mycelium_compiler-0.1.0/mycelium_compiler.egg-info/PKG-INFO +118 -0
- mycelium_compiler-0.1.0/mycelium_compiler.egg-info/SOURCES.txt +17 -0
- mycelium_compiler-0.1.0/mycelium_compiler.egg-info/dependency_links.txt +1 -0
- mycelium_compiler-0.1.0/mycelium_compiler.egg-info/top_level.txt +1 -0
- mycelium_compiler-0.1.0/pyproject.toml +14 -0
- mycelium_compiler-0.1.0/setup.cfg +4 -0
- mycelium_compiler-0.1.0/tests/test_compiler.py +98 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mycelium-compiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mycelium Python-to-Soroban-WASM compiler
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# Mycelium Python-to-Soroban Compiler
|
|
9
|
+
|
|
10
|
+
The Mycelium Compiler (`mycelium-compiler`) is a high-performance Python AST parser and transpilation engine that converts Python-DSL smart contracts into highly optimized, secure WebAssembly (WASM) binaries for the Stellar/Soroban virtual machine.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ποΈ Compiler Architecture
|
|
15
|
+
|
|
16
|
+
The compilation process is structured into four main phases:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ
|
|
20
|
+
β Python DSL β βββ> β AST Parsing β βββ> β Type Validation β βββ> β Transpilationβ
|
|
21
|
+
β (Source) β β (parser.py) β β (validator.py) β β (codegen/) β
|
|
22
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ ββββββββ¬βββββββ
|
|
23
|
+
β
|
|
24
|
+
βΌ
|
|
25
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ
|
|
26
|
+
β Soroban WASM β <βββ β Rust Cargo β <βββ β Stellar CLI β <βββ β Rust Code β
|
|
27
|
+
β (Binary) β β Build β β Compilation β β (src/lib.rs)β
|
|
28
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 1. AST Parsing (`parser.py`)
|
|
32
|
+
- Reads the Python source file and converts it into a Python Abstract Syntax Tree (AST) using Python's native `ast` library.
|
|
33
|
+
- Extracts module-level constants, `@contract` definitions, storage variables, and contract function schemas.
|
|
34
|
+
- Parses auxiliary classes representing custom structs, events, interfaces, and constant-based enums.
|
|
35
|
+
|
|
36
|
+
### 2. Type & AST Validation (`validator.py`)
|
|
37
|
+
- Ensures that all types specified in the Python contract strictly conform to Soroban-compatible primitives and collection types.
|
|
38
|
+
- Asserts that all function signatures, return types, and storage variables are valid.
|
|
39
|
+
- Supported Primitives: `int`, `str`, `bytes`, `bool`, `Symbol`, `i32`, `i64`, `i128`, `u32`, `u64`, `Address`, `U256`, `U128`, `U64`, `U32`, `I128`, `I32`, `Bool`, `Env`.
|
|
40
|
+
- Supported Collections: `Map[K, V]`, `Vec[T]`, `Bytes[N]`, `DynArray[T, N]`, `list`, `tuple`.
|
|
41
|
+
|
|
42
|
+
### 3. Rust Code Generation (`codegen/`)
|
|
43
|
+
- **Storage Type Inference (`inferrer.py`)**: Traverses function logic to infer the types of local variables and on-chain storage states to construct statically typed Rust equivalents.
|
|
44
|
+
- **Transpiler (`transpiler.py` & `core.py`)**: Translates Python statements, loops, branches, assignments, and expressions into clean, memory-safe, idiomatic Soroban Rust code.
|
|
45
|
+
- **Features**:
|
|
46
|
+
- Automatically handles local variable pre-declaration.
|
|
47
|
+
- Implements storage read/write virtualization (mapping `self.balances[key]` to Soroban persistent/instance storage access).
|
|
48
|
+
- Injects contextual state wrappers (e.g., `msg_sender.require_auth()`, block sequences, and transaction timestamps).
|
|
49
|
+
|
|
50
|
+
### 4. Compilation & Bootstrapping (`core.py`)
|
|
51
|
+
- Emits temporary Cargo workspaces with optimal release settings:
|
|
52
|
+
- `opt-level = "z"` (optimized for minimal WASM size).
|
|
53
|
+
- `overflow-checks = true`.
|
|
54
|
+
- Link-Time Optimization (`lto = true`) and single-unit compilation.
|
|
55
|
+
- **Stellar CLI Bootstrapper**: Checks for `stellar` in the system path. If not found, it automatically downloads the certified `stellar-cli` executable for your specific platform/architecture.
|
|
56
|
+
- Compiles the Rust intermediate file into a `.wasm` file.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## π Installation & CLI Usage
|
|
61
|
+
|
|
62
|
+
Install the compiler package:
|
|
63
|
+
```bash
|
|
64
|
+
pip install mycelium-compiler
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Compile a Python contract source file to WASM:
|
|
68
|
+
```bash
|
|
69
|
+
mycelium compile my_contract.py -o build/my_contract.wasm
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Script Execution (Python API)
|
|
73
|
+
You can also compile contracts programmatically inside Python scripts:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from mycelium_compiler.main import compile_file
|
|
77
|
+
|
|
78
|
+
compile_file("my_contract.py", "build/my_contract.wasm")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## π DSL Contract Example
|
|
84
|
+
|
|
85
|
+
The compiler translates Python files looking like this:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from mycelium import contract, external, view, U64, Address, Map
|
|
89
|
+
|
|
90
|
+
@contract
|
|
91
|
+
class TokenCounter:
|
|
92
|
+
# On-chain state variables
|
|
93
|
+
balances: Map[Address, U64]
|
|
94
|
+
owner: Address
|
|
95
|
+
|
|
96
|
+
@external
|
|
97
|
+
def initialize(self, owner: Address):
|
|
98
|
+
self.owner = owner
|
|
99
|
+
|
|
100
|
+
@external
|
|
101
|
+
def mint(self, to: Address, amount: U64):
|
|
102
|
+
# Implicitly requires auth from the owner (under the hood)
|
|
103
|
+
if msg_sender != self.owner:
|
|
104
|
+
panic("Unauthorized")
|
|
105
|
+
|
|
106
|
+
current = self.balances.get(to, 0)
|
|
107
|
+
self.balances[to] = current + amount
|
|
108
|
+
|
|
109
|
+
@view
|
|
110
|
+
def get_balance(self, account: Address) -> U64:
|
|
111
|
+
return self.balances.get(account, 0)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## π οΈ Sandbox Fallback Execution
|
|
117
|
+
|
|
118
|
+
When running inside cloud sandboxes or serverless backend instances (e.g., Render or AWS Lambda) where Docker is restricted, the compiler features a native python runner fallback that executes the cargo compiler in-process, bypassing the container requirement while enforcing standard resource safety limits.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Mycelium Python-to-Soroban Compiler
|
|
2
|
+
|
|
3
|
+
The Mycelium Compiler (`mycelium-compiler`) is a high-performance Python AST parser and transpilation engine that converts Python-DSL smart contracts into highly optimized, secure WebAssembly (WASM) binaries for the Stellar/Soroban virtual machine.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ποΈ Compiler Architecture
|
|
8
|
+
|
|
9
|
+
The compilation process is structured into four main phases:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ
|
|
13
|
+
β Python DSL β βββ> β AST Parsing β βββ> β Type Validation β βββ> β Transpilationβ
|
|
14
|
+
β (Source) β β (parser.py) β β (validator.py) β β (codegen/) β
|
|
15
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ ββββββββ¬βββββββ
|
|
16
|
+
β
|
|
17
|
+
βΌ
|
|
18
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ
|
|
19
|
+
β Soroban WASM β <βββ β Rust Cargo β <βββ β Stellar CLI β <βββ β Rust Code β
|
|
20
|
+
β (Binary) β β Build β β Compilation β β (src/lib.rs)β
|
|
21
|
+
ββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 1. AST Parsing (`parser.py`)
|
|
25
|
+
- Reads the Python source file and converts it into a Python Abstract Syntax Tree (AST) using Python's native `ast` library.
|
|
26
|
+
- Extracts module-level constants, `@contract` definitions, storage variables, and contract function schemas.
|
|
27
|
+
- Parses auxiliary classes representing custom structs, events, interfaces, and constant-based enums.
|
|
28
|
+
|
|
29
|
+
### 2. Type & AST Validation (`validator.py`)
|
|
30
|
+
- Ensures that all types specified in the Python contract strictly conform to Soroban-compatible primitives and collection types.
|
|
31
|
+
- Asserts that all function signatures, return types, and storage variables are valid.
|
|
32
|
+
- Supported Primitives: `int`, `str`, `bytes`, `bool`, `Symbol`, `i32`, `i64`, `i128`, `u32`, `u64`, `Address`, `U256`, `U128`, `U64`, `U32`, `I128`, `I32`, `Bool`, `Env`.
|
|
33
|
+
- Supported Collections: `Map[K, V]`, `Vec[T]`, `Bytes[N]`, `DynArray[T, N]`, `list`, `tuple`.
|
|
34
|
+
|
|
35
|
+
### 3. Rust Code Generation (`codegen/`)
|
|
36
|
+
- **Storage Type Inference (`inferrer.py`)**: Traverses function logic to infer the types of local variables and on-chain storage states to construct statically typed Rust equivalents.
|
|
37
|
+
- **Transpiler (`transpiler.py` & `core.py`)**: Translates Python statements, loops, branches, assignments, and expressions into clean, memory-safe, idiomatic Soroban Rust code.
|
|
38
|
+
- **Features**:
|
|
39
|
+
- Automatically handles local variable pre-declaration.
|
|
40
|
+
- Implements storage read/write virtualization (mapping `self.balances[key]` to Soroban persistent/instance storage access).
|
|
41
|
+
- Injects contextual state wrappers (e.g., `msg_sender.require_auth()`, block sequences, and transaction timestamps).
|
|
42
|
+
|
|
43
|
+
### 4. Compilation & Bootstrapping (`core.py`)
|
|
44
|
+
- Emits temporary Cargo workspaces with optimal release settings:
|
|
45
|
+
- `opt-level = "z"` (optimized for minimal WASM size).
|
|
46
|
+
- `overflow-checks = true`.
|
|
47
|
+
- Link-Time Optimization (`lto = true`) and single-unit compilation.
|
|
48
|
+
- **Stellar CLI Bootstrapper**: Checks for `stellar` in the system path. If not found, it automatically downloads the certified `stellar-cli` executable for your specific platform/architecture.
|
|
49
|
+
- Compiles the Rust intermediate file into a `.wasm` file.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## π Installation & CLI Usage
|
|
54
|
+
|
|
55
|
+
Install the compiler package:
|
|
56
|
+
```bash
|
|
57
|
+
pip install mycelium-compiler
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Compile a Python contract source file to WASM:
|
|
61
|
+
```bash
|
|
62
|
+
mycelium compile my_contract.py -o build/my_contract.wasm
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Script Execution (Python API)
|
|
66
|
+
You can also compile contracts programmatically inside Python scripts:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from mycelium_compiler.main import compile_file
|
|
70
|
+
|
|
71
|
+
compile_file("my_contract.py", "build/my_contract.wasm")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## π DSL Contract Example
|
|
77
|
+
|
|
78
|
+
The compiler translates Python files looking like this:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from mycelium import contract, external, view, U64, Address, Map
|
|
82
|
+
|
|
83
|
+
@contract
|
|
84
|
+
class TokenCounter:
|
|
85
|
+
# On-chain state variables
|
|
86
|
+
balances: Map[Address, U64]
|
|
87
|
+
owner: Address
|
|
88
|
+
|
|
89
|
+
@external
|
|
90
|
+
def initialize(self, owner: Address):
|
|
91
|
+
self.owner = owner
|
|
92
|
+
|
|
93
|
+
@external
|
|
94
|
+
def mint(self, to: Address, amount: U64):
|
|
95
|
+
# Implicitly requires auth from the owner (under the hood)
|
|
96
|
+
if msg_sender != self.owner:
|
|
97
|
+
panic("Unauthorized")
|
|
98
|
+
|
|
99
|
+
current = self.balances.get(to, 0)
|
|
100
|
+
self.balances[to] = current + amount
|
|
101
|
+
|
|
102
|
+
@view
|
|
103
|
+
def get_balance(self, account: Address) -> U64:
|
|
104
|
+
return self.balances.get(account, 0)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## π οΈ Sandbox Fallback Execution
|
|
110
|
+
|
|
111
|
+
When running inside cloud sandboxes or serverless backend instances (e.g., Render or AWS Lambda) where Docker is restricted, the compiler features a native python runner fallback that executes the cargo compiler in-process, bypassing the container requirement while enforcing standard resource safety limits.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Mycelium Compiler Package
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .core import generate_rust_intermediate, generate_wasm, ensure_stellar_cli
|
|
2
|
+
from .inferrer import StorageTypeInferrer
|
|
3
|
+
from .transpiler import RustTranspiler, collect_local_vars
|
|
4
|
+
from .utils import (
|
|
5
|
+
escape_keyword,
|
|
6
|
+
to_pascal_case,
|
|
7
|
+
eval_static_constant,
|
|
8
|
+
map_type,
|
|
9
|
+
get_subscript_type,
|
|
10
|
+
flatten_subscript,
|
|
11
|
+
check_keyword_usage,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'generate_rust_intermediate',
|
|
16
|
+
'generate_wasm',
|
|
17
|
+
'ensure_stellar_cli',
|
|
18
|
+
'StorageTypeInferrer',
|
|
19
|
+
'RustTranspiler',
|
|
20
|
+
'collect_local_vars',
|
|
21
|
+
'escape_keyword',
|
|
22
|
+
'to_pascal_case',
|
|
23
|
+
'eval_static_constant',
|
|
24
|
+
'map_type',
|
|
25
|
+
'get_subscript_type',
|
|
26
|
+
'flatten_subscript',
|
|
27
|
+
'check_keyword_usage',
|
|
28
|
+
]
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
import subprocess
|
|
6
|
+
import shutil
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from mycelium_compiler.parser import MyceliumCompilerVisitor
|
|
10
|
+
from .inferrer import StorageTypeInferrer
|
|
11
|
+
from .transpiler import RustTranspiler, collect_local_vars
|
|
12
|
+
from .utils import (
|
|
13
|
+
to_pascal_case,
|
|
14
|
+
escape_keyword,
|
|
15
|
+
map_type,
|
|
16
|
+
check_keyword_usage,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def generate_rust_intermediate(visitor: MyceliumCompilerVisitor) -> str:
|
|
20
|
+
"""Translates the validated Python AST into Soroban Rust code."""
|
|
21
|
+
lines = ["#![no_std]"]
|
|
22
|
+
|
|
23
|
+
# Build use statement
|
|
24
|
+
use_items = [
|
|
25
|
+
"contract", "contractimpl", "Env", "Symbol", "Address", "U256",
|
|
26
|
+
"Map", "Vec", "Bytes", "IntoVal", "Val", "TryFromVal"
|
|
27
|
+
]
|
|
28
|
+
if visitor.errors:
|
|
29
|
+
use_items.extend(["contracterror", "panic_with_error"])
|
|
30
|
+
lines.append(f"#[allow(unused_imports)]")
|
|
31
|
+
lines.append(f"use soroban_sdk::{{{', '.join(use_items)}}};")
|
|
32
|
+
lines.append("use soroban_sdk::xdr::ToXdr;")
|
|
33
|
+
lines.append("")
|
|
34
|
+
|
|
35
|
+
# Generate ContractError enum
|
|
36
|
+
has_errors = bool(visitor.errors and visitor.errors.get("fields"))
|
|
37
|
+
if has_errors:
|
|
38
|
+
lines.append("#[contracterror]")
|
|
39
|
+
lines.append("#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]")
|
|
40
|
+
lines.append("#[repr(u32)]")
|
|
41
|
+
lines.append("pub enum ContractError {")
|
|
42
|
+
for name, value in visitor.errors["fields"].items():
|
|
43
|
+
lines.append(f" {to_pascal_case(name)} = {value},")
|
|
44
|
+
lines.append("}")
|
|
45
|
+
lines.append("")
|
|
46
|
+
|
|
47
|
+
# Generate const class enums (RootStatus, etc.)
|
|
48
|
+
for class_name, variants in visitor.const_classes.items():
|
|
49
|
+
if not all(isinstance(value, int) and not isinstance(value, bool) for value in variants.values()):
|
|
50
|
+
continue
|
|
51
|
+
lines.append("#[derive(Copy, Clone, Debug, Eq, PartialEq)]")
|
|
52
|
+
lines.append("#[repr(u32)]")
|
|
53
|
+
lines.append(f"pub enum {class_name} {{")
|
|
54
|
+
for name, value in variants.items():
|
|
55
|
+
lines.append(f" {to_pascal_case(name)} = {value},")
|
|
56
|
+
lines.append("}")
|
|
57
|
+
lines.append("")
|
|
58
|
+
|
|
59
|
+
lines.append("#[contract]")
|
|
60
|
+
lines.append(f"pub struct {visitor.contract_name};")
|
|
61
|
+
lines.append("")
|
|
62
|
+
lines.append("#[contractimpl]")
|
|
63
|
+
lines.append(f"impl {visitor.contract_name} {{")
|
|
64
|
+
|
|
65
|
+
# Global storage type inference across all functions
|
|
66
|
+
global_inferrer = StorageTypeInferrer(
|
|
67
|
+
visitor.state_variables, visitor.functions,
|
|
68
|
+
local_var_types={}, module_constants=visitor.module_constants
|
|
69
|
+
)
|
|
70
|
+
global_inferrer.infer()
|
|
71
|
+
global_storage_key_types = global_inferrer.storage_key_types
|
|
72
|
+
|
|
73
|
+
for func in visitor.functions:
|
|
74
|
+
func_node = func.get("node")
|
|
75
|
+
|
|
76
|
+
# Skip __init__ constructor
|
|
77
|
+
if func["name"] == "__init__":
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Determine visibility
|
|
81
|
+
is_public = True
|
|
82
|
+
if func["name"].startswith("_"):
|
|
83
|
+
is_public = False
|
|
84
|
+
# Check decorators
|
|
85
|
+
if func_node:
|
|
86
|
+
for dec in func_node.decorator_list:
|
|
87
|
+
if isinstance(dec, ast.Name) and dec.id in ('external', 'view'):
|
|
88
|
+
is_public = True
|
|
89
|
+
|
|
90
|
+
pub_str = "pub " if is_public else ""
|
|
91
|
+
|
|
92
|
+
# Determine extra parameter injections (backward compat)
|
|
93
|
+
extra_args = []
|
|
94
|
+
if func_node:
|
|
95
|
+
if check_keyword_usage(func_node, "msg_sender"):
|
|
96
|
+
extra_args.append("msg_sender: Address")
|
|
97
|
+
if check_keyword_usage(func_node, "msg_value"):
|
|
98
|
+
extra_args.append("msg_value: U256")
|
|
99
|
+
|
|
100
|
+
args_list = ["env: Env"]
|
|
101
|
+
for arg_name, arg_type in func["args"]:
|
|
102
|
+
if arg_name == "self":
|
|
103
|
+
continue
|
|
104
|
+
if arg_type == "Env":
|
|
105
|
+
continue # env is auto-injected
|
|
106
|
+
safe_name = escape_keyword(arg_name)
|
|
107
|
+
args_list.append(f"{safe_name}: {map_type(arg_type)}")
|
|
108
|
+
|
|
109
|
+
args_list.extend(extra_args)
|
|
110
|
+
args_str = ", ".join(args_list)
|
|
111
|
+
|
|
112
|
+
ret_type = ""
|
|
113
|
+
mapped_ret_type = None
|
|
114
|
+
if func["returns"] != "None":
|
|
115
|
+
mapped_ret_type = map_type(func['returns'])
|
|
116
|
+
ret_type = f" -> {mapped_ret_type}"
|
|
117
|
+
|
|
118
|
+
lines.append(f" {pub_str}fn {func['name']}({args_str}){ret_type} {{")
|
|
119
|
+
|
|
120
|
+
# Inject global emulation bindings (backward compat)
|
|
121
|
+
body_prefix = []
|
|
122
|
+
if func_node:
|
|
123
|
+
if check_keyword_usage(func_node, "msg_sender"):
|
|
124
|
+
body_prefix.append(" msg_sender.require_auth();")
|
|
125
|
+
if check_keyword_usage(func_node, "block_timestamp"):
|
|
126
|
+
body_prefix.append(" let block_timestamp = env.ledger().timestamp();")
|
|
127
|
+
if check_keyword_usage(func_node, "block_number"):
|
|
128
|
+
body_prefix.append(" let block_number = env.ledger().sequence() as u64;")
|
|
129
|
+
if check_keyword_usage(func_node, "ZERO_ADDRESS"):
|
|
130
|
+
body_prefix.append(' let ZERO_ADDRESS = Address::from_string(&soroban_sdk::String::from_str(&env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"));')
|
|
131
|
+
if check_keyword_usage(func_node, "self_balance"):
|
|
132
|
+
body_prefix.append(" let self_balance = U256::from_u32(&env, 1000000);")
|
|
133
|
+
|
|
134
|
+
local_var_types = {}
|
|
135
|
+
inferred_locals = global_inferrer.func_local_types.get(func["name"], {})
|
|
136
|
+
for k, v in inferred_locals.items():
|
|
137
|
+
local_var_types[k] = map_type(v)
|
|
138
|
+
|
|
139
|
+
for arg_name, arg_type in func["args"]:
|
|
140
|
+
if arg_name != "self":
|
|
141
|
+
local_var_types[arg_name] = map_type(arg_type)
|
|
142
|
+
if func_node:
|
|
143
|
+
if check_keyword_usage(func_node, "msg_sender"):
|
|
144
|
+
local_var_types["msg_sender"] = "Address"
|
|
145
|
+
if check_keyword_usage(func_node, "msg_value"):
|
|
146
|
+
local_var_types["msg_value"] = "U256"
|
|
147
|
+
if check_keyword_usage(func_node, "block_timestamp"):
|
|
148
|
+
local_var_types["block_timestamp"] = "u64"
|
|
149
|
+
if check_keyword_usage(func_node, "block_number"):
|
|
150
|
+
local_var_types["block_number"] = "u64"
|
|
151
|
+
if check_keyword_usage(func_node, "ZERO_ADDRESS"):
|
|
152
|
+
local_var_types["ZERO_ADDRESS"] = "Address"
|
|
153
|
+
if check_keyword_usage(func_node, "self_balance"):
|
|
154
|
+
local_var_types["self_balance"] = "U256"
|
|
155
|
+
|
|
156
|
+
# If the function returns a Vec<T> and the function returns a local
|
|
157
|
+
# variable by name (e.g. `return matched_agents`), prefer the
|
|
158
|
+
# function's return Vec element type for that local variable. This
|
|
159
|
+
# fixes cases where `Vec()` is created without explicit element type
|
|
160
|
+
# and thus defaults to Vec<Val>.
|
|
161
|
+
if mapped_ret_type and mapped_ret_type.startswith("soroban_sdk::Vec<") and func_node is not None:
|
|
162
|
+
ret_var_name = None
|
|
163
|
+
for n in ast.walk(func_node):
|
|
164
|
+
if isinstance(n, ast.Return) and isinstance(n.value, ast.Name):
|
|
165
|
+
ret_var_name = n.value.id
|
|
166
|
+
break
|
|
167
|
+
if ret_var_name:
|
|
168
|
+
# override or set the local var type for the returned container
|
|
169
|
+
local_var_types[ret_var_name] = mapped_ret_type
|
|
170
|
+
|
|
171
|
+
# Collect local variables to pre-declare
|
|
172
|
+
local_vars_to_declare = collect_local_vars(func_node)
|
|
173
|
+
|
|
174
|
+
# Exclude arguments
|
|
175
|
+
args_names = {arg_name for arg_name, _ in func["args"]}
|
|
176
|
+
local_vars_to_declare = local_vars_to_declare - args_names
|
|
177
|
+
|
|
178
|
+
# Exclude injected variables
|
|
179
|
+
injected = {"self", "env", "msg_sender", "msg_value", "block_timestamp", "block_number", "ZERO_ADDRESS", "self_balance"}
|
|
180
|
+
local_vars_to_declare = local_vars_to_declare - injected
|
|
181
|
+
|
|
182
|
+
# Exclude module constants
|
|
183
|
+
local_vars_to_declare = local_vars_to_declare - set(visitor.module_constants.keys())
|
|
184
|
+
|
|
185
|
+
transpiler = RustTranspiler(
|
|
186
|
+
visitor.state_variables, visitor.contract_name, visitor.events,
|
|
187
|
+
local_var_types, return_type=mapped_ret_type,
|
|
188
|
+
functions_meta=visitor.functions, has_errors=has_errors,
|
|
189
|
+
storage_key_types=global_storage_key_types,
|
|
190
|
+
const_classes=visitor.const_classes,
|
|
191
|
+
module_constants=visitor.module_constants,
|
|
192
|
+
func_node=func_node
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Seed local_vars so that assignments do not declare 'let mut' inside blocks
|
|
196
|
+
transpiler.local_vars.update(local_vars_to_declare)
|
|
197
|
+
|
|
198
|
+
body_lines = []
|
|
199
|
+
if func_node and hasattr(func_node, "body"):
|
|
200
|
+
for stmt in func_node.body:
|
|
201
|
+
body_lines.append(" " + transpiler.transpile_stmt(stmt))
|
|
202
|
+
else:
|
|
203
|
+
body_lines.append(" // Default return fallback")
|
|
204
|
+
|
|
205
|
+
# Generate variable declarations with inferred types
|
|
206
|
+
declarations = []
|
|
207
|
+
for var_name in sorted(list(local_vars_to_declare)):
|
|
208
|
+
safe_name = escape_keyword(var_name)
|
|
209
|
+
var_type = transpiler.local_var_types.get(var_name)
|
|
210
|
+
if var_type:
|
|
211
|
+
declarations.append(f" let mut {safe_name}: {var_type};")
|
|
212
|
+
else:
|
|
213
|
+
declarations.append(f" let mut {safe_name};")
|
|
214
|
+
|
|
215
|
+
lines.extend(body_prefix)
|
|
216
|
+
lines.extend(declarations)
|
|
217
|
+
lines.extend(body_lines)
|
|
218
|
+
lines.append(" }")
|
|
219
|
+
|
|
220
|
+
lines.append("}")
|
|
221
|
+
return "\n".join(lines)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# βββ Stellar CLI Bootstrapper βββββββββββββββββββββββββββββββββββββββββββββ
|
|
225
|
+
|
|
226
|
+
def ensure_stellar_cli() -> str:
|
|
227
|
+
"""
|
|
228
|
+
Checks if 'stellar' CLI is available in system PATH or downloads it automatically.
|
|
229
|
+
Returns the path to the stellar binary.
|
|
230
|
+
"""
|
|
231
|
+
import platform
|
|
232
|
+
import urllib.request
|
|
233
|
+
import tarfile
|
|
234
|
+
|
|
235
|
+
system = platform.system().lower()
|
|
236
|
+
machine = platform.machine().lower()
|
|
237
|
+
|
|
238
|
+
stellar_bin_name = "stellar.exe" if "windows" in system else "stellar"
|
|
239
|
+
|
|
240
|
+
system_stellar = shutil.which("stellar")
|
|
241
|
+
if system_stellar:
|
|
242
|
+
return system_stellar
|
|
243
|
+
|
|
244
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
245
|
+
local_bin_dir = os.path.join(current_dir, "bin")
|
|
246
|
+
local_stellar = os.path.join(local_bin_dir, stellar_bin_name)
|
|
247
|
+
if os.path.exists(local_stellar):
|
|
248
|
+
return local_stellar
|
|
249
|
+
|
|
250
|
+
os.makedirs(local_bin_dir, exist_ok=True)
|
|
251
|
+
|
|
252
|
+
version = "27.0.0"
|
|
253
|
+
|
|
254
|
+
if "windows" in system and ("86" in machine or "amd64" in machine):
|
|
255
|
+
filename = f"stellar-cli-{version}-x86_64-pc-windows-msvc.zip"
|
|
256
|
+
elif "linux" in system and "86" in machine:
|
|
257
|
+
filename = f"stellar-cli-{version}-x86_64-unknown-linux-gnu.tar.gz"
|
|
258
|
+
elif "darwin" in system or "mac" in system:
|
|
259
|
+
if "arm" in machine or "aarch" in machine:
|
|
260
|
+
filename = f"stellar-cli-{version}-aarch64-apple-darwin.tar.gz"
|
|
261
|
+
else:
|
|
262
|
+
filename = f"stellar-cli-{version}-x86_64-apple-darwin.tar.gz"
|
|
263
|
+
else:
|
|
264
|
+
print(f"[Stellar CLI Bootstrapper] Unsupported platform ({system}) / architecture ({machine}) for auto-download. Using fallback 'stellar'.")
|
|
265
|
+
return "stellar"
|
|
266
|
+
|
|
267
|
+
url = f"https://github.com/stellar/stellar-cli/releases/download/v{version}/{filename}"
|
|
268
|
+
|
|
269
|
+
print(f"[Stellar CLI Bootstrapper] Downloading stellar-cli v{version} from {url}...")
|
|
270
|
+
try:
|
|
271
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
272
|
+
archive_path = os.path.join(tmpdir, filename)
|
|
273
|
+
urllib.request.urlretrieve(url, archive_path)
|
|
274
|
+
|
|
275
|
+
if filename.endswith(".tar.gz"):
|
|
276
|
+
with tarfile.open(archive_path, "r:gz") as tar:
|
|
277
|
+
tar.extractall(path=tmpdir)
|
|
278
|
+
elif filename.endswith(".zip"):
|
|
279
|
+
import zipfile
|
|
280
|
+
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
|
|
281
|
+
zip_ref.extractall(tmpdir)
|
|
282
|
+
|
|
283
|
+
found_path = None
|
|
284
|
+
for root, dirs, files in os.walk(tmpdir):
|
|
285
|
+
if stellar_bin_name in files:
|
|
286
|
+
found_path = os.path.join(root, stellar_bin_name)
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
if found_path and os.path.exists(found_path):
|
|
290
|
+
shutil.copy2(found_path, local_stellar)
|
|
291
|
+
if "windows" not in system:
|
|
292
|
+
os.chmod(local_stellar, 0o755)
|
|
293
|
+
print(f"[Stellar CLI Bootstrapper] Successfully installed stellar-cli at {local_stellar}")
|
|
294
|
+
return local_stellar
|
|
295
|
+
else:
|
|
296
|
+
raise FileNotFoundError(f"Could not find '{stellar_bin_name}' binary in extracted archive")
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
print(f"[Stellar CLI Bootstrapper] Failed to download or install stellar-cli: {e}. Using fallback 'stellar'.")
|
|
300
|
+
return "stellar"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def generate_wasm(visitor: MyceliumCompilerVisitor) -> bytes:
|
|
304
|
+
"""
|
|
305
|
+
Generate a valid Soroban-compatible WASM binary from parsed contract AST
|
|
306
|
+
by transpiling to Soroban Rust and invoking cargo/stellar CLI.
|
|
307
|
+
"""
|
|
308
|
+
rust_code = generate_rust_intermediate(visitor)
|
|
309
|
+
|
|
310
|
+
static_workspace = "/app/mycelium_contract_workspace"
|
|
311
|
+
is_static = False
|
|
312
|
+
if os.path.exists(static_workspace):
|
|
313
|
+
temp_dir = static_workspace
|
|
314
|
+
is_static = True
|
|
315
|
+
else:
|
|
316
|
+
temp_dir = os.path.join(tempfile.gettempdir(), f"mycelium_compile_{uuid.uuid4()}")
|
|
317
|
+
|
|
318
|
+
os.makedirs(os.path.join(temp_dir, "src"), exist_ok=True)
|
|
319
|
+
|
|
320
|
+
cargo_toml = """[package]
|
|
321
|
+
name = "mycelium_contract"
|
|
322
|
+
version = "0.1.0"
|
|
323
|
+
edition = "2021"
|
|
324
|
+
|
|
325
|
+
[lib]
|
|
326
|
+
crate-type = ["cdylib"]
|
|
327
|
+
|
|
328
|
+
[dependencies]
|
|
329
|
+
soroban-sdk = "26.1.0"
|
|
330
|
+
|
|
331
|
+
[profile.release]
|
|
332
|
+
opt-level = "z"
|
|
333
|
+
overflow-checks = true
|
|
334
|
+
lto = true
|
|
335
|
+
codegen-units = 1
|
|
336
|
+
panic = "abort"
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
with open(os.path.join(temp_dir, "Cargo.toml"), "w") as f:
|
|
341
|
+
f.write(cargo_toml)
|
|
342
|
+
|
|
343
|
+
with open(os.path.join(temp_dir, "src", "lib.rs"), "w") as f:
|
|
344
|
+
f.write(rust_code)
|
|
345
|
+
|
|
346
|
+
stellar_bin = ensure_stellar_cli()
|
|
347
|
+
|
|
348
|
+
cmd = [stellar_bin, "contract", "build", "--manifest-path", "Cargo.toml"]
|
|
349
|
+
|
|
350
|
+
cache_dir = "/app/cargo_target"
|
|
351
|
+
if os.path.exists(cache_dir):
|
|
352
|
+
target_dir = cache_dir
|
|
353
|
+
else:
|
|
354
|
+
target_dir = "/tmp/mycelium_cargo_target"
|
|
355
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
356
|
+
|
|
357
|
+
env = os.environ.copy()
|
|
358
|
+
env["CARGO_TARGET_DIR"] = target_dir
|
|
359
|
+
env["CARGO_NET_OFFLINE"] = "true"
|
|
360
|
+
|
|
361
|
+
print(f"DEBUG: Executing cmd: {cmd} in cwd: {temp_dir}", file=sys.stderr, flush=True)
|
|
362
|
+
print(f"DEBUG: CARGO_TARGET_DIR={env.get('CARGO_TARGET_DIR')}", file=sys.stderr, flush=True)
|
|
363
|
+
print(f"DEBUG: CARGO_NET_OFFLINE={env.get('CARGO_NET_OFFLINE')}", file=sys.stderr, flush=True)
|
|
364
|
+
|
|
365
|
+
res = subprocess.run(cmd, capture_output=True, text=True, env=env, cwd=temp_dir)
|
|
366
|
+
|
|
367
|
+
print(f"--- Cargo STDOUT ---\n{res.stdout}", file=sys.stderr)
|
|
368
|
+
print(f"--- Cargo STDERR ---\n{res.stderr}", file=sys.stderr)
|
|
369
|
+
|
|
370
|
+
if res.returncode != 0:
|
|
371
|
+
error_log = f"Rust Compilation Error:\n{res.stderr}\n{res.stdout}"
|
|
372
|
+
print(error_log, file=sys.stderr)
|
|
373
|
+
raise RuntimeError(error_log)
|
|
374
|
+
|
|
375
|
+
wasm_path = os.path.join(target_dir, "wasm32v1-none", "release", "mycelium_contract.wasm")
|
|
376
|
+
|
|
377
|
+
if not os.path.exists(wasm_path):
|
|
378
|
+
wasm_path = os.path.join(target_dir, "wasm32-unknown-unknown", "release", "mycelium_contract.wasm")
|
|
379
|
+
|
|
380
|
+
if not os.path.exists(wasm_path):
|
|
381
|
+
raise FileNotFoundError(f"Compiled WASM not found in target directories of {target_dir}")
|
|
382
|
+
|
|
383
|
+
with open(wasm_path, "rb") as f_wasm:
|
|
384
|
+
wasm_bytes = f_wasm.read()
|
|
385
|
+
|
|
386
|
+
return wasm_bytes
|
|
387
|
+
|
|
388
|
+
finally:
|
|
389
|
+
if 'is_static' in locals() and not is_static and os.path.exists(temp_dir):
|
|
390
|
+
shutil.rmtree(temp_dir)
|