Skip to main content

Type-safe DSLs for common patterns. Composable, local-first, lazy.

Project description

emergent

Write meaning, not code.

PyPI Python 3.13+ License: MIT Types: pyright strict

A compilation platform where frozen data compiles itself through fold. You build compilation languages — not applications. HTTP, CLI, Telegram, OpenAPI, Pydantic, SQL are libraries on the platform. Your custom compilers sit next to them.

@schema_meta(http_crud("/users", Users))
@dataclass
class User:
    id: Annotated[int, Identity]
    name: str
    email: Annotated[str, Unique, MaxLen(255)]

5 REST endpoints. Pydantic models. OpenAPI spec. You write fields, fold writes everything else.

# distribution is "emergent-py"; you import it as "emergent"
pip install emergent-py

# add extras for the targets you compile to:
pip install "emergent-py[fastapi]"   # HTTP + OpenAPI
pip install "emergent-py[sa]"        # SQLAlchemy
pip install "emergent-py[all]"       # everything (fastapi, sa, tg, cli)
import emergent

The idea

Frameworks today scatter meaning across files. A User lives in a model, a serializer, a view, a URL config, a migration, a test fixture — and none of those files know about each other. An LLM (or a human) has to hold the whole graph in their head to make a correct change.

emergent inverts this. Everything about a thing is on the thing:

@schema_meta(
    http_crud("/bounties", Board, ops=(LIST, GET, CREATE)),
    Methods(),
)
@dataclass
class Bounty:
    id: Annotated[int, Identity]
    title: str
    reward: Annotated[int, Min(0)]
    status: Annotated[str, MaxLen(20), Doc("Bounty status"),
                       cli.Help("Current status"), openapi.Description("Status field"),
                       sql.Index("idx_status")]

    @classmethod
    @post("/bounties/{bounty_id}/claim")
    async def claim(cls, db: ..., bounty_id: int, hunter: str) -> Result[Bounty, DomainError]: ...

One place. Every concern — validation, CLI help text, OpenAPI description, SQL index, HTTP routes — lives as an annotation on the field it belongs to. The fold compiler reads these annotations and produces correct output for each target. No sync bugs. No scattered state. No framework magic to reverse-engineer.

This is locality by construction, and it's what makes emergent work for humans and machines alike.

The entire platform is one function. Compilation, verification, explanation, query execution, derivation — all are fold applied to different data. Everything else is consequences.


Platform, not framework

Frameworks prescribe structure. Rails prescribes MVC. Django prescribes models-views-templates. You write code inside the framework.

emergent provides primitives. You build on the platform:

Primitive What it does
fold(items, ctx, protocol, method) Iterate capabilities, dispatch by protocol, accumulate context. 8 lines.
CompilationPhase(ctx_type, protocol, initial) Reified fold configuration. One phase = one compilation language.
SchemaCompiler(phases) Composable set of phases. Algebra: +, |, -, &.
TargetCompiler(trigger, codecs, pipeline, assemble) Surface-level compilation: Application → framework artifact.

The 4 axes (Schema, Surface, Storage, Query) are libraries built on these primitives. wire.derive is a library. Your custom compilers sit next to them — not inside a framework, but on the same platform.

Create your own compiler

to_pydantic(User, axes) reads capabilities, produces a Pydantic model. to_json_schema produces OpenAPI. You build to_search_index the same way. This runs:

from dataclasses import dataclass, replace
from typing import Annotated, Protocol, runtime_checkable

from emergent.wire.compile._core import Axes
from emergent.wire.compile._phase import CompilationPhase
from emergent.wire.compile import to_pydantic, to_json_schema
from emergent.wire.axis.schema import Identity, MaxLen, Min
from emergent.wire.axis.schema._universal import SchemaAxisCapability
from emergent.wire.axis._capability import OpenAPIContext, openapi_schema


# ── Your language: search index ──────────────────────────────

@dataclass(frozen=True, slots=True)
class IndexFieldCtx:
    field_name: str = ""
    field_type: type = object
    searchable: bool = False
    boost: float = 1.0
    facet: bool = False


@runtime_checkable
class IndexCompilable(Protocol):
    def compile_index(self, ctx: IndexFieldCtx) -> IndexFieldCtx: ...


INDEX_PHASE = CompilationPhase(
    IndexFieldCtx,
    IndexCompilable,
    lambda n, t: IndexFieldCtx(field_name=n, field_type=t),
)


# ── Capabilities: one atom, two languages ────────────────────

