py.rs

64.9 kB · rust · 1984 lines

1use crate::model::*;2use std::collections::{BTreeMap, BTreeSet};3use std::path::Path;45const NATIVE: &str = "mrlypy._mrlypy";67const PYTHON_KEYWORDS: &[&str] = &[8    "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",9    "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if",10    "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try",11    "while", "with", "yield",12];1314const RUST_KEYWORDS: &[&str] = &[15    "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",16    "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",17    "ref", "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use",18    "where", "while", "yield", "try", "gen", "box", "do", "final", "macro", "override", "priv",19    "typeof", "unsized", "virtual", "abstract", "become", "u8", "u16", "u32", "u64", "u128",20    "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64", "bool", "char", "str",21];2223const HAND_SELF: &[(&str, &str)] = &[24    ("Tensor", "tensor"),25    ("Cell", "cell"),26    ("CellNd", "cell"),27    ("Cell6d", "cell"),28    ("Color", "color"),29    ("Code", "code"),30    ("Rng", "rng"),31];3233const HEADER: &str = "#![allow(clippy::too_many_arguments)]\n";3435const HAND_NAMES: &[&str] = &[36    "ok", "PyCell", "PyCell2d", "PyCell3d", "PyCell6d", "PyCellNd", "PyCode", "PyColor",37    "PyPixels", "PyRgba", "PyRng", "PySerde", "PyTensor",38];3940pub fn write(manifest: &Manifest, root: &Path) -> Result<()> {41    let cx = Cx::new(manifest);42    let tree = build(&cx)?;43    let pkg = root.join("pkgs/mrlypy");44    save(&pkg.join("src/lib.rs"), &lib_file())?;45    save(&pkg.join("src/gen.rs"), &rust_file(&cx, &tree)?)?;46    let python = pkg.join("python/mrlypy");47    python_files(&cx, &tree, &python)?;48    save(&python.join("py.typed"), "")?;49    save(&pkg.join("tests/test_manifest.py"), &test_file(&cx))?;50    Ok(())51}5253fn save(path: &Path, text: &str) -> Result<()> {54    if let Some(dir) = path.parent() {55        std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;56    }57    std::fs::write(path, text).map_err(|e| format!("{}: {e}", path.display()))58}5960// NAMES6162fn parent(path: &str) -> &str {63    path.rsplit_once("::").map(|(head, _)| head).unwrap_or("")64}6566fn last(path: &str) -> &str {67    path.rsplit("::").next().unwrap_or(path)68}6970fn dotted(path: &str) -> String {71    path.replace("::", ".")72}7374fn py_name(name: &str) -> String {75    if PYTHON_KEYWORDS.contains(&name) {76        format!("{name}_")77    } else {78        name.to_string()79    }80}8182fn rust_ident(name: &str) -> String {83    if RUST_KEYWORDS.contains(&name) {84        format!("{name}_")85    } else {86        name.to_string()87    }88}8990fn snake(name: &str) -> String {91    let mut out = String::new();92    let chars: Vec<char> = name.chars().collect();93    for (i, c) in chars.iter().enumerate() {94        if c.is_uppercase() && i > 0 && !chars[i - 1].is_uppercase() {95            out.push('_');96        }97        out.push(c.to_ascii_lowercase());98    }99    out100}101102fn quote(text: &str) -> String {103    format!("{text:?}")104}105106fn py_str(text: &str) -> String {107    let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");108    format!("\"{escaped}\"")109}110111fn indent(depth: usize) -> String {112    "    ".repeat(depth)113}114115// CONTEXT116117struct Cx<'a> {118    manifest: &'a Manifest,119    types: BTreeMap<&'a str, &'a Type>,120}121122impl<'a> Cx<'a> {123    fn new(manifest: &'a Manifest) -> Cx<'a> {124        let types = manifest.types.iter().map(|t| (t.path.as_str(), t)).collect();125        Cx { manifest, types }126    }127128    fn ty(&self, path: &str) -> Result<&'a Type> {129        self.types130            .get(path)131            .copied()132            .ok_or_else(|| format!("{path} is not a type in the manifest"))133    }134135    fn wrapper(&self, path: &str) -> String {136        let mut segs: Vec<String> = parent(path)137            .split("::")138            .filter(|s| !s.is_empty())139            .map(rust_ident)140            .collect();141        segs.push(last(path).to_string());142        format!("crate::gen::{}", segs.join("::"))143    }144145    fn const_generic(&self, path: &str) -> bool {146        self.types.get(path).is_some_and(|t| t.const_generic)147    }148149    fn derives(&self, path: &str, derive: &str) -> bool {150        self.types151            .get(path)152            .is_some_and(|t| t.derives.iter().any(|d| d == derive))153    }154155    fn is_copy(&self, ty: &Ty) -> bool {156        match ty {157            Ty::Scalar { .. } | Ty::U128 | Ty::I128 | Ty::Code => true,158            Ty::Hand { name, .. } => name == "Color" || name == "Code",159            Ty::Tuple { items } => items.iter().all(|t| self.is_copy(t)),160            Ty::Array { item, .. } | Ty::Option { item } => self.is_copy(item),161            Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } => {162                self.derives(path, "Copy")163            }164            _ => false,165        }166    }167168    fn words(&self, path: &str) -> Vec<String> {169        let Some(ty) = self.types.get(path) else {170            return Vec::new();171        };172        let rename_all = ty173            .serde174            .iter()175            .find_map(|s| s.strip_prefix("rename_all = ").map(|v| v.trim_matches('"').to_string()));176        ty.variants177            .iter()178            .map(|v| {179                if let Some(word) = v180                    .serde181                    .iter()182                    .find_map(|s| s.strip_prefix("rename = ").map(|w| w.trim_matches('"').to_string()))183                {184                    return word;185                }186                match rename_all.as_deref() {187                    Some("lowercase") => v.name.to_lowercase(),188                    Some("UPPERCASE") => v.name.to_uppercase(),189                    Some("snake_case") => snake(&v.name),190                    Some("kebab-case") => snake(&v.name).replace('_', "-"),191                    _ => v.name.clone(),192                }193            })194            .collect()195    }196}197198// TREE199200#[derive(Default)]201struct Node<'a> {202    path: String,203    docs: Vec<String>,204    children: BTreeMap<String, Node<'a>>,205    exports: Vec<Export<'a>>,206    classes: Vec<Owned<'a>>,207    holders: Vec<Owned<'a>>,208    consts: Vec<&'a Const>,209}210211struct Export<'a> {212    name: String,213    variants: Vec<(&'a Function, Option<u8>)>,214}215216struct Owned<'a> {217    ty: &'a Type,218    fns: Vec<&'a Function>,219}220221impl<'a> Node<'a> {222    fn name(&self) -> &str {223        last(&self.path)224    }225226    fn reach(&mut self, path: &str) -> &mut Node<'a> {227        if path.is_empty() {228            return self;229        }230        let mut node = self;231        let mut sofar = String::new();232        for seg in path.split("::") {233            if !sofar.is_empty() {234                sofar.push_str("::");235            }236            sofar.push_str(seg);237            let full = sofar.clone();238            node = node.children.entry(seg.to_string()).or_insert_with(|| Node {239                path: full,240                ..Node::default()241            });242        }243        node244    }245246    fn push_export(&mut self, f: &'a Function) -> Result<()> {247        let name = last(&f.path).to_string();248        let dims: Vec<Option<u8>> = if f.dims.is_empty() {249            vec![None]250        } else {251            f.dims.iter().map(|d| Some(*d)).collect()252        };253        let path = self.path.clone();254        let index = match self.exports.iter().position(|e| e.name == name) {255            Some(index) => index,256            None => {257                self.exports.push(Export {258                    name: name.clone(),259                    variants: Vec::new(),260                });261                self.exports.len() - 1262            }263        };264        let export = &mut self.exports[index];265        for dim in dims {266            let clash = export.variants.iter().any(|(_, d)| d.is_none() || *d == dim)267                || (dim.is_none() && !export.variants.is_empty());268            if clash {269                return Err(format!("{name} is exported twice in {path}"));270            }271            export.variants.push((f, dim));272        }273        export.variants.sort_by_key(|(_, d)| *d);274        Ok(())275    }276277    fn prune(&mut self) -> bool {278        self.children.retain(|_, child| child.prune());279        !self.children.is_empty()280            || !self.exports.is_empty()281            || !self.classes.is_empty()282            || !self.holders.is_empty()283            || !self.consts.is_empty()284    }285286    fn item_names(&self) -> Vec<String> {287        let mut names: Vec<String> = self.exports.iter().map(|e| py_name(&e.name)).collect();288        names.extend(self.classes.iter().map(|c| c.ty.name.clone()));289        names.extend(self.holders.iter().map(|h| h.ty.name.clone()));290        names.extend(self.consts.iter().map(|c| c.name.clone()));291        if self.path == "core" {292            names.push("Rng".to_string());293        }294        names295    }296297    fn check(&self) -> Result<()> {298        let names = self.item_names();299        for (i, name) in names.iter().enumerate() {300            if names[..i].contains(name) {301                return Err(format!("{name} is named twice in mrlypy.{}", dotted(&self.path)));302            }303            if self.children.contains_key(name) {304                return Err(format!(305                    "{name} is both an item and a module in mrlypy.{}",306                    dotted(&self.path)307                ));308            }309        }310        for class in &self.classes {311            let mut seen: Vec<String> = Vec::new();312            for field in class.ty.fields.iter().filter(|f| f.public && crossable(&f.ty)) {313                seen.push(py_name(&field.name));314            }315            for f in &class.fns {316                let name = py_name(&f.name);317                if seen.contains(&name) {318                    return Err(format!("{} has a field and a method named {name}", class.ty.path));319                }320                seen.push(name);321            }322            for extra in ["from_dict", "to_dict"] {323                if seen.iter().any(|s| s == extra) {324                    return Err(format!("{} already has {extra}", class.ty.path));325                }326            }327        }328        self.children.values().try_for_each(Node::check)329    }330331    fn walk<'s>(&'s self, out: &mut Vec<&'s Node<'a>>) {332        out.push(self);333        for child in self.children.values() {334            child.walk(out);335        }336    }337}338339fn crossable(ty: &Ty) -> bool {340    !ty.any(&|t| matches!(t, Ty::Opaque { .. } | Ty::Unknown { .. }))341}342343fn build<'a>(cx: &Cx<'a>) -> Result<Node<'a>> {344    let mut root = Node::default();345    for m in &cx.manifest.modules {346        root.reach(&m.path).docs = m.docs.clone();347    }348    let mut owned: BTreeMap<&str, Vec<&'a Function>> = BTreeMap::new();349    for f in cx.manifest.functions.iter().filter(|f| f.cross == Cross::Ok) {350        match &f.owner {351            None => root.reach(&f.module).push_export(f)?,352            Some(owner) => match &cx.ty(owner)?.cross {353                TypeCross::Hand { .. } => root.reach(&f.module).push_export(f)?,354                TypeCross::Uncrossable { .. } => {355                    return Err(format!("{} is owned by an uncrossable type", f.path))356                }357                _ => owned.entry(owner.as_str()).or_default().push(f),358            },359        }360    }361    for ty in &cx.manifest.types {362        let fns = owned.remove(ty.path.as_str()).unwrap_or_default();363        match &ty.cross {364            TypeCross::Class => root.reach(parent(&ty.path)).classes.push(Owned { ty, fns }),365            TypeCross::Plain | TypeCross::Enum { .. } if !fns.is_empty() => {366                root.reach(parent(&ty.path)).holders.push(Owned { ty, fns })367            }368            _ => {}369        }370    }371    if let Some((owner, _)) = owned.iter().next() {372        return Err(format!("{owner} owns functions but is no type"));373    }374    for c in cx.manifest.consts.iter().filter(|c| c.cross == Cross::Ok) {375        root.reach(parent(&c.path)).consts.push(c);376    }377    root.prune();378    if !root.exports.is_empty() || !root.classes.is_empty() || !root.consts.is_empty() {379        return Err("the crate root holds items the Python bridge has no module for".into());380    }381    root.check()?;382    Ok(root)383}384385// TYPES386387fn is_u8(ty: &Ty) -> bool {388    matches!(ty, Ty::Scalar { name } if name == "u8")389}390391fn is_rgba(ty: &Ty) -> bool {392    matches!(ty, Ty::Array { item, len: 4 } if is_u8(item))393}394395fn hand_name(ty: &Ty) -> Option<&str> {396    match ty {397        Ty::Hand { name, .. } => Some(name),398        _ => None,399    }400}401402fn subst(ty: &Ty, dim: u8) -> Ty {403    match ty {404        Ty::Hand { name, dim: None } if name == "CellNd" => Ty::Hand {405            name: name.clone(),406            dim: Some(dim),407        },408        Ty::Vec { item } => Ty::Vec {409            item: Box::new(subst(item, dim)),410        },411        Ty::Slice { mutable, item } => Ty::Slice {412            mutable: *mutable,413            item: Box::new(subst(item, dim)),414        },415        Ty::Option { item } => Ty::Option {416            item: Box::new(subst(item, dim)),417        },418        Ty::Tuple { items } => Ty::Tuple {419            items: items.iter().map(|t| subst(t, dim)).collect(),420        },421        Ty::Array { item, len } => Ty::Array {422            item: Box::new(subst(item, dim)),423            len: *len,424        },425        Ty::Ref {426            mutable,427            lifetime,428            item,429        } => Ty::Ref {430            mutable: *mutable,431            lifetime: lifetime.clone(),432            item: Box::new(subst(item, dim)),433        },434        Ty::Result { item } => Ty::Result {435            item: Box::new(subst(item, dim)),436        },437        Ty::Map { key, value } => Ty::Map {438            key: Box::new(subst(key, dim)),439            value: Box::new(subst(value, dim)),440        },441        Ty::Set { item } => Ty::Set {442            item: Box::new(subst(item, dim)),443        },444        other => other.clone(),445    }446}447448fn undecided(ty: &Ty) -> bool {449    ty.any(&|t| matches!(t, Ty::Hand { name, dim: None } if name == "CellNd"))450}451452fn has_tensor(ty: &Ty) -> bool {453    ty.any(&|t| matches!(t, Ty::Hand { name, .. } if name == "Tensor"))454}455456fn hand_decl(name: &str, dim: Option<u8>) -> Result<String> {457    Ok(match name {458        "Tensor" => "PyTensor",459        "Cell" => "PyCell",460        "CellNd" => match dim {461            Some(2) => "PyCell2d",462            Some(3) => "PyCell3d",463            _ => return Err("a cell of undecided dimension".into()),464        },465        "Cell6d" => "PyCell6d",466        "Color" => "PyColor",467        "Code" => "PyCode",468        "Rng" => "PyRng",469        other => return Err(format!("no hand crossing for {other}")),470    }471    .to_string())472}473474fn hand_into(name: &str) -> Result<&'static str> {475    Ok(match name {476        "Tensor" => "PyTensor",477        "Cell" => "PyCell",478        "CellNd" => "PyCellNd",479        "Cell6d" => "PyCell6d",480        "Color" => "PyColor",481        "Code" => "PyCode",482        "Rng" => "PyRng",483        other => return Err(format!("no hand crossing for {other}")),484    })485}486487fn decl(cx: &Cx, ty: &Ty) -> Result<String> {488    Ok(match ty {489        Ty::Scalar { name } => name.clone(),490        Ty::Str | Ty::String => "String".into(),491        Ty::U128 => "u128".into(),492        Ty::I128 => "i128".into(),493        Ty::Code => "PyCode".into(),494        Ty::Json => "PySerde<serde_json::Value>".into(),495        Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => {496            if is_rgba(item) {497                "PyPixels".into()498            } else {499                format!("Vec<{}>", decl(cx, item)?)500            }501        }502        Ty::Option { item } => format!("Option<{}>", decl(cx, item)?),503        Ty::Tuple { items } => {504            let parts = items.iter().map(|t| decl(cx, t)).collect::<Result<Vec<_>>>()?;505            format!("({})", parts.join(", "))506        }507        Ty::Array { item, len } => {508            if is_rgba(ty) {509                "PyRgba".into()510            } else {511                format!("[{}; {len}]", decl(cx, item)?)512            }513        }514        Ty::Ref {515            mutable: false,516            item,517            ..518        } => match &**item {519            Ty::Class { .. } => return Err("a class by reference inside a container".into()),520            other => decl(cx, other)?,521        },522        Ty::Ref { mutable: true, .. } => {523            return Err("a mutable borrow inside a container".into())524        }525        Ty::Map { key, value } => format!(526            "std::collections::HashMap<{}, {}>",527            decl(cx, key)?,528            decl(cx, value)?529        ),530        Ty::Hand { name, dim } => hand_decl(name, *dim)?,531        Ty::Plain { path } => {532            if cx.const_generic(path) {533                return Err(format!("{path} is const generic inside a container"));534            }535            format!("PySerde<mrlyrs::{path}>")536        }537        Ty::Enum { path } => format!("PySerde<mrlyrs::{path}>"),538        Ty::Class { path, .. } => cx.wrapper(path),539        Ty::Unit | Ty::Result { .. } | Ty::Opaque { .. } | Ty::Unknown { .. } => {540            return Err(format!("{ty:?} cannot cross as a parameter"))541        }542    })543}544545fn own(ty: &Ty, var: &str) -> Option<String> {546    match ty {547        Ty::Code548        | Ty::Json549        | Ty::Hand { .. }550        | Ty::Plain { .. }551        | Ty::Enum { .. }552        | Ty::Class { .. } => Some(format!("{var}.0")),553        Ty::Vec { item } | Ty::Slice { item, .. } => {554            if is_rgba(item) {555                Some(format!("{var}.0"))556            } else {557                own(item, "x")558                    .map(|e| format!("{var}.into_iter().map(|x| {e}).collect::<Vec<_>>()"))559            }560        }561        Ty::Set { item } => Some(match own(item, "x") {562            Some(e) => format!("{var}.into_iter().map(|x| {e}).collect()"),563            None => format!("{var}.into_iter().collect()"),564        }),565        Ty::Option { item } => own(item, "x").map(|e| format!("{var}.map(|x| {e})")),566        Ty::Tuple { items } => {567            let mut any = false;568            let mut parts = Vec::new();569            for (i, t) in items.iter().enumerate() {570                let slot = format!("t.{i}");571                match own(t, &slot) {572                    Some(e) => {573                        any = true;574                        parts.push(e);575                    }576                    None => parts.push(slot),577                }578            }579            any.then(|| format!("{{ let t = {var}; ({}) }}", parts.join(", ")))580        }581        Ty::Array { item, .. } => {582            if is_rgba(ty) {583                Some(format!("{var}.0"))584            } else {585                own(item, "x").map(|e| format!("{var}.map(|x| {e})"))586            }587        }588        Ty::Map { value, .. } => Some(match own(value, "v") {589            Some(e) => format!("{var}.into_iter().map(|(k, v)| (k, {e})).collect()"),590            None => format!("{var}.into_iter().collect()"),591        }),592        Ty::Ref { item, .. } => own(item, var),593        _ => None,594    }595}596597fn needs_view(ty: &Ty) -> bool {598    match ty {599        Ty::Str | Ty::Slice { .. } | Ty::Ref { .. } => true,600        Ty::Tuple { items } => items.iter().any(needs_view),601        _ => false,602    }603}604605fn view(cx: &Cx, ty: &Ty, x: &str) -> Result<String> {606    Ok(match ty {607        Ty::Str => format!("{x}.as_str()"),608        Ty::Slice { item, .. } => {609            if needs_view(item) {610                return Err("a borrow two levels deep".into());611            }612            format!("{x}.as_slice()")613        }614        Ty::Ref { item, .. } => {615            if needs_view(item) {616                return Err("a borrow two levels deep".into());617            }618            x.to_string()619        }620        Ty::Tuple { items } => {621            let mut parts = Vec::new();622            for (i, t) in items.iter().enumerate() {623                let slot = format!("{x}.{i}");624                parts.push(if needs_view(t) {625                    view(cx, t, &slot)?626                } else if cx.is_copy(t) {627                    slot628                } else {629                    format!("{slot}.clone()")630                });631            }632            format!("({})", parts.join(", "))633        }634        _ => format!("{x}.clone()"),635    })636}637638#[derive(Default)]639struct Plan {640    decl: String,641    lets: Vec<String>,642    pass: String,643    post: Vec<String>,644    optional: bool,645}646647fn value(cx: &Cx, var: &str, ty: &Ty, p: &mut Plan) -> Result<()> {648    if let Ty::Plain { path } = ty {649        if cx.const_generic(path) {650            p.decl = "&Bound<'_, PyAny>".into();651            p.lets652                .push(format!("let {var} = crate::hand::serde_from_py({var})?;"));653            return Ok(());654        }655    }656    p.decl = decl(cx, ty)?;657    if let Some(e) = own(ty, var) {658        p.lets.push(format!("let {var} = {e};"));659    }660    if let Ty::Slice { item, .. } | Ty::Vec { item } = ty {661        if needs_view(item) {662            p.lets.push(format!(663                "let {var} = {var}.iter().map(|y| {}).collect::<Vec<_>>();",664                view(cx, item, "y")?665            ));666        }667    }668    Ok(())669}670671fn plan(cx: &Cx, var: &str, ty: &Ty) -> Result<Plan> {672    let mut p = Plan {673        optional: matches!(ty, Ty::Option { .. }),674        ..Plan::default()675    };676    match ty {677        Ty::Str => {678            p.decl = "&str".into();679            p.pass = var.into();680        }681        Ty::Ref {682            mutable: true,683            item,684            ..685        } => match &**item {686            Ty::Hand { name, .. } if name == "Rng" => {687                p.decl = "&mut PyRng".into();688                p.pass = format!("&mut {var}.0");689            }690            Ty::Hand { name, .. } if name == "Tensor" || name == "Cell" => {691                let kind = name.to_lowercase();692                p.decl = "&Bound<'_, PyAny>".into();693                p.lets.push(format!(694                    "let mut {var}_owned = crate::hand::{kind}_from_py({var})?;"695                ));696                p.pass = format!("&mut {var}_owned");697                let carried = if name == "Cell" {698                    format!("{var}_owned")699                } else {700                    format!("&{var}_owned")701                };702                p.post.push(format!(703                    "crate::hand::{kind}_write_back({var}, {carried})?;"704                ));705            }706            Ty::Class { path, .. } => {707                p.decl = format!("PyRefMut<'_, {}>", cx.wrapper(path));708                p.lets.push(format!("let mut {var} = {var};"));709                p.pass = format!("&mut {var}.0");710            }711            other => return Err(format!("{other:?} cannot cross by mutable borrow")),712        },713        Ty::Ref {714            mutable: false,715            item,716            ..717        } => match &**item {718            Ty::Class { path, .. } => {719                p.decl = format!("PyRef<'_, {}>", cx.wrapper(path));720                p.pass = format!("&{var}.0");721            }722            Ty::Str => {723                p.decl = "&str".into();724                p.pass = var.into();725            }726            other => {727                value(cx, var, other, &mut p)?;728                p.pass = format!("&{var}");729            }730        },731        Ty::Slice { .. } => {732            value(cx, var, ty, &mut p)?;733            p.pass = format!("&{var}");734        }735        Ty::Option { item } => match &**item {736            Ty::Ref {737                mutable: true,738                item: inner,739                ..740            } if hand_name(inner) == Some("Rng") => {741                p.decl = "Option<&mut PyRng>".into();742                p.pass = format!("{var}.map(|r| &mut r.0)");743            }744            Ty::Ref {745                mutable: false,746                item: inner,747                ..748            } if !matches!(**inner, Ty::Class { .. }) => {749                value(cx, var, ty, &mut p)?;750                p.pass = format!("{var}.as_ref()");751            }752            Ty::Slice { .. } => {753                value(cx, var, ty, &mut p)?;754                p.pass = format!("{var}.as_deref()");755            }756            _ => {757                value(cx, var, ty, &mut p)?;758                p.pass = var.into();759            }760        },761        _ => {762            value(cx, var, ty, &mut p)?;763            p.pass = var.into();764        }765    }766    Ok(p)767}768769fn needs_into(ty: &Ty) -> bool {770    ty.any(&|t| {771        is_rgba(t)772            || matches!(773                t,774                Ty::Result { .. }775                    | Ty::Ref { .. }776                    | Ty::Str777                    | Ty::Code778                    | Ty::Json779                    | Ty::Hand { .. }780                    | Ty::Plain { .. }781                    | Ty::Enum { .. }782                    | Ty::Class { .. }783                    | Ty::Set { .. }784            )785    })786}787788fn into(cx: &Cx, ty: &Ty, e: &str) -> Result<String> {789    Ok(match ty {790        Ty::Result { item } => into(cx, item, &format!("ok({e})?"))?,791        Ty::Ref { item, .. } if matches!(**item, Ty::Str) => into(cx, item, e)?,792        Ty::Ref { item, .. } => into(cx, item, &format!("({e}).clone()"))?,793        Ty::Str => format!("({e}).to_string()"),794        Ty::Code => format!("PyCode({e})"),795        Ty::Json => format!("PySerde({e})"),796        Ty::Hand { name, .. } => format!("{}({e})", hand_into(name)?),797        Ty::Plain { .. } | Ty::Enum { .. } => format!("PySerde({e})"),798        Ty::Class { path, .. } => format!("{}({e})", cx.wrapper(path)),799        Ty::Array { .. } if is_rgba(ty) => format!("PyRgba({e})"),800        Ty::Vec { item } if is_rgba(item) => format!("PyPixels({e})"),801        Ty::Slice { item, .. } if is_rgba(item) => format!("PyPixels(({e}).to_vec())"),802        Ty::Vec { item } | Ty::Array { item, .. } => {803            if needs_into(item) {804                format!(805                    "({e}).into_iter().map({}).collect::<Vec<_>>()",806                    mapper(into(cx, item, "x")?)807                )808            } else {809                e.into()810            }811        }812        Ty::Slice { item, .. } => into(813            cx,814            &Ty::Vec { item: item.clone() },815            &format!("({e}).to_vec()"),816        )?,817        Ty::Set { item } => format!(818            "({e}).into_iter().map({}).collect::<Vec<_>>()",819            mapper(into(cx, item, "x")?)820        ),821        Ty::Option { item } => {822            if needs_into(item) {823                format!("({e}).map({})", mapper(into(cx, item, "x")?))824            } else {825                e.into()826            }827        }828        Ty::Tuple { items } => {829            if items.iter().any(needs_into) {830                let parts = items831                    .iter()832                    .enumerate()833                    .map(|(i, t)| into(cx, t, &format!("t.{i}")))834                    .collect::<Result<Vec<_>>>()?;835                format!("{{ let t = {e}; ({}) }}", parts.join(", "))836            } else {837                e.into()838            }839        }840        Ty::Map { value, .. } => {841            if needs_into(value) {842                format!(843                    "({e}).into_iter().map(|(k, v)| (k, {})).collect::<std::collections::HashMap<_, _>>()",844                    into(cx, value, "v")?845                )846            } else {847                e.into()848            }849        }850        Ty::Unit | Ty::Scalar { .. } | Ty::String | Ty::U128 | Ty::I128 => e.into(),851        Ty::Opaque { .. } | Ty::Unknown { .. } => {852            return Err(format!("{ty:?} cannot cross as a return"))853        }854    })855}856857fn mapper(body: String) -> String {858    match body.strip_suffix("(x)") {859        Some(ctor) if !ctor.is_empty() && ctor.chars().all(|c| c.is_alphanumeric() || c == '_' || c == ':') => {860            ctor.to_string()861        }862        _ => format!("|x| {body}"),863    }864}865866// CALLS867868#[derive(Clone, Copy, PartialEq)]869enum Place<'a> {870    Free,871    Method(&'a Type),872    Static(&'a Type),873}874875struct Arg {876    py: String,877    rust: String,878    plan: Plan,879    ty: Ty,880}881882fn self_ty(kind: SelfKind, inner: Ty) -> Ty {883    match kind {884        SelfKind::Value => inner,885        SelfKind::Ref => Ty::Ref {886            mutable: false,887            lifetime: None,888            item: Box::new(inner),889        },890        SelfKind::Mut => Ty::Ref {891            mutable: true,892            lifetime: None,893            item: Box::new(inner),894        },895    }896}897898fn owner_ty(cx: &Cx, owner: &str, dim: Option<u8>) -> Result<Ty> {899    let ty = cx.ty(owner)?;900    Ok(match &ty.cross {901        TypeCross::Hand { name } => Ty::Hand {902            name: name.clone(),903            dim,904        },905        TypeCross::Plain => Ty::Plain {906            path: owner.to_string(),907        },908        TypeCross::Enum { .. } => Ty::Enum {909            path: owner.to_string(),910        },911        TypeCross::Class => Ty::Class {912            path: owner.to_string(),913            dim: None,914        },915        TypeCross::Uncrossable { .. } => return Err(format!("{owner} is uncrossable")),916    })917}918919fn self_name(cx: &Cx, f: &Function) -> Result<String> {920    let owner = f.owner.as_deref().ok_or("no owner")?;921    let ty = cx.ty(owner)?;922    let base = match &ty.cross {923        TypeCross::Hand { name } => HAND_SELF924            .iter()925            .find(|(hand, _)| hand == name)926            .map(|(_, word)| word.to_string())927            .unwrap_or_else(|| snake(name)),928        _ => snake(&ty.name),929    };930    let base = py_name(&base);931    if f.params.iter().any(|p| py_name(&p.name) == base) {932        Ok(format!("self_{base}"))933    } else {934        Ok(base)935    }936}937938fn raw_types(cx: &Cx, f: &Function, place: Place) -> Result<Vec<Ty>> {939    let mut out = Vec::new();940    if let (Some(kind), Place::Free | Place::Static(_)) = (f.self_kind, place) {941        let owner = f.owner.as_deref().ok_or("no owner")?;942        out.push(self_ty(kind, owner_ty(cx, owner, None)?));943    }944    out.extend(f.params.iter().map(|p| p.ty.clone()));945    Ok(out)946}947948fn args(cx: &Cx, f: &Function, place: Place, dim: Option<u8>) -> Result<Vec<Arg>> {949    let mut out = Vec::new();950    if let (Some(kind), Place::Free | Place::Static(_)) = (f.self_kind, place) {951        let owner = f.owner.as_deref().ok_or("no owner")?;952        let ty = self_ty(kind, owner_ty(cx, owner, dim)?);953        let name = self_name(cx, f)?;954        let rust = rust_ident(&name);955        out.push(Arg {956            plan: plan(cx, &rust, &ty)?,957            py: name,958            rust,959            ty,960        });961    }962    for p in &f.params {963        let ty = match dim {964            Some(d) => subst(&p.ty, d),965            None => p.ty.clone(),966        };967        let name = py_name(&p.name);968        let rust = rust_ident(&name);969        out.push(Arg {970            plan: plan(cx, &rust, &ty)?,971            py: name,972            rust,973            ty,974        });975    }976    Ok(out)977}978979fn callee(f: &Function, dim: Option<u8>) -> String {980    let turbo = match dim {981        Some(d) if !f.dims.is_empty() => format!("::<{d}>"),982        _ => String::new(),983    };984    match (&f.owner, &f.via) {985        (Some(owner), Some(via)) => {986            format!("<mrlyrs::{owner} as mrlyrs::{via}>::{}", f.name)987        }988        (Some(owner), None) => format!("mrlyrs::{owner}{turbo}::{}", f.name),989        (None, _) => format!("mrlyrs::{}{turbo}", f.path),990    }991}992993fn signature(args: &[Arg]) -> String {994    let optional_from = args995        .iter()996        .rposition(|a| !a.plan.optional)997        .map_or(0, |i| i + 1);998    let parts: Vec<String> = args999        .iter()1000        .enumerate()1001        .map(|(i, a)| {1002            if i >= optional_from {1003                format!("{}=None", a.rust)1004            } else {1005                a.rust.clone()1006            }1007        })1008        .collect();1009    format!("({})", parts.join(", "))1010}10111012fn summary(docs: &[String]) -> Vec<String> {1013    docs.iter()1014        .take_while(|line| !line.trim().is_empty())1015        .cloned()1016        .collect()1017}10181019fn doc_lines(out: &mut String, depth: usize, docs: &[String]) {1020    for line in docs {1021        if line.is_empty() {1022            out.push_str(&format!("{}///\n", indent(depth)));1023        } else {1024            out.push_str(&format!("{}/// {line}\n", indent(depth)));1025        }1026    }1027}10281029fn merged_docs(variants: &[&Function]) -> Vec<String> {1030    let first = summary(&variants[0].docs);1031    let mut docs = first.clone();1032    for v in &variants[1..] {1033        let more = summary(&v.docs);1034        if more != first {1035            docs.push(String::new());1036            docs.extend(more);1037        }1038    }1039    docs1040}10411042fn body(cx: &Cx, out: &mut String, depth: usize, f: &Function, place: Place, args: &[Arg], dim: Option<u8>) -> Result<()> {1043    let pad = indent(depth);1044    for a in args {1045        for l in &a.plan.lets {1046            out.push_str(&format!("{pad}{l}\n"));1047        }1048    }1049    let mut passes: Vec<String> = Vec::new();1050    if let (Place::Method(owner), Some(kind)) = (place, f.self_kind) {1051        passes.push(1052            match kind {1053                SelfKind::Value if cx.derives(&owner.path, "Copy") => "self.0",1054                SelfKind::Value => "self.0.clone()",1055                SelfKind::Ref => "&self.0",1056                SelfKind::Mut => "&mut self.0",1057            }1058            .to_string(),1059        );1060    }1061    passes.extend(args.iter().map(|a| a.plan.pass.clone()));1062    let ret = match dim {1063        Some(d) => subst(&f.ret, d),1064        None => f.ret.clone(),1065    };1066    let call = format!("{}({})", callee(f, dim), passes.join(", "));1067    if ret == Ty::Unit {1068        out.push_str(&format!("{pad}{call};\n"));1069    } else {1070        out.push_str(&format!("{pad}let out = {call};\n"));1071    }1072    for a in args {1073        for l in &a.plan.post {1074            out.push_str(&format!("{pad}{l}\n"));1075        }1076    }1077    let done = if ret == Ty::Unit { "()".to_string() } else { format!("({})", into(cx, &ret, "out")?) };1078    out.push_str(&format!("{pad}{done}.into_bound_py_any(py)\n"));1079    Ok(())1080}10811082fn receiver(place: Place, f: &Function) -> Option<&'static str> {1083    match (place, f.self_kind) {1084        (Place::Method(_), Some(SelfKind::Mut)) => Some("&mut self"),1085        (Place::Method(_), Some(_)) => Some("&self"),1086        _ => None,1087    }1088}10891090fn emit_simple(cx: &Cx, out: &mut String, depth: usize, f: &Function, place: Place, name: &str, dim: Option<u8>) -> Result<()> {1091    let pad = indent(depth);1092    let args = args(cx, f, place, dim)?;1093    doc_lines(out, depth, &summary(&f.docs));1094    match place {1095        Place::Free => out.push_str(&format!("{pad}#[pyfunction]\n")),1096        Place::Static(_) => out.push_str(&format!("{pad}#[staticmethod]\n")),1097        Place::Method(_) => {}1098    }1099    out.push_str(&format!(1100        "{pad}#[pyo3(name = {}, signature = {})]\n",1101        quote(name),1102        signature(&args)1103    ));1104    let mut params: Vec<String> = receiver(place, f).map(str::to_string).into_iter().collect();1105    params.push("py: Python<'py>".to_string());1106    params.extend(args.iter().map(|a| format!("{}: {}", a.rust, a.plan.decl)));1107    let ident = match place {1108        Place::Method(_) | Place::Static(_) if name == "new" => "new_".to_string(),1109        _ => rust_ident(name),1110    };1111    out.push_str(&format!(1112        "{pad}pub fn {ident}<'py>({}) -> PyResult<Bound<'py, PyAny>> {{\n",1113        params.join(", ")1114    ));1115    body(cx, out, depth + 1, f, place, &args, dim)?;1116    out.push_str(&format!("{pad}}}\n"));1117    Ok(())1118}11191120fn emit_dispatch(cx: &Cx, out: &mut String, depth: usize, pairs: &[(&Function, Option<u8>)], place: Place, name: &str) -> Result<()> {1121    let pad = indent(depth);1122    let variants: Vec<&Function> = pairs.iter().map(|(f, _)| *f).collect();1123    let mut per: Vec<(u8, Vec<Arg>)> = Vec::new();1124    for (v, dim) in pairs {1125        let dim = dim.ok_or("a dispatch variant without dims")?;1126        per.push((dim, args(cx, v, place, Some(dim))?));1127    }1128    let mut union: Vec<(String, String, String, bool, bool)> = Vec::new();1129    for (_, args) in &per {1130        for a in args {1131            if !union.iter().any(|u| u.0 == a.py) {1132                let shared = per.iter().all(|(_, other)| other.iter().any(|o| o.py == a.py));1133                union.push((a.py.clone(), a.rust.clone(), a.plan.decl.clone(), a.plan.optional, shared));1134            }1135        }1136    }1137    let raw = raw_types(cx, variants[0], place)?;1138    let undecided_at: Vec<usize> = raw1139        .iter()1140        .enumerate()1141        .filter(|(_, t)| undecided(t))1142        .map(|(i, _)| i)1143        .collect();1144    let pivot = undecided_at1145        .first()1146        .copied()1147        .or_else(|| raw.iter().position(has_tensor))1148        .ok_or_else(|| format!("{name} has no cell or tensor to dispatch on"))?;1149    let loose: Vec<usize> = if undecided_at.is_empty() {1150        vec![pivot]1151    } else {1152        undecided_at1153    };1154    let pivot_name = per[0].1[pivot].rust.clone();1155    let loose_names: Vec<String> = loose.iter().map(|&i| per[0].1[i].rust.clone()).collect();1156    doc_lines(out, depth, &merged_docs(&variants));1157    match place {1158        Place::Free => out.push_str(&format!("{pad}#[pyfunction]\n")),1159        Place::Static(_) => out.push_str(&format!("{pad}#[staticmethod]\n")),1160        Place::Method(_) => return Err(format!("{name} dispatches inside a class")),1161    }1162    let optional_from = union.iter().rposition(|u| !u.3 && u.4).map_or(0, |i| i + 1);1163    let sig: Vec<String> = union1164        .iter()1165        .enumerate()1166        .map(|(i, u)| if i >= optional_from { format!("{}=None", u.1) } else { u.1.clone() })1167        .collect();1168    out.push_str(&format!(1169        "{pad}#[pyo3(name = {}, signature = ({}))]\n",1170        quote(name),1171        sig.join(", ")1172    ));1173    let params: Vec<String> = union1174        .iter()1175        .map(|u| {1176            if loose_names.contains(&u.1) {1177                format!("{}: &Bound<'_, PyAny>", u.1)1178            } else if u.4 {1179                format!("{}: {}", u.1, u.2)1180            } else {1181                format!("{}: Option<{}>", u.1, u.2)1182            }1183        })1184        .collect();1185    out.push_str(&format!(1186        "{pad}pub fn {}<'py>(py: Python<'py>, {}) -> PyResult<Bound<'py, PyAny>> {{\n",1187        rust_ident(name),1188        params.join(", ")1189    ));1190    let inner = indent(depth + 1);1191    out.push_str(&format!("{inner}let dim = crate::hand::ndim({pivot_name})?;\n"));1192    out.push_str(&format!("{inner}match dim {{\n"));1193    for ((dim, args), v) in per.iter().zip(&variants) {1194        let arm = indent(depth + 2);1195        let deep = indent(depth + 3);1196        out.push_str(&format!("{arm}{dim} => {{\n"));1197        for &i in &loose {1198            let a = &args[i];1199            out.push_str(&format!(1200                "{deep}let {} = {}.extract::<{}>()?;\n",1201                a.rust, a.rust, a.plan.decl1202            ));1203        }1204        for a in args {1205            let shared = union.iter().find(|u| u.0 == a.py).map(|u| u.4).unwrap_or(true);1206            if !shared {1207                out.push_str(&format!(1208                    "{deep}let {} = {}.ok_or_else(|| PyValueError::new_err({}))?;\n",1209                    a.rust,1210                    a.rust,1211                    quote(&format!("a {dim}d {name} wants {}.", a.py))1212                ));1213            }1214        }1215        body(cx, out, depth + 3, v, place, args, Some(*dim))?;1216        out.push_str(&format!("{arm}}}\n"));1217    }1218    out.push_str(&format!(1219        "{}other => Err(PyValueError::new_err(format!({}))),\n",1220        indent(depth + 2),1221        quote(&format!("{name} wants a 2d or 3d argument, got {{other}}d."))1222    ));1223    out.push_str(&format!("{inner}}}\n"));1224    out.push_str(&format!("{pad}}}\n"));1225    Ok(())1226}12271228fn emit_export(cx: &Cx, out: &mut String, depth: usize, export: &Export, place: Place) -> Result<()> {1229    let name = py_name(&export.name);1230    if export.variants.len() == 1 {1231        let (f, dim) = export.variants[0];1232        emit_simple(cx, out, depth, f, place, &name, dim)1233    } else {1234        emit_dispatch(cx, out, depth, &export.variants, place, &name)1235    }1236}12371238fn is_constructor(f: &Function) -> bool {1239    let owner = f.owner.as_deref().unwrap_or("");1240    let returns_self = match &f.ret {1241        Ty::Class { path, .. } => path == owner,1242        Ty::Result { item } => matches!(&**item, Ty::Class { path, .. } if path == owner),1243        _ => false,1244    };1245    f.name == "new" && f.self_kind.is_none() && returns_self1246}12471248fn emit_constructor(cx: &Cx, out: &mut String, depth: usize, f: &Function) -> Result<()> {1249    let pad = indent(depth);1250    let args = args(cx, f, Place::Static(cx.ty(f.owner.as_deref().unwrap())?), None)?;1251    doc_lines(out, depth, &summary(&f.docs));1252    out.push_str(&format!("{pad}#[new]\n"));1253    out.push_str(&format!("{pad}#[pyo3(signature = {})]\n", signature(&args)));1254    let params: Vec<String> = args1255        .iter()1256        .map(|a| format!("{}: {}", a.rust, a.plan.decl))1257        .collect();1258    out.push_str(&format!(1259        "{pad}pub fn __new__({}) -> PyResult<Self> {{\n",1260        params.join(", ")1261    ));1262    let inner = indent(depth + 1);1263    for a in &args {1264        for l in &a.plan.lets {1265            out.push_str(&format!("{inner}{l}\n"));1266        }1267    }1268    let passes: Vec<String> = args.iter().map(|a| a.plan.pass.clone()).collect();1269    out.push_str(&format!(1270        "{inner}let out = {}({});\n",1271        callee(f, None),1272        passes.join(", ")1273    ));1274    for a in &args {1275        for l in &a.plan.post {1276            out.push_str(&format!("{inner}{l}\n"));1277        }1278    }1279    let unwrapped = if matches!(f.ret, Ty::Result { .. }) { "ok(out)?" } else { "out" };1280    out.push_str(&format!("{inner}Ok(Self({unwrapped}))\n"));1281    out.push_str(&format!("{pad}}}\n"));1282    Ok(())1283}12841285fn emit_class(cx: &Cx, out: &mut String, depth: usize, module: &str, class: &Owned) -> Result<()> {1286    let pad = indent(depth);1287    let inner = indent(depth + 1);1288    let ty = class.ty;1289    let clone = cx.derives(&ty.path, "Clone");1290    doc_lines(out, depth, &summary(&ty.docs));1291    out.push_str(&format!(1292        "{pad}#[pyclass(name = {}, module = {}, {})]\n",1293        quote(&ty.name),1294        quote(&format!("mrlypy.{}", dotted(module))),1295        if clone { "from_py_object" } else { "skip_from_py_object" }1296    ));1297    if clone {1298        out.push_str(&format!("{pad}#[derive(Clone)]\n"));1299    }1300    out.push_str(&format!(1301        "{pad}pub struct {}(pub mrlyrs::{});\n\n",1302        ty.name, ty.path1303    ));1304    out.push_str(&format!("{pad}#[pymethods]\n{pad}impl {} {{\n", ty.name));1305    for f in class.fns.iter().filter(|f| is_constructor(f)) {1306        emit_constructor(cx, out, depth + 1, f)?;1307    }1308    for field in ty.fields.iter().filter(|f| f.public && crossable(&f.ty)) {1309        doc_lines(out, depth + 1, &summary(&field.docs));1310        out.push_str(&format!("{inner}#[getter]\n"));1311        out.push_str(&format!(1312            "{inner}#[pyo3(name = {})]\n",1313            quote(&py_name(&field.name))1314        ));1315        out.push_str(&format!(1316            "{inner}pub fn {}<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {{\n",1317            rust_ident(&py_name(&field.name))1318        ));1319        let taken = if matches!(field.ty, Ty::Ref { .. }) || cx.is_copy(&field.ty) { "" } else { ".clone()" };1320        out.push_str(&format!(1321            "{}let value = self.0.{}{taken};\n",1322            indent(depth + 2),1323            field.name1324        ));1325        out.push_str(&format!(1326            "{}({}).into_bound_py_any(py)\n",1327            indent(depth + 2),1328            into(cx, &field.ty, "value")?1329        ));1330        out.push_str(&format!("{inner}}}\n"));1331        if field.ty.settable() {1332            emit_setter(cx, out, depth + 1, &field.name, &field.ty)?;1333        }1334    }1335    for f in &class.fns {1336        let place = if f.self_kind.is_some() {1337            if f.self_kind == Some(SelfKind::Value) && !clone {1338                return Err(format!("{} takes self by value without Clone", f.path));1339            }1340            Place::Method(ty)1341        } else {1342            Place::Static(ty)1343        };1344        emit_simple(cx, out, depth + 1, f, place, &py_name(&f.name), f.dims.first().copied())?;1345    }1346    if cx.derives(&ty.path, "Deserialize") {1347        out.push_str(&format!("{inner}/// Reads plain data into the class.\n"));1348        out.push_str(&format!("{inner}#[staticmethod]\n"));1349        out.push_str(&format!(1350            "{inner}pub fn from_dict(data: &Bound<'_, PyAny>) -> PyResult<Self> {{\n{}Ok(Self(crate::hand::serde_from_py(data)?))\n{inner}}}\n",1351            indent(depth + 2)1352        ));1353    }1354    if cx.derives(&ty.path, "Serialize") {1355        out.push_str(&format!("{inner}/// Returns the value as plain data.\n"));1356        out.push_str(&format!(1357            "{inner}pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {{\n{}crate::hand::serde_into_py(py, &self.0)\n{inner}}}\n",1358            indent(depth + 2)1359        ));1360    }1361    out.push_str(&format!("{pad}}}\n\n"));1362    Ok(())1363}13641365fn emit_setter(cx: &Cx, out: &mut String, depth: usize, field: &str, ty: &Ty) -> Result<()> {1366    let pad = indent(depth);1367    let deep = indent(depth + 1);1368    let plan = plan(cx, "value", ty)?;1369    out.push_str(&format!("{pad}#[setter]\n"));1370    out.push_str(&format!("{pad}#[pyo3(name = {})]\n", quote(&py_name(field))));1371    out.push_str(&format!(1372        "{pad}pub fn set_{field}(&mut self, value: {}) -> PyResult<()> {{\n",1373        plan.decl1374    ));1375    for line in &plan.lets {1376        out.push_str(&format!("{deep}{line}\n"));1377    }1378    out.push_str(&format!("{deep}self.0.{field} = {};\n", plan.pass));1379    out.push_str(&format!("{deep}Ok(())\n{pad}}}\n"));1380    Ok(())1381}13821383fn emit_holder(cx: &Cx, out: &mut String, depth: usize, module: &str, holder: &Owned) -> Result<()> {1384    let pad = indent(depth);1385    let ty = holder.ty;1386    doc_lines(out, depth, &summary(&ty.docs));1387    out.push_str(&format!(1388        "{pad}#[pyclass(name = {}, module = {}, skip_from_py_object)]\n",1389        quote(&ty.name),1390        quote(&format!("mrlypy.{}", dotted(module)))1391    ));1392    out.push_str(&format!("{pad}pub struct {};\n\n", ty.name));1393    out.push_str(&format!("{pad}#[pymethods]\n{pad}impl {} {{\n", ty.name));1394    for f in &holder.fns {1395        emit_simple(cx, out, depth + 1, f, Place::Static(ty), &py_name(&f.name), f.dims.first().copied())?;1396    }1397    out.push_str(&format!("{pad}}}\n\n"));1398    Ok(())1399}14001401fn free_words(text: &str) -> BTreeSet<&str> {1402    let mut words = BTreeSet::new();1403    for line in text.lines().filter(|l| !l.trim_start().starts_with("///")) {1404        let mut start = None;1405        let mut quoted = false;1406        let mut escaped = false;1407        for (i, c) in line.char_indices().chain([(line.len(), ' ')]) {1408            if quoted {1409                quoted = escaped || c != '"';1410                escaped = !escaped && c == '\\';1411                continue;1412            }1413            if c.is_alphanumeric() || c == '_' {1414                start.get_or_insert(i);1415                continue;1416            }1417            if let Some(s) = start.take() {1418                if !line[..s].ends_with("::") && !line[..s].ends_with('.') {1419                    words.insert(&line[s..i]);1420                }1421            }1422            quoted = c == '"';1423        }1424    }1425    words1426}14271428fn uses(body: &str) -> Vec<String> {1429    let words = free_words(body);1430    let mut lines = Vec::new();1431    let hand: Vec<&str> = HAND_NAMES.iter().copied().filter(|n| words.contains(n)).collect();1432    if !hand.is_empty() {1433        lines.push(format!("use crate::hand::{{{}}};", hand.join(", ")));1434    }1435    if words.contains("PyValueError") {1436        lines.push("use pyo3::exceptions::PyValueError;".to_string());1437    }1438    lines.push("use pyo3::prelude::*;".to_string());1439    lines.push("use pyo3::types::PyDict;".to_string());1440    if body.contains(".into_bound_py_any(") {1441        lines.push("use pyo3::IntoPyObjectExt;".to_string());1442    }1443    lines1444}14451446fn emit_module(cx: &Cx, out: &mut String, depth: usize, node: &Node) -> Result<()> {1447    let pad = indent(depth);1448    let inner = indent(depth + 1);1449    let mut children = String::new();1450    for child in node.children.values() {1451        emit_module(cx, &mut children, depth + 1, child)?;1452    }1453    let mut body = String::new();1454    emit_items(cx, &mut body, depth, node)?;1455    doc_lines(out, depth, &summary(&node.docs));1456    out.push_str(&format!("{pad}pub mod {} {{\n", rust_ident(node.name())));1457    for line in uses(&body) {1458        out.push_str(&format!("{inner}{line}\n"));1459    }1460    out.push('\n');1461    out.push_str(&children);1462    out.push_str(&body);1463    out.push_str(&format!("{pad}}}\n\n"));1464    Ok(())1465}14661467fn emit_items(cx: &Cx, out: &mut String, depth: usize, node: &Node) -> Result<()> {1468    let inner = indent(depth + 1);1469    for class in &node.classes {1470        emit_class(cx, out, depth + 1, &node.path, class)?;1471    }1472    for holder in &node.holders {1473        emit_holder(cx, out, depth + 1, &node.path, holder)?;1474    }1475    for export in &node.exports {1476        emit_export(cx, out, depth + 1, export, Place::Free)?;1477        out.push('\n');1478    }1479    out.push_str(&format!(1480        "{inner}pub fn init(py: Python<'_>, parent: &Bound<'_, PyModule>, sys: &Bound<'_, PyDict>) -> PyResult<()> {{\n"1481    ));1482    let deep = indent(depth + 2);1483    out.push_str(&format!(1484        "{deep}let m = PyModule::new(py, {})?;\n",1485        quote(&format!("mrlypy.{}", dotted(&node.path)))1486    ));1487    if !node.docs.is_empty() {1488        out.push_str(&format!(1489            "{deep}m.setattr(\"__doc__\", {})?;\n",1490            quote(&node.docs.join("\n"))1491        ));1492    }1493    if node.path == "core" {1494        out.push_str(&format!("{deep}m.add_class::<PyRng>()?;\n"));1495    }1496    for class in &node.classes {1497        out.push_str(&format!("{deep}m.add_class::<{}>()?;\n", class.ty.name));1498    }1499    for holder in &node.holders {1500        out.push_str(&format!("{deep}m.add_class::<{}>()?;\n", holder.ty.name));1501    }1502    for export in &node.exports {1503        out.push_str(&format!(1504            "{deep}m.add_function(wrap_pyfunction!({}, &m)?)?;\n",1505            rust_ident(&py_name(&export.name))1506        ));1507    }1508    for c in &node.consts {1509        out.push_str(&format!(1510            "{deep}m.add({}, {})?;\n",1511            quote(&c.name),1512            into(cx, &c.ty, &format!("mrlyrs::{}", c.path))?1513        ));1514    }1515    let names: Vec<String> = node.item_names().iter().map(|n| quote(n)).collect();1516    out.push_str(&format!(1517        "{deep}let names: Vec<&str> = vec![{}];\n{deep}m.add(\"__all__\", names)?;\n",1518        names.join(", ")1519    ));1520    for child in node.children.values() {1521        out.push_str(&format!(1522            "{deep}{}::init(py, &m, sys)?;\n",1523            rust_ident(child.name())1524        ));1525    }1526    out.push_str(&format!("{deep}parent.add({}, &m)?;\n", quote(node.name())));1527    out.push_str(&format!(1528        "{deep}sys.set_item({}, &m)?;\n",1529        quote(&format!("{NATIVE}.{}", dotted(&node.path)))1530    ));1531    out.push_str(&format!("{deep}Ok(())\n{inner}}}\n"));1532    Ok(())1533}15341535fn lib_file() -> String {1536    let module = NATIVE.rsplit_once('.').map_or(NATIVE, |(_, m)| m);1537    format!(1538        "mod gen;\npub mod hand;\n\nuse pyo3::prelude::*;\n\n#[pymodule]\nfn {module}(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {{\n    module.add(\"__version__\", env!(\"CARGO_PKG_VERSION\"))?;\n    gen::init(py, module)\n}}\n"1539    )1540}15411542fn rust_file(cx: &Cx, root: &Node) -> Result<String> {1543    let mut out = String::from(HEADER);1544    out.push_str("\nuse pyo3::prelude::*;\nuse pyo3::types::PyDict;\n\n");1545    for child in root.children.values() {1546        emit_module(cx, &mut out, 0, child)?;1547    }1548    out.push_str("pub fn init(py: Python<'_>, root: &Bound<'_, PyModule>) -> PyResult<()> {\n");1549    out.push_str("    let sys = py.import(\"sys\")?.getattr(\"modules\")?.cast_into::<PyDict>()?;\n");1550    for child in root.children.values() {1551        out.push_str(&format!(1552            "    {}::init(py, root, &sys)?;\n",1553            rust_ident(child.name())1554        ));1555    }1556    out.push_str("    Ok(())\n}\n");1557    Ok(out)1558}15591560// PYTHON15611562fn py_type(cx: &Cx, ty: &Ty, here: &str) -> String {1563    match ty {1564        Ty::Unit => "None".into(),1565        Ty::Scalar { name } => match name.as_str() {1566            "bool" => "bool".into(),1567            "f32" | "f64" => "float".into(),1568            "char" => "str".into(),1569            _ => "int".into(),1570        },1571        Ty::Str | Ty::String => "str".into(),1572        Ty::U128 | Ty::I128 | Ty::Code => "int".into(),1573        Ty::Json => "Any".into(),1574        Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => {1575            if is_u8(item) {1576                "bytes".into()1577            } else if is_rgba(item) {1578                "NDArray[Any]".into()1579            } else {1580                format!("list[{}]", py_type(cx, item, here))1581            }1582        }1583        Ty::Option { item } => format!("{} | None", py_type(cx, item, here)),1584        Ty::Tuple { items } => format!(1585            "tuple[{}]",1586            items1587                .iter()1588                .map(|t| py_type(cx, t, here))1589                .collect::<Vec<_>>()1590                .join(", ")1591        ),1592        Ty::Array { item, .. } => {1593            if is_rgba(ty) {1594                "tuple[int, int, int, int]".into()1595            } else if is_u8(item) {1596                "bytes".into()1597            } else {1598                format!("list[{}]", py_type(cx, item, here))1599            }1600        }1601        Ty::Ref { item, .. } | Ty::Result { item } => py_type(cx, item, here),1602        Ty::Map { key, value } => format!(1603            "dict[{}, {}]",1604            py_type(cx, key, here),1605            py_type(cx, value, here)1606        ),1607        Ty::Hand { name, .. } => match name.as_str() {1608            "Tensor" => "NDArray[Any]".into(),1609            "Color" => "tuple[int, int, int, int]".into(),1610            "Code" => "int".into(),1611            "Rng" => qualified("core", "Rng", here),1612            _ => "dict[str, Any]".into(),1613        },1614        Ty::Plain { path } => match cx.types.get(path.as_str()).map(|t| t.kind) {1615            Some(TypeKind::Enum) => "Any".into(),1616            _ => "dict[str, Any]".into(),1617        },1618        Ty::Enum { path } => {1619            let words = cx.words(path);1620            if words.is_empty() {1621                "str".into()1622            } else {1623                format!(1624                    "Literal[{}]",1625                    words.iter().map(|w| py_str(w)).collect::<Vec<_>>().join(", ")1626                )1627            }1628        }1629        Ty::Class { path, .. } => qualified(parent(path), last(path), here),1630        Ty::Opaque { .. } | Ty::Unknown { .. } => "Any".into(),1631    }1632}16331634fn qualified(module: &str, name: &str, here: &str) -> String {1635    if module == here {1636        name.to_string()1637    } else {1638        format!("mrlypy.{}.{name}", dotted(module))1639    }1640}16411642fn docstring(out: &mut String, depth: usize, docs: &[String]) {1643    if docs.is_empty() {1644        return;1645    }1646    let text = docs1647        .join(&format!("\n{}", indent(depth)))1648        .replace('\\', "\\\\")1649        .replace("\"\"\"", "\\\"\\\"\\\"");1650    out.push_str(&format!("{}\"\"\"{text}\"\"\"\n", indent(depth)));1651}16521653fn stub_params(cx: &Cx, args: &[Arg], here: &str) -> Vec<String> {1654    let optional_from = args1655        .iter()1656        .rposition(|a| !a.plan.optional)1657        .map_or(0, |i| i + 1);1658    args.iter()1659        .enumerate()1660        .map(|(i, a)| {1661            let hint = py_type(cx, &a.ty, here);1662            if i >= optional_from {1663                format!("{}: {hint} = None", a.py)1664            } else {1665                format!("{}: {hint}", a.py)1666            }1667        })1668        .collect()1669}16701671fn stub_fn(cx: &Cx, out: &mut String, depth: usize, export: &Export, place: Place, here: &str) -> Result<()> {1672    let pad = indent(depth);1673    let name = py_name(&export.name);1674    let (f, dim) = export.variants[0];1675    let mut params = stub_params(cx, &args(cx, f, place, dim)?, here);1676    let mut ret = py_type(cx, &subst_opt(&f.ret, dim), here);1677    if export.variants.len() > 1 {1678        let mut seen: Vec<String> = params.iter().map(|p| p.split(':').next().unwrap().to_string()).collect();1679        for (v, d) in &export.variants[1..] {1680            for a in &args(cx, v, place, *d)? {1681                if !seen.contains(&a.py) {1682                    seen.push(a.py.clone());1683                    let hint = py_type(cx, &a.ty, here);1684                    params.push(format!("{}: {hint} | None = None", a.py));1685                }1686            }1687            let other = py_type(cx, &subst_opt(&v.ret, *d), here);1688            if other != ret {1689                ret = format!("{ret} | {other}");1690            }1691        }1692    }1693    let variants: Vec<&Function> = export.variants.iter().map(|(f, _)| *f).collect();1694    let receiver = match place {1695        Place::Method(_) => "self".to_string(),1696        _ => String::new(),1697    };1698    let mut all = Vec::new();1699    if !receiver.is_empty() {1700        all.push(receiver);1701    }1702    all.extend(params);1703    if matches!(place, Place::Static(_)) {1704        out.push_str(&format!("{pad}@staticmethod\n"));1705    }1706    out.push_str(&format!(1707        "{pad}def {name}({}) -> {ret}:\n",1708        all.join(", ")1709    ));1710    let docs = merged_docs(&variants);1711    if docs.is_empty() {1712        out.push_str(&format!("{}...\n", indent(depth + 1)));1713    } else {1714        docstring(out, depth + 1, &docs);1715    }1716    Ok(())1717}17181719fn subst_opt(ty: &Ty, dim: Option<u8>) -> Ty {1720    match dim {1721        Some(d) => subst(ty, d),1722        None => ty.clone(),1723    }1724}17251726fn stub_class(cx: &Cx, out: &mut String, class: &Owned, here: &str) -> Result<()> {1727    let ty = class.ty;1728    out.push_str(&format!("class {}:\n", ty.name));1729    docstring(out, 1, &summary(&ty.docs));1730    let mut wrote = !ty.docs.is_empty();1731    for f in class.fns.iter().filter(|f| is_constructor(f)) {1732        let args = args(cx, f, Place::Static(ty), None)?;1733        let params = stub_params(cx, &args, here);1734        let mut all = vec!["self".to_string()];1735        all.extend(params);1736        out.push_str(&format!("    def __init__({}) -> None: ...\n", all.join(", ")));1737        wrote = true;1738    }1739    for field in ty.fields.iter().filter(|f| f.public && crossable(&f.ty)) {1740        out.push_str("    @property\n");1741        out.push_str(&format!(1742            "    def {}(self) -> {}:\n",1743            py_name(&field.name),1744            py_type(cx, &field.ty, here)1745        ));1746        if field.docs.is_empty() {1747            out.push_str("        ...\n");1748        } else {1749            docstring(out, 2, &summary(&field.docs));1750        }1751        if field.ty.settable() {1752            out.push_str(&format!(1753                "    @{0}.setter\n    def {0}(self, value: {1}) -> None: ...\n",1754                py_name(&field.name),1755                py_type(cx, &field.ty, here)1756            ));1757        }1758        wrote = true;1759    }1760    for f in &class.fns {1761        let place = if f.self_kind.is_some() {1762            Place::Method(ty)1763        } else {1764            Place::Static(ty)1765        };1766        let export = Export {1767            name: f.name.clone(),1768            variants: vec![(f, None)],1769        };1770        stub_fn(cx, out, 1, &export, place, here)?;1771        wrote = true;1772    }1773    if cx.derives(&ty.path, "Deserialize") {1774        out.push_str(&format!(1775            "    @staticmethod\n    def from_dict(data: Any) -> {}:\n        \"\"\"Reads plain data into the class.\"\"\"\n",1776            ty.name1777        ));1778        wrote = true;1779    }1780    if cx.derives(&ty.path, "Serialize") {1781        out.push_str("    def to_dict(self) -> Any:\n        \"\"\"Returns the value as plain data.\"\"\"\n");1782        wrote = true;1783    }1784    if !wrote {1785        out.push_str("    ...\n");1786    }1787    out.push('\n');1788    Ok(())1789}17901791fn stub_holder(cx: &Cx, out: &mut String, holder: &Owned, here: &str) -> Result<()> {1792    let ty = holder.ty;1793    out.push_str(&format!("class {}:\n", ty.name));1794    docstring(out, 1, &summary(&ty.docs));1795    for f in &holder.fns {1796        let export = Export {1797            name: f.name.clone(),1798            variants: vec![(f, None)],1799        };1800        stub_fn(cx, out, 1, &export, Place::Static(ty), here)?;1801    }1802    out.push('\n');1803    Ok(())1804}18051806fn stub_rng(cx: &Cx, out: &mut String, here: &str) -> Result<()> {1807    out.push_str("class Rng:\n");1808    out.push_str("    \"\"\"The seeded random stream, one class, passed wherever Rust takes a mutable stream.\"\"\"\n");1809    let mut fns: Vec<&Function> = cx1810        .manifest1811        .functions1812        .iter()1813        .filter(|f| f.cross == Cross::Ok && f.owner.as_deref() == Some("core::Rng"))1814        .collect();1815    fns.sort_by_key(|f| f.name != "new");1816    for f in &fns {1817        let export = Export {1818            name: f.name.clone(),1819            variants: vec![(f, None)],1820        };1821        if f.name == "new" {1822            let args = args(cx, f, Place::Free, None)?;1823            let mut all = vec!["self".to_string()];1824            all.extend(stub_params(cx, &args, here));1825            out.push_str(&format!("    def __init__({}) -> None:\n", all.join(", ")));1826            docstring(out, 2, &summary(&f.docs));1827        } else if f.self_kind.is_some() {1828            stub_fn(cx, out, 1, &export, Place::Method(cx.ty("core::Rng")?), here)?;1829        }1830    }1831    out.push_str("    def choice(self, seq: Any) -> Any:\n        \"\"\"Draws one item of the sequence, the same draw as Rust's choice.\"\"\"\n");1832    out.push_str("    def shuffle(self, seq: list[Any]) -> None:\n        \"\"\"Shuffles the list in place, the same permutation as Rust's shuffle.\"\"\"\n");1833    out.push('\n');1834    Ok(())1835}18361837fn stub_file(cx: &Cx, node: &Node) -> Result<String> {1838    let here = node.path.as_str();1839    let mut out = String::new();1840    let mut body = String::new();1841    for c in &node.consts {1842        body.push_str(&format!("{}: {}\n", c.name, py_type(cx, &c.ty, here)));1843    }1844    if !node.consts.is_empty() {1845        body.push('\n');1846    }1847    if here == "core" {1848        stub_rng(cx, &mut body, here)?;1849    }1850    for class in &node.classes {1851        stub_class(cx, &mut body, class, here)?;1852    }1853    for holder in &node.holders {1854        stub_holder(cx, &mut body, holder, here)?;1855    }1856    for export in &node.exports {1857        stub_fn(cx, &mut body, 0, export, Place::Free, here)?;1858        body.push('\n');1859    }1860    let mut packages: Vec<String> = Vec::new();1861    for word in body.split(|c: char| !(c.is_alphanumeric() || c == '_' || c == '.')) {1862        if let Some(rest) = word.strip_prefix("mrlypy.") {1863            let module = rest.rsplit_once('.').map(|(m, _)| m).unwrap_or(rest);1864            let pkg = format!("mrlypy.{module}");1865            if !packages.contains(&pkg) {1866                packages.push(pkg);1867            }1868        }1869    }1870    packages.sort();1871    out.push_str("from typing import Any, Literal\n\nfrom numpy.typing import NDArray\n");1872    for pkg in &packages {1873        out.push_str(&format!("import {pkg}\n"));1874    }1875    if !node.children.is_empty() {1876        let children: Vec<&str> = node.children.keys().map(String::as_str).collect();1877        out.push_str(&format!("from . import {}\n", children.join(", ")));1878    }1879    out.push('\n');1880    out.push_str(body.trim_end());1881    out.push('\n');1882    Ok(out)1883}18841885fn init_file(node: &Node) -> String {1886    let mut out = String::new();1887    out.push_str(&format!(1888        "from {NATIVE}.{} import *\n",1889        dotted(&node.path)1890    ));1891    if !node.children.is_empty() {1892        let children: Vec<&str> = node.children.keys().map(String::as_str).collect();1893        out.push_str(&format!("from . import {}\n", children.join(", ")));1894    }1895    let mut names = node.item_names();1896    names.extend(node.children.keys().cloned());1897    let listed: Vec<String> = names.iter().map(|n| py_str(n)).collect();1898    out.push_str(&format!("\n__all__ = [{}]\n", listed.join(", ")));1899    out1900}19011902fn python_files(cx: &Cx, root: &Node, python: &Path) -> Result<()> {1903    let children: Vec<&str> = root.children.keys().map(String::as_str).collect();1904    let listed: Vec<String> = children.iter().map(|n| py_str(n)).collect();1905    let init = format!(1906        "from mrlypy import _mrlypy\nfrom mrlypy import {}\n\n__version__ = _mrlypy.__version__\n__all__ = [{}]\n",1907        children.join(", "),1908        listed.join(", ")1909    );1910    save(&python.join("__init__.py"), &init)?;1911    let stub = format!(1912        "from . import {}\n\n__version__: str\n",1913        children.join(", ")1914    );1915    save(&python.join("__init__.pyi"), &stub)?;1916    let mut nodes = Vec::new();1917    for child in root.children.values() {1918        child.walk(&mut nodes);1919    }1920    for node in nodes {1921        let dir = python.join(node.path.replace("::", "/"));1922        save(&dir.join("__init__.py"), &init_file(node))?;1923        save(&dir.join("__init__.pyi"), &stub_file(cx, node)?)?;1924    }1925    Ok(())1926}19271928// TEST19291930fn test_file(cx: &Cx) -> String {1931    let ok = cx1932        .manifest1933        .functions1934        .iter()1935        .filter(|f| f.cross == Cross::Ok)1936        .count();1937    format!(1938        r#"import importlib1939import json1940import keyword1941import pathlib19421943import mrlypy19441945MANIFEST = pathlib.Path(__file__).resolve().parents[3] / "bridge" / "manifest.json"194619471948def load():1949    return json.loads(MANIFEST.read_text())195019511952def name_of(word):1953    return word + "_" if keyword.iskeyword(word) else word195419551956def locate(fn, kinds):1957    module = importlib.import_module("mrlypy." + fn["module"].replace("::", "."))1958    exported = name_of(fn["path"].rsplit("::", 1)[-1])1959    owner = fn["owner"]1960    if owner and kinds[owner] in ("class", "plain", "enum"):1961        return getattr(getattr(module, owner.rsplit("::", 1)[-1]), exported)1962    return getattr(module, exported)196319641965def test_every_ok_function_is_callable_under_its_rust_doc():1966    manifest = load()1967    kinds = {{t["path"]: t["cross"]["kind"] for t in manifest["types"]}}1968    count = 01969    for fn in manifest["functions"]:1970        if fn["cross"]["status"] != "ok":1971            continue1972        target = locate(fn, kinds)1973        assert callable(target), fn["path"]1974        doc = target.__doc__ or ""1975        if fn["docs"]:1976            if fn["dims"]:1977                assert fn["docs"][0] in doc, fn["path"]1978            else:1979                assert doc.startswith(fn["docs"][0]), fn["path"]1980        count += 11981    assert count == {ok}1982"#1983    )1984}