astichi 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.
Files changed (50) hide show
  1. astichi/__init__.py +17 -0
  2. astichi/ast_provenance.py +131 -0
  3. astichi/asttools/__init__.py +40 -0
  4. astichi/asttools/imports.py +47 -0
  5. astichi/asttools/inserts.py +33 -0
  6. astichi/asttools/scopes.py +157 -0
  7. astichi/asttools/shapes.py +86 -0
  8. astichi/builder/__init__.py +35 -0
  9. astichi/builder/api.py +10 -0
  10. astichi/builder/graph.py +452 -0
  11. astichi/builder/handles.py +1257 -0
  12. astichi/diagnostics/__init__.py +13 -0
  13. astichi/diagnostics/formatting.py +48 -0
  14. astichi/emit/__init__.py +19 -0
  15. astichi/emit/api.py +87 -0
  16. astichi/frontend/__init__.py +18 -0
  17. astichi/frontend/api.py +239 -0
  18. astichi/frontend/compiled.py +9 -0
  19. astichi/frontend/source_kind.py +51 -0
  20. astichi/hygiene/__init__.py +29 -0
  21. astichi/hygiene/api.py +1316 -0
  22. astichi/lowering/__init__.py +129 -0
  23. astichi/lowering/boundaries.py +324 -0
  24. astichi/lowering/call_argument_payloads.py +375 -0
  25. astichi/lowering/external_bind.py +492 -0
  26. astichi/lowering/external_ref.py +517 -0
  27. astichi/lowering/marker_contexts.py +61 -0
  28. astichi/lowering/markers.py +1260 -0
  29. astichi/lowering/parameters.py +138 -0
  30. astichi/lowering/pyimport.py +371 -0
  31. astichi/lowering/sentinel_attrs.py +41 -0
  32. astichi/lowering/unroll.py +565 -0
  33. astichi/lowering/unroll_domain.py +147 -0
  34. astichi/materialize/__init__.py +9 -0
  35. astichi/materialize/api.py +4227 -0
  36. astichi/materialize/pyimport.py +217 -0
  37. astichi/model/__init__.py +99 -0
  38. astichi/model/basic.py +639 -0
  39. astichi/model/composable.py +29 -0
  40. astichi/model/descriptors.py +398 -0
  41. astichi/model/external_values.py +224 -0
  42. astichi/model/origin.py +14 -0
  43. astichi/model/ports.py +295 -0
  44. astichi/model/semantics.py +352 -0
  45. astichi/path_resolution.py +710 -0
  46. astichi/shell_refs.py +340 -0
  47. astichi-0.1.0.dist-info/METADATA +310 -0
  48. astichi-0.1.0.dist-info/RECORD +50 -0
  49. astichi-0.1.0.dist-info/WHEEL +4 -0
  50. astichi-0.1.0.dist-info/licenses/LICENSE +21 -0