@dataclass(frozen=True, slots=True)
class Searchable(SchemaAxisCapability):
    boost: float = 1.0

    # YOUR language
    def compile_index(self, ctx: IndexFieldCtx) -> IndexFieldCtx:  
        return replace(ctx, searchable=True, boost=self.boost)

    # emergent's!
    def compile_openapi(self, ctx: OpenAPIContext) -> OpenAPIContext:
        return openapi_schema(
            ctx, **{"x-searchable": True, "x-search-boost": self.boost}
        )


@dataclass(frozen=True, slots=True)
class Facet(SchemaAxisCapability):
    def compile_index(self, ctx: IndexFieldCtx) -> IndexFieldCtx:
        return replace(ctx, facet=True)

    def compile_openapi(self, ctx: OpenAPIContext) -> OpenAPIContext:
        return openapi_schema(ctx, **{"x-facet": True})


# ── Your compiler: to_search_index — same shape as to_pydantic

@dataclass(frozen=True, slots=True)
class IndexField:
    name: str
    searchable: bool = False
    boost: float = 1.0
    facet: bool = False


@dataclass(frozen=True, slots=True)
class SearchIndex:
    entity: str
    fields: tuple[IndexField, ...]


INDEX_COMPILER = SchemaCompiler(phases=(INDEX_PHASE,))


def to_search_index(cls: type, axes: Axes) -> SearchIndex:
    ec = INDEX_COMPILER.compile(cls, axes)
    return SearchIndex(
        entity=cls.__name__,
        fields=tuple(
            IndexField(
                name=c.field_name,
                searchable=c.searchable,
                boost=c.boost,
                facet=c.facet,
            )
            for fc in ec
            if (c := fc[INDEX_PHASE]).searchable or c.facet
        ),
    )


# ── Entity ───────────────────────────────────────────────────

@dataclass
class Product:
    id: Annotated[int, Identity]
    name: Annotated[str, MaxLen(200), Searchable(boost=3.0)]
    description: Annotated[str, Searchable()]
    category: Annotated[str, Facet()]
    price: Annotated[float, Min(0), Facet()]


# ── Same axes, three compilers ───────────────────────────────

axes = Axes.default()

index  = to_search_index(Product, axes)   # YOUR compiler   → SearchIndex
model  = to_pydantic(Product, axes)       # emergent's       → Pydantic BaseModel
schema = to_json_schema(Product, axes)    # emergent's       → OpenAPI JSON Schema
index  SearchIndex(entity='Product', fields=(
    IndexField(name='name', searchable=True, boost=3.0),
    IndexField(name='description', searchable=True, boost=1.0),
    IndexField(name='category', facet=True),
    IndexField(name='price', facet=True),
))

model  <class 'Product'>  (Pydantic BaseModel with MaxLen, Min validation)

schema  {
    "name": {"type": "string", "maxLength": 200, "x-searchable": true, "x-search-boost": 3.0},
    "category": {"type": "string", "x-facet": true},
    "price": {"type": "number", "minimum": 0, "x-facet": true},
    ...
}

to_search_index has the same shape as to_pydantic — takes a class and axes, returns a typed artifact. Searchable implements both compile_index and compile_openapi, so it participates in both compilers. MaxLen has no compile_index — your compiler never sees it.

Execution: nodnod

emergent has two co-equal primitives. fold compiles descriptions. nodnod executes dependency graphs:

@G.node
class FetchPrice:
    @classmethod
    async def __compose__(cls, product: Product, db: Database) -> float:
        return await db.get_price(product.id)

@G.node
class FetchStock:
    @classmethod
    async def __compose__(cls, product: Product, db: Database) -> int:
        return await db.get_stock(product.id)

# FetchPrice and FetchStock have no dependency on each other → run in parallel.
# No asyncio.gather. No concurrency code. The type signatures ARE the graph.
result = await G.compose(BuildReport, product, db)

Types are the specification. fold reads capabilities from Annotated. nodnod reads dependencies from __compose__ signatures. Both: the structure IS the program.


Why this matters

A User in Django lives in a model, a serializer, a view, a URL config, a migration. Five files that must agree. When you change one, you grep for the others and hope. When a new developer joins, they trace the chain by hand. When a coding agent tries to add a field, it hallucinates a signal that doesn't exist because the pattern looked right from the three files it saw.

This is the scattered meaning problem. It is not a tooling problem — it is a language problem. The absence of a primitive that lets you state a fact once and have every consumer derive what it needs.

