mapFolding 0.8.6__py3-none-any.whl → 0.9.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.
Files changed (34) hide show
  1. mapFolding/__init__.py +60 -13
  2. mapFolding/basecamp.py +32 -17
  3. mapFolding/beDRY.py +4 -5
  4. mapFolding/oeis.py +94 -7
  5. mapFolding/someAssemblyRequired/RecipeJob.py +103 -0
  6. mapFolding/someAssemblyRequired/__init__.py +71 -50
  7. mapFolding/someAssemblyRequired/_theTypes.py +11 -15
  8. mapFolding/someAssemblyRequired/_tool_Make.py +36 -9
  9. mapFolding/someAssemblyRequired/_tool_Then.py +59 -25
  10. mapFolding/someAssemblyRequired/_toolboxAntecedents.py +159 -272
  11. mapFolding/someAssemblyRequired/_toolboxContainers.py +155 -70
  12. mapFolding/someAssemblyRequired/_toolboxPython.py +168 -44
  13. mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +154 -39
  14. mapFolding/someAssemblyRequired/toolboxNumba.py +72 -230
  15. mapFolding/someAssemblyRequired/transformationTools.py +370 -141
  16. mapFolding/syntheticModules/{numbaCount_doTheNeedful.py → numbaCount.py} +7 -4
  17. mapFolding/theDao.py +19 -16
  18. mapFolding/theSSOT.py +165 -62
  19. mapFolding/toolboxFilesystem.py +1 -1
  20. mapfolding-0.9.1.dist-info/METADATA +177 -0
  21. mapfolding-0.9.1.dist-info/RECORD +47 -0
  22. tests/__init__.py +44 -0
  23. tests/conftest.py +75 -7
  24. tests/test_computations.py +92 -10
  25. tests/test_filesystem.py +32 -33
  26. tests/test_other.py +0 -1
  27. tests/test_tasks.py +1 -1
  28. mapFolding/someAssemblyRequired/newInliner.py +0 -22
  29. mapfolding-0.8.6.dist-info/METADATA +0 -190
  30. mapfolding-0.8.6.dist-info/RECORD +0 -47
  31. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/WHEEL +0 -0
  32. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/entry_points.txt +0 -0
  33. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/licenses/LICENSE +0 -0
  34. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,23 @@
1
1
  """
2
- Tools for transforming Python code through abstract syntax tree (AST) manipulation.
3
-
4
- This module provides a comprehensive set of utilities for programmatically analyzing,
5
- transforming, and generating Python code through AST manipulation. It implements
6
- a highly flexible framework that enables:
7
-
8
- 1. Precise identification of code patterns through composable predicates
9
- 2. Targeted modification of code structures while preserving semantics
10
- 3. Code generation with proper syntax and import management
11
- 4. Analysis of code dependencies and relationships
12
- 5. Clean transformation of one algorithmic implementation to another
13
-
14
- The utilities are organized into several key components:
15
- - Predicate factories (ifThis): Create composable functions for matching AST patterns
16
- - Node transformers: Modify AST structures in targeted ways
17
- - Code generation helpers (Make): Create well-formed AST nodes programmatically
18
- - Import tracking: Maintain proper imports during code transformation
19
- - Analysis tools: Extract and organize code information
20
-
21
- While these tools were developed to transform the baseline algorithm into optimized formats,
22
- they are designed as general-purpose utilities applicable to a wide range of code
23
- transformation scenarios beyond the scope of this package.
2
+ AST Transformation Tools for Python Code Generation
3
+
4
+ This module provides tools for manipulating and transforming Python abstract syntax trees
5
+ to generate optimized code. It implements a system that:
6
+
7
+ 1. Extracts functions and classes from existing modules.
8
+ 2. Reshapes and transforms them through AST manipulation.
9
+ 3. Manages dependencies and imports.
10
+ 4. Generates optimized code with specialized implementations.
11
+
12
+ The module is particularly focused on transforming general-purpose Python code into
13
+ high-performance implementations, especially through dataclass decomposition and
14
+ function inlining for Numba compatibility.
15
+
16
+ At its core, the module implements a transformation assembly-line where code flows from
17
+ readable, maintainable implementations to highly optimized versions while preserving
18
+ logical structure and correctness.
24
19
  """
20
+
25
21
  from autoflake import fix_code as autoflake_fix_code
26
22
  from collections.abc import Callable, Mapping
27
23
  from copy import deepcopy
