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 +9 -0
- pyfasm/ast_parser.py +314 -0
- pyfasm/bundler.py +698 -0
- pyfasm/cli.py +141 -0
- pyfasm/codegen_x64.py +666 -0
- pyfasm/compiler.py +125 -0
- pyfasm/fasm_exporter.py +211 -0
- pyfasm/fasm_runner.py +254 -0
- pyfasm/runtime_linux.py +296 -0
- pyfasm/runtime_macos.py +336 -0
- pyfasm/runtime_myos.py +153 -0
- pyfasm/runtime_win64.py +414 -0
- pyfasm/types.py +99 -0
- pyfasm64/__init__.py +9 -0
- pyfasm64-0.1.1.dist-info/METADATA +350 -0
- pyfasm64-0.1.1.dist-info/RECORD +20 -0
- pyfasm64-0.1.1.dist-info/WHEEL +5 -0
- pyfasm64-0.1.1.dist-info/entry_points.txt +3 -0
- pyfasm64-0.1.1.dist-info/licenses/LICENSE +21 -0
- pyfasm64-0.1.1.dist-info/top_level.txt +2 -0
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
|
+
)
|