emergent is that primitive. Four properties make it work:

Property What it means
Locality All concerns for a field live on the field. Annotated[str, MaxLen(255), Unique, sql.Index()] — validation, schema, DDL in one place.
Defunctionalization Behavior is data. MaxLen(255) is a frozen dataclass you can print, compare, serialize. Not a closure. Not a decorator. Data.
Open-world dispatch New capabilities participate via isinstance. No registration. No plugin system. Implement the protocol → fold picks you up.
Algebraic composition Compilers compose: FASTAPI_SCHEMA + YOUR_PHASE. Capabilities compose: (MaxLen(255), Unique). No interference between independent concerns.

The result: any observer — human, LLM, or your future self at 3 AM — reads one declaration and knows everything. One change propagates to all targets. Not by convention. By construction.


Self-describing

emergent's IR reads itself. Every axis has an explain system — structured dicts for tools, human-readable text for you:

explain_schema(User)
# === User ===
#   [SchemaName('users')]
#
#   id (int):
#     [Identity]
#
#   email (str):
#     [Unique, MaxLen(255)]
#     cli: Help('Email address')
#     openapi: Description('User email'), Format('email')
#     sql: Index('idx_email')

explain_application(app)
# === Application (3 endpoints) ===
#
#   Endpoint #1 (2 exposures):
#     [POST /api/v1/players] RequestResponseCodec
#       request: RegisterRequest, response: RegisterResponse
#     [register (cli)] RequestResponseCodec
#       request: RegisterRequest, response: RegisterResponse

explain_full(User)  # 3-layer trace
# === User (full trace) ===
#
#   Schema: 3 fields (1 identity)
#     id (int) [Identity]
#     name (str)
#     email (str) [Unique]
#
#   Derivation: 1 pattern
#     Pattern #1: Dialect (11 steps), provider=Users
#       steps: InspectEntity → RequireIdentity → BindProvider → ...
#       ops: List, Get, Create, Update, Patch, Delete
#
#   Surface: 6 exposures across 1 endpoint
#     GET /api/users [ListUserRequest → ListUserResponse]
#     GET /api/users/{id} [GetUserRequest → GetUserResponse]
#     ...

Schema, query, storage, surface, compilation trace, derivation pipeline — all explainable. No execution, no side effects. The program describes itself from its own IR.


Verified at import time

verify catches semantic contradictions before your code runs:

from emergent.wire.verify import verify_raising

@dataclass
class Sensor:
    id: Annotated[int, Identity]
    name: Annotated[str, MinLen(50), MaxLen(10)]       # ERROR: MinLen > MaxLen
    temp: Annotated[float, Min(200), Max(125)]          # ERROR: Min > Max
    secret: Annotated[str, ReadOnly(), WriteOnly()]      # ERROR: inaccessible field

verify_raising(Sensor)  # VerificationError with all issues

Three built-in phases — numeric constraints, length constraints, semantic flags — each running as a standard compilation phase. Open-world: add your own verification phases without modifying emergent.


Four levels of control

# Level 1 — auto CRUD: one decorator, full API
@schema_meta(http_crud("/products", Store))
@dataclass
class Product:
    id: Annotated[int, Identity]
    name: str
    price: float

# Level 2 — CRUD + methods: derive CRUD, hand-write domain logic
@schema_meta(http_crud("/bounties", Board, ops=(LIST, GET, CREATE)), Methods())
@dataclass
class Bounty:
    id: Annotated[int, Identity]
    title: str
    reward: int

    @classmethod
    @post("/bounties/{bounty_id}/claim")
    async def claim(cls, db: ..., bounty_id: int, hunter: str) -> Result[Bounty, DomainError]: ...

# Level 3 — hand-written methods: every endpoint explicit, full control
@schema_meta(Methods())
@dataclass
class Auth:
    @classmethod
    @post("/login")
    async def login(cls, creds: Credentials) -> Result[Token, AuthError]: ...

# Level 4 — raw wire: one endpoint, three targets
endpoint(runner)
    .expose(HTTPRouteTrigger("POST", "/register"), rrc(Req, Resp))
    .expose(CLITrigger("register"), rrc(Req, Resp))
    .expose(TelegrindTrigger(Command("register")), rrc(Req, Resp))

Pick what fits. Mix in one app. Drop down a level when you need control, stay high when you don't.


Semantic transforms

Transforms in emergent dispatch on domain meaning, not syntax. This is a novel mechanism — no existing macro system combines domain-semantic awareness with compositional algebra:

