fastpython-native 0.1.0__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.
- fastpython/__init__.py +19 -0
- fastpython/__main__.py +4 -0
- fastpython/api.py +185 -0
- fastpython/auto.py +10 -0
- fastpython/cli.py +84 -0
- fastpython/codegen/__init__.py +3 -0
- fastpython/codegen/transpiler.py +545 -0
- fastpython/compiler/__init__.py +4 -0
- fastpython/compiler/cache.py +57 -0
- fastpython/compiler/toolchain.py +147 -0
- fastpython/hardware/__init__.py +3 -0
- fastpython/hardware/cpu.py +209 -0
- fastpython/importer.py +181 -0
- fastpython/memory/__init__.py +3 -0
- fastpython/server/__init__.py +38 -0
- fastpython/simd/__init__.py +10 -0
- fastpython_native-0.1.0.dist-info/METADATA +108 -0
- fastpython_native-0.1.0.dist-info/RECORD +21 -0
- fastpython_native-0.1.0.dist-info/WHEEL +5 -0
- fastpython_native-0.1.0.dist-info/entry_points.txt +2 -0
- fastpython_native-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Transpiler for FastPython.
|
|
3
|
+
Translates Python numerical functions to ultra-high-performance C99/C++ code
|
|
4
|
+
optimized for execution on the host CPU with AVX2 SIMD and OpenMP multi-threading.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import inspect
|
|
9
|
+
import textwrap
|
|
10
|
+
from typing import Dict, List, Tuple, Optional, Any, Set
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TYPE_MAP = {
|
|
14
|
+
int: "int64_t",
|
|
15
|
+
float: "double",
|
|
16
|
+
bool: "bool",
|
|
17
|
+
"int": "int64_t",
|
|
18
|
+
"float": "double",
|
|
19
|
+
"bool": "bool",
|
|
20
|
+
"int64_t": "int64_t",
|
|
21
|
+
"double": "double",
|
|
22
|
+
"list": "double* __restrict",
|
|
23
|
+
"buffer": "double* __restrict",
|
|
24
|
+
"array": "double* __restrict",
|
|
25
|
+
"list[float]": "double* __restrict",
|
|
26
|
+
"list[int]": "int64_t* __restrict",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
MATH_FUNCS = {
|
|
30
|
+
"sqrt": "sqrt",
|
|
31
|
+
"sin": "sin",
|
|
32
|
+
"cos": "cos",
|
|
33
|
+
"tan": "tan",
|
|
34
|
+
"exp": "exp",
|
|
35
|
+
"log": "log",
|
|
36
|
+
"log2": "log2",
|
|
37
|
+
"log10": "log10",
|
|
38
|
+
"fabs": "fabs",
|
|
39
|
+
"floor": "floor",
|
|
40
|
+
"ceil": "ceil",
|
|
41
|
+
"pow": "pow",
|
|
42
|
+
"hypot": "hypot",
|
|
43
|
+
"atan": "atan",
|
|
44
|
+
"atan2": "atan2",
|
|
45
|
+
"sinh": "sinh",
|
|
46
|
+
"cosh": "cosh",
|
|
47
|
+
"tanh": "tanh",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TranspileError(Exception):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PythonToCTranspiler(ast.NodeVisitor):
|
|
56
|
+
def __init__(self, func_name: str, type_hints: Dict[str, str], parallel: bool = False):
|
|
57
|
+
self.func_name = func_name
|
|
58
|
+
self.type_hints = type_hints.copy()
|
|
59
|
+
self.parallel = parallel
|
|
60
|
+
self.indent_level = 1
|
|
61
|
+
self.declared_vars: Dict[str, str] = {} # var_name -> c_type
|
|
62
|
+
self.loop_depth = 0
|
|
63
|
+
self.code_lines: List[str] = []
|
|
64
|
+
self.return_type = self.type_hints.get("return", "double")
|
|
65
|
+
self.used_math_funcs: Set[str] = set()
|
|
66
|
+
|
|
67
|
+
def indent(self) -> str:
|
|
68
|
+
return " " * self.indent_level
|
|
69
|
+
|
|
70
|
+
def emit(self, line: str):
|
|
71
|
+
self.code_lines.append(self.indent() + line)
|
|
72
|
+
|
|
73
|
+
def transpile_function(self, node: ast.FunctionDef) -> Tuple[str, Dict[str, str], str]:
|
|
74
|
+
# Identify arguments and assign C types
|
|
75
|
+
args_c_signature = []
|
|
76
|
+
for arg in node.args.args:
|
|
77
|
+
arg_name = arg.arg
|
|
78
|
+
if arg_name in self.type_hints:
|
|
79
|
+
c_type = self.type_hints[arg_name]
|
|
80
|
+
elif arg.annotation:
|
|
81
|
+
c_type = self._resolve_type_annotation(arg.annotation)
|
|
82
|
+
else:
|
|
83
|
+
c_type = "double"
|
|
84
|
+
|
|
85
|
+
self.type_hints[arg_name] = c_type
|
|
86
|
+
self.declared_vars[arg_name] = c_type
|
|
87
|
+
args_c_signature.append(f"{c_type} {arg_name}")
|
|
88
|
+
|
|
89
|
+
# Check return annotation
|
|
90
|
+
if node.returns:
|
|
91
|
+
self.return_type = self._resolve_type_annotation(node.returns)
|
|
92
|
+
|
|
93
|
+
# Process function body
|
|
94
|
+
for stmt in node.body:
|
|
95
|
+
self.visit(stmt)
|
|
96
|
+
|
|
97
|
+
body_code = "\n".join(self.code_lines)
|
|
98
|
+
sig = f"{self.return_type} kernel_{self.func_name}({', '.join(args_c_signature)})"
|
|
99
|
+
c_func = f"{sig} {{\n{body_code}\n}}"
|
|
100
|
+
return c_func, self.type_hints, self.return_type
|
|
101
|
+
|
|
102
|
+
def _resolve_type_annotation(self, node: ast.AST) -> str:
|
|
103
|
+
if isinstance(node, ast.Name):
|
|
104
|
+
return TYPE_MAP.get(node.id, "double")
|
|
105
|
+
elif isinstance(node, ast.Constant):
|
|
106
|
+
return TYPE_MAP.get(node.value, "double")
|
|
107
|
+
elif isinstance(node, ast.Subscript):
|
|
108
|
+
if isinstance(node.value, ast.Name) and node.value.id in ("list", "List", "Sequence", "array", "buffer"):
|
|
109
|
+
sub_t = self._resolve_type_annotation(node.slice)
|
|
110
|
+
base = "int64_t" if "int" in sub_t else "double"
|
|
111
|
+
return f"{base}* __restrict"
|
|
112
|
+
return "double"
|
|
113
|
+
|
|
114
|
+
def visit_Assign(self, node: ast.Assign):
|
|
115
|
+
if len(node.targets) != 1:
|
|
116
|
+
raise TranspileError("Multiple assignment targets not supported in fast kernel")
|
|
117
|
+
|
|
118
|
+
target = node.targets[0]
|
|
119
|
+
val_expr = self.visit(node.value)
|
|
120
|
+
|
|
121
|
+
if isinstance(target, ast.Name):
|
|
122
|
+
var_name = target.id
|
|
123
|
+
if var_name not in self.declared_vars:
|
|
124
|
+
inferred_type = self._infer_expr_type(node.value)
|
|
125
|
+
self.declared_vars[var_name] = inferred_type
|
|
126
|
+
self.emit(f"{inferred_type} {var_name} = {val_expr};")
|
|
127
|
+
else:
|
|
128
|
+
self.emit(f"{var_name} = {val_expr};")
|
|
129
|
+
elif isinstance(target, ast.Subscript):
|
|
130
|
+
target_expr = self.visit(target)
|
|
131
|
+
self.emit(f"{target_expr} = {val_expr};")
|
|
132
|
+
else:
|
|
133
|
+
raise TranspileError(f"Unsupported assignment target: {ast.dump(target)}")
|
|
134
|
+
|
|
135
|
+
def visit_AugAssign(self, node: ast.AugAssign):
|
|
136
|
+
target = self.visit(node.target)
|
|
137
|
+
val = self.visit(node.value)
|
|
138
|
+
op_map = {
|
|
139
|
+
ast.Add: "+=",
|
|
140
|
+
ast.Sub: "-=",
|
|
141
|
+
ast.Mult: "*=",
|
|
142
|
+
ast.Div: "/=",
|
|
143
|
+
ast.BitAnd: "&=",
|
|
144
|
+
ast.BitOr: "|=",
|
|
145
|
+
ast.BitXor: "^=",
|
|
146
|
+
ast.LShift: "<<=",
|
|
147
|
+
ast.RShift: ">>=",
|
|
148
|
+
}
|
|
149
|
+
op = op_map.get(type(node.op))
|
|
150
|
+
if not op:
|
|
151
|
+
raise TranspileError(f"Unsupported augmented operator: {type(node.op)}")
|
|
152
|
+
self.emit(f"{target} {op} {val};")
|
|
153
|
+
|
|
154
|
+
def _find_reductions(self, node: ast.For) -> List[str]:
|
|
155
|
+
reductions = []
|
|
156
|
+
for stmt in ast.walk(node):
|
|
157
|
+
if isinstance(stmt, ast.AugAssign) and isinstance(stmt.target, ast.Name):
|
|
158
|
+
var = stmt.target.id
|
|
159
|
+
if var in self.declared_vars and var not in reductions:
|
|
160
|
+
reductions.append(var)
|
|
161
|
+
return reductions
|
|
162
|
+
|
|
163
|
+
def visit_For(self, node: ast.For):
|
|
164
|
+
self.loop_depth += 1
|
|
165
|
+
is_outer = (self.loop_depth == 1)
|
|
166
|
+
|
|
167
|
+
if isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) and node.iter.func.id == "range":
|
|
168
|
+
args = node.iter.args
|
|
169
|
+
var_name = node.target.id
|
|
170
|
+
self.declared_vars[var_name] = "int64_t"
|
|
171
|
+
|
|
172
|
+
if len(args) == 1:
|
|
173
|
+
start = "0"
|
|
174
|
+
stop = self.visit(args[0])
|
|
175
|
+
step = "1"
|
|
176
|
+
elif len(args) == 2:
|
|
177
|
+
start = self.visit(args[0])
|
|
178
|
+
stop = self.visit(args[1])
|
|
179
|
+
step = "1"
|
|
180
|
+
elif len(args) == 3:
|
|
181
|
+
start = self.visit(args[0])
|
|
182
|
+
stop = self.visit(args[1])
|
|
183
|
+
step = self.visit(args[2])
|
|
184
|
+
else:
|
|
185
|
+
raise TranspileError("range() with invalid arguments")
|
|
186
|
+
|
|
187
|
+
if is_outer and self.parallel:
|
|
188
|
+
reductions = self._find_reductions(node)
|
|
189
|
+
red_clause = ""
|
|
190
|
+
if reductions:
|
|
191
|
+
red_clause = f" reduction(+:{', '.join(reductions)})"
|
|
192
|
+
|
|
193
|
+
self.emit(f"int {var_name};")
|
|
194
|
+
self.emit(f"#pragma omp parallel for schedule(guided){red_clause}")
|
|
195
|
+
step_expr = f"{var_name}++" if step == "1" else f"{var_name} += {step}"
|
|
196
|
+
self.emit(f"for ({var_name} = (int)({start}); {var_name} < (int)({stop}); {step_expr}) {{")
|
|
197
|
+
else:
|
|
198
|
+
# Add SIMD auto-vectorization hint for inner loops
|
|
199
|
+
self.emit("#pragma loop(ivdep)")
|
|
200
|
+
step_expr = f"{var_name}++" if step == "1" else f"{var_name} += {step}"
|
|
201
|
+
self.emit(f"for (int64_t {var_name} = {start}; {var_name} < {stop}; {step_expr}) {{")
|
|
202
|
+
|
|
203
|
+
self.indent_level += 1
|
|
204
|
+
for stmt in node.body:
|
|
205
|
+
self.visit(stmt)
|
|
206
|
+
self.indent_level -= 1
|
|
207
|
+
self.emit("}")
|
|
208
|
+
else:
|
|
209
|
+
raise TranspileError("Only range() iterations are currently supported in accelerated for-loops")
|
|
210
|
+
|
|
211
|
+
self.loop_depth -= 1
|
|
212
|
+
|
|
213
|
+
def visit_While(self, node: ast.While):
|
|
214
|
+
cond = self.visit(node.test)
|
|
215
|
+
self.emit(f"while ({cond}) {{")
|
|
216
|
+
self.indent_level += 1
|
|
217
|
+
for stmt in node.body:
|
|
218
|
+
self.visit(stmt)
|
|
219
|
+
self.indent_level -= 1
|
|
220
|
+
self.emit("}")
|
|
221
|
+
|
|
222
|
+
def visit_If(self, node: ast.If):
|
|
223
|
+
cond = self.visit(node.test)
|
|
224
|
+
self.emit(f"if ({cond}) {{")
|
|
225
|
+
self.indent_level += 1
|
|
226
|
+
for stmt in node.body:
|
|
227
|
+
self.visit(stmt)
|
|
228
|
+
self.indent_level -= 1
|
|
229
|
+
|
|
230
|
+
if node.orelse:
|
|
231
|
+
self.emit("} else {")
|
|
232
|
+
self.indent_level += 1
|
|
233
|
+
for stmt in node.orelse:
|
|
234
|
+
self.visit(stmt)
|
|
235
|
+
self.indent_level -= 1
|
|
236
|
+
self.emit("}")
|
|
237
|
+
|
|
238
|
+
def visit_Return(self, node: ast.Return):
|
|
239
|
+
if node.value is None:
|
|
240
|
+
self.emit("return;")
|
|
241
|
+
else:
|
|
242
|
+
val = self.visit(node.value)
|
|
243
|
+
self.emit(f"return ({self.return_type})({val});")
|
|
244
|
+
|
|
245
|
+
def visit_Break(self, node: ast.Break):
|
|
246
|
+
self.emit("break;")
|
|
247
|
+
|
|
248
|
+
def visit_Continue(self, node: ast.Continue):
|
|
249
|
+
self.emit("continue;")
|
|
250
|
+
|
|
251
|
+
def visit_Pass(self, node: ast.Pass):
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
def visit_Subscript(self, node: ast.Subscript) -> str:
|
|
255
|
+
val = self.visit(node.value)
|
|
256
|
+
slc = self.visit(node.slice)
|
|
257
|
+
return f"{val}[{slc}]"
|
|
258
|
+
|
|
259
|
+
def visit_BinOp(self, node: ast.BinOp) -> str:
|
|
260
|
+
left = self.visit(node.left)
|
|
261
|
+
right = self.visit(node.right)
|
|
262
|
+
if isinstance(node.op, ast.Pow):
|
|
263
|
+
return f"pow((double)({left}), (double)({right}))"
|
|
264
|
+
elif isinstance(node.op, ast.FloorDiv):
|
|
265
|
+
return f"((int64_t)({left}) / (int64_t)({right}))"
|
|
266
|
+
|
|
267
|
+
op_symbols = {
|
|
268
|
+
ast.Add: "+",
|
|
269
|
+
ast.Sub: "-",
|
|
270
|
+
ast.Mult: "*",
|
|
271
|
+
ast.Div: "/",
|
|
272
|
+
ast.Mod: "%",
|
|
273
|
+
ast.BitAnd: "&",
|
|
274
|
+
ast.BitOr: "|",
|
|
275
|
+
ast.BitXor: "^",
|
|
276
|
+
ast.LShift: "<<",
|
|
277
|
+
ast.RShift: ">>",
|
|
278
|
+
}
|
|
279
|
+
sym = op_symbols.get(type(node.op), "+")
|
|
280
|
+
return f"({left} {sym} {right})"
|
|
281
|
+
|
|
282
|
+
def visit_UnaryOp(self, node: ast.UnaryOp) -> str:
|
|
283
|
+
operand = self.visit(node.operand)
|
|
284
|
+
if isinstance(node.op, ast.USub):
|
|
285
|
+
return f"(-{operand})"
|
|
286
|
+
elif isinstance(node.op, ast.UAdd):
|
|
287
|
+
return f"(+{operand})"
|
|
288
|
+
elif isinstance(node.op, ast.Not):
|
|
289
|
+
return f"(!{operand})"
|
|
290
|
+
elif isinstance(node.op, ast.Invert):
|
|
291
|
+
return f"(~{operand})"
|
|
292
|
+
return operand
|
|
293
|
+
|
|
294
|
+
def visit_Compare(self, node: ast.Compare) -> str:
|
|
295
|
+
left = self.visit(node.left)
|
|
296
|
+
parts = []
|
|
297
|
+
curr_left = left
|
|
298
|
+
comp_ops = {
|
|
299
|
+
ast.Eq: "==",
|
|
300
|
+
ast.NotEq: "!=",
|
|
301
|
+
ast.Lt: "<",
|
|
302
|
+
ast.LtE: "<=",
|
|
303
|
+
ast.Gt: ">",
|
|
304
|
+
ast.GtE: ">=",
|
|
305
|
+
}
|
|
306
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
307
|
+
sym = comp_ops.get(type(op))
|
|
308
|
+
if not sym:
|
|
309
|
+
raise TranspileError(f"Unsupported comparison operator: {type(op)}")
|
|
310
|
+
right = self.visit(comparator)
|
|
311
|
+
parts.append(f"({curr_left} {sym} {right})")
|
|
312
|
+
curr_left = right
|
|
313
|
+
return " && ".join(parts)
|
|
314
|
+
|
|
315
|
+
def visit_BoolOp(self, node: ast.BoolOp) -> str:
|
|
316
|
+
op = "&&" if isinstance(node.op, ast.And) else "||"
|
|
317
|
+
values = [self.visit(v) for v in node.values]
|
|
318
|
+
return f"({f' {op} '.join(values)})"
|
|
319
|
+
|
|
320
|
+
def visit_Name(self, node: ast.Name) -> str:
|
|
321
|
+
if node.id == "True":
|
|
322
|
+
return "true"
|
|
323
|
+
elif node.id == "False":
|
|
324
|
+
return "false"
|
|
325
|
+
return node.id
|
|
326
|
+
|
|
327
|
+
def visit_Constant(self, node: ast.Constant) -> str:
|
|
328
|
+
if isinstance(node.value, bool):
|
|
329
|
+
return "true" if node.value else "false"
|
|
330
|
+
elif isinstance(node.value, (int, float)):
|
|
331
|
+
return str(node.value)
|
|
332
|
+
elif isinstance(node.value, str):
|
|
333
|
+
return f'"{node.value}"'
|
|
334
|
+
return str(node.value)
|
|
335
|
+
|
|
336
|
+
def visit_Call(self, node: ast.Call) -> str:
|
|
337
|
+
func_name = None
|
|
338
|
+
if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "math":
|
|
339
|
+
func_name = node.func.attr
|
|
340
|
+
elif isinstance(node.func, ast.Name):
|
|
341
|
+
func_name = node.func.id
|
|
342
|
+
|
|
343
|
+
if func_name in MATH_FUNCS:
|
|
344
|
+
c_name = MATH_FUNCS[func_name]
|
|
345
|
+
args = [self.visit(a) for a in node.args]
|
|
346
|
+
return f"{c_name}({', '.join(args)})"
|
|
347
|
+
elif func_name == "abs":
|
|
348
|
+
arg = self.visit(node.args[0])
|
|
349
|
+
return f"fabs((double)({arg}))"
|
|
350
|
+
elif func_name == "min":
|
|
351
|
+
a = self.visit(node.args[0])
|
|
352
|
+
b = self.visit(node.args[1])
|
|
353
|
+
return f"(({a}) < ({b}) ? ({a}) : ({b}))"
|
|
354
|
+
elif func_name == "max":
|
|
355
|
+
a = self.visit(node.args[0])
|
|
356
|
+
b = self.visit(node.args[1])
|
|
357
|
+
return f"(({a}) > ({b}) ? ({a}) : ({b}))"
|
|
358
|
+
elif func_name in ("int", "int64_t"):
|
|
359
|
+
arg = self.visit(node.args[0])
|
|
360
|
+
return f"((int64_t)({arg}))"
|
|
361
|
+
elif func_name in ("float", "double"):
|
|
362
|
+
arg = self.visit(node.args[0])
|
|
363
|
+
return f"((double)({arg}))"
|
|
364
|
+
|
|
365
|
+
raise TranspileError(f"Unsupported call: {func_name}()")
|
|
366
|
+
|
|
367
|
+
def _infer_expr_type(self, node: ast.AST) -> str:
|
|
368
|
+
if isinstance(node, ast.Constant):
|
|
369
|
+
if isinstance(node.value, bool):
|
|
370
|
+
return "bool"
|
|
371
|
+
elif isinstance(node.value, int):
|
|
372
|
+
return "int64_t"
|
|
373
|
+
elif isinstance(node.value, float):
|
|
374
|
+
return "double"
|
|
375
|
+
elif isinstance(node, ast.Name):
|
|
376
|
+
return self.declared_vars.get(node.id, "double")
|
|
377
|
+
elif isinstance(node, ast.BinOp):
|
|
378
|
+
left_t = self._infer_expr_type(node.left)
|
|
379
|
+
right_t = self._infer_expr_type(node.right)
|
|
380
|
+
if left_t == "double" or right_t == "double" or isinstance(node.op, ast.Div):
|
|
381
|
+
return "double"
|
|
382
|
+
return "int64_t"
|
|
383
|
+
elif isinstance(node, ast.UnaryOp):
|
|
384
|
+
return self._infer_expr_type(node.operand)
|
|
385
|
+
elif isinstance(node, ast.Call):
|
|
386
|
+
if isinstance(node.func, ast.Name) and node.func.id in ("int", "len"):
|
|
387
|
+
return "int64_t"
|
|
388
|
+
return "double"
|
|
389
|
+
elif isinstance(node, ast.Subscript):
|
|
390
|
+
if isinstance(node.value, ast.Name):
|
|
391
|
+
arr_t = self.declared_vars.get(node.value.id, "")
|
|
392
|
+
if "int64_t" in arr_t:
|
|
393
|
+
return "int64_t"
|
|
394
|
+
return "double"
|
|
395
|
+
return "double"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def generate_c_extension_source(
|
|
399
|
+
func: Any,
|
|
400
|
+
module_name: Optional[str] = None,
|
|
401
|
+
type_hints: Optional[Dict[str, Any]] = None,
|
|
402
|
+
parallel: bool = False,
|
|
403
|
+
fastmath: bool = True,
|
|
404
|
+
source_code: Optional[str] = None,
|
|
405
|
+
ast_node: Optional[ast.FunctionDef] = None
|
|
406
|
+
) -> str:
|
|
407
|
+
if ast_node is not None:
|
|
408
|
+
func_def = ast_node
|
|
409
|
+
else:
|
|
410
|
+
if source_code is None:
|
|
411
|
+
source_code = inspect.getsource(func)
|
|
412
|
+
dedented = textwrap.dedent(source_code)
|
|
413
|
+
tree = ast.parse(dedented)
|
|
414
|
+
|
|
415
|
+
func_def = None
|
|
416
|
+
for item in tree.body:
|
|
417
|
+
if isinstance(item, ast.FunctionDef):
|
|
418
|
+
func_def = item
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
if not func_def:
|
|
422
|
+
raise TranspileError("Could not find function definition in source code")
|
|
423
|
+
|
|
424
|
+
if module_name is None:
|
|
425
|
+
module_name = f"native_{func_def.name}"
|
|
426
|
+
|
|
427
|
+
hints = {}
|
|
428
|
+
if type_hints:
|
|
429
|
+
for k, v in type_hints.items():
|
|
430
|
+
if v in TYPE_MAP:
|
|
431
|
+
hints[k] = TYPE_MAP[v]
|
|
432
|
+
elif isinstance(v, str):
|
|
433
|
+
if v in TYPE_MAP:
|
|
434
|
+
hints[k] = TYPE_MAP[v]
|
|
435
|
+
elif "list" in v or "array" in v:
|
|
436
|
+
hints[k] = "int64_t* __restrict" if "int" in v else "double* __restrict"
|
|
437
|
+
|
|
438
|
+
transpiler = PythonToCTranspiler(func_def.name, hints, parallel=parallel)
|
|
439
|
+
kernel_code, resolved_hints, ret_type = transpiler.transpile_function(func_def)
|
|
440
|
+
|
|
441
|
+
format_chars = []
|
|
442
|
+
c_var_decls = []
|
|
443
|
+
c_var_names = []
|
|
444
|
+
buffer_pre = []
|
|
445
|
+
buffer_post = []
|
|
446
|
+
|
|
447
|
+
for arg in func_def.args.args:
|
|
448
|
+
name = arg.arg
|
|
449
|
+
t = resolved_hints.get(name, "double")
|
|
450
|
+
if t == "int64_t":
|
|
451
|
+
format_chars.append("L")
|
|
452
|
+
c_var_decls.append(f" long long {name};")
|
|
453
|
+
c_var_names.append(f"(int64_t){name}")
|
|
454
|
+
elif t == "double":
|
|
455
|
+
format_chars.append("d")
|
|
456
|
+
c_var_decls.append(f" double {name};")
|
|
457
|
+
c_var_names.append(name)
|
|
458
|
+
elif t == "bool":
|
|
459
|
+
format_chars.append("p")
|
|
460
|
+
c_var_decls.append(f" int {name};")
|
|
461
|
+
c_var_names.append(f"(bool){name}")
|
|
462
|
+
elif "*" in t:
|
|
463
|
+
# Buffer / array pointer parameter
|
|
464
|
+
format_chars.append("O")
|
|
465
|
+
c_var_decls.append(f" PyObject* py_{name};")
|
|
466
|
+
c_var_decls.append(f" Py_buffer view_{name};")
|
|
467
|
+
base_type = "int64_t" if "int64_t" in t else "double"
|
|
468
|
+
buffer_pre.append(
|
|
469
|
+
f" if (PyObject_GetBuffer(py_{name}, &view_{name}, PyBUF_SIMPLE | PyBUF_WRITABLE) != 0) {{\n"
|
|
470
|
+
f" return NULL;\n"
|
|
471
|
+
f" }}\n"
|
|
472
|
+
f" {base_type}* {name} = ({base_type}*)view_{name}.buf;"
|
|
473
|
+
)
|
|
474
|
+
buffer_post.append(f" PyBuffer_Release(&view_{name});")
|
|
475
|
+
c_var_names.append(name)
|
|
476
|
+
|
|
477
|
+
parse_fmt = "".join(format_chars)
|
|
478
|
+
parse_ptrs = ", ".join([f"&py_{arg.arg}" if "*" in resolved_hints.get(arg.arg, "") else f"&{arg.arg}" for arg in func_def.args.args])
|
|
479
|
+
|
|
480
|
+
if ret_type == "int64_t":
|
|
481
|
+
ret_conv = "PyLong_FromLongLong((long long)result)"
|
|
482
|
+
elif ret_type == "double":
|
|
483
|
+
ret_conv = "PyFloat_FromDouble(result)"
|
|
484
|
+
elif ret_type == "bool":
|
|
485
|
+
ret_conv = "PyBool_FromLong(result ? 1 : 0)"
|
|
486
|
+
else:
|
|
487
|
+
ret_conv = "Py_None; Py_INCREF(Py_None)"
|
|
488
|
+
|
|
489
|
+
decls_str = "\n".join(c_var_decls)
|
|
490
|
+
buf_pre_str = "\n".join(buffer_pre)
|
|
491
|
+
buf_post_str = "\n".join(buffer_post)
|
|
492
|
+
kernel_call_str = f"kernel_{func_def.name}({', '.join(c_var_names)})"
|
|
493
|
+
|
|
494
|
+
c_source = f"""/* Auto-generated by FastPython Hardware-AOT Compiler */
|
|
495
|
+
#define PY_SSIZE_T_CLEAN
|
|
496
|
+
#include <Python.h>
|
|
497
|
+
#include <stdint.h>
|
|
498
|
+
#include <stdbool.h>
|
|
499
|
+
#include <math.h>
|
|
500
|
+
|
|
501
|
+
#ifdef _OPENMP
|
|
502
|
+
#include <omp.h>
|
|
503
|
+
#endif
|
|
504
|
+
|
|
505
|
+
/* Native Hardware-Optimized Kernel */
|
|
506
|
+
{kernel_code}
|
|
507
|
+
|
|
508
|
+
/* Python C-API Wrapper */
|
|
509
|
+
static PyObject* py_{func_def.name}(PyObject* self, PyObject* args) {{
|
|
510
|
+
{decls_str}
|
|
511
|
+
if (!PyArg_ParseTuple(args, "{parse_fmt}", {parse_ptrs})) {{
|
|
512
|
+
return NULL;
|
|
513
|
+
}}
|
|
514
|
+
|
|
515
|
+
{buf_pre_str}
|
|
516
|
+
|
|
517
|
+
/* Release GIL for pure bare-metal execution across all host cores */
|
|
518
|
+
{ret_type} result;
|
|
519
|
+
Py_BEGIN_ALLOW_THREADS
|
|
520
|
+
result = {kernel_call_str};
|
|
521
|
+
Py_END_ALLOW_THREADS
|
|
522
|
+
|
|
523
|
+
{buf_post_str}
|
|
524
|
+
|
|
525
|
+
return {ret_conv};
|
|
526
|
+
}}
|
|
527
|
+
|
|
528
|
+
static PyMethodDef FastMethods[] = {{
|
|
529
|
+
{{"{func_def.name}", py_{func_def.name}, METH_VARARGS, "FastPython Hardware-Optimized Native Function"}},
|
|
530
|
+
{{NULL, NULL, 0, NULL}}
|
|
531
|
+
}};
|
|
532
|
+
|
|
533
|
+
static struct PyModuleDef fastmodule = {{
|
|
534
|
+
PyModuleDef_HEAD_INIT,
|
|
535
|
+
"{module_name}",
|
|
536
|
+
"FastPython compiled hardware-native module",
|
|
537
|
+
-1,
|
|
538
|
+
FastMethods
|
|
539
|
+
}};
|
|
540
|
+
|
|
541
|
+
PyMODINIT_FUNC PyInit_{module_name}(void) {{
|
|
542
|
+
return PyModule_Create(&fastmodule);
|
|
543
|
+
}}
|
|
544
|
+
"""
|
|
545
|
+
return c_source
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache manager for FastPython.
|
|
3
|
+
Manages disk caching of compiled native extensions using SHA-256 hashes
|
|
4
|
+
of code AST, CPU instruction sets, and compilation options.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import hashlib
|
|
10
|
+
import importlib.util
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Callable, Optional
|
|
13
|
+
from ..hardware.cpu import get_cpu_info
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModuleCache:
|
|
17
|
+
def __init__(self, cache_dir: Optional[str] = None):
|
|
18
|
+
if cache_dir is None:
|
|
19
|
+
self.cache_dir = Path.cwd() / ".fastpython_cache"
|
|
20
|
+
else:
|
|
21
|
+
self.cache_dir = Path(cache_dir)
|
|
22
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def compute_hash(self, source_code: str, extra_meta: str = "") -> str:
|
|
25
|
+
cpu = get_cpu_info()
|
|
26
|
+
hasher = hashlib.sha256()
|
|
27
|
+
hasher.update(source_code.encode("utf-8"))
|
|
28
|
+
hasher.update(cpu.summary().encode("utf-8"))
|
|
29
|
+
hasher.update(f"py{sys.version_info.major}.{sys.version_info.minor}".encode("utf-8"))
|
|
30
|
+
hasher.update(extra_meta.encode("utf-8"))
|
|
31
|
+
return hasher.hexdigest()[:16]
|
|
32
|
+
|
|
33
|
+
def get_module_paths(self, module_name: str, code_hash: str):
|
|
34
|
+
suffix = ".pyd" if sys.platform == "win32" else ".so"
|
|
35
|
+
c_file = self.cache_dir / f"{module_name}.c"
|
|
36
|
+
pyd_file = self.cache_dir / f"{module_name}{suffix}"
|
|
37
|
+
return str(c_file), str(pyd_file)
|
|
38
|
+
|
|
39
|
+
def is_cached(self, pyd_path: str) -> bool:
|
|
40
|
+
return os.path.exists(pyd_path) and os.path.getsize(pyd_path) > 0
|
|
41
|
+
|
|
42
|
+
def load_extension(self, module_name: str, pyd_path: str, func_name: str) -> Callable[..., Any]:
|
|
43
|
+
"""Dynamically load the compiled native shared library and return the function."""
|
|
44
|
+
# Use unique module spec name to avoid collisions
|
|
45
|
+
spec_name = Path(pyd_path).stem
|
|
46
|
+
spec = importlib.util.spec_from_file_location(spec_name, pyd_path)
|
|
47
|
+
if spec is None or spec.loader is None:
|
|
48
|
+
raise ImportError(f"Could not create module spec from {pyd_path}")
|
|
49
|
+
|
|
50
|
+
module = importlib.util.module_from_spec(spec)
|
|
51
|
+
sys.modules[spec_name] = module
|
|
52
|
+
spec.loader.exec_module(module)
|
|
53
|
+
|
|
54
|
+
if not hasattr(module, func_name):
|
|
55
|
+
raise AttributeError(f"Module {spec_name} does not export function '{func_name}'")
|
|
56
|
+
|
|
57
|
+
return getattr(module, func_name)
|