Skip to main content

AST composition helpers: markers, hygiene, lifting, and lowering for composable Python code generation

Project description

astichi

AST-level composition and stitching for Python code generation.

Astichi takes small, marker-bearing Python snippets, composes them into one coherent program, and emits runnable Python. It is built for generators that need low-overhead output without falling back to brittle string templates or runtime abstraction in hot paths.

Astichi is a fit when you want to:

  • describe codegen intent in Python-shaped snippets
  • stitch block and expression fragments at named composition sites
  • bind compile-time values into source before final lowering
  • unroll compile-time loops into straight-line Python
  • synthesize managed imports that participate in hygiene
  • inspect composables with descriptors before wiring them
  • emit inspectable source with provenance instead of opaque runtime machinery

It is not a general macro system and not a generic codemod framework. It is a focused library for generators that need a reliable AST stitcher.

Why Astichi

Code generators often hit the same wall: the desired output is simple, specialized Python, but the implementation ends up split between string concatenation, ad hoc templates, and fragile scope management.

Astichi handles the parts that usually go wrong:

  • valid Python ASTs instead of fragile template fragments
  • deterministic insertion order for stitched code
  • compile-time binding and loop unrolling before emission
  • specialized straight-line Python instead of runtime dispatch layers
  • emitted source you can diff, test, and round-trip

Marker mental model

Astichi is marker-bearing Python source plus a small build pipeline.

  • Markers are recognized from authored Python source.
  • Marker meaning comes from AST position, not string matching alone.
  • compile(...) parses marker-bearing source into a Composable.
  • build() wires composables together.
  • describe() exposes holes, binds, ports, and builder target addresses for data-driven composition.
  • materialize() resolves inserts, bindings, and hygiene, then produces real Python.

The core markers are:

  • astichi_hole(name) -> insertion site
  • astichi_keep(name) -> hygiene-preserved name in expression / statement source
  • name__astichi_keep__ -> hygiene-preserved name in identifier position
  • name__astichi_arg__ -> identifier demand
  • name__astichi_param_hole__ -> function-parameter insertion target
  • astichi_funcargs(...) -> call-argument payload
  • astichi_bind_external(name) -> external/literal value slot
  • astichi_ref(path) -> compile-time reducible identifier / attribute path
  • astichi_pyimport(module=..., names=(...)) -> managed Python import
  • astichi_comment("...") -> final-output source comment
  • astichi_pass(name, outer_bind=True) -> explicit same-name boundary read
  • astichi_import(name) -> explicit whole-scope boundary import
  • astichi_export(name) -> explicit outward supply
  • astichi_insert(...) -> internal emitted metadata, not general authored API

Comment marker note:

  • astichi_comment("...") is statement-only. Ordinary materialize() strips it for executable output; emit_commented() renders it as real # comments.
  • Multi-line payloads keep the marker statement's indentation, and only exact {__file__} / {__line__} substrings are expanded.

Value-form target note:

  • astichi_ref(...) and astichi_pass(...) are ordinary value-form surfaces in expressions.
  • If the marker result itself must occupy an Assign / AugAssign / Delete target position, append ._ or .astichi_v: astichi_ref("self.f0")._ = 1, astichi_pass(counter).astichi_v = 1.
  • If you immediately continue to a real attribute, plain Python target syntax already works: astichi_pass(obj).field = 1.

The one rule that matters most is scope:

  • astichi_insert is the basic Astichi boundary.
  • Each inserted composable lives in its own Astichi scope.
  • There is no implicit capture across that boundary.
  • If a name crosses the boundary, make it explicit with keep, pass, import, or export.
  • Function parameters are the pinned exception: parameter names and uses in the function scope stay attached to that parameter binding.

Small example:

import astichi

builder = astichi.build()
builder.add.Root(
    astichi.compile(
        """
items = []
astichi_hole(body)
result = tuple(items)
"""
    )
)
builder.add.Step(
    astichi.compile(
        """
astichi_pass(items, outer_bind=True).append("x")
"""
    )
)
builder.Root.body.add.Step(order=0)

materialized = builder.build().materialize()
print(materialized.emit(provenance=False))

Emitted Python:

items = []
items.append("x")
result = tuple(items)

Without astichi_pass(items, outer_bind=True), the inner snippet does not get to reuse items just because the spelling matches. That is deliberate. Astichi defaults to isolated scopes and only crosses them when the source says so.

The fluent builder is also available as a data-driven named API. Descriptor target data can feed that API directly:

hole = root.describe().single_hole_named("body")

builder = astichi.build()
builder.add("Root", root)
builder.add("Step", astichi.compile("value = 1\n"))
builder.target(hole.with_root_instance("Root")).add("Step")

That builder.target(...) call uses the same target address as builder.Root.body.add.Step(), but the address came from describe() instead of a Python attribute chain.

Example: schema-specialized row projector

Suppose an ingestion pipeline knows its event schema at build time, and each field needs its own normalization step. A runtime loop or dispatch table adds overhead to every row. String templating works until ordering, scope, and correctness start fighting each other.

