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.
@@ -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)