shpe 0.2.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.
- shpe/__init__.py +5 -0
- shpe/__main__.py +4 -0
- shpe/checker.py +159 -0
- shpe/cli.py +193 -0
- shpe/diagnostics.py +85 -0
- shpe/environment.py +71 -0
- shpe/extractor.py +214 -0
- shpe/handlers.py +768 -0
- shpe/py.typed +0 -0
- shpe/resolver.py +340 -0
- shpe/server.py +155 -0
- shpe-0.2.0.dist-info/METADATA +100 -0
- shpe-0.2.0.dist-info/RECORD +16 -0
- shpe-0.2.0.dist-info/WHEEL +4 -0
- shpe-0.2.0.dist-info/entry_points.txt +2 -0
- shpe-0.2.0.dist-info/licenses/LICENSE +201 -0
shpe/__init__.py
ADDED
shpe/__main__.py
ADDED
shpe/checker.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from shpe.diagnostics import Diagnostics, ErrorCode
|
|
5
|
+
from shpe.environment import Environment, ShapeState
|
|
6
|
+
from shpe.extractor import Extractor
|
|
7
|
+
from shpe.resolver import Resolver
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Checker(ast.NodeVisitor):
|
|
11
|
+
"""AST visitor that walks through Python code to infer and check NumPy shapes"""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.env = Environment()
|
|
15
|
+
self.diagnostics = Diagnostics()
|
|
16
|
+
self.extractor = Extractor(self.env, self.diagnostics)
|
|
17
|
+
self.resolver = Resolver(
|
|
18
|
+
self.extractor,
|
|
19
|
+
self.env,
|
|
20
|
+
self.diagnostics,
|
|
21
|
+
self,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def shapes(self) -> dict[str, tuple[Any, ...] | ShapeState]:
|
|
26
|
+
"""Convenience property for tests and CLI to access global shapes."""
|
|
27
|
+
return self.env.shapes
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def scalar_values(self) -> dict[str, int | float | ShapeState]:
|
|
31
|
+
"""Convenience property for tests and CLI to access global scalar values."""
|
|
32
|
+
return self.env.scalar_values
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def errors(self) -> list[dict[str, Any]]:
|
|
36
|
+
"""Convenience property for tests and CLI to access collected errors."""
|
|
37
|
+
return self.diagnostics.errors
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def warnings(self) -> list[dict[str, Any]]:
|
|
41
|
+
"""Convenience property for tests and CLI to access collected warnings."""
|
|
42
|
+
return self.diagnostics.warnings
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def inlay_hints(self) -> list[dict[str, Any]]:
|
|
46
|
+
"""Convenience property for tests and CLI to access collected inlay hints."""
|
|
47
|
+
return self.diagnostics.inlay_hints
|
|
48
|
+
|
|
49
|
+
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
|
|
50
|
+
"""Handles explicit annotated variable assignments."""
|
|
51
|
+
|
|
52
|
+
if not isinstance(node.target, ast.Name):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
var_name = node.target.id
|
|
56
|
+
|
|
57
|
+
if isinstance(node.value, ast.Constant) and isinstance(
|
|
58
|
+
node.value.value, (int, float)
|
|
59
|
+
):
|
|
60
|
+
self.env.set_scalar(var_name, node.value.value)
|
|
61
|
+
|
|
62
|
+
annotated_shape = self.extractor.annotation_shape(node.annotation)
|
|
63
|
+
inferred_shape = self.resolver.shape(node.value) if node.value else None
|
|
64
|
+
|
|
65
|
+
resolved_shape = (
|
|
66
|
+
annotated_shape if annotated_shape is not None else inferred_shape
|
|
67
|
+
)
|
|
68
|
+
if resolved_shape is None:
|
|
69
|
+
resolved_shape = ShapeState.UNKNOWN
|
|
70
|
+
self.env.set_shape(var_name, resolved_shape)
|
|
71
|
+
|
|
72
|
+
if inferred_shape is not None and inferred_shape is not ShapeState.UNKNOWN:
|
|
73
|
+
self.diagnostics.hint(node, inferred_shape)
|
|
74
|
+
if (
|
|
75
|
+
annotated_shape is not None
|
|
76
|
+
and inferred_shape is not None
|
|
77
|
+
and annotated_shape is not ShapeState.UNKNOWN
|
|
78
|
+
and inferred_shape is not ShapeState.UNKNOWN
|
|
79
|
+
and annotated_shape != inferred_shape
|
|
80
|
+
):
|
|
81
|
+
self.diagnostics.error(
|
|
82
|
+
node,
|
|
83
|
+
ErrorCode.ANNOTATION,
|
|
84
|
+
(
|
|
85
|
+
f"{var_name} annotated as {annotated_shape}, "
|
|
86
|
+
f"but expression has the shape {inferred_shape}. "
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def visit_Assign(self, node: ast.Assign) -> None:
|
|
91
|
+
"""Handles implicit variable assignments."""
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
node.value
|
|
95
|
+
and isinstance(node.value, ast.Constant)
|
|
96
|
+
and isinstance(node.value.value, (int, float))
|
|
97
|
+
):
|
|
98
|
+
for target in node.targets:
|
|
99
|
+
if isinstance(target, ast.Name):
|
|
100
|
+
self.env.set_scalar(target.id, node.value.value)
|
|
101
|
+
|
|
102
|
+
inferred_shape = self.resolver.shape(node.value) if node.value else None
|
|
103
|
+
|
|
104
|
+
if inferred_shape is not None and inferred_shape is not ShapeState.UNKNOWN:
|
|
105
|
+
self.diagnostics.hint(node, inferred_shape)
|
|
106
|
+
|
|
107
|
+
shape_to_set = (
|
|
108
|
+
inferred_shape if inferred_shape is not None else ShapeState.UNKNOWN
|
|
109
|
+
)
|
|
110
|
+
for target in node.targets:
|
|
111
|
+
if isinstance(target, ast.Name):
|
|
112
|
+
self.env.set_shape(target.id, shape_to_set)
|
|
113
|
+
|
|
114
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
115
|
+
"""Tracks custom function definitions."""
|
|
116
|
+
self.env.set_function(node.name, node)
|
|
117
|
+
self.env.push_child()
|
|
118
|
+
self.resolver.user_function_def(node)
|
|
119
|
+
self.env.pop_child()
|
|
120
|
+
|
|
121
|
+
def visit_If(self, node: ast.If) -> None:
|
|
122
|
+
"""Handles if statements by tainting variables modified inside branches."""
|
|
123
|
+
test_str = ast.unparse(node.test).replace('"', "'")
|
|
124
|
+
|
|
125
|
+
# Evaluate normally for the standard main block
|
|
126
|
+
if test_str == "__name__ == '__main__'":
|
|
127
|
+
self.generic_visit(node)
|
|
128
|
+
else:
|
|
129
|
+
self._visit_tainted_block(node.body)
|
|
130
|
+
if node.orelse:
|
|
131
|
+
self._visit_tainted_block(node.orelse)
|
|
132
|
+
|
|
133
|
+
def visit_For(self, node: ast.For) -> None:
|
|
134
|
+
"""Handles for loops by tainting variables modified inside the loop."""
|
|
135
|
+
self._visit_tainted_block(node.body)
|
|
136
|
+
|
|
137
|
+
def visit_While(self, node: ast.While) -> None:
|
|
138
|
+
"""Handles while loops by tainting variables modified inside the loop."""
|
|
139
|
+
self._visit_tainted_block(node.body)
|
|
140
|
+
|
|
141
|
+
def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
|
|
142
|
+
"""Handles async for loops."""
|
|
143
|
+
self._visit_tainted_block(node.body)
|
|
144
|
+
|
|
145
|
+
def _visit_tainted_block(self, body: list[ast.stmt]) -> None:
|
|
146
|
+
"""Walks a block of code and forces any assigned variables to Unknown.
|
|
147
|
+
Used for complex code blocks where we cannot guarantee shape inference.
|
|
148
|
+
We would rather have Unkown than incorrect shapes.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
body: A list of AST statements representing the code block.
|
|
152
|
+
"""
|
|
153
|
+
for stmt in body:
|
|
154
|
+
self.visit(stmt)
|
|
155
|
+
for target in ast.walk(stmt):
|
|
156
|
+
if isinstance(target, ast.Name) and isinstance(target.ctx, ast.Store):
|
|
157
|
+
self.env.set_shape(target.id, ShapeState.UNKNOWN)
|
|
158
|
+
if target.id in self.env.scalar_values:
|
|
159
|
+
self.env.scalar_values[target.id] = ShapeState.UNKNOWN
|
shpe/cli.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import ast
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from shpe.checker import Checker
|
|
9
|
+
from shpe.environment import ShapeState
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = importlib.metadata.version("shpe")
|
|
13
|
+
except importlib.metadata.PackageNotFoundError:
|
|
14
|
+
__version__ = "unknown"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
"""CLI entry point for the local Python shape checker."""
|
|
19
|
+
parser = argparse.ArgumentParser(description="A local Python shape checker.")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"paths", nargs="+", type=Path, help="Files or directories to check"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--show-shapes",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="Display inferred NumPy shapes for all variables",
|
|
30
|
+
)
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
files = discover_files(args.paths)
|
|
34
|
+
all_errors: list[str] = []
|
|
35
|
+
all_shapes: dict[Path, dict[str, tuple[Any, ...] | ShapeState]] = {}
|
|
36
|
+
all_scalars: dict[Path, dict[str, int | float | ShapeState]] = {}
|
|
37
|
+
|
|
38
|
+
for filepath in files:
|
|
39
|
+
file_errors, shapes, scalars = _process_file(filepath)
|
|
40
|
+
all_errors.extend(file_errors)
|
|
41
|
+
if shapes:
|
|
42
|
+
all_shapes[filepath] = shapes
|
|
43
|
+
if scalars:
|
|
44
|
+
all_scalars[filepath] = scalars
|
|
45
|
+
|
|
46
|
+
if args.show_shapes and all_shapes:
|
|
47
|
+
_print_shape_report(all_shapes, all_scalars)
|
|
48
|
+
|
|
49
|
+
for err in all_errors:
|
|
50
|
+
print(err)
|
|
51
|
+
|
|
52
|
+
if all_errors:
|
|
53
|
+
print(f"\nFound {len(all_errors)} error(s) across {len(files)} file(s).")
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
else:
|
|
56
|
+
print(f"Success: Checked {len(files)} file(s), no shape errors found.")
|
|
57
|
+
sys.exit(0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def discover_files(paths: list[Path]) -> list[Path]:
|
|
61
|
+
"""Discovers and collects all Python (.py) source files from the provided paths.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
paths: A list of file or directory Path objects to scan.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A list of resolved Path objects pointing to valid Python source files.
|
|
68
|
+
"""
|
|
69
|
+
files_to_check = []
|
|
70
|
+
for path in paths:
|
|
71
|
+
if not path.exists():
|
|
72
|
+
print(f"Error: Path '{path}' not found.", file=sys.stderr)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if path.is_file() and path.suffix == ".py":
|
|
76
|
+
files_to_check.append(path)
|
|
77
|
+
elif path.is_dir():
|
|
78
|
+
for subpath in path.rglob("*.py"):
|
|
79
|
+
# Skip hidden directories like .git, .venv, __pycache__
|
|
80
|
+
if not any(part.startswith(".") for part in subpath.parts):
|
|
81
|
+
files_to_check.append(subpath)
|
|
82
|
+
|
|
83
|
+
return files_to_check
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _process_file(
|
|
87
|
+
filepath: Path,
|
|
88
|
+
) -> tuple[
|
|
89
|
+
list[str],
|
|
90
|
+
dict[str, tuple[Any, ...] | ShapeState],
|
|
91
|
+
dict[str, int | float | ShapeState],
|
|
92
|
+
]:
|
|
93
|
+
"""Processes a single source file, running syntax checks and shape analysis.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
filepath: The path to the file to check.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A tuple containing a list of formatted error messages,
|
|
100
|
+
the extracted shapes dictionary, and the extracted scalars dictionary.
|
|
101
|
+
"""
|
|
102
|
+
errors: list[str] = []
|
|
103
|
+
try:
|
|
104
|
+
code = filepath.read_text(encoding="utf-8")
|
|
105
|
+
lines = code.splitlines()
|
|
106
|
+
tree = ast.parse(code, filename=str(filepath))
|
|
107
|
+
except SyntaxError as e:
|
|
108
|
+
errors.append(f"{filepath}:{e.lineno}: [SyntaxError] {e.msg} ")
|
|
109
|
+
return errors, {}, {}
|
|
110
|
+
|
|
111
|
+
checker = Checker()
|
|
112
|
+
checker.visit(tree)
|
|
113
|
+
|
|
114
|
+
for warning in checker.warnings:
|
|
115
|
+
line = warning["line"]
|
|
116
|
+
line_idx = max(0, line - 1)
|
|
117
|
+
if line_idx < len(lines):
|
|
118
|
+
line_content = lines[line_idx]
|
|
119
|
+
if "# shpe: ignore" in line_content:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
code = warning["code"]
|
|
123
|
+
msg = warning["message"]
|
|
124
|
+
errors.append(f"{filepath}:{line}: warning: [{code}] {msg}")
|
|
125
|
+
|
|
126
|
+
for error in checker.errors:
|
|
127
|
+
line = error["line"]
|
|
128
|
+
line_idx = max(0, line - 1)
|
|
129
|
+
if line_idx < len(lines):
|
|
130
|
+
line_content = lines[line_idx]
|
|
131
|
+
if "# shpe: ignore" in line_content:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
code = error["code"]
|
|
135
|
+
msg = error["message"]
|
|
136
|
+
errors.append(f"{filepath}:{line}: [{code}] {msg}")
|
|
137
|
+
|
|
138
|
+
return errors, checker.env.shapes, checker.env.scalar_values
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _print_shape_report(
|
|
142
|
+
all_shapes: dict[Path, dict[str, tuple[Any, ...] | ShapeState]],
|
|
143
|
+
all_scalars: dict[Path, dict[str, int | float | ShapeState]],
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Prints a formatted report of all inferred shapes and scalar values across files.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
all_shapes: Mapping of file paths to their symbol shape definitions.
|
|
149
|
+
all_scalars: Mapping of file paths to their tracked scalar variables.
|
|
150
|
+
"""
|
|
151
|
+
print("\n-------- Symbol State (Shapes & Scalars) --------")
|
|
152
|
+
|
|
153
|
+
for filepath in sorted(all_shapes.keys(), key=str):
|
|
154
|
+
print(f"\n{filepath}:")
|
|
155
|
+
symbols = all_shapes[filepath]
|
|
156
|
+
scalars = all_scalars.get(filepath, {})
|
|
157
|
+
|
|
158
|
+
if scalars:
|
|
159
|
+
print(" Scalars:")
|
|
160
|
+
for name, val in sorted(scalars.items()):
|
|
161
|
+
val_str = "unknown" if val is ShapeState.UNKNOWN else val
|
|
162
|
+
print(f" - {name} = {val_str}")
|
|
163
|
+
|
|
164
|
+
if symbols:
|
|
165
|
+
print(" Shapes:")
|
|
166
|
+
symbols_with_no_shape: list[str] = []
|
|
167
|
+
symbols_with_unknown_shape: list[str] = []
|
|
168
|
+
for var_name, shape in sorted(symbols.items()):
|
|
169
|
+
if shape is ShapeState.UNKNOWN:
|
|
170
|
+
symbols_with_unknown_shape.append(var_name)
|
|
171
|
+
elif shape is not None:
|
|
172
|
+
print(f" - {var_name}: {shape}")
|
|
173
|
+
else:
|
|
174
|
+
symbols_with_no_shape.append(var_name)
|
|
175
|
+
|
|
176
|
+
if symbols_with_unknown_shape:
|
|
177
|
+
print(
|
|
178
|
+
" - unknown shape (tainted) for: "
|
|
179
|
+
f"{', '.join(symbols_with_unknown_shape)}"
|
|
180
|
+
)
|
|
181
|
+
if symbols_with_no_shape:
|
|
182
|
+
print(
|
|
183
|
+
f" - no shape inferred for: {', '.join(symbols_with_no_shape)}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if not symbols and not scalars:
|
|
187
|
+
print(" - (no symbols tracked)")
|
|
188
|
+
|
|
189
|
+
print("\n" + "-" * 30 + "\n")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|
shpe/diagnostics.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorCode(Enum):
|
|
7
|
+
ANNOTATION = "Annotation"
|
|
8
|
+
ELEMENTWISE = "Elementwise"
|
|
9
|
+
MATMUL = "MatMul"
|
|
10
|
+
RESHAPE = "Reshape"
|
|
11
|
+
VALUE = "Value"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _position(node: ast.AST) -> tuple[int, int, int]:
|
|
15
|
+
col = getattr(node, "col_offset", 0)
|
|
16
|
+
return getattr(node, "lineno", 0), col, getattr(node, "end_col_offset", col)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Diagnostics:
|
|
20
|
+
"""Collects errors, warnings, and inlay hints during a check pass."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self.errors: list[dict[str, Any]] = []
|
|
24
|
+
self.warnings: list[dict[str, Any]] = []
|
|
25
|
+
self.inlay_hints: list[dict[str, Any]] = []
|
|
26
|
+
self.active_error_collection = True
|
|
27
|
+
self.active_warning_collection = True
|
|
28
|
+
self.active_hint_collection = True
|
|
29
|
+
|
|
30
|
+
def deactivate_hints(self) -> None:
|
|
31
|
+
"""Deactivate inlay hints collection."""
|
|
32
|
+
self.active_hint_collection = False
|
|
33
|
+
|
|
34
|
+
def activate_hints(self) -> None:
|
|
35
|
+
"""Activate inlay hints collection."""
|
|
36
|
+
self.active_hint_collection = True
|
|
37
|
+
|
|
38
|
+
def deactivate_errors(self) -> None:
|
|
39
|
+
"""Deactivate error collection."""
|
|
40
|
+
self.active_error_collection = False
|
|
41
|
+
|
|
42
|
+
def activate_errors(self) -> None:
|
|
43
|
+
"""Activate error collection."""
|
|
44
|
+
self.active_error_collection = True
|
|
45
|
+
|
|
46
|
+
def deactivate_warnings(self) -> None:
|
|
47
|
+
"""Deactivate warning collection."""
|
|
48
|
+
self.active_warning_collection = False
|
|
49
|
+
|
|
50
|
+
def activate_warnings(self) -> None:
|
|
51
|
+
"""Activate warning collection."""
|
|
52
|
+
self.active_warning_collection = True
|
|
53
|
+
|
|
54
|
+
def error(self, node: ast.AST, code: ErrorCode, message: str) -> None:
|
|
55
|
+
if not self.active_error_collection:
|
|
56
|
+
return
|
|
57
|
+
line, col, end_col = _position(node)
|
|
58
|
+
self.errors.append(
|
|
59
|
+
{
|
|
60
|
+
"line": line,
|
|
61
|
+
"col": col,
|
|
62
|
+
"end_col": end_col,
|
|
63
|
+
"code": code.value,
|
|
64
|
+
"message": message,
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def warning(self, node: ast.AST, message: str) -> None:
|
|
69
|
+
if not self.active_warning_collection:
|
|
70
|
+
return
|
|
71
|
+
line, col, end_col = _position(node)
|
|
72
|
+
self.warnings.append(
|
|
73
|
+
{"line": line, "col": col, "end_col": end_col, "message": message}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def hint(self, node: ast.AST, shape: tuple[Any, ...]) -> None:
|
|
77
|
+
if not self.active_hint_collection:
|
|
78
|
+
return
|
|
79
|
+
self.inlay_hints.append(
|
|
80
|
+
{
|
|
81
|
+
"line": getattr(node, "lineno", 0),
|
|
82
|
+
"col": getattr(node, "end_col_offset", 0),
|
|
83
|
+
"shape": shape,
|
|
84
|
+
}
|
|
85
|
+
)
|
shpe/environment.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ShapeState(Enum):
|
|
7
|
+
UNKNOWN = "UNKNOWN"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Environment:
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.stack: list[dict[str, Any]] = []
|
|
13
|
+
self.push_child() # Initialize the root environment
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def shapes(self) -> dict[str, tuple[Any, ...] | ShapeState]:
|
|
17
|
+
return self.stack[-1]["shapes"]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def scalar_values(self) -> dict[str, int | float | ShapeState]:
|
|
21
|
+
return self.stack[-1]["scalar_values"]
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def functions(self) -> dict[str, Any]:
|
|
25
|
+
return self.stack[-1]["functions"]
|
|
26
|
+
|
|
27
|
+
def __len__(self) -> int:
|
|
28
|
+
return len(self.stack)
|
|
29
|
+
|
|
30
|
+
def get_shape(self, name: str) -> tuple[Any, ...] | ShapeState | None:
|
|
31
|
+
for frame in reversed(self.stack):
|
|
32
|
+
if name in frame["shapes"]:
|
|
33
|
+
return frame["shapes"][name]
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def get_scalar(self, name: str) -> int | float | ShapeState | None:
|
|
37
|
+
for frame in reversed(self.stack):
|
|
38
|
+
if name in frame["scalar_values"]:
|
|
39
|
+
return frame["scalar_values"][name]
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def get_function(self, name: str) -> ast.FunctionDef | None:
|
|
43
|
+
for frame in reversed(self.stack):
|
|
44
|
+
if name in frame["functions"]:
|
|
45
|
+
return frame["functions"][name]
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def set_shape(self, name: str, shape: tuple[Any, ...] | ShapeState) -> None:
|
|
49
|
+
self.stack[-1]["shapes"][name] = shape
|
|
50
|
+
|
|
51
|
+
def set_scalar(self, name: str, value: int | float | ShapeState) -> None:
|
|
52
|
+
self.stack[-1]["scalar_values"][name] = value
|
|
53
|
+
|
|
54
|
+
def set_function(self, name: str, func: Any) -> None:
|
|
55
|
+
self.stack[-1]["functions"][name] = func
|
|
56
|
+
|
|
57
|
+
def push_child(self) -> None:
|
|
58
|
+
"""Creates a new child environment for local variables and shapes."""
|
|
59
|
+
shapes: dict[str, tuple[Any, ...] | ShapeState] = {}
|
|
60
|
+
scalar_values: dict[str, int | float | ShapeState] = {}
|
|
61
|
+
functions: dict[str, ast.FunctionDef] = {}
|
|
62
|
+
self.stack.append(
|
|
63
|
+
{"shapes": shapes, "scalar_values": scalar_values, "functions": functions}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def pop_child(self) -> None:
|
|
67
|
+
"""Removes the most recent child environment."""
|
|
68
|
+
if len(self.stack) > 1:
|
|
69
|
+
self.stack.pop()
|
|
70
|
+
else:
|
|
71
|
+
raise RuntimeError("Cannot pop the root environment.")
|