Astichi lets you define the skeleton once, stitch in field-specific steps, and emit the straight-line Python you actually want to run.

import astichi

root = astichi.compile(
    """
astichi_bind_external(FIELDS)

def project_row(row):
    out = {}
    for field in astichi_for(FIELDS):
        astichi_hole(step)
    return out
"""
).bind(FIELDS=("user_id", "total_cents", "created_at"))

builder = astichi.build()
builder.add.Root(root)

builder.add.UserId(
    astichi.compile("out['user_id'] = int(row['user_id'])\n")
)
builder.add.TotalCents(
    astichi.compile("out['total_cents'] = int(row['total_cents'])\n")
)
builder.add.CreatedAt(
    astichi.compile("out['created_at'] = row['created_at'][:10]\n")
)

builder.Root.step[0].add.UserId()
builder.Root.step[1].add.TotalCents()
builder.Root.step[2].add.CreatedAt()

projector = builder.build().materialize()
print(projector.emit(provenance=False))

Emitted Python:

def project_row(row):
    out = {}
    out["user_id"] = int(row["user_id"])
    out["total_cents"] = int(row["total_cents"])
    out["created_at"] = row["created_at"][:10]
    return out

That is the point: no runtime field loop, no dispatch registry, no handwritten template surgery. The generated function is plain Python, specialized to the known schema, and suitable for hot-path use.

This is exactly the class of problem where a reliable AST stitcher matters:

  • block fragments must land in the right lexical scope
  • per-field steps must keep deterministic order
  • compile-time schema data must become literal Python
  • the final output must still be valid, inspectable source

Current surface

Astichi currently provides:

  • astichi.compile(source, file_name=None, line_number=1, offset=0)
  • astichi.build() for builder-based composition
  • concrete composables with .bind(...), .describe(), .materialize(), and .emit(...) / .emit_commented()
  • data-driven builder calls such as builder.add("Root", root) and builder.target(hole.with_root_instance("Root")).add("Step")
  • provenance helpers in astichi.emit

Supported pieces today include block holes, expression inserts, external binding, managed Python imports, materialization, emission, and builder-driven loop unrolling.

Assembler scope (astichi.assembler)

For generators that already have their own planner (for example YIDL lifecycle assembly), AssemblyScope wraps a builder and drives inventory-backed candidate lookup instead of fluent builder.Root...add chains. You register composables with scope.add(...), describe a resource plus optional selectors (name, build_match, owner_match), then find_candidates / require_one / apply or an ordered apply_batch of BindingRequest rows.

Resource helpers: as_composable(...), as_external_value(...), as_identifier(...). Build and materialize() stay authoritative; the scope layer only resolves which hole or demand site a resource satisfies and applies that binding to the lower-backed assembly graph.

Native rust fast path: when the optional _astichi_native_engine extension is built (uv run python native_engine/build.py), lower-engine selection defaults to auto and prefers native Rust for batch resolve/apply. In that mode apply_batch keeps occurrence/edge state in the native engine; Python mirror replay is off by default (set ASTICHI_NATIVE_SCOPE_MIRROR_REPLAY=1 only for compatibility/oracle work). Without the extension, the same API falls back to Python. See docs/reference/assembler-scope.md.

Self-native production boundary (plan dev-docs/perf-refactor/HotPathNoPythonPlan.md): when the extension advertises the full self-native slice stack including native.self_native.current_surfaces.v1, the lifecycle production path uses native compile parse, native compile validation, native scope materialize, one copy_python_ast at native materialize, and transfer handoff in to_executable_ast (no second clone_ast when handoff_transfer.v1 is on). engine=python remains the differential oracle for tests. Explicit native without self-native caps fails with a diagnostic instead of silently using the hybrid path.

Layout

Path Role
src/astichi/ Library code
docs/ User-facing docs
tests/ Pytest suite
dev-docs/ Design notes, active summary, and requirements

Development

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest

Start with:

  • docs/ for the user-facing surface
  • dev-docs/AstichiSingleSourceSummary.md for the current implementation snapshot and known gaps

License

LGPL-2.1-or-later. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

