# Rylai — Project Context for AI Agents

## What This Project Is

Rylai is a **static AST analysis tool** that generates Python `.pyi` stub files from pyo3-annotated Rust source code. It uses `syn` to parse Rust AST without requiring compilation or a Python runtime.

**Key constraint**: pure static analysis — no `cargo build`, no Python interpreter needed.

---

## pyo3 Writing Styles

pyo3 has two module styles. The tool must handle both.

### Style A: Inline mod (modern, pyo3 ≥ 0.21) — preferred in this project

```rust
#[pymodule]
mod Rylai {
    #[pyfunction]
    fn sum_as_string(a: usize, b: usize) -> PyResult<String> { ... }

    #[pyclass]
    struct Point { ... }
}
```

Detection strategy: find `ItemMod` annotated with `#[pymodule]`, then walk its items for `#[pyfunction]`, `#[pyclass]`, nested `#[pymodule]`.

### Style B: Function-based (legacy, still common)

```rust
#[pymodule]
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
    m.add_class::<Point>()?;
    Ok(())
}
```

Detection strategy: find `ItemFn` annotated with `#[pymodule]`, parse `ExprMethodCall` nodes in the body for `add_function` / `add_class` calls, extract symbol names from `wrap_pyfunction!` macro args.

**Style B limitation**: For `add_class::<T>()`, the stub currently uses the **Rust type name** `T` as the class name. A `#[pyclass(name = "...")]` on the struct is not visible in this path, so the Python-side name override is not applied. **Planned**: in a future change, resolve the struct/enum by type name and use its `#[pyclass(name)]` when present so the stub matches the Python API. Until then, use Style A if you need the renamed class name in the stub.

---

## Stub Generation Rules

### Type Mapping (Rust → Python)

| Rust | Python `.pyi` |
|------|--------------|
| `i8`..`i128`, `u8`..`u128`, `isize`, `usize` | `int` |
| `f32`, `f64` | `float` |
| `String`, `&str` | `str` |
| `bool` | `bool` |
| `PyResult<T>` | unwrap to `T` |
| `Option<T>` | `T \| None` (py ≥ 3.10) or `t.Optional[T]` (py < 3.10; stubs use `import typing as t`) |
| `Vec<T>` | `list[T]` (py ≥ 3.9, PEP 585) or `t.List[T]` (py 3.8) |
| `HashMap<K, V>` / `BTreeMap<K, V>` | `dict[K, V]` (py ≥ 3.9) or `t.Dict[K, V]` (py 3.8) |
| `(A, B, ...)` tuple | `tuple[A, B, ...]` (py ≥ 3.9) or `t.Tuple[A, B, ...]` (py 3.8) |
| `HashSet<T>` / `BTreeSet<T>` | `set[T]` (py ≥ 3.9) or `t.Set[T]` (py 3.8) |
| `&Bound<PyAny>` / `PyObject` / `impl IntoPyObject` | `t.Any` + emit warning (`import typing as t`) |
| unknown / unresolvable cross-crate type | `t.Any` + emit warning (`import typing as t`) |

### `#[pyclass]` Handling

- Emit a `class` block in the stub.
- Find associated `#[pymethods]` impl blocks (may be in a different file).
- Map method attributes: `#[new]` → `__init__`, `#[getter]` → `@property`, `#[setter]` → `@<name>.setter`, `#[staticmethod]` → `@staticmethod`, `#[classmethod]` → `@classmethod`.

### `#[pyo3(signature = ...)]` Override

When present, the `signature` meta takes priority over the function's Rust parameter list for generating the Python signature.

### Doc Comments

`/// ...` lines in Rust are `#[doc = "..."]` attributes in `syn`. Collect and concatenate them as the docstring in the stub (triple-quoted, indented under the `def` or `class`).

---

## `rylai.toml` Configuration

Optional file at project root. All fields are optional; zero-config must work.

Stub banner text and the `import typing as t` line live in `src/stub_constants.rs` (shared with `generator` and `add_content`); change them in one place only.

```toml
[type_map]
# Keys must be Rust path types (ident or a::b::Type). Literal tuples like (u8, u8, u8, u8) are not
# paths and cannot be keyed — stub becomes tuple[...] or t.Tuple[...] (see output.python_version);
# use `type Color = (...);` + map Color here.
# If a `type` alias is listed below, Rylai keeps that name (no expand) so Vec<Alias> etc. still resolve.
# Conflicting keys (same last segment, different Python types) → stderr warning; no short-name merge.
"numpy::PyReadonlyArray1" = "numpy.ndarray"
"pyo3_chrono::NaiveDateTime" = "datetime.datetime"

[features]
# Which cfg features are considered active during parsing
enabled = ["numpy", "serde"]

[fallback]
# "any" (default) | "error" | "skip"
strategy = "any"

[output]
# Affects t.Optional[T] vs T | None, PEP 585 list[T] vs t.List, t.Self vs class name, etc.
python_version = "3.10"
add_header = true   # prepend "# Auto-generated by Rylai"
all_include_private = false   # include `_`-prefixed top-level names in `__all__` (optional; default false)

[[override]]
# `stub` alone, OR non-empty `param_types` and/or `return_type` (not with `stub`). Single-line
# def/class header for `stub`; Rust `///` doc is merged into the .pyi; optional trailing `...` is stripped.
item = "my_module::complex_function"
stub = "def complex_function(x: t.Any, **kwargs: t.Any) -> dict[str, t.Any]:"
# Alternative: param_types = { ... }; return_type = "SomeType" — merge into generated `def` lines.

# Class methods: item = "{logical_module}::{RustOrPythonClass}::{rust_fn_or_pyo3_name}" — any
# #[pymethods] member (not only #[new]); #[new] already maps to __init__ in generated stubs,
# override is for custom __init__ signatures when needed.

[[all]]
# Optional per-file `__all__` rules (same `file` path convention as `[[add_content]]`, relative to `-o`).
file = "subdir/mod.pyi"
# include_private = true
# include = ["_keep_me"]    # only names already emitted in that stub; overrides `_` filter for those
# exclude = ["Internal"]

[[add_content]]
# Raw Python spliced into a generated `.pyi` by path under `-o` (must match an emitted file)
file = "subdir/mod.pyi"
location = "after-import-typing"   # head (after banner) | tail | after-import-typing
content = "from subdir._internal import X"
```

---

## Implementation Phases

Build in this order to keep scope manageable:

1. **Phase 1** — `#[pyfunction]` + primitive types + doc comments (Style A only)
2. **Phase 2** — `#[pyclass]` + `#[pymethods]` (Style A only)
3. **Phase 3** — `#[pyo3(signature)]`, `Option`/`Vec`/`HashMap` generics, `PyResult` unwrap
4. **Phase 4** — Style B (function-based modules), `wrap_pyfunction!` macro arg extraction
5. **Phase 5** — `rylai.toml` config, `#[cfg(feature)]` gating, cross-file resolution

---

## Design Principles

- **Zero-config first**: useful output without any `rylai.toml`.
- **Honest fallback**: unknown types become `Any` with a warning, never silently wrong.
- **Config supplements, not replaces**: config only covers static analysis blind spots.
- **Don't require compilation**: the tool must work on source files alone.