@schema_meta(
    http_crud("/posts", Posts),
    WithoutDelete(),    # ← removes DELETE op across all targets
    Paginated(20),      # ← adds pagination to ops with Pageable effect
    Sorted(),           # ← adds sorting to LIST
    Filtered("author"), # ← adds exact-match filter
)
@dataclass
class Post:
    id: Annotated[int, Identity]
    title: str
    body: str
    author: str

Capabilities stack as arguments in @schema_meta() — frozen data in, frozen data out. They compose because they operate on algebraic structure, not string templates or AST nodes.


Examples

Example What it shows
roulette/ HTTP + CLI + Telegram from one codebase
cross_compile/ Bridge legacy FastAPI → CLI
full_stack/ Full-stack example
wiring.py Raw wire: endpoint + trigger + codec

Tutorial

A story-driven, 27-chapter walkthrough — from first API to handing your codebase to a coding agent.

Start here → docs/tutorial/00-intro.md

Part Chapters What you'll build
I–IV 0114 CRUD, methods, transforms, auth, nested resources, multi-target, custom dialects, raw wire, bridge
V–VI 1518 Query axis, providers, ops & runners, scope & DI
VII–VIII 1924 Enrichers, storage, compilation internals, stateful codecs, roulette walkthrough, design philosophy
IX 2527 Verify & explain, why emergent is LLM-native, agent workflows

Docs

docs/intro.md Introduction (EN)
docs/intro_ru.md Введение (RU)
docs/essence.md The essence — one function, one operator
docs/philosophy.md Design philosophy
docs/theory/architecture.md Architecture — theory, invariants, algebraic properties
docs/internal/wire-reference.md Wire reference — axes, capabilities, compile, bridge
docs/internal/cheatsheet.md Cheatsheet — all axes, every import, every pattern
docs/theory/universal-derivation.md Derivation — entity → endpoints via fold
docs/theory/compiler-deep-dive.md Compiler deep-dive — developing custom compilers
docs/emergent-and-ai.md emergent + AI agents
docs/book/ Book — 5-chapter SICP-style deep dive into compilation thinking

Where we are

emergent is young — started January 2026. Already runs in production. The core architecture (fold, CompilationPhase, SchemaCompiler, TargetCompiler, verify, explain, derive) is stable. What's still growing: the ecosystem, the stdlib, the number of built-in targets and dialects. Breaking changes happen, but we keep them well-motivated.


Stack

Layer What
deployme.py Application → infrastructure (compose, k8s)
emergent platform: fold, CompilationPhase, SchemaCompiler, TargetCompiler
emergent.wire libraries: axes (schema, surface, storage, query), derive, bridge, verify
emergent.graph runtime: Composer, ScopeFamily, RuntimePolicy (Cooperative / WorkStealing)
emergent.ops/saga/cache patterns: data-driven dispatch, compensation chains, tiered caching
nodnod typed dependency graphs, auto-parallelization, Either, Scope
combinators.py retry, timeout, fallback, race
kungfu Result, Option, LazyCoroResult

Define a context. Define a protocol. Call fold.

You just created a compilation language.

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

emergent_py-0.9.5.tar.gz (393.4 kB view details)

Uploaded Source

Built Distribution

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

emergent_py-0.9.5-py3-none-any.whl (494.7 kB view details)

Uploaded Python 3

File details

Details for the file emergent_py-0.9.5.tar.gz.

File metadata

  • Download URL: emergent_py-0.9.5.tar.gz
  • Upload date:
  • Size: 393.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.1

File hashes

Hashes for emergent_py-0.9.5.tar.gz
Algorithm Hash digest
SHA256 d80eabb02192ad00aee1882067ce77f648dbe4b610e766353625ceafa7bd798f
MD5 03619a3315a0024b1de8e95d4449e65a
BLAKE2b-256 bacc166fe3a04d622907ccbf2ad8a73407a38669a18bf6fcb54bf48b0aaf6019

See more details on using hashes here.

File details

Details for the file emergent_py-0.9.5-py3-none-any.whl.

File metadata

File hashes

Hashes for emergent_py-0.9.5-py3-none-any.whl
Algorithm Hash digest
SHA256 97491ce249af1a3aa072542ef52878b185b9786a2680fd1897587bcb3659b1a8
MD5 0834dd8b2fd83ae48dcf435861600b36
BLAKE2b-256 54df5785831ba73a40e599c4cadcd6b4fde59847acc7d20641ccba43c786feed

See more details on using hashes here.

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