Mizan codegen substrate: Rust kernel + Rust codegen binary, JS generator deleted
The Mizan codegen substrate moves off JavaScript template-literal emission
onto a compiled Rust binary that consumes the same OpenAPI + x-mizan-* IR
the JS substrate consumed. Three structural wins fall out of one move:
1. Moat closes. The codegen logic (how `affects` becomes auto-invalidation,
how named contexts collapse onto bundled fetches, how the registry-to-
Provider mapping is shaped) ships compiled instead of as source bytes
in every consumer's node_modules.
2. Pattern F (lines.push append-walls) becomes structurally unauthorable.
The emit substrate is askama templates in templates/<target>/*.j2 —
actual target-language files with {{ ... }} substitution markers,
syntax-highlighted natively, type-checked against the render context
structs at compile time. The Rust emit modules build typed render
contexts and call .render(); no string-builder surface exists.
3. OpenAPI `default`-bearing fields now emit as non-optional in TS / Python
/ Rust — the server always populates them, so consumer code reads them
without nullable checks. Surfaced by Blazr's typecheck on regeneration.
Layout:
frontends/mizan-rust/ — Rust port of @mizan/base; #[cfg(feature="pyo3")]
exposes PyMizanClient for the Python target.
protocol/mizan-codegen/ — codegen binary source + askama templates.
protocol/mizan-generate/ — npm-package shim. bin/launcher.mjs dispatches
to the platform-appropriate prebuilt binary.
Old generator/ JS tree deleted.
tests/rust/ — wire-parity drivers. drive_kernel exercises
raw client.call() / fetch_context(); drive_emitted
exercises the typed crate the codegen emits.
tests/afi/afi_codegen_app.py — codegen entrypoint module (imports + registers).
backends/mizan-fastapi/.../schema.py — adds outputNullable so the Rust
codegen can wrap T | None responses in Option<T>.
Verification:
- 20 mizan-codegen tests green (IR deserialization, byte-equivalent
parity vs JS baseline for stage1/rust/python/react/vue/svelte,
structural test for channels).
- tests/rust/run_wire_parity.py — 12/12 probes green via the Rust binary
driving the FastAPI fixture end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
103
tests/rust/run_wire_parity.py
Normal file
103
tests/rust/run_wire_parity.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Drive the wire-parity check end-to-end.
|
||||
|
||||
1. Boot the FastAPI fixture app via uvicorn on a free port.
|
||||
2. Poll /openapi.json until the server is up.
|
||||
3. Run the Rust `drive_kernel` binary (raw kernel calls) against it.
|
||||
4. Run the Rust `drive_emitted` binary (typed codegen functions) against
|
||||
the same server.
|
||||
5. Tear the server down.
|
||||
|
||||
Either non-zero driver exit propagates as the script's exit code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
AFI_DIR = REPO_ROOT / "tests" / "afi"
|
||||
RUST_DIR = REPO_ROOT / "tests" / "rust"
|
||||
BOOT_TIMEOUT_S = 15.0
|
||||
POLL_INTERVAL_S = 0.25
|
||||
|
||||
|
||||
def pick_free_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def wait_for_server(port: int, timeout_s: float) -> bool:
|
||||
deadline = time.monotonic() + timeout_s
|
||||
url = f"http://127.0.0.1:{port}/openapi.json"
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=1.0) as resp:
|
||||
if resp.status == 200:
|
||||
return True
|
||||
except (urllib.error.URLError, ConnectionError, OSError) as e:
|
||||
# Surface the kind of failure so a stuck boot doesn't read
|
||||
# as "silently waiting"; the loop continues until timeout.
|
||||
sys.stderr.write(f"[wire_parity] waiting for server: {type(e).__name__}\n")
|
||||
time.sleep(POLL_INTERVAL_S)
|
||||
return False
|
||||
|
||||
|
||||
def run_driver(name: str, base_url: str) -> int:
|
||||
sys.stdout.write(f"\n=== {name} ===\n")
|
||||
sys.stdout.flush()
|
||||
return subprocess.run(
|
||||
["cargo", "run", "--quiet", "--bin", name, "--", base_url],
|
||||
cwd=RUST_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = pick_free_port()
|
||||
base_url = f"http://127.0.0.1:{port}/api/mizan"
|
||||
|
||||
server = subprocess.Popen(
|
||||
["uv", "run", "uvicorn", "fastapi_app:make_app",
|
||||
"--factory", "--port", str(port), "--log-level", "warning"],
|
||||
cwd=AFI_DIR,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
try:
|
||||
if not wait_for_server(port, BOOT_TIMEOUT_S):
|
||||
sys.stderr.write(
|
||||
f"[wire_parity] server failed to start within {BOOT_TIMEOUT_S}s\n",
|
||||
)
|
||||
stderr_tail = server.stderr.read(4096) if server.stderr else b""
|
||||
if stderr_tail:
|
||||
sys.stderr.write(stderr_tail.decode("utf-8", errors="replace"))
|
||||
return 1
|
||||
|
||||
failures = 0
|
||||
for driver in ("drive_kernel", "drive_emitted"):
|
||||
rc = run_driver(driver, base_url)
|
||||
if rc != 0:
|
||||
sys.stderr.write(f"[wire_parity] {driver} exited {rc}\n")
|
||||
failures += 1
|
||||
|
||||
return 0 if failures == 0 else 1
|
||||
finally:
|
||||
server.terminate()
|
||||
try:
|
||||
server.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
server.kill()
|
||||
server.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user