astichi/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """astichi — AST composition for ahead-of-time Python codegen."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from astichi.builder import build
6
+ from astichi.frontend import compile
7
+ from astichi.model import Composable, ComposableDescription, ComposableHole, TargetAddress
8
+
9
+ __all__ = [
10
+ "__version__",
11
+ "Composable",
12
+ "ComposableDescription",
13
+ "ComposableHole",
14
+ "TargetAddress",
15
+ "build",
16
+ "compile",
17
+ ]
@@ -0,0 +1,131 @@
1
+ """AST source location (lineno / col_offset) propagation for synthetic nodes.
2
+
3
+ Astichi constructs many `ast.AST` nodes programmatically. Those nodes must
4
+ inherit line/column information from the authored subtree they replace or from
5
+ an immediate surrounding node so diagnostics and downstream passes can anchor
6
+ errors without reading implementation code.
7
+
8
+ Policy:
9
+
10
+ - After building a fresh subtree, call :func:`propagate_ast_source_locations`
11
+ with a *donor* that already carries a valid ``lineno`` (typically the hole,
12
+ insert site, or a copied authored node).
13
+ - :func:`ast.fix_missing_locations` fills remaining gaps inside the subtree.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import ast
19
+ from collections.abc import Iterator
20
+ from typing import TypeGuard
21
+
22
+ ASTICHI_SRC_FILE_ATTR = "_astichi_src_file"
23
+
24
+ # `ast.type_param` exists on Python 3.12+; omit when absent (e.g. older runtimes).
25
+ _located: list[type] = [
26
+ ast.stmt,
27
+ ast.expr,
28
+ ast.excepthandler,
29
+ ast.pattern,
30
+ ]
31
+ if hasattr(ast, "type_param"):
32
+ _located.append(ast.type_param)
33
+ _AST_LOCATION_TYPES: tuple[type, ...] = tuple(_located)
34
+
35
+
36
+ def requires_ast_source_location(node: ast.AST) -> bool:
37
+ """Whether *node* should carry ``lineno`` for user-facing provenance."""
38
+ return isinstance(node, _AST_LOCATION_TYPES)
39
+
40
+
41
+ def _lineno_ok(node: ast.AST) -> TypeGuard[ast.AST]:
42
+ lo = getattr(node, "lineno", None)
43
+ return isinstance(lo, int) and lo >= 1
44
+
45
+
46
+ def has_valid_ast_source_location(node: ast.AST) -> bool:
47
+ """Return True if *node* has a usable ``lineno`` (and is a located kind)."""
48
+ if not requires_ast_source_location(node):
49
+ return True
50
+ return _lineno_ok(node)
51
+
52
+
53
+ def iter_nodes_missing_ast_source_location(tree: ast.AST) -> Iterator[ast.AST]:
54
+ """Yield located AST nodes that lack a valid ``lineno``."""
55
+ for node in ast.walk(tree):
56
+ if requires_ast_source_location(node) and not _lineno_ok(node):
57
+ yield node
58
+
59
+
60
+ def first_ast_source_location_donor(tree: ast.AST) -> ast.AST | None:
61
+ """Return the first node in *tree* that already has a valid ``lineno``."""
62
+ for node in ast.walk(tree):
63
+ if _lineno_ok(node):
64
+ return node
65
+ return None
66
+
67
+
68
+ def astichi_source_file(node: ast.AST) -> str | None:
69
+ """Return Astichi's private source-file metadata for *node*, if present."""
70
+ value = getattr(node, ASTICHI_SRC_FILE_ATTR, None)
71
+ return value if isinstance(value, str) else None
72
+
73
+
74
+ def attach_astichi_source_file(tree: ast.AST, file_name: str) -> None:
75
+ """Attach Astichi source-file metadata to every node in *tree*."""
76
+ for node in ast.walk(tree):
77
+ setattr(node, ASTICHI_SRC_FILE_ATTR, file_name)
78
+
79
+
80
+ def copy_astichi_location(target: ast.AST, source: ast.AST) -> ast.AST:
81
+ """Copy Python and Astichi source location from *source* to *target*."""
82
+ ast.copy_location(target, source)
83
+ src_file = astichi_source_file(source)
84
+ if src_file is not None:
85
+ setattr(target, ASTICHI_SRC_FILE_ATTR, src_file)
86
+ return target
87
+
88
+
89
+ def propagate_astichi_source_file(root: ast.AST, donor: ast.AST | None) -> None:
90
+ """Fill missing Astichi source-file metadata on *root* from *donor*."""
91
+ if donor is None:
92
+ return
93
+ src_file = astichi_source_file(donor)
94
+ if src_file is None:
95
+ return
96
+ for node in ast.walk(root):
97
+ if astichi_source_file(node) is None:
98
+ setattr(node, ASTICHI_SRC_FILE_ATTR, src_file)
99
+
100
+
101
+ def propagate_ast_source_locations(root: ast.AST, donor: ast.AST | None) -> None:
102
+ """Attach line/column info to *root* and its descendants.
103
+
104
+ If *donor* has a valid ``lineno``, copy it onto *root* with
105
+ :func:`ast.copy_location` (when supported), then run
106
+ :func:`ast.fix_missing_locations` on *root*.
107
+
108
+ If *donor* is missing or has no line, only :func:`ast.fix_missing_locations`
109
+ runs (best-effort defaults — callers should prefer a real donor).
110
+ """
111
+ if isinstance(root, ast.Module):
112
+ ast.fix_missing_locations(root)
113
+ propagate_astichi_source_file(root, donor)
114
+ return
115
+ if donor is not None and _lineno_ok(donor):
116
+ try:
117
+ copy_astichi_location(root, donor)
118
+ except (TypeError, ValueError):
119
+ pass
120
+ ast.fix_missing_locations(root)
121
+ propagate_astichi_source_file(root, donor)
122
+
123
+
124
+ def assert_tree_has_ast_source_locations(tree: ast.AST) -> None:
125
+ """Raise ``AssertionError`` if any located node lacks a valid ``lineno``."""
126
+ missing = tuple(iter_nodes_missing_ast_source_location(tree))
127
+ if missing:
128
+ kinds = ", ".join(sorted({type(n).__name__ for n in missing}))
129
+ raise AssertionError(
130
+ f"{len(missing)} AST node(s) lack source location (lineno): {kinds}"
131
+ )
@@ -0,0 +1,40 @@
1
+ """AST helper utilities for Astichi."""
2
+
3
+ from astichi.asttools.shapes import (
4
+ BLOCK,
5
+ IDENTIFIER,
6
+ NAMED_VARIADIC,
7
+ PARAMETER,
8
+ POSITIONAL_VARIADIC,
9
+ SCALAR_EXPR,
10
+ MarkerShape,
11
+ )
12
+ from astichi.asttools.imports import (
13
+ import_alias_binding_name,
14
+ import_statement_binding_names,
15
+ )
16
+ from astichi.asttools.inserts import (
17
+ has_astichi_insert_decorator,
18
+ is_astichi_insert_call,
19
+ is_astichi_insert_shell,
20
+ is_expression_insert_call,
21
+ )
22
+ from astichi.asttools.scopes import AstichiScope, AstichiScopeMap
23
+
24
+ __all__ = [
25
+ "AstichiScope",
26
+ "AstichiScopeMap",
27
+ "BLOCK",
28
+ "IDENTIFIER",
29
+ "NAMED_VARIADIC",
30
+ "PARAMETER",
31
+ "POSITIONAL_VARIADIC",
32
+ "SCALAR_EXPR",
33
+ "MarkerShape",
34
+ "has_astichi_insert_decorator",
35
+ "import_alias_binding_name",
36
+ "import_statement_binding_names",
37
+ "is_astichi_insert_call",
38
+ "is_astichi_insert_shell",
39
+ "is_expression_insert_call",
40
+ ]
@@ -0,0 +1,47 @@
1
+ """Helpers for Python import-statement binding names."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+
7
+
8
+ def import_alias_binding_name(
9
+ alias: ast.alias,
10
+ *,
11
+ from_import: bool,
12
+ include_star: bool = False,
13
+ ) -> str | None:
14
+ """Return the local name bound by one ordinary Python import alias."""
15
+ if from_import:
16
+ if alias.name == "*" and not include_star:
17
+ return None
18
+ return alias.asname or alias.name
19
+ return alias.asname or alias.name.split(".")[0]
20
+
21
+
22
+ def import_statement_binding_names(
23
+ node: ast.Import | ast.ImportFrom,
24
+ *,
25
+ include_star: bool = False,
26
+ ) -> tuple[str, ...]:
27
+ """Return the local binding names introduced by an import statement."""
28
+ names: list[str] = []
29
+ if isinstance(node, ast.Import):
30
+ for alias in node.names:
31
+ name = import_alias_binding_name(
32
+ alias,
33
+ from_import=False,
34
+ include_star=include_star,
35
+ )
36
+ if name is not None:
37
+ names.append(name)
38
+ return tuple(names)
39
+ for alias in node.names:
40
+ name = import_alias_binding_name(
41
+ alias,
42
+ from_import=True,
43
+ include_star=include_star,
44
+ )
45
+ if name is not None:
46
+ names.append(name)
47
+ return tuple(names)
@@ -0,0 +1,33 @@
1
+ """Predicates for internal ``astichi_insert`` surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ import ast
7
+
8
+
9
+ def is_astichi_insert_call(node: ast.AST) -> bool:
10
+ """Whether ``node`` is a direct ``astichi_insert(...)`` call."""
11
+ return (
12
+ isinstance(node, ast.Call)
13
+ and isinstance(node.func, ast.Name)
14
+ and node.func.id == "astichi_insert"
15
+ )
16
+
17
+
18
+ def is_expression_insert_call(node: ast.AST) -> bool:
19
+ """Whether ``node`` is expression-form ``astichi_insert(target, payload)``."""
20
+ return is_astichi_insert_call(node) and len(node.args) == 2
21
+
22
+
23
+ def has_astichi_insert_decorator(decorators: Iterable[ast.expr]) -> bool:
24
+ """Whether a decorator list contains direct ``@astichi_insert(...)``."""
25
+ return any(is_astichi_insert_call(decorator) for decorator in decorators)
26
+
27
+
28
+ def is_astichi_insert_shell(node: ast.AST) -> bool:
29
+ """Whether ``node`` is a class/def decorated by ``astichi_insert(...)``."""
30
+ return (
31
+ isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
32
+ and has_astichi_insert_decorator(node.decorator_list)
33
+ )
@@ -0,0 +1,157 @@
1
+ """Astichi owner-scope mapping helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from dataclasses import dataclass, field
7
+
8
+ from astichi.asttools.inserts import (
9
+ is_astichi_insert_shell,
10
+ is_expression_insert_call,
11
+ )
12
+
13
+
14
+ @dataclass(eq=False)
15
+ class AstichiScope:
16
+ """Astichi owner-scope identity for one AST owner root."""
17
+
18
+ root: ast.AST
19
+ label: str
20
+ parent: "AstichiScope | None" = None
21
+ _node_ids: set[int] = field(default_factory=set, repr=False, compare=False)
22
+
23
+ @property
24
+ def root_id(self) -> int:
25
+ return id(self.root)
26
+
27
+ def owns(self, node: ast.AST) -> bool:
28
+ return id(node) in self._node_ids
29
+
30
+ def is_root(self, node: ast.AST) -> bool:
31
+ return id(node) == self.root_id
32
+
33
+
34
+ class AstichiScopeMap:
35
+ """Per-node Astichi owner-scope lookup."""
36
+
37
+ def __init__(self, tree: ast.Module) -> None:
38
+ self.root = AstichiScope(root=tree, label="module body", parent=None)
39
+ self._by_id: dict[int, AstichiScope] = {}
40
+ self._nested_python_root_by_id: dict[int, ast.AST] = {}
41
+ self._walk(tree, self.root, nested_python_root=None)
42
+
43
+ @classmethod
44
+ def from_tree(cls, tree: ast.Module) -> "AstichiScopeMap":
45
+ return cls(tree)
46
+
47
+ def scope_for(self, node: ast.AST) -> AstichiScope:
48
+ return self._by_id.get(id(node), self.root)
49
+
50
+ def parent_scope_for(self, scope: AstichiScope) -> AstichiScope | None:
51
+ return scope.parent
52
+
53
+ def nested_python_root_for(self, node: ast.AST) -> ast.AST | None:
54
+ return self._nested_python_root_by_id.get(id(node))
55
+
56
+ def _record(
57
+ self,
58
+ node: ast.AST,
59
+ scope: AstichiScope,
60
+ *,
61
+ nested_python_root: ast.AST | None,
62
+ ) -> None:
63
+ self._by_id[id(node)] = scope
64
+ scope._node_ids.add(id(node))
65
+ if nested_python_root is not None:
66
+ self._nested_python_root_by_id[id(node)] = nested_python_root
67
+
68
+ def _walk(
69
+ self,
70
+ node: ast.AST,
71
+ scope: AstichiScope,
72
+ *,
73
+ nested_python_root: ast.AST | None,
74
+ ) -> None:
75
+ self._record(node, scope, nested_python_root=nested_python_root)
76
+ if isinstance(node, ast.Module):
77
+ for child in node.body:
78
+ self._walk(child, scope, nested_python_root=None)
79
+ return
80
+ if is_expression_insert_call(node):
81
+ # Intentional ownership widening over the historical boundary-only
82
+ # mapper: expression-insert payloads are Astichi scopes too.
83
+ self._walk(node.func, scope, nested_python_root=nested_python_root)
84
+ self._walk(node.args[0], scope, nested_python_root=nested_python_root)
85
+ payload_scope = AstichiScope(
86
+ root=node,
87
+ label="expression insert payload",
88
+ parent=scope,
89
+ )
90
+ self._walk(node.args[1], payload_scope, nested_python_root=None)
91
+ for keyword in node.keywords:
92
+ self._walk(keyword, scope, nested_python_root=nested_python_root)
93
+ return
94
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
95
+ is_shell = is_astichi_insert_shell(node)
96
+ body_scope = scope
97
+ body_nested_root = node if not is_shell else None
98
+ if is_shell:
99
+ body_scope = AstichiScope(
100
+ root=node,
101
+ label=f"shell {node.name!r} body",
102
+ parent=scope,
103
+ )
104
+ for decorator in node.decorator_list:
105
+ self._walk(decorator, scope, nested_python_root=nested_python_root)
106
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
107
+ # Preserve pre-refactor asymmetry: shell args/defaults are
108
+ # body-owned, while decorators/returns remain outer-owned.
109
+ self._walk_arguments(
110
+ node.args,
111
+ body_scope,
112
+ nested_python_root=body_nested_root,
113
+ )
114
+ if node.returns is not None:
115
+ self._walk(
116
+ node.returns,
117
+ scope,
118
+ nested_python_root=nested_python_root,
119
+ )
120
+ if isinstance(node, ast.ClassDef):
121
+ # Preserve pre-refactor behavior: class bases/keywords are
122
+ # evaluated in the outer scope, not the shell body scope.
123
+ for base in node.bases:
124
+ self._walk(base, scope, nested_python_root=nested_python_root)
125
+ for keyword in node.keywords:
126
+ self._walk(keyword, scope, nested_python_root=nested_python_root)
127
+ for child in node.body:
128
+ self._walk(
129
+ child,
130
+ body_scope,
131
+ nested_python_root=body_nested_root,
132
+ )
133
+ return
134
+ for child in ast.iter_child_nodes(node):
135
+ self._walk(child, scope, nested_python_root=nested_python_root)
136
+
137
+ def _walk_arguments(
138
+ self,
139
+ args: ast.arguments,
140
+ scope: AstichiScope,
141
+ *,
142
+ nested_python_root: ast.AST | None,
143
+ ) -> None:
144
+ self._record(args, scope, nested_python_root=nested_python_root)
145
+ for argument in (
146
+ list(args.posonlyargs)
147
+ + list(args.args)
148
+ + list(args.kwonlyargs)
149
+ ):
150
+ self._walk(argument, scope, nested_python_root=nested_python_root)
151
+ if args.vararg is not None:
152
+ self._walk(args.vararg, scope, nested_python_root=nested_python_root)
153
+ if args.kwarg is not None:
154
+ self._walk(args.kwarg, scope, nested_python_root=nested_python_root)
155
+ for default in args.defaults + args.kw_defaults:
156
+ if default is not None:
157
+ self._walk(default, scope, nested_python_root=nested_python_root)
@@ -0,0 +1,86 @@
1
+ """Shape semantics for Astichi marker usage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+
7
+
8
+ class MarkerShape(ABC):
9
+ """Behavior-bearing marker usage shape."""
10
+
11
+ def __init__(self, name: str) -> None:
12
+ self.name = name
13
+
14
+ def is_scalar_expr(self) -> bool:
15
+ return False
16
+
17
+ def is_positional_variadic(self) -> bool:
18
+ return False
19
+
20
+ def is_named_variadic(self) -> bool:
21
+ return False
22
+
23
+ def is_block(self) -> bool:
24
+ return False
25
+
26
+ def is_identifier(self) -> bool:
27
+ return False
28
+
29
+ def is_parameter(self) -> bool:
30
+ return False
31
+
32
+
33
+ class _ScalarExprShape(MarkerShape):
34
+ def __init__(self) -> None:
35
+ super().__init__("scalar_expr")
36
+
37
+ def is_scalar_expr(self) -> bool:
38
+ return True
39
+
40
+
41
+ class _PositionalVariadicShape(MarkerShape):
42
+ def __init__(self) -> None:
43
+ super().__init__("positional_variadic")
44
+
45
+ def is_positional_variadic(self) -> bool:
46
+ return True
47
+
48
+
49
+ class _NamedVariadicShape(MarkerShape):
50
+ def __init__(self) -> None:
51
+ super().__init__("named_variadic")
52
+
53
+ def is_named_variadic(self) -> bool:
54
+ return True
55
+
56
+
57
+ class _BlockShape(MarkerShape):
58
+ def __init__(self) -> None:
59
+ super().__init__("block")
60
+
61
+ def is_block(self) -> bool:
62
+ return True
63
+
64
+
65
+ class _IdentifierShape(MarkerShape):
66
+ def __init__(self) -> None:
67
+ super().__init__("identifier")
68
+
69
+ def is_identifier(self) -> bool:
70
+ return True
71
+
72
+
73
+ class _ParameterShape(MarkerShape):
74
+ def __init__(self) -> None:
75
+ super().__init__("parameter")
76
+
77
+ def is_parameter(self) -> bool:
78
+ return True
79
+
80
+
81
+ SCALAR_EXPR = _ScalarExprShape()
82
+ POSITIONAL_VARIADIC = _PositionalVariadicShape()
83
+ NAMED_VARIADIC = _NamedVariadicShape()
84
+ BLOCK = _BlockShape()
85
+ IDENTIFIER = _IdentifierShape()
86
+ PARAMETER = _ParameterShape()
@@ -0,0 +1,35 @@
1
+ """Mutable builder graph and handle surfaces for Astichi."""
2
+
3
+ from astichi.builder.api import build
4
+ from astichi.builder.graph import (
5
+ AdditiveEdge,
6
+ BuilderGraph,
7
+ EdgeSourceOverlay,
8
+ IdentifierBinding,
9
+ InstanceRecord,
10
+ TargetRef,
11
+ )
12
+ from astichi.builder.handles import (
13
+ AddProxy,
14
+ AddToTargetProxy,
15
+ BindIdentifierProxy,
16
+ BuilderHandle,
17
+ InstanceHandle,
18
+ TargetHandle,
19
+ )
20
+
21
+ __all__ = [
22
+ "AddProxy",
23
+ "AddToTargetProxy",
24
+ "AdditiveEdge",
25
+ "BindIdentifierProxy",
26
+ "BuilderGraph",
27
+ "BuilderHandle",
28
+ "EdgeSourceOverlay",
29
+ "IdentifierBinding",
30
+ "InstanceHandle",
31
+ "InstanceRecord",
32
+ "TargetHandle",
33
+ "TargetRef",
34
+ "build",
35
+ ]
astichi/builder/api.py ADDED
@@ -0,0 +1,10 @@
1
+ """Public builder entrypoints for Astichi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from astichi.builder.handles import BuilderHandle
6
+
7
+
8
+ def build() -> BuilderHandle:
9
+ """Create a new Astichi builder."""
10
+ return BuilderHandle()