Adds first-class Rust-backed Mizan to sit alongside mizan-django and
mizan-fastapi. A Rust dev writes:
#[derive(Mizan, Serialize, Deserialize)]
pub struct ProfileOutput { pub user_id: i64, pub name: String }
#[mizan::context("user")]
pub struct UserCtx;
#[mizan::client(context = UserCtx)]
pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { ... }
…and gets byte-identical KDL to the Python emitters, served over the
same wire protocol the React / Rust / Vue / Svelte kernels speak.
New crates:
- cores/mizan-rust/ (Cargo: mizan-core) — IR types, KDL emitter, traits, registry,
runtime (compute_invalidation / compute_merges
ported from mizan-fastapi), graph_check with
structural type-matching
- cores/mizan-rust-macros/ (Cargo: mizan-macros) — #[derive(Mizan)], #[mizan::context],
#[mizan::client] proc macros
- backends/mizan-rust-axum/ (Cargo: mizan-axum) — axum HTTP adapter: /session/, /call/, /ctx/:name/
- tests/afi/rust_app/ — AFI fixture port + server / export-ir binaries
Substrate-shape moves required by cross-language equivalence:
- IR canonicalization: functions / contexts / context-members / shared-by
now sort alphabetically in both Python and Rust emitters. The IR is a
contract; linkme doesn't preserve declaration order, so canonical sort
is the only stable mapping. afi_ir.kdl + per-target baselines regenerated.
- MizanType::TYPE_NAME is a const (with a default type_name() reader) so
it's usable in linkme TypeEntry static initializers.
- Tree-shaken type registry: #[derive(Mizan)] only emits the trait impl;
the #[mizan::client] macro registers canonical-named entries from
fn signatures, including Vec<T> element types for ref resolution.
- Merge resolution is structural (NamedType shape comparison) rather than
by name — matches the Python types_match_for_merge semantics.
Three-way forcing functions:
- tests/afi/test_codegen_parity.py — Django ≡ FastAPI ≡ Rust on KDL bytes (3 pass)
- tests/rust/run_wire_parity.py — 12/12 probes against FastAPI + Rust (EXIT=0)
Incidental fixes surfaced by the new tests:
- Stale `from .registry import validate_registry` import removed from
mizan-django/setup/discovery.py (referenced a function that no longer
exists; was masking codegen-parity).
- BASE_DIR added to tests/afi/django_app/project/settings.py.
- /session/ endpoint added to mizan-fastapi for protocol-shaped readiness
probe parity (wire-parity harness now polls /api/mizan/session/ on both
backends rather than FastAPI's /openapi.json).
- Root .gitignore picks up Rust target/ across the tree so new crates
don't need per-crate gitignore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
3.3 KiB
Python
117 lines
3.3 KiB
Python
"""
|
|
AFI conformance — same fixture, same Mizan IR (KDL), all three adapters.
|
|
|
|
Gates that mizan-django, mizan-fastapi, and the Rust mizan-axum backend
|
|
emit byte-equivalent KDL for the same registered functions. The IR is the
|
|
contract; whatever language wrote the backend, the wire/codegen-facing
|
|
artifact is identical.
|
|
|
|
Substrate-level gate, not e2e. Catches adapter symmetry problems —
|
|
type-introspection divergence, ordering non-determinism — across all
|
|
backends in one place.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
HERE = Path(__file__).parent
|
|
DJANGO_MANAGE = HERE / "django_app" / "manage.py"
|
|
RUST_APP_DIR = HERE / "rust_app"
|
|
|
|
|
|
def _fetch_django_ir() -> str:
|
|
"""Spawn Django's management command and parse stdout as KDL."""
|
|
result = subprocess.run(
|
|
[sys.executable, str(DJANGO_MANAGE), "export_mizan_ir"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
|
|
)
|
|
if result.returncode != 0:
|
|
pytest.fail(
|
|
f"export_mizan_ir failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}",
|
|
)
|
|
return result.stdout
|
|
|
|
|
|
def _fetch_fastapi_ir() -> str:
|
|
"""Build the FastAPI app inline (fresh registry) and call build_ir()."""
|
|
sys.path.insert(0, str(HERE))
|
|
try:
|
|
from mizan_core.registry import clear_registry
|
|
from mizan_core.ir import build_ir
|
|
from fastapi_app import make_app
|
|
|
|
clear_registry()
|
|
make_app()
|
|
return build_ir()
|
|
finally:
|
|
sys.path.remove(str(HERE))
|
|
|
|
|
|
def _fetch_rust_ir() -> str:
|
|
"""Spawn the Rust `export-ir` bin and capture stdout."""
|
|
result = subprocess.run(
|
|
[
|
|
"cargo", "run",
|
|
"--quiet", "--release",
|
|
"--manifest-path", str(RUST_APP_DIR / "Cargo.toml"),
|
|
"--bin", "export-ir",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
pytest.fail(
|
|
f"Rust export-ir failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}",
|
|
)
|
|
return result.stdout
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def fastapi_ir() -> str:
|
|
return _fetch_fastapi_ir()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def rust_ir() -> str:
|
|
return _fetch_rust_ir()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def django_ir() -> str:
|
|
return _fetch_django_ir()
|
|
|
|
|
|
def test_fastapi_matches_rust(fastapi_ir: str, rust_ir: str) -> None:
|
|
"""FastAPI ≡ Rust. The Mizan IR is the contract across languages."""
|
|
assert fastapi_ir == rust_ir, (
|
|
"FastAPI and Rust emit divergent Mizan IR for the same registered "
|
|
"functions. Substrate gate is red."
|
|
)
|
|
|
|
|
|
def test_django_matches_fastapi(django_ir: str, fastapi_ir: str) -> None:
|
|
"""Django ≡ FastAPI."""
|
|
assert django_ir == fastapi_ir, (
|
|
"Django and FastAPI emit divergent Mizan IR for the same "
|
|
"registered functions. Substrate gate is red."
|
|
)
|
|
|
|
|
|
def test_all_three_match(django_ir: str, fastapi_ir: str, rust_ir: str) -> None:
|
|
"""All three backends emit byte-identical KDL."""
|
|
assert django_ir == fastapi_ir == rust_ir, (
|
|
"Three-way IR divergence — see test_django_matches_fastapi and "
|
|
"test_fastapi_matches_rust for which pair drifts."
|
|
)
|