astichi-1.0.7.tar.gz (973.0 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

astichi-1.0.7-cp314-cp314-win_amd64.whl (5.6 MB view details)

Uploaded CPython 3.14Windows x86-64

astichi-1.0.7-cp313-cp313-win_amd64.whl (3.6 MB view details)

Uploaded CPython 3.13Windows x86-64

astichi-1.0.7-cp312-cp312-win_amd64.whl (1.9 MB view details)

Uploaded CPython 3.12Windows x86-64

astichi-1.0.7-cp312-abi3-manylinux_2_28_x86_64.whl (1.9 MB view details)

Uploaded CPython 3.12+manylinux: glibc 2.28+ x86-64

astichi-1.0.7-cp312-abi3-manylinux_2_28_aarch64.whl (1.8 MB view details)

Uploaded CPython 3.12+manylinux: glibc 2.28+ ARM64

astichi-1.0.7-cp312-abi3-macosx_11_0_arm64.whl (1.8 MB view details)

Uploaded CPython 3.12+macOS 11.0+ ARM64

astichi-1.0.7-cp312-abi3-macosx_10_13_x86_64.whl (1.9 MB view details)

Uploaded CPython 3.12+macOS 10.13+ x86-64

File details

Details for the file astichi-1.0.7.tar.gz.

File metadata

  • Download URL: astichi-1.0.7.tar.gz
  • Upload date:
  • Size: 973.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for astichi-1.0.7.tar.gz
Algorithm Hash digest
SHA256 75f230301e493d8d427ce307a7dd901368b4cce3ac3aca2bc33c33208ca43ad5
MD5 a52dbfabc1c13b6b9ffa02a0f01df265
BLAKE2b-256 d5fba575af00304a00f2a23b03d3b7d71da17cb783bd84058823b6746aafe386

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7.tar.gz:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astichi-1.0.7-cp314-cp314-win_amd64.whl.

File metadata

  • Download URL: astichi-1.0.7-cp314-cp314-win_amd64.whl
  • Upload date:
  • Size: 5.6 MB
  • Tags: CPython 3.14, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for astichi-1.0.7-cp314-cp314-win_amd64.whl
Algorithm Hash digest
SHA256 6341f2b109132f1efd4b39e0ec9e5994c68cd109b1b5f994bf33ca7d6713f49e
MD5 9a21ad712e885542b20ffa30559be0d3
BLAKE2b-256 28a278bfa4751711d06b070587031d40f8f2cfad0cc4e40601f0165a3364ebd0

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7-cp314-cp314-win_amd64.whl:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astichi-1.0.7-cp313-cp313-win_amd64.whl.

File metadata

  • Download URL: astichi-1.0.7-cp313-cp313-win_amd64.whl
  • Upload date:
  • Size: 3.6 MB
  • Tags: CPython 3.13, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for astichi-1.0.7-cp313-cp313-win_amd64.whl
Algorithm Hash digest
SHA256 6e486e9168adef99646966a78c96df3e3f7eb4421f6689d386ab7457919549f0
MD5 339062ec74889ca26688024d1c85a58f
BLAKE2b-256 2ca633a7cd9015f739c963e07c425604cf4fa612a02c2a939cdcec4ac2130cd8

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7-cp313-cp313-win_amd64.whl:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astichi-1.0.7-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: astichi-1.0.7-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 1.9 MB
  • Tags: CPython 3.12, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for astichi-1.0.7-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 62185e982baf563cc6d1d19902388da2235848cf216e408c544cdca81b428202
MD5 1f38c017f715c524634c32044693974e
BLAKE2b-256 858604d8542ed9cc864b5eb07ce85f3ae0b038a355b2550d92c38018cbf98f6e

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7-cp312-cp312-win_amd64.whl:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astichi-1.0.7-cp312-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for astichi-1.0.7-cp312-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 c951a607dda91751fc282e128be1fb31ee883152622bbafe6a5023fc10e15413
MD5 21f5033cf8497f0b5d17041636619a68
BLAKE2b-256 fd682c7a2982897ee20ae4c4959de9670a8236b7fbe437d1b3d2f59c367c1e4d

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7-cp312-abi3-manylinux_2_28_x86_64.whl:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astichi-1.0.7-cp312-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for astichi-1.0.7-cp312-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 4d0586cf7efc5dee2af86bb020ed9d2b7860c099a45bdd4c37228e074a532879
MD5 cb9bcec43023cb25f999f5ab7f15b554
BLAKE2b-256 71ad1759bbc9ca830f680beceace4e0ebd0403b8738f1eb55443b1b24dda542c

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7-cp312-abi3-manylinux_2_28_aarch64.whl:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astichi-1.0.7-cp312-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for astichi-1.0.7-cp312-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 6d3984eccb5e3258ace7f477fc3e83f2b73ccccb6dd7f2ce2fb20fcce23ec13f
MD5 017c9f49e8f0daed779f0ed64734bb0b
BLAKE2b-256 b9f6ab42b5707004c7bc7920a0e31f9533d04df4bd6dc31027006a9ab9d0a5fd

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7-cp312-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astichi-1.0.7-cp312-abi3-macosx_10_13_x86_64.whl.

File metadata

File hashes

Hashes for astichi-1.0.7-cp312-abi3-macosx_10_13_x86_64.whl
Algorithm Hash digest
SHA256 e695e76351cf38dc1b670a7c604f32aee9f56905a0000891718335fc61d8dbdf
MD5 63980c6da34e0b7e71020d91017ad971
BLAKE2b-256 1357f48c0333019bd944d4e69a55231ff657bfc0d49afcb1080c175412ad3f14

See more details on using hashes here.

Provenance

The following attestation bundles were made for astichi-1.0.7-cp312-abi3-macosx_10_13_x86_64.whl:

Publisher: publish.yml on owebeeone/astichi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page