pyfasm64 0.1.1__py3-none-any.whl

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.
pyfasm/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ PyFASM: Free-Threaded (No-GIL) Python to Pure FASM x64 Native Compiler.
3
+ """
4
+ from pyfasm.compiler import PyFasmCompiler
5
+ from pyfasm.bundler import ProjectBundler
6
+ from pyfasm.fasm_exporter import FasmExporter
7
+ from pyfasm.cli import main
8
+
9
+ __all__ = ["PyFasmCompiler", "ProjectBundler", "FasmExporter", "main"]
pyfasm/ast_parser.py ADDED
@@ -0,0 +1,314 @@
1
+ """
2
+ AST Parser and Semantic Analyzer for PyFASM.
3
+ Traverses Python AST, extracts functions, variables, and type information
4
+ across single files or complete multi-module project libraries.
5
+ """
6
+ from __future__ import annotations
7
+ import ast
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Any, Set
11
+ from pyfasm.types import PyFasmType, resolve_type, TYPE_INT, TYPE_VOID, TYPE_STR
12
+
13
+
14
+ @dataclass
15
+ class VariableSymbol:
16
+ name: str
17
+ var_type: PyFasmType
18
+ stack_offset: int # Offset from RBP: [rbp - stack_offset]
19
+ is_arg: bool = False
20
+ arg_index: int = 0
21
+
22
+
23
+ @dataclass
24
+ class FunctionSymbol:
25
+ name: str
26
+ args: List[VariableSymbol]
27
+ return_type: PyFasmType
28
+ local_vars: Dict[str, VariableSymbol] = field(default_factory=dict)
29
+ stack_size: int = 0
30
+ ast_node: Optional[ast.FunctionDef] = None
31
+
32
+
33
+ @dataclass
34
+ class StringLiteral:
35
+ label: str
36
+ value: str
37
+
38
+
39
+ @dataclass
40
+ class FloatLiteral:
41
+ label: str
42
+ value: float
43
+
44
+
45
+ @dataclass
46
+ class ParsedModule:
47
+ functions: Dict[str, FunctionSymbol] = field(default_factory=dict)
48
+ string_literals: List[StringLiteral] = field(default_factory=list)
49
+ float_literals: List[FloatLiteral] = field(default_factory=list)
50
+ global_vars: Dict[str, int] = field(default_factory=dict)
51
+ main_func: Optional[FunctionSymbol] = None
52
+
53
+
54
+ class AstAnalyzer:
55
+ """Extracts function signatures, local variables, and literals across single files or multi-module projects."""
56
+
57
+ def __init__(self, project_root: Optional[Path | str] = None):
58
+ self.strings: List[StringLiteral] = []
59
+ self.floats: List[FloatLiteral] = []
60
+ self.project_root: Optional[Path] = Path(project_root).resolve() if project_root else None
61
+ self._visited_files: Set[Path] = set()
62
+ self._str_counter = 0
63
+ self._flt_counter = 0
64
+
65
+ def get_or_create_string(self, text: str) -> str:
66
+ for s in self.strings:
67
+ if s.value == text:
68
+ return s.label
69
+ self._str_counter += 1
70
+ label = f"_str_lit_{self._str_counter}"
71
+ self.strings.append(StringLiteral(label=label, value=text))
72
+ return label
73
+
74
+ def get_or_create_float(self, val: float) -> str:
75
+ for f in self.floats:
76
+ if abs(f.value - val) < 1e-12:
77
+ return f.label
78
+ self._flt_counter += 1
79
+ label = f"_flt_lit_{self._flt_counter}"
80
+ self.floats.append(FloatLiteral(label=label, value=val))
81
+ return label
82
+
83
+ def parse_source(self, source_code: str) -> ParsedModule:
84
+ """Parses a raw Python source code string into a ParsedModule."""
85
+ tree = ast.parse(source_code)
86
+ module = ParsedModule()
87
+
88
+ # First pass: find string and float constants across the AST
89
+ for node in ast.walk(tree):
90
+ if isinstance(node, ast.Constant):
91
+ if isinstance(node.value, str):
92
+ self.get_or_create_string(node.value)
93
+ elif isinstance(node.value, float):
94
+ self.get_or_create_float(node.value)
95
+
96
+ module.string_literals = self.strings
97
+ module.float_literals = self.floats
98
+
99
+ # Second pass: extract functions
100
+ for stmt in tree.body:
101
+ if isinstance(stmt, ast.FunctionDef):
102
+ fn = self._analyze_function(stmt)
103
+ module.functions[fn.name] = fn
104
+ if fn.name == "main":
105
+ module.main_func = fn
106
+
107
+ return module
108
+
109
+ def parse_file(self, file_path: str | Path, project_root: Optional[Path | str] = None) -> ParsedModule:
110
+ """
111
+ Parses an entry Python file and recursively discovers and compiles all
112
+ imported local project modules into 1 unified standalone module.
113
+ """
114
+ entry_path = Path(file_path).resolve()
115
+ if project_root:
116
+ self.project_root = Path(project_root).resolve()
117
+ elif not self.project_root:
118
+ self.project_root = entry_path.parent
119
+
120
+ module = ParsedModule()
121
+ self._visited_files.clear()
122
+
123
+ # Recursively parse entry script and all internal project dependencies
124
+ self._parse_file_recursive(entry_path, module, is_entry=True)
125
+
126
+ module.string_literals = self.strings
127
+ module.float_literals = self.floats
128
+ return module
129
+
130
+ def _resolve_local_module(self, mod_name: str, current_dir: Path) -> Optional[Path]:
131
+ """Resolves an imported module name to a local .py file, strictly ignoring pyfasm itself."""
132
+ # Never add or import pyfasm itself ("естественно добавлять саму себя не должна уже")
133
+ if mod_name == "pyfasm" or mod_name.startswith("pyfasm."):
134
+ return None
135
+
136
+ parts = mod_name.split(".")
137
+ # 1. Try relative to current directory
138
+ cand1 = current_dir.joinpath(*parts).with_suffix(".py")
139
+ if cand1.exists() and cand1.is_file():
140
+ return cand1
141
+ cand2 = current_dir.joinpath(*parts, "__init__.py")
142
+ if cand2.exists() and cand2.is_file():
143
+ return cand2
144
+
145
+ # 2. Try relative to project root
146
+ if self.project_root:
147
+ cand3 = self.project_root.joinpath(*parts).with_suffix(".py")
148
+ if cand3.exists() and cand3.is_file():
149
+ return cand3
150
+ cand4 = self.project_root.joinpath(*parts, "__init__.py")
151
+ if cand4.exists() and cand4.is_file():
152
+ return cand4
153
+
154
+ return None
155
+
156
+ def _parse_file_recursive(self, path: Path, module: ParsedModule, is_entry: bool = False) -> None:
157
+ real_path = path.resolve()
158
+
159
+ # Prevent circular imports and duplicate processing
160
+ if real_path in self._visited_files:
161
+ return
162
+ self._visited_files.add(real_path)
163
+
164
+ # Ignore virtual environments, cache, and git folders
165
+ path_str = str(real_path).lower()
166
+ if any(ign in path_str for ign in [".venv", "venv", "__pycache__", "site-packages", ".git"]):
167
+ return
168
+
169
+ # Never add pyfasm source itself
170
+ if "pyfasm" in real_path.parts and "src" in real_path.parts:
171
+ try:
172
+ import pyfasm
173
+ pyfasm_pkg_dir = Path(pyfasm.__file__).parent.resolve()
174
+ if pyfasm_pkg_dir in real_path.parents or real_path == pyfasm_pkg_dir:
175
+ return
176
+ except Exception:
177
+ pass
178
+
179
+ if not real_path.exists() or not real_path.is_file():
180
+ return
181
+
182
+ try:
183
+ source_code = real_path.read_text(encoding="utf-8")
184
+ tree = ast.parse(source_code)
185
+ except Exception:
186
+ return
187
+
188
+ # 1. Collect string & float constants across the AST
189
+ for node in ast.walk(tree):
190
+ if isinstance(node, ast.Constant):
191
+ if isinstance(node.value, str):
192
+ self.get_or_create_string(node.value)
193
+ elif isinstance(node.value, float):
194
+ self.get_or_create_float(node.value)
195
+
196
+ # 2. Extract functions from this module
197
+ mod_prefix = real_path.stem
198
+ for stmt in tree.body:
199
+ if isinstance(stmt, ast.FunctionDef):
200
+ fn = self._analyze_function(stmt)
201
+ # Primary name
202
+ if fn.name not in module.functions:
203
+ module.functions[fn.name] = fn
204
+ # Namespaced alias (e.g., utils_add, math_calc)
205
+ alias_name = f"{mod_prefix}_{fn.name}"
206
+ if alias_name not in module.functions:
207
+ module.functions[alias_name] = fn
208
+
209
+ if is_entry and fn.name == "main":
210
+ module.main_func = fn
211
+
212
+ # 3. Discover and recursively parse imported project modules
213
+ for stmt in tree.body:
214
+ if isinstance(stmt, ast.Import):
215
+ for alias in stmt.names:
216
+ # Never import self or pyfasm
217
+ if alias.name == "pyfasm" or alias.name.startswith("pyfasm."):
218
+ continue
219
+ dep = self._resolve_local_module(alias.name, real_path.parent)
220
+ if dep and dep != real_path:
221
+ self._parse_file_recursive(dep, module, is_entry=False)
222
+ elif isinstance(stmt, ast.ImportFrom):
223
+ if stmt.module:
224
+ if stmt.module == "pyfasm" or stmt.module.startswith("pyfasm."):
225
+ continue
226
+ dep = self._resolve_local_module(stmt.module, real_path.parent)
227
+ if dep and dep != real_path:
228
+ self._parse_file_recursive(dep, module, is_entry=False)
229
+
230
+ def _analyze_function(self, node: ast.FunctionDef) -> FunctionSymbol:
231
+ # Determine return type
232
+ ret_type = TYPE_VOID
233
+ if node.returns:
234
+ if isinstance(node.returns, ast.Name):
235
+ ret_type = resolve_type(node.returns.id)
236
+ elif isinstance(node.returns, ast.Constant) and isinstance(node.returns.value, str):
237
+ ret_type = resolve_type(node.returns.value)
238
+
239
+ args: List[VariableSymbol] = []
240
+ local_vars: Dict[str, VariableSymbol] = {}
241
+ offset = 8
242
+
243
+ # Process function arguments
244
+ for idx, arg in enumerate(node.args.args):
245
+ arg_name = arg.arg
246
+ arg_type = TYPE_INT
247
+ if arg.annotation:
248
+ if isinstance(arg.annotation, ast.Name):
249
+ arg_type = resolve_type(arg.annotation.id)
250
+ elif isinstance(arg.annotation, ast.Constant) and isinstance(arg.annotation.value, str):
251
+ arg_type = resolve_type(arg.annotation.value)
252
+
253
+ var_sym = VariableSymbol(
254
+ name=arg_name,
255
+ var_type=arg_type,
256
+ stack_offset=offset,
257
+ is_arg=True,
258
+ arg_index=idx
259
+ )
260
+ args.append(var_sym)
261
+ local_vars[arg_name] = var_sym
262
+ offset += 8
263
+
264
+ # Discover all assigned variables in the function body
265
+ for sub_node in ast.walk(node):
266
+ if isinstance(sub_node, ast.Assign):
267
+ for target in sub_node.targets:
268
+ if isinstance(target, ast.Name):
269
+ vname = target.id
270
+ if vname not in local_vars:
271
+ local_vars[vname] = VariableSymbol(
272
+ name=vname,
273
+ var_type=TYPE_INT,
274
+ stack_offset=offset,
275
+ is_arg=False
276
+ )
277
+ offset += 8
278
+ elif isinstance(sub_node, ast.AnnAssign) and isinstance(sub_node.target, ast.Name):
279
+ vname = sub_node.target.id
280
+ vtype = TYPE_INT
281
+ if isinstance(sub_node.annotation, ast.Name):
282
+ vtype = resolve_type(sub_node.annotation.id)
283
+ elif isinstance(sub_node.annotation, ast.Constant) and isinstance(sub_node.annotation.value, str):
284
+ vtype = resolve_type(sub_node.annotation.value)
285
+ if vname not in local_vars:
286
+ local_vars[vname] = VariableSymbol(
287
+ name=vname,
288
+ var_type=vtype,
289
+ stack_offset=offset,
290
+ is_arg=False
291
+ )
292
+ offset += 8
293
+ elif isinstance(sub_node, ast.For) and isinstance(sub_node.target, ast.Name):
294
+ vname = sub_node.target.id
295
+ if vname not in local_vars:
296
+ local_vars[vname] = VariableSymbol(
297
+ name=vname,
298
+ var_type=TYPE_INT,
299
+ stack_offset=offset,
300
+ is_arg=False
301
+ )
302
+ offset += 8
303
+
304
+ # 16-byte alignment + shadow space
305
+ aligned_stack = ((offset + 15) // 16) * 16 + 32
306
+
307
+ return FunctionSymbol(
308
+ name=node.name,
309
+ args=args,
310
+ return_type=ret_type,
311
+ local_vars=local_vars,
312
+ stack_size=aligned_stack,
313
+ ast_node=node
314
+ )