@@ -31,8 +27,8 @@ from mapFolding.someAssemblyRequired import (
31
27
  ast_Identifier,
32
28
  be,
33
29
  DOT,
30
+ grab,
34
31
  ifThis,
35
- ImaAnnotationType,
36
32
  importLogicalPath2Callable,
37
33
  IngredientsFunction,
38
34
  IngredientsModule,
@@ -41,75 +37,124 @@ from mapFolding.someAssemblyRequired import (
41
37
  NodeChanger,
42
38
  NodeTourist,
43
39
  parseLogicalPath2astModule,
40
+ RecipeSynthesizeFlow,
44
41
  ShatteredDataclass,
45
42
  str_nameDOTname,
46
43
  Then,
47
- TypeCertified,
48
- 又,
44
+ 个,
49
45
  )
50
- from mapFolding.theSSOT import ComputationState, The, raiseIfNoneGitHubIssueNumber3
46
+ from mapFolding.theSSOT import ComputationState, raiseIfNoneGitHubIssueNumber3, The
51
47
  from os import PathLike
52
48
  from pathlib import Path, PurePath
53
49
  from typing import Any, Literal, overload
54
50
  import ast
55
51
  import dataclasses
56
52
  import pickle
53
+ import python_minifier
57
54
 
58
55
  def astModuleToIngredientsFunction(astModule: ast.AST, identifierFunctionDef: ast_Identifier) -> IngredientsFunction:
56
+ """
57
+ Extract a function definition from an AST module and create an IngredientsFunction.
58
+
59
+ This function finds a function definition with the specified identifier in the given
60
+ AST module and wraps it in an IngredientsFunction object along with its import context.
61
+
62
+ Parameters:
63
+ astModule: The AST module containing the function definition.
64
+ identifierFunctionDef: The name of the function to extract.
65
+
66
+ Returns:
67
+ An IngredientsFunction object containing the function definition and its imports.
68
+
69
+ Raises:
70
+ raiseIfNoneGitHubIssueNumber3: If the function definition is not found.
71
+ """
59
72
  astFunctionDef = extractFunctionDef(astModule, identifierFunctionDef)
60
73
  if not astFunctionDef: raise raiseIfNoneGitHubIssueNumber3
61
74
  return IngredientsFunction(astFunctionDef, LedgerOfImports(astModule))
62
75
 
63
76
  def extractClassDef(module: ast.AST, identifier: ast_Identifier) -> ast.ClassDef | None:
64
- return NodeTourist(ifThis.isClassDef_Identifier(identifier), Then.getIt).captureLastMatch(module)
77
+ """
78
+ Extract a class definition with a specific name from an AST module.
79
+
80
+ This function searches through an AST module for a class definition that
81
+ matches the provided identifier and returns it if found.
82
+
83
+ Parameters:
84
+ module: The AST module to search within.
85
+ identifier: The name of the class to find.
86
+
87
+ Returns:
88
+ The matching class definition AST node, or None if not found.
89
+ """
90
+ return NodeTourist(ifThis.isClassDef_Identifier(identifier), Then.extractIt).captureLastMatch(module)
65
91
 
66
92
  def extractFunctionDef(module: ast.AST, identifier: ast_Identifier) -> ast.FunctionDef | None:
67
- return NodeTourist(ifThis.isFunctionDef_Identifier(identifier), Then.getIt).captureLastMatch(module)
93
+ """
94
+ Extract a function definition with a specific name from an AST module.
95
+
96
+ This function searches through an AST module for a function definition that
97
+ matches the provided identifier and returns it if found.
98
+
99
+ Parameters:
100
+ module: The AST module to search within.
101
+ identifier: The name of the function to find.
102
+
103
+ Returns:
104
+ astFunctionDef: The matching function definition AST node, or None if not found.
105
+ """
106
+ return NodeTourist(ifThis.isFunctionDef_Identifier(identifier), Then.extractIt).captureLastMatch(module)
107
+
108
+ def makeDictionaryFunctionDef(module: ast.Module) -> dict[ast_Identifier, ast.FunctionDef]:
109
+ """
110
+ Create a dictionary mapping function names to their AST definitions.
111
+
112
+ This function creates a dictionary that maps function names to their AST function
113
+ definition nodes for all functions defined in the given module.
68
114
 
69
- def makeDictionaryFunctionDef(module: ast.AST) -> dict[ast_Identifier, ast.FunctionDef]:
115
+ Parameters:
116
+ module: The AST module to extract function definitions from.
117
+
118
+ Returns:
119
+ A dictionary mapping function identifiers to their AST function definition nodes.
120
+ """
70
121
  dictionaryIdentifier2FunctionDef: dict[ast_Identifier, ast.FunctionDef] = {}
71
- NodeTourist(be.FunctionDef, Then.updateKeyValueIn(DOT.name, Then.getIt, dictionaryIdentifier2FunctionDef)).visit(module)
122
+ NodeTourist(lambda node: isinstance(node, ast.FunctionDef), Then.updateKeyValueIn(DOT.name, Then.extractIt, dictionaryIdentifier2FunctionDef)).visit(module) # type: ignore
72
123
  return dictionaryIdentifier2FunctionDef
73
124
 
74
- def makeDictionary4InliningFunction(identifierToInline: ast_Identifier, dictionaryFunctionDef: dict[ast_Identifier, ast.FunctionDef], FunctionDefToInline: ast.FunctionDef | None = None) -> dict[str, ast.FunctionDef]:
125
+ def inlineFunctionDef(identifierToInline: ast_Identifier, module: ast.Module) -> ast.FunctionDef:
75
126
  """
76
- Creates a dictionary of function definitions required for inlining a target function.
77
- This function analyzes a target function and recursively collects all function definitions
78
- that are called within it (and any functions called by those functions), preparing them for inlining.
127
+ Inline function calls within a function definition to create a self-contained function.
128
+
129
+ This function takes a function identifier and a module, finds the function definition,
130
+ and then recursively inlines all function calls within that function with their
131
+ implementation bodies. This produces a fully inlined function that doesn't depend
132
+ on other function definitions from the module.
133
+
79
134
  Parameters:
80
- ----------
81
- identifierToInline : ast_Identifier
82
- The identifier of the function to be inlined.
83
- dictionaryFunctionDef : dict[ast_Identifier, ast.FunctionDef]
84
- A dictionary mapping function identifiers to their AST function definitions.
85
- FunctionDefToInline : ast.FunctionDef | None, optional
86
- The AST function definition to inline. If None, it will be retrieved from dictionaryFunctionDef using identifierToInline.
135
+ identifierToInline: The name of the function to inline.
136
+ module: The AST module containing the function and its dependencies.
137
+
87
138
  Returns:
88
- -------
89
- dict[str, ast.FunctionDef]
90
- A dictionary mapping function names to their AST function definitions, containing all functions needed for inlining.
139
+ A modified function definition with all function calls inlined.
140
+
91
141
  Raises:
92
- ------
93
- ValueError
94
- If the function to inline is not found in the dictionary, or if recursion is detected during analysis.
95
- Notes:
96
- -----
97
- The function performs a recursive analysis to find all dependent functions needed for inlining.
98
- It detects and prevents recursive function calls that could cause infinite inlining.
142
+ ValueError: If the function to inline is not found in the module.
99
143
  """
100
- if FunctionDefToInline is None:
101
- try:
102
- FunctionDefToInline = dictionaryFunctionDef[identifierToInline]
103
- except KeyError as ERRORmessage:
104
- raise ValueError(f"FunctionDefToInline not found in dictionaryIdentifier2FunctionDef: {identifierToInline = }") from ERRORmessage
144
+ dictionaryFunctionDef: dict[ast_Identifier, ast.FunctionDef] = makeDictionaryFunctionDef(module)
145
+ try:
146
+ FunctionDefToInline = dictionaryFunctionDef[identifierToInline]
147
+ except KeyError as ERRORmessage:
148
+ raise ValueError(f"FunctionDefToInline not found in dictionaryIdentifier2FunctionDef: {identifierToInline = }") from ERRORmessage
105
149
 
106
150
  listIdentifiersCalledFunctions: list[ast_Identifier] = []
107
- findIdentifiersToInline = NodeTourist(ifThis.isCallToName, lambda node: Then.appendTo(listIdentifiersCalledFunctions)(DOT.id(DOT.func(node)))) # pyright: ignore[reportArgumentType]
151
+ findIdentifiersToInline = NodeTourist(ifThis.isCallToName, lambda node: Then.appendTo(listIdentifiersCalledFunctions)(DOT.id(DOT.func(node)))) # type: ignore
108
152
  findIdentifiersToInline.visit(FunctionDefToInline)
109
153
 
110
154
  dictionary4Inlining: dict[ast_Identifier, ast.FunctionDef] = {}
111
155
  for identifier in sorted(set(listIdentifiersCalledFunctions).intersection(dictionaryFunctionDef.keys())):
112
- dictionary4Inlining[identifier] = dictionaryFunctionDef[identifier]
156
+ if NodeTourist(ifThis.matchesMeButNotAnyDescendant(ifThis.isCall_Identifier(identifier)), Then.extractIt).captureLastMatch(module) is not None:
157
+ dictionary4Inlining[identifier] = dictionaryFunctionDef[identifier]
113
158
 
114
159
  keepGoing = True
115
160
  while keepGoing:
@@ -117,17 +162,30 @@ def makeDictionary4InliningFunction(identifierToInline: ast_Identifier, dictiona
117
162
  listIdentifiersCalledFunctions.clear()
118
163
  findIdentifiersToInline.visit(Make.Module(list(dictionary4Inlining.values())))
119
164
 
120
- # NOTE: This is simple not comprehensive recursion protection. # TODO think about why I dislike `ifThis.CallDoesNotCallItself`
121
- if identifierToInline in listIdentifiersCalledFunctions: raise ValueError(f"Recursion found: {identifierToInline = }.")
122
-
123
165
  listIdentifiersCalledFunctions = sorted((set(listIdentifiersCalledFunctions).difference(dictionary4Inlining.keys())).intersection(dictionaryFunctionDef.keys()))
124
166
  if len(listIdentifiersCalledFunctions) > 0:
125
167
  keepGoing = True
126
168
  for identifier in listIdentifiersCalledFunctions:
127
- if identifier in dictionaryFunctionDef:
128
- dictionary4Inlining[identifier] = dictionaryFunctionDef[identifier]
129
-
130
- return dictionary4Inlining
169
+ if NodeTourist(ifThis.matchesMeButNotAnyDescendant(ifThis.isCall_Identifier(identifier)), Then.extractIt).captureLastMatch(module) is not None:
170
+ FunctionDefTarget = dictionaryFunctionDef[identifier]
171
+ if len(FunctionDefTarget.body) == 1:
172
+ inliner = NodeChanger(ifThis.isCall_Identifier(identifier), Then.replaceWith(FunctionDefTarget.body[0].value)) # type: ignore
173
+ for astFunctionDef in dictionary4Inlining.values():
174
+ inliner.visit(astFunctionDef)
175
+ else:
176
+ inliner = NodeChanger(ifThis.isAssignAndValueIs(ifThis.isCall_Identifier(identifier)),Then.replaceWith(FunctionDefTarget.body[0:-1]))
177
+ for astFunctionDef in dictionary4Inlining.values():
178
+ inliner.visit(astFunctionDef)
179
+
180
+ for identifier, FunctionDefTarget in dictionary4Inlining.items():
181
+ if len(FunctionDefTarget.body) == 1:
182
+ inliner = NodeChanger(ifThis.isCall_Identifier(identifier), Then.replaceWith(FunctionDefTarget.body[0].value)) # type: ignore
183
+ inliner.visit(FunctionDefToInline)
184
+ else:
185
+ inliner = NodeChanger(ifThis.isAssignAndValueIs(ifThis.isCall_Identifier(identifier)),Then.replaceWith(FunctionDefTarget.body[0:-1]))
186
+ inliner.visit(FunctionDefToInline)
187
+ ast.fix_missing_locations(FunctionDefToInline)
188
+ return FunctionDefToInline
131
189
 
132
190
  @overload
133
191
  def makeInitializedComputationState(mapShape: tuple[int, ...], writeJob: Literal[True], *, pathFilename: PathLike[str] | PurePath | None = None, **keywordArguments: Any) -> Path: ...
@@ -164,11 +222,28 @@ def makeInitializedComputationState(mapShape: tuple[int, ...], writeJob: bool =
164
222
  else:
165
223
  pathFilenameJob = getPathFilenameFoldsTotal(stateUniversal.mapShape).with_suffix('.pkl')
166
224
 
225
+ # Fix code scanning alert - Consider possible security implications associated with pickle module. #17
167
226
  pathFilenameJob.write_bytes(pickle.dumps(stateUniversal))
168
227
  return pathFilenameJob
169
228
 
170
229
  @dataclasses.dataclass
171
230
  class DeReConstructField2ast:
231
+ """
232
+ Transform a dataclass field into AST node representations for code generation.
233
+
234
+ This class extracts and transforms a dataclass Field object into various AST node
235
+ representations needed for code generation. It handles the conversion of field
236
+ attributes, type annotations, and metadata into AST constructs that can be used
237
+ to reconstruct the field in generated code.
238
+
239
+ The class is particularly important for decomposing dataclass fields (like those in
240
+ ComputationState) to enable their use in specialized contexts like Numba-optimized
241
+ functions, where the full dataclass cannot be directly used but its contents need
242
+ to be accessible.
243
+
244
+ Each field is processed according to its type and metadata to create appropriate
245
+ variable declarations, type annotations, and initialization code as AST nodes.
246
+ """
172
247
  dataclassesDOTdataclassLogicalPathModule: dataclasses.InitVar[str_nameDOTname]
173
248
  dataclassClassDef: dataclasses.InitVar[ast.ClassDef]
174
249
  dataclassesDOTdataclassInstance_Identifier: dataclasses.InitVar[ast_Identifier]
@@ -190,10 +265,10 @@ class DeReConstructField2ast:
190
265
  astName: ast.Name = dataclasses.field(init=False)
191
266
  ast_keyword_field__field: ast.keyword = dataclasses.field(init=False)
192
267
  ast_nameDOTname: ast.Attribute = dataclasses.field(init=False)
193
- astAnnotation: ImaAnnotationType = dataclasses.field(init=False)
268
+ astAnnotation: ast.expr = dataclasses.field(init=False)
194
269
  ast_argAnnotated: ast.arg = dataclasses.field(init=False)
195
- astAnnAssignConstructor: ast.AnnAssign = dataclasses.field(init=False)
196
- Z0Z_hack: tuple[ast.AnnAssign, str] = dataclasses.field(init=False)
270
+ astAnnAssignConstructor: ast.AnnAssign|ast.Assign = dataclasses.field(init=False)
271
+ Z0Z_hack: tuple[ast.AnnAssign|ast.Assign, str] = dataclasses.field(init=False)
197
272
 
198
273
  def __post_init__(self, dataclassesDOTdataclassLogicalPathModule: str_nameDOTname, dataclassClassDef: ast.ClassDef, dataclassesDOTdataclassInstance_Identifier: ast_Identifier, field: dataclasses.Field[Any]) -> None:
199
274
  self.compare = field.compare
@@ -211,52 +286,95 @@ class DeReConstructField2ast:
211
286
  self.ast_keyword_field__field = Make.keyword(self.name, self.astName)
212
287
  self.ast_nameDOTname = Make.Attribute(Make.Name(dataclassesDOTdataclassInstance_Identifier), self.name)
213
288
 
214
- sherpa = NodeTourist(ifThis.isAnnAssign_targetIs(ifThis.isName_Identifier(self.name)), 又.annotation(Then.getIt)).captureLastMatch(dataclassClassDef)
289
+ sherpa = NodeTourist(ifThis.isAnnAssign_targetIs(ifThis.isName_Identifier(self.name)), Then.extractIt(DOT.annotation)).captureLastMatch(dataclassClassDef) # type: ignore
215
290
  if sherpa is None: raise raiseIfNoneGitHubIssueNumber3
216
291
  else: self.astAnnotation = sherpa
217
292
 
218
293
  self.ast_argAnnotated = Make.arg(self.name, self.astAnnotation)
294
+ """
295
+ from ast import Module, Expr, Subscript, Name, Tuple, Load
296
+ Subscript(
297
+ value=Name(id='ndarray', ctx=Load()),
298
+ slice=Tuple(
299
+ elts=[
300
+ Subscript(
301
+ value=Name(id='tuple', ctx=Load()),
302
+ slice=Name(id='int', ctx=Load()),
303
+ ctx=Load()),
304
+ Subscript(
305
+ value=Name(id='dtype', ctx=Load()),
306
+ slice=Name(id='NumPyLeavesTotal', ctx=Load()),
307
+ ctx=Load())],
308
+ ctx=Load()),
309
+ ctx=Load()
310
+ )
219
311
 
312
+ """
220
313
  dtype = self.metadata.get('dtype', None)
221
314
  if dtype:
222
315
  moduleWithLogicalPath: str_nameDOTname = 'numpy'
223
- annotation = 'ndarray'
224
- self.ledger.addImportFrom_asStr(moduleWithLogicalPath, annotation)
316
+ annotationType = 'ndarray'
317
+ self.ledger.addImportFrom_asStr(moduleWithLogicalPath, annotationType)
318
+ self.ledger.addImportFrom_asStr(moduleWithLogicalPath, 'dtype')
319
+ axesSubscript = Make.Subscript(Make.Name('tuple'), Make.Name('uint8'))
320
+ dtype_asnameName: ast.Name = self.astAnnotation # type: ignore
321
+ if dtype_asnameName.id == 'Array3D':
322
+ axesSubscript = Make.Subscript(Make.Name('tuple'), Make.Tuple([Make.Name('uint8'), Make.Name('uint8'), Make.Name('uint8')]))
323
+ ast_expr = Make.Subscript(Make.Name(annotationType), Make.Tuple([axesSubscript, Make.Subscript(Make.Name('dtype'), dtype_asnameName)]))
225
324
  constructor = 'array'
226
325
  self.ledger.addImportFrom_asStr(moduleWithLogicalPath, constructor)
227
326
  dtypeIdentifier: ast_Identifier = dtype.__name__
228
- dtype_asnameName: ast.Name = self.astAnnotation
229
- # dtypeIdentifier_asname: ast_Identifier = moduleWithLogicalPath + '_' + dtypeIdentifier
230
327
  self.ledger.addImportFrom_asStr(moduleWithLogicalPath, dtypeIdentifier, dtype_asnameName.id)
231
- self.astAnnAssignConstructor = Make.AnnAssign(self.astName, Make.Name(annotation), Make.Call(Make.Name(constructor), list_astKeywords=[Make.keyword('dtype', dtype_asnameName)]))
232
- # self.astAnnAssignConstructor = Make.AnnAssign(self.astName, Make.Name(annotation), Make.Call(Make.Name(constructor), list_astKeywords=[Make.keyword('dtype', Make.Name(dtypeIdentifier_asname))]))
233
- # self.astAnnAssignConstructor = Make.AnnAssign(self.astName, self.astAnnotation, Make.Call(Make.Name(constructor), list_astKeywords=[Make.keyword('dtype', Make.Name(dtypeIdentifier_asname))]))
328
+ self.astAnnAssignConstructor = Make.AnnAssign(self.astName, ast_expr, Make.Call(Make.Name(constructor), list_astKeywords=[Make.keyword('dtype', dtype_asnameName)]))
329
+ self.astAnnAssignConstructor = Make.Assign([self.astName], Make.Call(Make.Name(constructor), list_astKeywords=[Make.keyword('dtype', dtype_asnameName)]))
234
330
  self.Z0Z_hack = (self.astAnnAssignConstructor, 'array')
235
- elif be.Name(self.astAnnotation):
331
+ elif isinstance(self.astAnnotation, ast.Name):
236
332
  self.astAnnAssignConstructor = Make.AnnAssign(self.astName, self.astAnnotation, Make.Call(self.astAnnotation, [Make.Constant(-1)]))
237
- # self.ledger.addImportFrom_asStr(dataclassesDOTdataclassLogicalPathModule, self.astAnnotation.id)
238
333
  self.Z0Z_hack = (self.astAnnAssignConstructor, 'scalar')
239
- elif be.Subscript(self.astAnnotation):
334
+ elif isinstance(self.astAnnotation, ast.Subscript):
240
335
  elementConstructor: ast_Identifier = self.metadata['elementConstructor']
241
336
  self.ledger.addImportFrom_asStr(dataclassesDOTdataclassLogicalPathModule, elementConstructor)
242
337
  takeTheTuple: ast.Tuple = deepcopy(self.astAnnotation.slice) # type: ignore
243
338
  self.astAnnAssignConstructor = Make.AnnAssign(self.astName, self.astAnnotation, takeTheTuple)
244
339
  self.Z0Z_hack = (self.astAnnAssignConstructor, elementConstructor)
245
- if be.Name(self.astAnnotation):
340
+ if isinstance(self.astAnnotation, ast.Name):
246
341
  self.ledger.addImportFrom_asStr(dataclassesDOTdataclassLogicalPathModule, self.astAnnotation.id) # pyright: ignore [reportUnknownArgumentType, reportUnknownMemberType, reportIJustCalledATypeGuardMethod_WTF]
247
342
 
248
343
  def shatter_dataclassesDOTdataclass(logicalPathModule: str_nameDOTname, dataclass_Identifier: ast_Identifier, instance_Identifier: ast_Identifier) -> ShatteredDataclass:
249
344
  """
345
+ Decompose a dataclass definition into AST components for manipulation and code generation.
346
+
347
+ This function breaks down a complete dataclass (like ComputationState) into its constituent
348
+ parts as AST nodes, enabling fine-grained manipulation of its fields for code generation.
349
+ It extracts all field definitions, annotations, and metadata, organizing them into a
350
+ ShatteredDataclass that provides convenient access to AST representations needed for
351
+ different code generation contexts.
352
+
353
+ The function identifies a special "counting variable" (marked with 'theCountingIdentifier'
354
+ metadata) which is crucial for map folding algorithms, ensuring it's properly accessible
355
+ in the generated code.
356
+
357
+ This decomposition is particularly important when generating optimized code (e.g., for Numba)
358
+ where dataclass instances can't be directly used but their fields need to be individually
359
+ manipulated and passed to computational functions.
360
+
250
361
  Parameters:
251
- logicalPathModule: gimme string cuz python is stoopid
252
- dataclass_Identifier: The identifier of the dataclass to be dismantled.
253
- instance_Identifier: In the synthesized module/function/scope, the identifier that will be used for the instance.
362
+ logicalPathModule: The fully qualified module path containing the dataclass definition.
363
+ dataclass_Identifier: The name of the dataclass to decompose.
364
+ instance_Identifier: The variable name to use for the dataclass instance in generated code.
365
+
366
+ Returns:
367
+ A ShatteredDataclass containing AST representations of all dataclass components,
368
+ with imports, field definitions, annotations, and repackaging code.
369
+
370
+ Raises:
371
+ ValueError: If the dataclass cannot be found in the specified module or if no counting variable is identified in the dataclass.
254
372
  """
255
373
  Official_fieldOrder: list[ast_Identifier] = []
256
374
  dictionaryDeReConstruction: dict[ast_Identifier, DeReConstructField2ast] = {}
257
375
 
258
376
  dataclassClassDef = extractClassDef(parseLogicalPath2astModule(logicalPathModule), dataclass_Identifier)
259
- if not isinstance(dataclassClassDef, ast.ClassDef): raise ValueError(f"I could not find {dataclass_Identifier=} in {logicalPathModule=}.")
377
+ if not isinstance(dataclassClassDef, ast.ClassDef): raise ValueError(f"I could not find `{dataclass_Identifier = }` in `{logicalPathModule = }`.")
260
378
 
261
379
  countingVariable = None
262
380
  for aField in dataclasses.fields(importLogicalPath2Callable(logicalPathModule, dataclass_Identifier)): # pyright: ignore [reportArgumentType]
@@ -266,7 +384,7 @@ def shatter_dataclassesDOTdataclass(logicalPathModule: str_nameDOTname, dataclas
266
384
  countingVariable = dictionaryDeReConstruction[aField.name].name
267
385
 
268
386
  if countingVariable is None:
269
- raise ValueError(f"I could not find the counting variable in {dataclass_Identifier=} in {logicalPathModule=}.")
387
+ raise ValueError(f"I could not find the counting variable in `{dataclass_Identifier = }` in `{logicalPathModule = }`.")
270
388
 
271
389
  shatteredDataclass = ShatteredDataclass(
272
390
  countingVariableAnnotation=dictionaryDeReConstruction[countingVariable].astAnnotation,
@@ -290,6 +408,31 @@ def shatter_dataclassesDOTdataclass(logicalPathModule: str_nameDOTname, dataclas
290
408
  return shatteredDataclass
291
409
 
292
410
  def write_astModule(ingredients: IngredientsModule, pathFilename: PathLike[Any] | PurePath, packageName: ast_Identifier | None = None) -> None:
411
+ """
412
+ Convert an IngredientsModule to Python source code and write it to a file.
413
+
414
+ This function renders an IngredientsModule into executable Python code,
415
+ applies code quality improvements like import organization via autoflake,
416
+ and writes the result to the specified file path.
417
+
418
+ The function performs several key steps:
419
+ 1. Converts the AST module structure to a valid Python AST
420
+ 2. Fixes location attributes in the AST for proper formatting
421
+ 3. Converts the AST to Python source code
422
+ 4. Optimizes imports using autoflake
423
+ 5. Writes the final source code to the specified file location
424
+
425
+ This is typically the final step in the code generation pipeline,
426
+ producing optimized Python modules ready for execution.
427
+
428
+ Parameters:
429
+ ingredients: The IngredientsModule containing the module definition.
430
+ pathFilename: The file path where the module should be written.
431
+ packageName: Optional package name to preserve in import optimization.
432
+
433
+ Raises:
434
+ raiseIfNoneGitHubIssueNumber3: If the generated source code is empty.
435
+ """
293
436
  astModule = Make.Module(ingredients.body, ingredients.type_ignores)
294
437
  ast.fix_missing_locations(astModule)
295
438
  pythonSource: str = ast.unparse(astModule)
@@ -298,19 +441,47 @@ def write_astModule(ingredients: IngredientsModule, pathFilename: PathLike[Any]
298
441
  if packageName:
299
442
  autoflake_additional_imports.append(packageName)
300
443
  pythonSource = autoflake_fix_code(pythonSource, autoflake_additional_imports, expand_star_imports=False, remove_all_unused_imports=True, remove_duplicate_keys = False, remove_unused_variables = False)
444
+ # pythonSource = python_minifier.minify(pythonSource)
301
445
  writeStringToHere(pythonSource, pathFilename)
302
446
 
303
447
  # END of acceptable classes and functions ======================================================
448
+ def removeUnusedParameters(ingredientsFunction: IngredientsFunction):
449
+ list_argCuzMyBrainRefusesToThink = ingredientsFunction.astFunctionDef.args.args + ingredientsFunction.astFunctionDef.args.posonlyargs + ingredientsFunction.astFunctionDef.args.kwonlyargs
450
+ list_arg_arg: list[ast_Identifier] = [ast_arg.arg for ast_arg in list_argCuzMyBrainRefusesToThink]
451
+ listName: list[ast.Name] = []
452
+ NodeTourist(be.Name, Then.appendTo(listName)).visit(ingredientsFunction.astFunctionDef)
453
+ list_Identifiers: list[ast_Identifier] = [astName.id for astName in listName]
454
+ list_IdentifiersNotUsed: list[ast_Identifier] = list(set(list_arg_arg) - set(list_Identifiers))
455
+
304
456
  dictionaryEstimates: dict[tuple[int, ...], int] = {
305
- (2,2,2,2,2,2,2,2): 362794844160000,
306
- (2,21): 1493028892051200,
307
- (3,15): 9842024675968800,
457
+ (2,2,2,2,2,2,2,2): 798148657152000,
458
+ (2,21): 776374224866624,
459
+ (3,15): 824761667826225,
308
460
  (3,3,3,3): 85109616000000000000000000000000,
309
- (8,8): 129950723279272000,
461
+ (8,8): 791274195985524900,
310
462
  }
311
463
 
312
464
  # END of marginal classes and functions ======================================================
313
- def Z0Z_lameFindReplace(astTree, mappingFindReplaceNodes: Mapping[ast.AST, ast.AST]):
465
+ def Z0Z_lameFindReplace(astTree: 个, mappingFindReplaceNodes: Mapping[ast.AST, ast.AST]) -> 个:
466
+ """
467
+ Recursively replace AST nodes based on a mapping of find-replace pairs.
468
+
469
+ This function applies brute-force node replacement throughout an AST tree
470
+ by comparing textual representations of nodes. While not the most efficient
471
+ approach, it provides a reliable way to replace complex nested structures
472
+ when more precise targeting methods are difficult to implement.
473
+
474
+ The function continues replacing nodes until no more changes are detected
475
+ in the AST's textual representation, ensuring complete replacement throughout
476
+ the tree structure.
477
+
478
+ Parameters:
479
+ astTree: The AST structure to modify.
480
+ mappingFindReplaceNodes: A mapping from source nodes to replacement nodes.
481
+
482
+ Returns:
483
+ The modified AST structure with all matching nodes replaced.
484
+ """
314
485
  keepGoing = True
315
486
  newTree = deepcopy(astTree)
316
487
 
@@ -324,54 +495,112 @@ def Z0Z_lameFindReplace(astTree, mappingFindReplaceNodes: Mapping[ast.AST, ast.A
324
495
  astTree = deepcopy(newTree)
325
496
  return newTree
326
497
 
327
- # Start of I HATE PROGRAMMING ==========================================================
328
- def Z0Z_makeDictionaryReplacementStatements(module: ast.AST) -> dict[ast_Identifier, ast.stmt | list[ast.stmt]]:
329
- """Return a dictionary of function names and their replacement statements."""
330
- dictionaryFunctionDef: dict[ast_Identifier, ast.FunctionDef] = makeDictionaryFunctionDef(module)
331
- dictionaryReplacementStatements: dict[ast_Identifier, ast.stmt | list[ast.stmt]] = {}
332
- for name, astFunctionDef in dictionaryFunctionDef.items():
333
- if ifThis.onlyReturnAnyCompare(astFunctionDef):
334
- dictionaryReplacementStatements[name] = astFunctionDef.body[0].value # type: ignore
335
- elif ifThis.onlyReturnUnaryOp(astFunctionDef):
336
- dictionaryReplacementStatements[name] = astFunctionDef.body[0].value # type: ignore
337
- else:
338
- dictionaryReplacementStatements[name] = astFunctionDef.body[0:-1]
339
- return dictionaryReplacementStatements
340
-
341
- def Z0Z_inlineThisFunctionWithTheseValues(astFunctionDef: ast.FunctionDef, dictionaryReplacementStatements: dict[str, ast.stmt | list[ast.stmt]]) -> ast.FunctionDef:
342
- class FunctionInliner(ast.NodeTransformer):
343
- def __init__(self, dictionaryReplacementStatements: dict[str, ast.stmt | list[ast.stmt]]) -> None:
344
- self.dictionaryReplacementStatements = dictionaryReplacementStatements
345
-
346
- def generic_visit(self, node: ast.AST) -> ast.AST:
347
- """Visit all nodes and replace them if necessary."""
348
- return super().generic_visit(node)
349
-
350
- def visit_Expr(self, node: ast.Expr) -> ast.AST | list[ast.stmt]:
351
- if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node.value):
352
- return self.dictionaryReplacementStatements[node.value.func.id] # type: ignore
353
- return node
354
-
355
- def visit_Assign(self, node: ast.Assign) -> ast.AST | list[ast.stmt]:
356
- if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node.value):
357
- return self.dictionaryReplacementStatements[node.value.func.id] # type: ignore
358
- return node
359
-
360
- def visit_Call(self, node: ast.Call) -> ast.AST | list[ast.stmt]:
361
- if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node):
362
- replacement = self.dictionaryReplacementStatements[node.func.id] # type: ignore
363
- if not isinstance(replacement, list):
364
- return replacement
365
- return node
366
-
367
- keepGoing = True
368
- ImaInlineFunction = deepcopy(astFunctionDef)
369
- while keepGoing:
370
- ImaInlineFunction = deepcopy(astFunctionDef)
371
- FunctionInliner(deepcopy(dictionaryReplacementStatements)).visit(ImaInlineFunction)
372
- if ast.unparse(ImaInlineFunction) == ast.unparse(astFunctionDef):
373
- keepGoing = False
374
- else:
375
- astFunctionDef = deepcopy(ImaInlineFunction)
376
- ast.fix_missing_locations(astFunctionDef)
377
- return ImaInlineFunction
498
+ def makeNewFlow(recipeFlow: RecipeSynthesizeFlow) -> IngredientsModule:
499
+ # TODO a tool to automatically remove unused variables from the ArgumentsSpecification (return, and returns) _might_ be nice.
500
+ # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
501
+ listAllIngredientsFunctions = [
502
+ (ingredientsInitialize := astModuleToIngredientsFunction(recipeFlow.source_astModule, recipeFlow.sourceCallableInitialize)),
503
+ (ingredientsParallel := astModuleToIngredientsFunction(recipeFlow.source_astModule, recipeFlow.sourceCallableParallel)),
504
+ (ingredientsSequential := astModuleToIngredientsFunction(recipeFlow.source_astModule, recipeFlow.sourceCallableSequential)),
505
+ (ingredientsDispatcher := astModuleToIngredientsFunction(recipeFlow.source_astModule, recipeFlow.sourceCallableDispatcher)),
506
+ ]
507
+
508
+ # Inline functions ========================================================
509
+ # NOTE Replacements statements are based on the identifiers in the _source_, so operate on the source identifiers.
510
+ ingredientsInitialize.astFunctionDef = inlineFunctionDef(recipeFlow.sourceCallableInitialize, recipeFlow.source_astModule)
511
+ ingredientsParallel.astFunctionDef = inlineFunctionDef(recipeFlow.sourceCallableParallel, recipeFlow.source_astModule)
512
+ ingredientsSequential.astFunctionDef = inlineFunctionDef(recipeFlow.sourceCallableSequential, recipeFlow.source_astModule)
513
+
514
+ # assignRecipeIdentifiersToCallable. =============================
515
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
516
+ # How can I use dataclass settings as the SSOT for specific actions? https://github.com/hunterhogan/mapFolding/issues/16
517
+ # NOTE reminder: you are updating these `ast.Name` here (and not in a more general search) because this is a
518
+ # narrow search for `ast.Call` so you won't accidentally replace unrelated `ast.Name`.
519
+ listFindReplace = [(recipeFlow.sourceCallableDispatcher, recipeFlow.callableDispatcher),
520
+ (recipeFlow.sourceCallableInitialize, recipeFlow.callableInitialize),
521
+ (recipeFlow.sourceCallableParallel, recipeFlow.callableParallel),
522
+ (recipeFlow.sourceCallableSequential, recipeFlow.callableSequential),]
523
+ for ingredients in listAllIngredientsFunctions:
524
+ for source_Identifier, recipe_Identifier in listFindReplace:
525
+ updateCallName = NodeChanger(ifThis.isCall_Identifier(source_Identifier), grab.funcAttribute(Then.replaceWith(Make.Name(recipe_Identifier))))
526
+ updateCallName.visit(ingredients.astFunctionDef)
527
+
528
+ ingredientsDispatcher.astFunctionDef.name = recipeFlow.callableDispatcher
529
+ ingredientsInitialize.astFunctionDef.name = recipeFlow.callableInitialize
530
+ ingredientsParallel.astFunctionDef.name = recipeFlow.callableParallel
531
+ ingredientsSequential.astFunctionDef.name = recipeFlow.callableSequential
532
+
533
+ # Assign identifiers per the recipe. ==============================
534
+ listFindReplace = [(recipeFlow.sourceDataclassInstance, recipeFlow.dataclassInstance),
535
+ (recipeFlow.sourceDataclassInstanceTaskDistribution, recipeFlow.dataclassInstanceTaskDistribution),
536
+ (recipeFlow.sourceConcurrencyManagerNamespace, recipeFlow.concurrencyManagerNamespace),]
537
+ for ingredients in listAllIngredientsFunctions:
538
+ for source_Identifier, recipe_Identifier in listFindReplace:
539
+ updateName = NodeChanger(ifThis.isName_Identifier(source_Identifier) , grab.idAttribute(Then.replaceWith(recipe_Identifier)))
540
+ update_arg = NodeChanger(ifThis.isArgument_Identifier(source_Identifier), grab.argAttribute(Then.replaceWith(recipe_Identifier)))
541
+ updateName.visit(ingredients.astFunctionDef)
542
+ update_arg.visit(ingredients.astFunctionDef)
543
+
544
+ updateConcurrencyManager = NodeChanger(ifThis.isCallAttributeNamespace_Identifier(recipeFlow.sourceConcurrencyManagerNamespace, recipeFlow.sourceConcurrencyManagerIdentifier)
545
+ , grab.funcAttribute(Then.replaceWith(Make.Attribute(Make.Name(recipeFlow.concurrencyManagerNamespace), recipeFlow.concurrencyManagerIdentifier))))
546
+ updateConcurrencyManager.visit(ingredientsDispatcher.astFunctionDef)
547
+
548
+ # shatter Dataclass =======================================================
549
+ instance_Identifier = recipeFlow.dataclassInstance
550
+ getTheOtherRecord_damn = recipeFlow.dataclassInstanceTaskDistribution
551
+ shatteredDataclass = shatter_dataclassesDOTdataclass(recipeFlow.logicalPathModuleDataclass, recipeFlow.sourceDataclassIdentifier, instance_Identifier)
552
+ ingredientsDispatcher.imports.update(shatteredDataclass.ledger)
553
+
554
+ # How can I use dataclass settings as the SSOT for specific actions? https://github.com/hunterhogan/mapFolding/issues/16
555
+ # Change callable parameters and Call to the callable at the same time ====
556
+ # sequentialCallable =========================================================
557
+ if recipeFlow.removeDataclassSequential:
558
+ ingredientsSequential.astFunctionDef.args = Make.argumentsSpecification(args=shatteredDataclass.list_argAnnotated4ArgumentsSpecification)
559
+ astCallSequentialCallable = Make.Call(Make.Name(recipeFlow.callableSequential), shatteredDataclass.listName4Parameters)
560
+ changeReturnSequentialCallable = NodeChanger(be.Return, Then.replaceWith(Make.Return(shatteredDataclass.fragments4AssignmentOrParameters)))
561
+ ingredientsSequential.astFunctionDef.returns = shatteredDataclass.signatureReturnAnnotation
562
+ replaceAssignSequentialCallable = NodeChanger(ifThis.isAssignAndValueIs(ifThis.isCall_Identifier(recipeFlow.callableSequential)), Then.replaceWith(Make.Assign(listTargets=[shatteredDataclass.fragments4AssignmentOrParameters], value=astCallSequentialCallable)))
563
+
564
+ unpack4sequentialCallable = NodeChanger(ifThis.isAssignAndValueIs(ifThis.isCall_Identifier(recipeFlow.callableSequential)), Then.insertThisAbove(shatteredDataclass.listUnpack))
565
+ repack4sequentialCallable = NodeChanger(ifThis.isAssignAndValueIs(ifThis.isCall_Identifier(recipeFlow.callableSequential)), Then.insertThisBelow([shatteredDataclass.repack]))
566
+
567
+ changeReturnSequentialCallable.visit(ingredientsSequential.astFunctionDef)
568
+ replaceAssignSequentialCallable.visit(ingredientsDispatcher.astFunctionDef)
569
+ unpack4sequentialCallable.visit(ingredientsDispatcher.astFunctionDef)
570
+ repack4sequentialCallable.visit(ingredientsDispatcher.astFunctionDef)
571
+
572
+ ingredientsSequential.astFunctionDef = Z0Z_lameFindReplace(ingredientsSequential.astFunctionDef, shatteredDataclass.map_stateDOTfield2Name)
573
+
574
+ # parallelCallable =========================================================
575
+ if recipeFlow.removeDataclassParallel:
576
+ ingredientsParallel.astFunctionDef.args = Make.argumentsSpecification(args=shatteredDataclass.list_argAnnotated4ArgumentsSpecification)
577
+ replaceCall2concurrencyManager = NodeChanger(ifThis.isCallAttributeNamespace_Identifier(recipeFlow.concurrencyManagerNamespace, recipeFlow.concurrencyManagerIdentifier), Then.replaceWith(Make.Call(Make.Attribute(Make.Name(recipeFlow.concurrencyManagerNamespace), recipeFlow.concurrencyManagerIdentifier), listArguments=[Make.Name(recipeFlow.callableParallel)] + shatteredDataclass.listName4Parameters)))
578
+
579
+ # NOTE I am dissatisfied with this logic for many reasons, including that it requires separate NodeCollector and NodeReplacer instances.
580
+ astCallConcurrencyResult: list[ast.Call] = []
581
+ get_astCallConcurrencyResult = NodeTourist(ifThis.isAssignAndTargets0Is(ifThis.isSubscript_Identifier(getTheOtherRecord_damn)), getIt(astCallConcurrencyResult))
582
+ get_astCallConcurrencyResult.visit(ingredientsDispatcher.astFunctionDef)
583
+ replaceAssignParallelCallable = NodeChanger(ifThis.isAssignAndTargets0Is(ifThis.isSubscript_Identifier(getTheOtherRecord_damn)), grab.valueAttribute(Then.replaceWith(astCallConcurrencyResult[0])))
584
+ replaceAssignParallelCallable.visit(ingredientsDispatcher.astFunctionDef)
585
+ changeReturnParallelCallable = NodeChanger(be.Return, Then.replaceWith(Make.Return(shatteredDataclass.countingVariableName)))
586
+ ingredientsParallel.astFunctionDef.returns = shatteredDataclass.countingVariableAnnotation
587
+
588
+ unpack4parallelCallable = NodeChanger(ifThis.isAssignAndValueIs(ifThis.isCallAttributeNamespace_Identifier(recipeFlow.concurrencyManagerNamespace, recipeFlow.concurrencyManagerIdentifier)), Then.insertThisAbove(shatteredDataclass.listUnpack))
589
+
590
+ unpack4parallelCallable.visit(ingredientsDispatcher.astFunctionDef)
591
+ replaceCall2concurrencyManager.visit(ingredientsDispatcher.astFunctionDef)
592
+ changeReturnParallelCallable.visit(ingredientsParallel.astFunctionDef)
593
+
594
+ ingredientsParallel.astFunctionDef = Z0Z_lameFindReplace(ingredientsParallel.astFunctionDef, shatteredDataclass.map_stateDOTfield2Name)
595
+
596
+ # Module-level transformations ===========================================================
597
+ ingredientsModuleNumbaUnified = IngredientsModule(ingredientsFunction=listAllIngredientsFunctions, imports=LedgerOfImports(recipeFlow.source_astModule))
598
+ ingredientsModuleNumbaUnified.removeImportFromModule('numpy')
599
+
600
+ return ingredientsModuleNumbaUnified
601
+
602
+ def getIt(astCallConcurrencyResult: list[ast.Call]) -> Callable[[ast.AST], ast.AST]:
603
+ def workhorse(node: ast.AST) -> ast.AST:
604
+ NodeTourist(be.Call, Then.appendTo(astCallConcurrencyResult)).visit(node)
605
+ return node
606
+ return workhorse