py.rs

66.4 kB · rust · 2135 lines

1use crate::disk::save;2use crate::model::*;3use std::collections::{BTreeMap, BTreeSet};4use std::path::Path;56const NATIVE: &str = "mrlypy._mrlypy";78const PYTHON_KEYWORDS: &[&str] = &[9    "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue",10    "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",11    "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",12    "with", "yield",13];1415const RUST_KEYWORDS: &[&str] = &[16    "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",17    "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",18    "ref", "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use",19    "where", "while", "yield", "try", "gen", "box", "do", "final", "macro", "override", "priv",20    "typeof", "unsized", "virtual", "abstract", "become", "u8", "u16", "u32", "u64", "u128",21    "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64", "bool", "char", "str",22];2324const HAND_SELF: &[(&str, &str)] = &[25    ("Tensor", "tensor"),26    ("Cell", "cell"),27    ("CellNd", "cell"),28    ("Cell6d", "cell"),29    ("Image", "image"),30    ("Color", "color"),31    ("Code", "code"),32    ("Rng", "rng"),33];3435const HEADER: &str = "#![allow(clippy::too_many_arguments)]\n";3637const HAND_NAMES: &[&str] = &[38    "ok", "PyCell", "PyCell2d", "PyCell3d", "PyCell6d", "PyCellNd", "PyCode", "PyColor", "PyImage",39    "PyPixels", "PyRgba", "PyRng", "PySerde", "PyTensor",40];4142pub fn write(manifest: &Manifest, root: &Path) -> Result<()> {43    let cx = Cx::new(manifest);44    let tree = build(&cx)?;45    let pkg = root.join("pkgs/mrlypy");46    save(&pkg.join("src/lib.rs"), &lib_file())?;47    save(&pkg.join("src/gen.rs"), &rust_file(&cx, &tree)?)?;48    let python = pkg.join("python/mrlypy");49    python_files(&cx, &tree, &python)?;50    save(&python.join("py.typed"), "")?;51    save(&pkg.join("tests/test_manifest.py"), &test_file(&cx))?;52    Ok(())53}5455// NAMES5657fn parent(path: &str) -> &str {58    path.rsplit_once("::").map(|(head, _)| head).unwrap_or("")59}6061fn last(path: &str) -> &str {62    path.rsplit("::").next().unwrap_or(path)63}6465fn dotted(path: &str) -> String {66    path.replace("::", ".")67}6869fn py_name(name: &str) -> String {70    if PYTHON_KEYWORDS.contains(&name) {71        format!("{name}_")72    } else {73        name.to_string()74    }75}7677fn rust_ident(name: &str) -> String {78    if RUST_KEYWORDS.contains(&name) {79        format!("{name}_")80    } else {81        name.to_string()82    }83}8485fn snake(name: &str) -> String {86    let mut out = String::new();87    let chars: Vec<char> = name.chars().collect();88    for (i, c) in chars.iter().enumerate() {89        if c.is_uppercase() && i > 0 && !chars[i - 1].is_uppercase() {90            out.push('_');91        }92        out.push(c.to_ascii_lowercase());93    }94    out95}9697fn quote(text: &str) -> String {98    format!("{text:?}")99}100101fn py_str(text: &str) -> String {102    let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");103    format!("\"{escaped}\"")104}105106fn indent(depth: usize) -> String {107    "    ".repeat(depth)108}109110// CONTEXT111112struct Cx<'a> {113    manifest: &'a Manifest,114    types: BTreeMap<&'a str, &'a Type>,115}116117impl<'a> Cx<'a> {118    fn new(manifest: &'a Manifest) -> Cx<'a> {119        let types = manifest120            .types121            .iter()122            .map(|t| (t.path.as_str(), t))123            .collect();124        Cx { manifest, types }125    }126127    fn ty(&self, path: &str) -> Result<&'a Type> {128        self.types129            .get(path)130            .copied()131            .ok_or_else(|| format!("{path} is not a type in the manifest"))132    }133134    fn wrapper(&self, path: &str) -> String {135        let mut segs: Vec<String> = parent(path)136            .split("::")137            .filter(|s| !s.is_empty())138            .map(rust_ident)139            .collect();140        segs.push(last(path).to_string());141        format!("crate::gen::{}", segs.join("::"))142    }143144    fn const_generic(&self, path: &str) -> bool {145        self.types.get(path).is_some_and(|t| t.const_generic)146    }147148    fn derives(&self, path: &str, derive: &str) -> bool {149        self.types150            .get(path)151            .is_some_and(|t| t.derives.iter().any(|d| d == derive))152    }153154    fn is_copy(&self, ty: &Ty) -> bool {155        match ty {156            Ty::Scalar { .. } | Ty::U128 | Ty::I128 | Ty::Code => true,157            Ty::Hand { name, .. } => name == "Color" || name == "Code",158            Ty::Tuple { items } => items.iter().all(|t| self.is_copy(t)),159            Ty::Array { item, .. } | Ty::Option { item } => self.is_copy(item),160            Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } => {161                self.derives(path, "Copy")162            }163            _ => false,164        }165    }166167    fn words(&self, path: &str) -> Vec<String> {168        let Some(ty) = self.types.get(path) else {169            return Vec::new();170        };171        let rename_all = ty.serde.iter().find_map(|s| {172            s.strip_prefix("rename_all = ")173                .map(|v| v.trim_matches('"').to_string())174        });175        ty.variants176            .iter()177            .map(|v| {178                if let Some(word) = v.serde.iter().find_map(|s| {179                    s.strip_prefix("rename = ")180                        .map(|w| w.trim_matches('"').to_string())181                }) {182                    return word;183                }184                match rename_all.as_deref() {185                    Some("lowercase") => v.name.to_lowercase(),186                    Some("UPPERCASE") => v.name.to_uppercase(),187                    Some("snake_case") => snake(&v.name),188                    Some("kebab-case") => snake(&v.name).replace('_', "-"),189                    _ => v.name.clone(),190                }191            })192            .collect()193    }194}195196// TREE197198#[derive(Default)]199struct Node<'a> {200    path: String,201    docs: Vec<String>,202    children: BTreeMap<String, Node<'a>>,203    exports: Vec<Export<'a>>,204    classes: Vec<Owned<'a>>,205    holders: Vec<Owned<'a>>,206    consts: Vec<&'a Const>,207}208209struct Export<'a> {210    name: String,211    variants: Vec<(&'a Function, Option<u8>)>,212}213214struct Owned<'a> {215    ty: &'a Type,216    fns: Vec<&'a Function>,217}218219impl<'a> Node<'a> {220    fn name(&self) -> &str {221        last(&self.path)222    }223224    fn reach(&mut self, path: &str) -> &mut Node<'a> {225        if path.is_empty() {226            return self;227        }228        let mut node = self;229        let mut sofar = String::new();230        for seg in path.split("::") {231            if !sofar.is_empty() {232                sofar.push_str("::");233            }234            sofar.push_str(seg);235            let full = sofar.clone();236            node = node237                .children238                .entry(seg.to_string())239                .or_insert_with(|| Node {240                    path: full,241                    ..Node::default()242                });243        }244        node245    }246247    fn push_export(&mut self, f: &'a Function) -> Result<()> {248        let name = last(&f.path).to_string();249        let dims: Vec<Option<u8>> = if f.dims.is_empty() {250            vec![None]251        } else {252            f.dims.iter().map(|d| Some(*d)).collect()253        };254        let path = self.path.clone();255        let index = match self.exports.iter().position(|e| e.name == name) {256            Some(index) => index,257            None => {258                self.exports.push(Export {259                    name: name.clone(),260                    variants: Vec::new(),261                });262                self.exports.len() - 1263            }264        };265        let export = &mut self.exports[index];266        for dim in dims {267            let clash = export268                .variants269                .iter()270                .any(|(_, d)| d.is_none() || *d == dim)271                || (dim.is_none() && !export.variants.is_empty());272            if clash {273                return Err(format!("{name} is exported twice in {path}"));274            }275            export.variants.push((f, dim));276        }277        export.variants.sort_by_key(|(_, d)| *d);278        Ok(())279    }280281    fn prune(&mut self) -> bool {282        self.children.retain(|_, child| child.prune());283        !self.children.is_empty()284            || !self.exports.is_empty()285            || !self.classes.is_empty()286            || !self.holders.is_empty()287            || !self.consts.is_empty()288    }289290    fn item_names(&self) -> Vec<String> {291        let mut names: Vec<String> = self.exports.iter().map(|e| py_name(&e.name)).collect();292        names.extend(self.classes.iter().map(|c| c.ty.name.clone()));293        names.extend(self.holders.iter().map(|h| h.ty.name.clone()));294        names.extend(self.consts.iter().map(|c| c.name.clone()));295        if self.path == "core" {296            names.push("Rng".to_string());297        }298        names299    }300301    fn check(&self) -> Result<()> {302        let names = self.item_names();303        for (i, name) in names.iter().enumerate() {304            if names[..i].contains(name) {305                return Err(format!(306                    "{name} is named twice in mrlypy.{}",307                    dotted(&self.path)308                ));309            }310            if self.children.contains_key(name) {311                return Err(format!(312                    "{name} is both an item and a module in mrlypy.{}",313                    dotted(&self.path)314                ));315            }316        }317        for class in &self.classes {318            let mut seen: Vec<String> = Vec::new();319            for field in class320                .ty321                .fields322                .iter()323                .filter(|f| f.public && crossable(&f.ty))324            {325                seen.push(py_name(&field.name));326            }327            for f in &class.fns {328                let name = py_name(&f.name);329                if seen.contains(&name) {330                    return Err(format!(331                        "{} has a field and a method named {name}",332                        class.ty.path333                    ));334                }335                seen.push(name);336            }337            for extra in ["from_dict", "to_dict"] {338                if seen.iter().any(|s| s == extra) {339                    return Err(format!("{} already has {extra}", class.ty.path));340                }341            }342        }343        self.children.values().try_for_each(Node::check)344    }345346    fn walk<'s>(&'s self, out: &mut Vec<&'s Node<'a>>) {347        out.push(self);348        for child in self.children.values() {349            child.walk(out);350        }351    }352}353354fn crossable(ty: &Ty) -> bool {355    !ty.any(&|t| matches!(t, Ty::Opaque { .. } | Ty::Unknown { .. }))356}357358fn build<'a>(cx: &Cx<'a>) -> Result<Node<'a>> {359    let mut root = Node::default();360    for m in &cx.manifest.modules {361        root.reach(&m.path).docs = m.docs.clone();362    }363    let mut owned: BTreeMap<&str, Vec<&'a Function>> = BTreeMap::new();364    for f in cx365        .manifest366        .functions367        .iter()368        .filter(|f| f.cross == Cross::Ok)369    {370        match &f.owner {371            None => root.reach(&f.module).push_export(f)?,372            Some(owner) => match &cx.ty(owner)?.cross {373                TypeCross::Hand { .. } => root.reach(&f.module).push_export(f)?,374                TypeCross::Uncrossable { .. } => {375                    return Err(format!("{} is owned by an uncrossable type", f.path))376                }377                _ => owned.entry(owner.as_str()).or_default().push(f),378            },379        }380    }381    for ty in &cx.manifest.types {382        let fns = owned.remove(ty.path.as_str()).unwrap_or_default();383        match &ty.cross {384            TypeCross::Class => root.reach(parent(&ty.path)).classes.push(Owned { ty, fns }),385            TypeCross::Plain | TypeCross::Enum { .. } if !fns.is_empty() => {386                root.reach(parent(&ty.path)).holders.push(Owned { ty, fns })387            }388            _ => {}389        }390    }391    if let Some((owner, _)) = owned.iter().next() {392        return Err(format!("{owner} owns functions but is no type"));393    }394    for c in cx.manifest.consts.iter().filter(|c| c.cross == Cross::Ok) {395        root.reach(parent(&c.path)).consts.push(c);396    }397    root.prune();398    if !root.exports.is_empty() || !root.classes.is_empty() || !root.consts.is_empty() {399        return Err("the crate root holds items the Python bridge has no module for".into());400    }401    root.check()?;402    Ok(root)403}404405// TYPES406407fn is_u8(ty: &Ty) -> bool {408    matches!(ty, Ty::Scalar { name } if name == "u8")409}410411fn is_rgba(ty: &Ty) -> bool {412    matches!(ty, Ty::Array { item, len: 4 } if is_u8(item))413}414415fn hand_name(ty: &Ty) -> Option<&str> {416    match ty {417        Ty::Hand { name, .. } => Some(name),418        _ => None,419    }420}421422fn subst(ty: &Ty, dim: u8) -> Ty {423    match ty {424        Ty::Hand { name, dim: None } if name == "CellNd" => Ty::Hand {425            name: name.clone(),426            dim: Some(dim),427        },428        Ty::Vec { item } => Ty::Vec {429            item: Box::new(subst(item, dim)),430        },431        Ty::Slice { mutable, item } => Ty::Slice {432            mutable: *mutable,433            item: Box::new(subst(item, dim)),434        },435        Ty::Option { item } => Ty::Option {436            item: Box::new(subst(item, dim)),437        },438        Ty::Tuple { items } => Ty::Tuple {439            items: items.iter().map(|t| subst(t, dim)).collect(),440        },441        Ty::Array { item, len } => Ty::Array {442            item: Box::new(subst(item, dim)),443            len: *len,444        },445        Ty::Ref {446            mutable,447            lifetime,448            item,449        } => Ty::Ref {450            mutable: *mutable,451            lifetime: lifetime.clone(),452            item: Box::new(subst(item, dim)),453        },454        Ty::Result { item } => Ty::Result {455            item: Box::new(subst(item, dim)),456        },457        Ty::Map { key, value } => Ty::Map {458            key: Box::new(subst(key, dim)),459            value: Box::new(subst(value, dim)),460        },461        Ty::Set { item } => Ty::Set {462            item: Box::new(subst(item, dim)),463        },464        other => other.clone(),465    }466}467468fn undecided(ty: &Ty) -> bool {469    ty.any(&|t| matches!(t, Ty::Hand { name, dim: None } if name == "CellNd"))470}471472fn has_tensor(ty: &Ty) -> bool {473    ty.any(&|t| matches!(t, Ty::Hand { name, .. } if name == "Tensor"))474}475476fn hand_decl(name: &str, dim: Option<u8>) -> Result<String> {477    Ok(match name {478        "Tensor" => "PyTensor",479        "Cell" => "PyCell",480        "CellNd" => match dim {481            Some(2) => "PyCell2d",482            Some(3) => "PyCell3d",483            _ => return Err("a cell of undecided dimension".into()),484        },485        "Cell6d" => "PyCell6d",486        "Image" => "PyImage",487        "Color" => "PyColor",488        "Code" => "PyCode",489        "Rng" => "PyRng",490        other => return Err(format!("no hand crossing for {other}")),491    }492    .to_string())493}494495fn hand_into(name: &str) -> Result<&'static str> {496    Ok(match name {497        "Tensor" => "PyTensor",498        "Cell" => "PyCell",499        "CellNd" => "PyCellNd",500        "Cell6d" => "PyCell6d",501        "Image" => "PyImage",502        "Color" => "PyColor",503        "Code" => "PyCode",504        "Rng" => "PyRng",505        other => return Err(format!("no hand crossing for {other}")),506    })507}508509fn decl(cx: &Cx, ty: &Ty) -> Result<String> {510    Ok(match ty {511        Ty::Scalar { name } => name.clone(),512        Ty::Str | Ty::String => "String".into(),513        Ty::U128 => "u128".into(),514        Ty::I128 => "i128".into(),515        Ty::Code => "PyCode".into(),516        Ty::Json => "PySerde<serde_json::Value>".into(),517        Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => {518            if is_rgba(item) {519                "PyPixels".into()520            } else {521                format!("Vec<{}>", decl(cx, item)?)522            }523        }524        Ty::Option { item } => format!("Option<{}>", decl(cx, item)?),525        Ty::Tuple { items } => {526            let parts = items527                .iter()528                .map(|t| decl(cx, t))529                .collect::<Result<Vec<_>>>()?;530            format!("({})", parts.join(", "))531        }532        Ty::Array { item, len } => {533            if is_rgba(ty) {534                "PyRgba".into()535            } else {536                format!("[{}; {len}]", decl(cx, item)?)537            }538        }539        Ty::Ref {540            mutable: false,541            item,542            ..543        } => match &**item {544            Ty::Class { .. } => return Err("a class by reference inside a container".into()),545            other => decl(cx, other)?,546        },547        Ty::Ref { mutable: true, .. } => return Err("a mutable borrow inside a container".into()),548        Ty::Map { key, value } => format!(549            "std::collections::HashMap<{}, {}>",550            decl(cx, key)?,551            decl(cx, value)?552        ),553        Ty::Hand { name, dim } => hand_decl(name, *dim)?,554        Ty::Plain { path } => {555            if cx.const_generic(path) {556                return Err(format!("{path} is const generic inside a container"));557            }558            format!("PySerde<mrlyrs::{path}>")559        }560        Ty::Enum { path } => format!("PySerde<mrlyrs::{path}>"),561        Ty::Class { path, .. } => cx.wrapper(path),562        Ty::Unit | Ty::Result { .. } | Ty::Opaque { .. } | Ty::Unknown { .. } => {563            return Err(format!("{ty:?} cannot cross as a parameter"))564        }565    })566}567568fn own(ty: &Ty, var: &str) -> Option<String> {569    match ty {570        Ty::Code571        | Ty::Json572        | Ty::Hand { .. }573        | Ty::Plain { .. }574        | Ty::Enum { .. }575        | Ty::Class { .. } => Some(format!("{var}.0")),576        Ty::Vec { item } | Ty::Slice { item, .. } => {577            if is_rgba(item) {578                Some(format!("{var}.0"))579            } else {580                own(item, "x")581                    .map(|e| format!("{var}.into_iter().map(|x| {e}).collect::<Vec<_>>()"))582            }583        }584        Ty::Set { item } => Some(match own(item, "x") {585            Some(e) => format!("{var}.into_iter().map(|x| {e}).collect()"),586            None => format!("{var}.into_iter().collect()"),587        }),588        Ty::Option { item } => own(item, "x").map(|e| format!("{var}.map(|x| {e})")),589        Ty::Tuple { items } => {590            let mut any = false;591            let mut parts = Vec::new();592            for (i, t) in items.iter().enumerate() {593                let slot = format!("t.{i}");594                match own(t, &slot) {595                    Some(e) => {596                        any = true;597                        parts.push(e);598                    }599                    None => parts.push(slot),600                }601            }602            any.then(|| format!("{{ let t = {var}; ({}) }}", parts.join(", ")))603        }604        Ty::Array { item, .. } => {605            if is_rgba(ty) {606                Some(format!("{var}.0"))607            } else {608                own(item, "x").map(|e| format!("{var}.map(|x| {e})"))609            }610        }611        Ty::Map { value, .. } => Some(match own(value, "v") {612            Some(e) => format!("{var}.into_iter().map(|(k, v)| (k, {e})).collect()"),613            None => format!("{var}.into_iter().collect()"),614        }),615        Ty::Ref { item, .. } => own(item, var),616        _ => None,617    }618}619620fn needs_view(ty: &Ty) -> bool {621    match ty {622        Ty::Str | Ty::Slice { .. } | Ty::Ref { .. } => true,623        Ty::Tuple { items } => items.iter().any(needs_view),624        _ => false,625    }626}627628fn view(cx: &Cx, ty: &Ty, x: &str) -> Result<String> {629    Ok(match ty {630        Ty::Str => format!("{x}.as_str()"),631        Ty::Slice { item, .. } => {632            if needs_view(item) {633                return Err("a borrow two levels deep".into());634            }635            format!("{x}.as_slice()")636        }637        Ty::Ref { item, .. } => {638            if needs_view(item) {639                return Err("a borrow two levels deep".into());640            }641            x.to_string()642        }643        Ty::Tuple { items } => {644            let mut parts = Vec::new();645            for (i, t) in items.iter().enumerate() {646                let slot = format!("{x}.{i}");647                parts.push(if needs_view(t) {648                    view(cx, t, &slot)?649                } else if cx.is_copy(t) {650                    slot651                } else {652                    format!("{slot}.clone()")653                });654            }655            format!("({})", parts.join(", "))656        }657        _ => format!("{x}.clone()"),658    })659}660661#[derive(Default)]662struct Plan {663    decl: String,664    lets: Vec<String>,665    pass: String,666    post: Vec<String>,667    optional: bool,668}669670fn value(cx: &Cx, var: &str, ty: &Ty, p: &mut Plan) -> Result<()> {671    if let Ty::Plain { path } = ty {672        if cx.const_generic(path) {673            p.decl = "&Bound<'_, PyAny>".into();674            p.lets675                .push(format!("let {var} = crate::hand::serde_from_py({var})?;"));676            return Ok(());677        }678    }679    p.decl = decl(cx, ty)?;680    if let Some(e) = own(ty, var) {681        p.lets.push(format!("let {var} = {e};"));682    }683    if let Ty::Slice { item, .. } | Ty::Vec { item } = ty {684        if needs_view(item) {685            p.lets.push(format!(686                "let {var} = {var}.iter().map(|y| {}).collect::<Vec<_>>();",687                view(cx, item, "y")?688            ));689        }690    }691    Ok(())692}693694fn plan(cx: &Cx, var: &str, ty: &Ty) -> Result<Plan> {695    let mut p = Plan {696        optional: matches!(ty, Ty::Option { .. }),697        ..Plan::default()698    };699    match ty {700        Ty::Str => {701            p.decl = "&str".into();702            p.pass = var.into();703        }704        Ty::Ref {705            mutable: true,706            item,707            ..708        } => match &**item {709            Ty::Hand { name, .. } if name == "Rng" => {710                p.decl = "&mut PyRng".into();711                p.pass = format!("&mut {var}.0");712            }713            Ty::Hand { name, .. } if name == "Tensor" || name == "Cell" => {714                let kind = name.to_lowercase();715                p.decl = "&Bound<'_, PyAny>".into();716                p.lets.push(format!(717                    "let mut {var}_owned = crate::hand::{kind}_from_py({var})?;"718                ));719                p.pass = format!("&mut {var}_owned");720                let carried = if name == "Cell" {721                    format!("{var}_owned")722                } else {723                    format!("&{var}_owned")724                };725                p.post.push(format!(726                    "crate::hand::{kind}_write_back({var}, {carried})?;"727                ));728            }729            Ty::Class { path, .. } => {730                p.decl = format!("PyRefMut<'_, {}>", cx.wrapper(path));731                p.lets.push(format!("let mut {var} = {var};"));732                p.pass = format!("&mut {var}.0");733            }734            other => return Err(format!("{other:?} cannot cross by mutable borrow")),735        },736        Ty::Ref {737            mutable: false,738            item,739            ..740        } => match &**item {741            Ty::Class { path, .. } => {742                p.decl = format!("PyRef<'_, {}>", cx.wrapper(path));743                p.pass = format!("&{var}.0");744            }745            Ty::Str => {746                p.decl = "&str".into();747                p.pass = var.into();748            }749            other => {750                value(cx, var, other, &mut p)?;751                p.pass = format!("&{var}");752            }753        },754        Ty::Slice { .. } => {755            value(cx, var, ty, &mut p)?;756            p.pass = format!("&{var}");757        }758        Ty::Option { item } => match &**item {759            Ty::Ref {760                mutable: true,761                item: inner,762                ..763            } if hand_name(inner) == Some("Rng") => {764                p.decl = "Option<&mut PyRng>".into();765                p.pass = format!("{var}.map(|r| &mut r.0)");766            }767            Ty::Ref {768                mutable: false,769                item: inner,770                ..771            } if !matches!(**inner, Ty::Class { .. }) => {772                value(cx, var, ty, &mut p)?;773                p.pass = format!("{var}.as_ref()");774            }775            Ty::Slice { .. } => {776                value(cx, var, ty, &mut p)?;777                p.pass = format!("{var}.as_deref()");778            }779            _ => {780                value(cx, var, ty, &mut p)?;781                p.pass = var.into();782            }783        },784        _ => {785            value(cx, var, ty, &mut p)?;786            p.pass = var.into();787        }788    }789    Ok(p)790}791792fn needs_into(ty: &Ty) -> bool {793    ty.any(&|t| {794        is_rgba(t)795            || matches!(796                t,797                Ty::Result { .. }798                    | Ty::Ref { .. }799                    | Ty::Str800                    | Ty::Code801                    | Ty::Json802                    | Ty::Hand { .. }803                    | Ty::Plain { .. }804                    | Ty::Enum { .. }805                    | Ty::Class { .. }806                    | Ty::Set { .. }807            )808    })809}810811fn into(cx: &Cx, ty: &Ty, e: &str) -> Result<String> {812    Ok(match ty {813        Ty::Result { item } => into(cx, item, &format!("ok({e})?"))?,814        Ty::Ref { item, .. } if matches!(**item, Ty::Str) => into(cx, item, e)?,815        Ty::Ref { item, .. } => into(cx, item, &format!("({e}).clone()"))?,816        Ty::Str => format!("({e}).to_string()"),817        Ty::Code => format!("PyCode({e})"),818        Ty::Json => format!("PySerde({e})"),819        Ty::Hand { name, .. } => format!("{}({e})", hand_into(name)?),820        Ty::Plain { .. } | Ty::Enum { .. } => format!("PySerde({e})"),821        Ty::Class { path, .. } => format!("{}({e})", cx.wrapper(path)),822        Ty::Array { .. } if is_rgba(ty) => format!("PyRgba({e})"),823        Ty::Vec { item } if is_rgba(item) => format!("PyPixels({e})"),824        Ty::Slice { item, .. } if is_rgba(item) => format!("PyPixels(({e}).to_vec())"),825        Ty::Vec { item } | Ty::Array { item, .. } => {826            if needs_into(item) {827                format!(828                    "({e}).into_iter().map({}).collect::<Vec<_>>()",829                    mapper(into(cx, item, "x")?)830                )831            } else {832                e.into()833            }834        }835        Ty::Slice { item, .. } => into(836            cx,837            &Ty::Vec { item: item.clone() },838            &format!("({e}).to_vec()"),839        )?,840        Ty::Set { item } => format!(841            "({e}).into_iter().map({}).collect::<Vec<_>>()",842            mapper(into(cx, item, "x")?)843        ),844        Ty::Option { item } => {845            if needs_into(item) {846                format!("({e}).map({})", mapper(into(cx, item, "x")?))847            } else {848                e.into()849            }850        }851        Ty::Tuple { items } => {852            if items.iter().any(needs_into) {853                let parts = items854                    .iter()855                    .enumerate()856                    .map(|(i, t)| into(cx, t, &format!("t.{i}")))857                    .collect::<Result<Vec<_>>>()?;858                format!("{{ let t = {e}; ({}) }}", parts.join(", "))859            } else {860                e.into()861            }862        }863        Ty::Map { value, .. } => {864            if needs_into(value) {865                format!(866                    "({e}).into_iter().map(|(k, v)| (k, {})).collect::<std::collections::HashMap<_, _>>()",867                    into(cx, value, "v")?868                )869            } else {870                e.into()871            }872        }873        Ty::Unit | Ty::Scalar { .. } | Ty::String | Ty::U128 | Ty::I128 => e.into(),874        Ty::Opaque { .. } | Ty::Unknown { .. } => {875            return Err(format!("{ty:?} cannot cross as a return"))876        }877    })878}879880fn mapper(body: String) -> String {881    match body.strip_suffix("(x)") {882        Some(ctor)883            if !ctor.is_empty()884                && ctor885                    .chars()886                    .all(|c| c.is_alphanumeric() || c == '_' || c == ':') =>887        {888            ctor.to_string()889        }890        _ => format!("|x| {body}"),891    }892}893894// CALLS895896#[derive(Clone, Copy, PartialEq)]897enum Place<'a> {898    Free,899    Method(&'a Type),900    Static(&'a Type),901}902903struct Arg {904    py: String,905    rust: String,906    plan: Plan,907    ty: Ty,908}909910fn self_ty(kind: SelfKind, inner: Ty) -> Ty {911    match kind {912        SelfKind::Value => inner,913        SelfKind::Ref => Ty::Ref {914            mutable: false,915            lifetime: None,916            item: Box::new(inner),917        },918        SelfKind::Mut => Ty::Ref {919            mutable: true,920            lifetime: None,921            item: Box::new(inner),922        },923    }924}925926fn owner_ty(cx: &Cx, owner: &str, dim: Option<u8>) -> Result<Ty> {927    let ty = cx.ty(owner)?;928    Ok(match &ty.cross {929        TypeCross::Hand { name } => Ty::Hand {930            name: name.clone(),931            dim,932        },933        TypeCross::Plain => Ty::Plain {934            path: owner.to_string(),935        },936        TypeCross::Enum { .. } => Ty::Enum {937            path: owner.to_string(),938        },939        TypeCross::Class => Ty::Class {940            path: owner.to_string(),941            dim: None,942        },943        TypeCross::Uncrossable { .. } => return Err(format!("{owner} is uncrossable")),944    })945}946947fn self_name(cx: &Cx, f: &Function) -> Result<String> {948    let owner = f.owner.as_deref().ok_or("no owner")?;949    let ty = cx.ty(owner)?;950    let base = match &ty.cross {951        TypeCross::Hand { name } => HAND_SELF952            .iter()953            .find(|(hand, _)| hand == name)954            .map(|(_, word)| word.to_string())955            .unwrap_or_else(|| snake(name)),956        _ => snake(&ty.name),957    };958    let base = py_name(&base);959    if f.params.iter().any(|p| py_name(&p.name) == base) {960        Ok(format!("self_{base}"))961    } else {962        Ok(base)963    }964}965966fn raw_types(cx: &Cx, f: &Function, place: Place) -> Result<Vec<Ty>> {967    let mut out = Vec::new();968    if let (Some(kind), Place::Free | Place::Static(_)) = (f.self_kind, place) {969        let owner = f.owner.as_deref().ok_or("no owner")?;970        out.push(self_ty(kind, owner_ty(cx, owner, None)?));971    }972    out.extend(f.params.iter().map(|p| p.ty.clone()));973    Ok(out)974}975976fn args(cx: &Cx, f: &Function, place: Place, dim: Option<u8>) -> Result<Vec<Arg>> {977    let mut out = Vec::new();978    if let (Some(kind), Place::Free | Place::Static(_)) = (f.self_kind, place) {979        let owner = f.owner.as_deref().ok_or("no owner")?;980        let ty = self_ty(kind, owner_ty(cx, owner, dim)?);981        let name = self_name(cx, f)?;982        let rust = rust_ident(&name);983        out.push(Arg {984            plan: plan(cx, &rust, &ty)?,985            py: name,986            rust,987            ty,988        });989    }990    for p in &f.params {991        let ty = match dim {992            Some(d) => subst(&p.ty, d),993            None => p.ty.clone(),994        };995        let name = py_name(&p.name);996        let rust = rust_ident(&name);997        out.push(Arg {998            plan: plan(cx, &rust, &ty)?,999            py: name,1000            rust,1001            ty,1002        });1003    }1004    Ok(out)1005}10061007fn callee(f: &Function, dim: Option<u8>) -> String {1008    let turbo = match dim {1009        Some(d) if !f.dims.is_empty() => format!("::<{d}>"),1010        _ => String::new(),1011    };1012    match (&f.owner, &f.via) {1013        (Some(owner), Some(via)) => {1014            format!("<mrlyrs::{owner} as mrlyrs::{via}>::{}", f.name)1015        }1016        (Some(owner), None) => format!("mrlyrs::{owner}{turbo}::{}", f.name),1017        (None, _) => format!("mrlyrs::{}{turbo}", f.path),1018    }1019}10201021fn signature(args: &[Arg]) -> String {1022    let optional_from = args1023        .iter()1024        .rposition(|a| !a.plan.optional)1025        .map_or(0, |i| i + 1);1026    let parts: Vec<String> = args1027        .iter()1028        .enumerate()1029        .map(|(i, a)| {1030            if i >= optional_from {1031                format!("{}=None", a.rust)1032            } else {1033                a.rust.clone()1034            }1035        })1036        .collect();1037    format!("({})", parts.join(", "))1038}10391040fn summary(docs: &[String]) -> Vec<String> {1041    docs.iter()1042        .take_while(|line| !line.trim().is_empty())1043        .cloned()1044        .collect()1045}10461047fn doc_lines(out: &mut String, depth: usize, docs: &[String]) {1048    for line in docs {1049        if line.is_empty() {1050            out.push_str(&format!("{}///\n", indent(depth)));1051        } else {1052            out.push_str(&format!("{}/// {line}\n", indent(depth)));1053        }1054    }1055}10561057fn merged_docs(variants: &[&Function]) -> Vec<String> {1058    let first = summary(&variants[0].docs);1059    let mut docs = first.clone();1060    for v in &variants[1..] {1061        let more = summary(&v.docs);1062        if more != first {1063            docs.push(String::new());1064            docs.extend(more);1065        }1066    }1067    docs1068}10691070fn body(1071    cx: &Cx,1072    out: &mut String,1073    depth: usize,1074    f: &Function,1075    place: Place,1076    args: &[Arg],1077    dim: Option<u8>,1078) -> Result<()> {1079    let pad = indent(depth);1080    for a in args {1081        for l in &a.plan.lets {1082            out.push_str(&format!("{pad}{l}\n"));1083        }1084    }1085    let mut passes: Vec<String> = Vec::new();1086    if let (Place::Method(owner), Some(kind)) = (place, f.self_kind) {1087        passes.push(1088            match kind {1089                SelfKind::Value if cx.derives(&owner.path, "Copy") => "self.0",1090                SelfKind::Value => "self.0.clone()",1091                SelfKind::Ref => "&self.0",1092                SelfKind::Mut => "&mut self.0",1093            }1094            .to_string(),1095        );1096    }1097    passes.extend(args.iter().map(|a| a.plan.pass.clone()));1098    let ret = match dim {1099        Some(d) => subst(&f.ret, d),1100        None => f.ret.clone(),1101    };1102    let call = format!("{}({})", callee(f, dim), passes.join(", "));1103    if ret == Ty::Unit {1104        out.push_str(&format!("{pad}{call};\n"));1105    } else {1106        out.push_str(&format!("{pad}let out = {call};\n"));1107    }1108    for a in args {1109        for l in &a.plan.post {1110            out.push_str(&format!("{pad}{l}\n"));1111        }1112    }1113    let done = if ret == Ty::Unit {1114        "()".to_string()1115    } else {1116        format!("({})", into(cx, &ret, "out")?)1117    };1118    out.push_str(&format!("{pad}{done}.into_bound_py_any(py)\n"));1119    Ok(())1120}11211122fn receiver(place: Place, f: &Function) -> Option<&'static str> {1123    match (place, f.self_kind) {1124        (Place::Method(_), Some(SelfKind::Mut)) => Some("&mut self"),1125        (Place::Method(_), Some(_)) => Some("&self"),1126        _ => None,1127    }1128}11291130fn emit_simple(1131    cx: &Cx,1132    out: &mut String,1133    depth: usize,1134    f: &Function,1135    place: Place,1136    name: &str,1137    dim: Option<u8>,1138) -> Result<()> {1139    let pad = indent(depth);1140    let args = args(cx, f, place, dim)?;1141    doc_lines(out, depth, &summary(&f.docs));1142    match place {1143        Place::Free => out.push_str(&format!("{pad}#[pyfunction]\n")),1144        Place::Static(_) => out.push_str(&format!("{pad}#[staticmethod]\n")),1145        Place::Method(_) => {}1146    }1147    out.push_str(&format!(1148        "{pad}#[pyo3(name = {}, signature = {})]\n",1149        quote(name),1150        signature(&args)1151    ));1152    let mut params: Vec<String> = receiver(place, f).map(str::to_string).into_iter().collect();1153    params.push("py: Python<'py>".to_string());1154    params.extend(args.iter().map(|a| format!("{}: {}", a.rust, a.plan.decl)));1155    let ident = match place {1156        Place::Method(_) | Place::Static(_) if name == "new" => "new_".to_string(),1157        _ => rust_ident(name),1158    };1159    out.push_str(&format!(1160        "{pad}pub fn {ident}<'py>({}) -> PyResult<Bound<'py, PyAny>> {{\n",1161        params.join(", ")1162    ));1163    body(cx, out, depth + 1, f, place, &args, dim)?;1164    out.push_str(&format!("{pad}}}\n"));1165    Ok(())1166}11671168fn emit_dispatch(1169    cx: &Cx,1170    out: &mut String,1171    depth: usize,1172    pairs: &[(&Function, Option<u8>)],1173    place: Place,1174    name: &str,1175) -> Result<()> {1176    let pad = indent(depth);1177    let variants: Vec<&Function> = pairs.iter().map(|(f, _)| *f).collect();1178    let mut per: Vec<(u8, Vec<Arg>)> = Vec::new();1179    for (v, dim) in pairs {1180        let dim = dim.ok_or("a dispatch variant without dims")?;1181        per.push((dim, args(cx, v, place, Some(dim))?));1182    }1183    let mut union: Vec<(String, String, String, bool, bool)> = Vec::new();1184    for (_, args) in &per {1185        for a in args {1186            if !union.iter().any(|u| u.0 == a.py) {1187                let shared = per1188                    .iter()1189                    .all(|(_, other)| other.iter().any(|o| o.py == a.py));1190                union.push((1191                    a.py.clone(),1192                    a.rust.clone(),1193                    a.plan.decl.clone(),1194                    a.plan.optional,1195                    shared,1196                ));1197            }1198        }1199    }1200    let raw = raw_types(cx, variants[0], place)?;1201    let undecided_at: Vec<usize> = raw1202        .iter()1203        .enumerate()1204        .filter(|(_, t)| undecided(t))1205        .map(|(i, _)| i)1206        .collect();1207    let pivot = undecided_at1208        .first()1209        .copied()1210        .or_else(|| raw.iter().position(has_tensor))1211        .ok_or_else(|| format!("{name} has no cell or tensor to dispatch on"))?;1212    let loose: Vec<usize> = if undecided_at.is_empty() {1213        vec![pivot]1214    } else {1215        undecided_at1216    };1217    let pivot_name = per[0].1[pivot].rust.clone();1218    let loose_names: Vec<String> = loose.iter().map(|&i| per[0].1[i].rust.clone()).collect();1219    doc_lines(out, depth, &merged_docs(&variants));1220    match place {1221        Place::Free => out.push_str(&format!("{pad}#[pyfunction]\n")),1222        Place::Static(_) => out.push_str(&format!("{pad}#[staticmethod]\n")),1223        Place::Method(_) => return Err(format!("{name} dispatches inside a class")),1224    }1225    let optional_from = union.iter().rposition(|u| !u.3 && u.4).map_or(0, |i| i + 1);1226    let sig: Vec<String> = union1227        .iter()1228        .enumerate()1229        .map(|(i, u)| {1230            if i >= optional_from {1231                format!("{}=None", u.1)1232            } else {1233                u.1.clone()1234            }1235        })1236        .collect();1237    out.push_str(&format!(1238        "{pad}#[pyo3(name = {}, signature = ({}))]\n",1239        quote(name),1240        sig.join(", ")1241    ));1242    let params: Vec<String> = union1243        .iter()1244        .map(|u| {1245            if loose_names.contains(&u.1) {1246                format!("{}: &Bound<'_, PyAny>", u.1)1247            } else if u.4 {1248                format!("{}: {}", u.1, u.2)1249            } else {1250                format!("{}: Option<{}>", u.1, u.2)1251            }1252        })1253        .collect();1254    out.push_str(&format!(1255        "{pad}pub fn {}<'py>(py: Python<'py>, {}) -> PyResult<Bound<'py, PyAny>> {{\n",1256        rust_ident(name),1257        params.join(", ")1258    ));1259    let inner = indent(depth + 1);1260    out.push_str(&format!(1261        "{inner}let dim = crate::hand::ndim({pivot_name})?;\n"1262    ));1263    out.push_str(&format!("{inner}match dim {{\n"));1264    for ((dim, args), v) in per.iter().zip(&variants) {1265        let arm = indent(depth + 2);1266        let deep = indent(depth + 3);1267        out.push_str(&format!("{arm}{dim} => {{\n"));1268        for &i in &loose {1269            let a = &args[i];1270            out.push_str(&format!(1271                "{deep}let {} = {}.extract::<{}>()?;\n",1272                a.rust, a.rust, a.plan.decl1273            ));1274        }1275        for a in args {1276            let shared = union1277                .iter()1278                .find(|u| u.0 == a.py)1279                .map(|u| u.4)1280                .unwrap_or(true);1281            if !shared {1282                out.push_str(&format!(1283                    "{deep}let {} = {}.ok_or_else(|| PyValueError::new_err({}))?;\n",1284                    a.rust,1285                    a.rust,1286                    quote(&format!("a {dim}d {name} wants {}.", a.py))1287                ));1288            }1289        }1290        body(cx, out, depth + 3, v, place, args, Some(*dim))?;1291        out.push_str(&format!("{arm}}}\n"));1292    }1293    out.push_str(&format!(1294        "{}other => Err(PyValueError::new_err(format!({}))),\n",1295        indent(depth + 2),1296        quote(&format!(1297            "{name} wants a 2d or 3d argument, got {{other}}d."1298        ))1299    ));1300    out.push_str(&format!("{inner}}}\n"));1301    out.push_str(&format!("{pad}}}\n"));1302    Ok(())1303}13041305fn emit_export(1306    cx: &Cx,1307    out: &mut String,1308    depth: usize,1309    export: &Export,1310    place: Place,1311) -> Result<()> {1312    let name = py_name(&export.name);1313    if export.variants.len() == 1 {1314        let (f, dim) = export.variants[0];1315        emit_simple(cx, out, depth, f, place, &name, dim)1316    } else {1317        emit_dispatch(cx, out, depth, &export.variants, place, &name)1318    }1319}13201321fn is_constructor(f: &Function) -> bool {1322    let owner = f.owner.as_deref().unwrap_or("");1323    let returns_self = match &f.ret {1324        Ty::Class { path, .. } => path == owner,1325        Ty::Result { item } => matches!(&**item, Ty::Class { path, .. } if path == owner),1326        _ => false,1327    };1328    f.name == "new" && f.self_kind.is_none() && returns_self1329}13301331fn emit_constructor(cx: &Cx, out: &mut String, depth: usize, f: &Function) -> Result<()> {1332    let pad = indent(depth);1333    let args = args(1334        cx,1335        f,1336        Place::Static(cx.ty(f.owner.as_deref().unwrap())?),1337        None,1338    )?;1339    doc_lines(out, depth, &summary(&f.docs));1340    out.push_str(&format!("{pad}#[new]\n"));1341    out.push_str(&format!("{pad}#[pyo3(signature = {})]\n", signature(&args)));1342    let params: Vec<String> = args1343        .iter()1344        .map(|a| format!("{}: {}", a.rust, a.plan.decl))1345        .collect();1346    out.push_str(&format!(1347        "{pad}pub fn __new__({}) -> PyResult<Self> {{\n",1348        params.join(", ")1349    ));1350    let inner = indent(depth + 1);1351    for a in &args {1352        for l in &a.plan.lets {1353            out.push_str(&format!("{inner}{l}\n"));1354        }1355    }1356    let passes: Vec<String> = args.iter().map(|a| a.plan.pass.clone()).collect();1357    out.push_str(&format!(1358        "{inner}let out = {}({});\n",1359        callee(f, None),1360        passes.join(", ")1361    ));1362    for a in &args {1363        for l in &a.plan.post {1364            out.push_str(&format!("{inner}{l}\n"));1365        }1366    }1367    let unwrapped = if matches!(f.ret, Ty::Result { .. }) {1368        "ok(out)?"1369    } else {1370        "out"1371    };1372    out.push_str(&format!("{inner}Ok(Self({unwrapped}))\n"));1373    out.push_str(&format!("{pad}}}\n"));1374    Ok(())1375}13761377fn emit_class(cx: &Cx, out: &mut String, depth: usize, module: &str, class: &Owned) -> Result<()> {1378    let pad = indent(depth);1379    let inner = indent(depth + 1);1380    let ty = class.ty;1381    let clone = cx.derives(&ty.path, "Clone");1382    doc_lines(out, depth, &summary(&ty.docs));1383    out.push_str(&format!(1384        "{pad}#[pyclass(name = {}, module = {}, {})]\n",1385        quote(&ty.name),1386        quote(&format!("mrlypy.{}", dotted(module))),1387        if clone {1388            "from_py_object"1389        } else {1390            "skip_from_py_object"1391        }1392    ));1393    if clone {1394        out.push_str(&format!("{pad}#[derive(Clone)]\n"));1395    }1396    out.push_str(&format!(1397        "{pad}pub struct {}(pub mrlyrs::{});\n\n",1398        ty.name, ty.path1399    ));1400    out.push_str(&format!("{pad}#[pymethods]\n{pad}impl {} {{\n", ty.name));1401    for f in class.fns.iter().filter(|f| is_constructor(f)) {1402        emit_constructor(cx, out, depth + 1, f)?;1403    }1404    for field in ty.fields.iter().filter(|f| f.public && crossable(&f.ty)) {1405        doc_lines(out, depth + 1, &summary(&field.docs));1406        out.push_str(&format!("{inner}#[getter]\n"));1407        out.push_str(&format!(1408            "{inner}#[pyo3(name = {})]\n",1409            quote(&py_name(&field.name))1410        ));1411        out.push_str(&format!(1412            "{inner}pub fn {}<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {{\n",1413            rust_ident(&py_name(&field.name))1414        ));1415        let taken = if matches!(field.ty, Ty::Ref { .. }) || cx.is_copy(&field.ty) {1416            ""1417        } else {1418            ".clone()"1419        };1420        out.push_str(&format!(1421            "{}let value = self.0.{}{taken};\n",1422            indent(depth + 2),1423            field.name1424        ));1425        out.push_str(&format!(1426            "{}({}).into_bound_py_any(py)\n",1427            indent(depth + 2),1428            into(cx, &field.ty, "value")?1429        ));1430        out.push_str(&format!("{inner}}}\n"));1431        if field.ty.settable() {1432            emit_setter(cx, out, depth + 1, &field.name, &field.ty)?;1433        }1434    }1435    for f in &class.fns {1436        let place = if f.self_kind.is_some() {1437            if f.self_kind == Some(SelfKind::Value) && !clone {1438                return Err(format!("{} takes self by value without Clone", f.path));1439            }1440            Place::Method(ty)1441        } else {1442            Place::Static(ty)1443        };1444        emit_simple(1445            cx,1446            out,1447            depth + 1,1448            f,1449            place,1450            &py_name(&f.name),1451            f.dims.first().copied(),1452        )?;1453    }1454    if cx.derives(&ty.path, "Deserialize") {1455        out.push_str(&format!("{inner}/// Reads plain data into the class.\n"));1456        out.push_str(&format!("{inner}#[staticmethod]\n"));1457        out.push_str(&format!(1458            "{inner}pub fn from_dict(data: &Bound<'_, PyAny>) -> PyResult<Self> {{\n{}Ok(Self(crate::hand::serde_from_py(data)?))\n{inner}}}\n",1459            indent(depth + 2)1460        ));1461    }1462    if cx.derives(&ty.path, "Serialize") {1463        out.push_str(&format!("{inner}/// Returns the value as plain data.\n"));1464        out.push_str(&format!(1465            "{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",1466            indent(depth + 2)1467        ));1468    }1469    out.push_str(&format!("{pad}}}\n\n"));1470    Ok(())1471}14721473fn emit_setter(cx: &Cx, out: &mut String, depth: usize, field: &str, ty: &Ty) -> Result<()> {1474    let pad = indent(depth);1475    let deep = indent(depth + 1);1476    let plan = plan(cx, "value", ty)?;1477    out.push_str(&format!("{pad}#[setter]\n"));1478    out.push_str(&format!(1479        "{pad}#[pyo3(name = {})]\n",1480        quote(&py_name(field))1481    ));1482    out.push_str(&format!(1483        "{pad}pub fn set_{field}(&mut self, value: {}) -> PyResult<()> {{\n",1484        plan.decl1485    ));1486    for line in &plan.lets {1487        out.push_str(&format!("{deep}{line}\n"));1488    }1489    out.push_str(&format!("{deep}self.0.{field} = {};\n", plan.pass));1490    out.push_str(&format!("{deep}Ok(())\n{pad}}}\n"));1491    Ok(())1492}14931494fn emit_holder(1495    cx: &Cx,1496    out: &mut String,1497    depth: usize,1498    module: &str,1499    holder: &Owned,1500) -> Result<()> {1501    let pad = indent(depth);1502    let ty = holder.ty;1503    doc_lines(out, depth, &summary(&ty.docs));1504    out.push_str(&format!(1505        "{pad}#[pyclass(name = {}, module = {}, skip_from_py_object)]\n",1506        quote(&ty.name),1507        quote(&format!("mrlypy.{}", dotted(module)))1508    ));1509    out.push_str(&format!("{pad}pub struct {};\n\n", ty.name));1510    out.push_str(&format!("{pad}#[pymethods]\n{pad}impl {} {{\n", ty.name));1511    for f in &holder.fns {1512        emit_simple(1513            cx,1514            out,1515            depth + 1,1516            f,1517            Place::Static(ty),1518            &py_name(&f.name),1519            f.dims.first().copied(),1520        )?;1521    }1522    out.push_str(&format!("{pad}}}\n\n"));1523    Ok(())1524}15251526fn free_words(text: &str) -> BTreeSet<&str> {1527    let mut words = BTreeSet::new();1528    for line in text.lines().filter(|l| !l.trim_start().starts_with("///")) {1529        let mut start = None;1530        let mut quoted = false;1531        let mut escaped = false;1532        for (i, c) in line.char_indices().chain([(line.len(), ' ')]) {1533            if quoted {1534                quoted = escaped || c != '"';1535                escaped = !escaped && c == '\\';1536                continue;1537            }1538            if c.is_alphanumeric() || c == '_' {1539                start.get_or_insert(i);1540                continue;1541            }1542            if let Some(s) = start.take() {1543                if !line[..s].ends_with("::") && !line[..s].ends_with('.') {1544                    words.insert(&line[s..i]);1545                }1546            }1547            quoted = c == '"';1548        }1549    }1550    words1551}15521553fn uses(body: &str) -> Vec<String> {1554    let words = free_words(body);1555    let mut lines = Vec::new();1556    let hand: Vec<&str> = HAND_NAMES1557        .iter()1558        .copied()1559        .filter(|n| words.contains(n))1560        .collect();1561    if !hand.is_empty() {1562        lines.push(format!("use crate::hand::{{{}}};", hand.join(", ")));1563    }1564    if words.contains("PyValueError") {1565        lines.push("use pyo3::exceptions::PyValueError;".to_string());1566    }1567    lines.push("use pyo3::prelude::*;".to_string());1568    lines.push("use pyo3::types::PyDict;".to_string());1569    if body.contains(".into_bound_py_any(") {1570        lines.push("use pyo3::IntoPyObjectExt;".to_string());1571    }1572    lines1573}15741575fn emit_module(cx: &Cx, out: &mut String, depth: usize, node: &Node) -> Result<()> {1576    let pad = indent(depth);1577    let inner = indent(depth + 1);1578    let mut children = String::new();1579    for child in node.children.values() {1580        emit_module(cx, &mut children, depth + 1, child)?;1581    }1582    let mut body = String::new();1583    emit_items(cx, &mut body, depth, node)?;1584    doc_lines(out, depth, &summary(&node.docs));1585    out.push_str(&format!("{pad}pub mod {} {{\n", rust_ident(node.name())));1586    for line in uses(&body) {1587        out.push_str(&format!("{inner}{line}\n"));1588    }1589    out.push('\n');1590    out.push_str(&children);1591    out.push_str(&body);1592    out.push_str(&format!("{pad}}}\n\n"));1593    Ok(())1594}15951596fn emit_items(cx: &Cx, out: &mut String, depth: usize, node: &Node) -> Result<()> {1597    let inner = indent(depth + 1);1598    for class in &node.classes {1599        emit_class(cx, out, depth + 1, &node.path, class)?;1600    }1601    for holder in &node.holders {1602        emit_holder(cx, out, depth + 1, &node.path, holder)?;1603    }1604    for export in &node.exports {1605        emit_export(cx, out, depth + 1, export, Place::Free)?;1606        out.push('\n');1607    }1608    out.push_str(&format!(1609        "{inner}pub fn init(py: Python<'_>, parent: &Bound<'_, PyModule>, sys: &Bound<'_, PyDict>) -> PyResult<()> {{\n"1610    ));1611    let deep = indent(depth + 2);1612    out.push_str(&format!(1613        "{deep}let m = PyModule::new(py, {})?;\n",1614        quote(&format!("mrlypy.{}", dotted(&node.path)))1615    ));1616    if !node.docs.is_empty() {1617        out.push_str(&format!(1618            "{deep}m.setattr(\"__doc__\", {})?;\n",1619            quote(&node.docs.join("\n"))1620        ));1621    }1622    if node.path == "core" {1623        out.push_str(&format!("{deep}m.add_class::<PyRng>()?;\n"));1624    }1625    for class in &node.classes {1626        out.push_str(&format!("{deep}m.add_class::<{}>()?;\n", class.ty.name));1627    }1628    for holder in &node.holders {1629        out.push_str(&format!("{deep}m.add_class::<{}>()?;\n", holder.ty.name));1630    }1631    for export in &node.exports {1632        out.push_str(&format!(1633            "{deep}m.add_function(wrap_pyfunction!({}, &m)?)?;\n",1634            rust_ident(&py_name(&export.name))1635        ));1636    }1637    for c in &node.consts {1638        out.push_str(&format!(1639            "{deep}m.add({}, {})?;\n",1640            quote(&c.name),1641            into(cx, &c.ty, &format!("mrlyrs::{}", c.path))?1642        ));1643    }1644    let names: Vec<String> = node.item_names().iter().map(|n| quote(n)).collect();1645    out.push_str(&format!(1646        "{deep}let names: Vec<&str> = vec![{}];\n{deep}m.add(\"__all__\", names)?;\n",1647        names.join(", ")1648    ));1649    for child in node.children.values() {1650        out.push_str(&format!(1651            "{deep}{}::init(py, &m, sys)?;\n",1652            rust_ident(child.name())1653        ));1654    }1655    out.push_str(&format!("{deep}parent.add({}, &m)?;\n", quote(node.name())));1656    out.push_str(&format!(1657        "{deep}sys.set_item({}, &m)?;\n",1658        quote(&format!("{NATIVE}.{}", dotted(&node.path)))1659    ));1660    out.push_str(&format!("{deep}Ok(())\n{inner}}}\n"));1661    Ok(())1662}16631664fn lib_file() -> String {1665    let module = NATIVE.rsplit_once('.').map_or(NATIVE, |(_, m)| m);1666    format!(1667        "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"1668    )1669}16701671fn rust_file(cx: &Cx, root: &Node) -> Result<String> {1672    let mut out = String::from(HEADER);1673    out.push_str("\nuse pyo3::prelude::*;\nuse pyo3::types::PyDict;\n\n");1674    for child in root.children.values() {1675        emit_module(cx, &mut out, 0, child)?;1676    }1677    out.push_str("pub fn init(py: Python<'_>, root: &Bound<'_, PyModule>) -> PyResult<()> {\n");1678    out.push_str(1679        "    let sys = py.import(\"sys\")?.getattr(\"modules\")?.cast_into::<PyDict>()?;\n",1680    );1681    for child in root.children.values() {1682        out.push_str(&format!(1683            "    {}::init(py, root, &sys)?;\n",1684            rust_ident(child.name())1685        ));1686    }1687    out.push_str("    Ok(())\n}\n");1688    Ok(out)1689}16901691// PYTHON16921693fn py_type(cx: &Cx, ty: &Ty, here: &str) -> String {1694    match ty {1695        Ty::Unit => "None".into(),1696        Ty::Scalar { name } => match name.as_str() {1697            "bool" => "bool".into(),1698            "f32" | "f64" => "float".into(),1699            "char" => "str".into(),1700            _ => "int".into(),1701        },1702        Ty::Str | Ty::String => "str".into(),1703        Ty::U128 | Ty::I128 | Ty::Code => "int".into(),1704        Ty::Json => "Any".into(),1705        Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => {1706            if is_u8(item) {1707                "bytes".into()1708            } else if is_rgba(item) {1709                "NDArray[Any]".into()1710            } else {1711                format!("list[{}]", py_type(cx, item, here))1712            }1713        }1714        Ty::Option { item } => format!("{} | None", py_type(cx, item, here)),1715        Ty::Tuple { items } => format!(1716            "tuple[{}]",1717            items1718                .iter()1719                .map(|t| py_type(cx, t, here))1720                .collect::<Vec<_>>()1721                .join(", ")1722        ),1723        Ty::Array { item, .. } => {1724            if is_rgba(ty) {1725                "tuple[int, int, int, int]".into()1726            } else if is_u8(item) {1727                "bytes".into()1728            } else {1729                format!("list[{}]", py_type(cx, item, here))1730            }1731        }1732        Ty::Ref { item, .. } | Ty::Result { item } => py_type(cx, item, here),1733        Ty::Map { key, value } => format!(1734            "dict[{}, {}]",1735            py_type(cx, key, here),1736            py_type(cx, value, here)1737        ),1738        Ty::Hand { name, .. } => match name.as_str() {1739            "Tensor" | "Image" => "NDArray[Any]".into(),1740            "Color" => "tuple[int, int, int, int]".into(),1741            "Code" => "int".into(),1742            "Rng" => qualified("core", "Rng", here),1743            _ => "dict[str, Any]".into(),1744        },1745        Ty::Plain { path } => match cx.types.get(path.as_str()).map(|t| t.kind) {1746            Some(TypeKind::Enum) => "Any".into(),1747            _ => "dict[str, Any]".into(),1748        },1749        Ty::Enum { path } => {1750            let words = cx.words(path);1751            if words.is_empty() {1752                "str".into()1753            } else {1754                format!(1755                    "Literal[{}]",1756                    words1757                        .iter()1758                        .map(|w| py_str(w))1759                        .collect::<Vec<_>>()1760                        .join(", ")1761                )1762            }1763        }1764        Ty::Class { path, .. } => qualified(parent(path), last(path), here),1765        Ty::Opaque { .. } | Ty::Unknown { .. } => "Any".into(),1766    }1767}17681769fn qualified(module: &str, name: &str, here: &str) -> String {1770    if module == here {1771        name.to_string()1772    } else {1773        format!("mrlypy.{}.{name}", dotted(module))1774    }1775}17761777fn docstring(out: &mut String, depth: usize, docs: &[String]) {1778    if docs.is_empty() {1779        return;1780    }1781    let text = docs1782        .join(&format!("\n{}", indent(depth)))1783        .replace('\\', "\\\\")1784        .replace("\"\"\"", "\\\"\\\"\\\"");1785    out.push_str(&format!("{}\"\"\"{text}\"\"\"\n", indent(depth)));1786}17871788fn stub_params(cx: &Cx, args: &[Arg], here: &str) -> Vec<String> {1789    let optional_from = args1790        .iter()1791        .rposition(|a| !a.plan.optional)1792        .map_or(0, |i| i + 1);1793    args.iter()1794        .enumerate()1795        .map(|(i, a)| {1796            let hint = py_type(cx, &a.ty, here);1797            if i >= optional_from {1798                format!("{}: {hint} = None", a.py)1799            } else {1800                format!("{}: {hint}", a.py)1801            }1802        })1803        .collect()1804}18051806fn stub_fn(1807    cx: &Cx,1808    out: &mut String,1809    depth: usize,1810    export: &Export,1811    place: Place,1812    here: &str,1813) -> Result<()> {1814    let pad = indent(depth);1815    let name = py_name(&export.name);1816    let (f, dim) = export.variants[0];1817    let mut params = stub_params(cx, &args(cx, f, place, dim)?, here);1818    let mut ret = py_type(cx, &subst_opt(&f.ret, dim), here);1819    if export.variants.len() > 1 {1820        let mut seen: Vec<String> = params1821            .iter()1822            .map(|p| p.split(':').next().unwrap().to_string())1823            .collect();1824        for (v, d) in &export.variants[1..] {1825            for a in &args(cx, v, place, *d)? {1826                if !seen.contains(&a.py) {1827                    seen.push(a.py.clone());1828                    let hint = py_type(cx, &a.ty, here);1829                    params.push(format!("{}: {hint} | None = None", a.py));1830                }1831            }1832            let other = py_type(cx, &subst_opt(&v.ret, *d), here);1833            if other != ret {1834                ret = format!("{ret} | {other}");1835            }1836        }1837    }1838    let variants: Vec<&Function> = export.variants.iter().map(|(f, _)| *f).collect();1839    let receiver = match place {1840        Place::Method(_) => "self".to_string(),1841        _ => String::new(),1842    };1843    let mut all = Vec::new();1844    if !receiver.is_empty() {1845        all.push(receiver);1846    }1847    all.extend(params);1848    if matches!(place, Place::Static(_)) {1849        out.push_str(&format!("{pad}@staticmethod\n"));1850    }1851    out.push_str(&format!("{pad}def {name}({}) -> {ret}:\n", all.join(", ")));1852    let docs = merged_docs(&variants);1853    if docs.is_empty() {1854        out.push_str(&format!("{}...\n", indent(depth + 1)));1855    } else {1856        docstring(out, depth + 1, &docs);1857    }1858    Ok(())1859}18601861fn subst_opt(ty: &Ty, dim: Option<u8>) -> Ty {1862    match dim {1863        Some(d) => subst(ty, d),1864        None => ty.clone(),1865    }1866}18671868fn stub_class(cx: &Cx, out: &mut String, class: &Owned, here: &str) -> Result<()> {1869    let ty = class.ty;1870    out.push_str(&format!("class {}:\n", ty.name));1871    docstring(out, 1, &summary(&ty.docs));1872    let mut wrote = !ty.docs.is_empty();1873    for f in class.fns.iter().filter(|f| is_constructor(f)) {1874        let args = args(cx, f, Place::Static(ty), None)?;1875        let params = stub_params(cx, &args, here);1876        let mut all = vec!["self".to_string()];1877        all.extend(params);1878        out.push_str(&format!(1879            "    def __init__({}) -> None: ...\n",1880            all.join(", ")1881        ));1882        wrote = true;1883    }1884    for field in ty.fields.iter().filter(|f| f.public && crossable(&f.ty)) {1885        out.push_str("    @property\n");1886        out.push_str(&format!(1887            "    def {}(self) -> {}:\n",1888            py_name(&field.name),1889            py_type(cx, &field.ty, here)1890        ));1891        if field.docs.is_empty() {1892            out.push_str("        ...\n");1893        } else {1894            docstring(out, 2, &summary(&field.docs));1895        }1896        if field.ty.settable() {1897            out.push_str(&format!(1898                "    @{0}.setter\n    def {0}(self, value: {1}) -> None: ...\n",1899                py_name(&field.name),1900                py_type(cx, &field.ty, here)1901            ));1902        }1903        wrote = true;1904    }1905    for f in &class.fns {1906        let place = if f.self_kind.is_some() {1907            Place::Method(ty)1908        } else {1909            Place::Static(ty)1910        };1911        let export = Export {1912            name: f.name.clone(),1913            variants: vec![(f, None)],1914        };1915        stub_fn(cx, out, 1, &export, place, here)?;1916        wrote = true;1917    }1918    if cx.derives(&ty.path, "Deserialize") {1919        out.push_str(&format!(1920            "    @staticmethod\n    def from_dict(data: Any) -> {}:\n        \"\"\"Reads plain data into the class.\"\"\"\n",1921            ty.name1922        ));1923        wrote = true;1924    }1925    if cx.derives(&ty.path, "Serialize") {1926        out.push_str(1927            "    def to_dict(self) -> Any:\n        \"\"\"Returns the value as plain data.\"\"\"\n",1928        );1929        wrote = true;1930    }1931    if !wrote {1932        out.push_str("    ...\n");1933    }1934    out.push('\n');1935    Ok(())1936}19371938fn stub_holder(cx: &Cx, out: &mut String, holder: &Owned, here: &str) -> Result<()> {1939    let ty = holder.ty;1940    out.push_str(&format!("class {}:\n", ty.name));1941    docstring(out, 1, &summary(&ty.docs));1942    for f in &holder.fns {1943        let export = Export {1944            name: f.name.clone(),1945            variants: vec![(f, None)],1946        };1947        stub_fn(cx, out, 1, &export, Place::Static(ty), here)?;1948    }1949    out.push('\n');1950    Ok(())1951}19521953fn stub_rng(cx: &Cx, out: &mut String, here: &str) -> Result<()> {1954    out.push_str("class Rng:\n");1955    out.push_str("    \"\"\"The seeded random stream, one class, passed wherever Rust takes a mutable stream.\"\"\"\n");1956    let mut fns: Vec<&Function> = cx1957        .manifest1958        .functions1959        .iter()1960        .filter(|f| f.cross == Cross::Ok && f.owner.as_deref() == Some("core::Rng"))1961        .collect();1962    fns.sort_by_key(|f| f.name != "new");1963    for f in &fns {1964        let export = Export {1965            name: f.name.clone(),1966            variants: vec![(f, None)],1967        };1968        if f.name == "new" {1969            let args = args(cx, f, Place::Free, None)?;1970            let mut all = vec!["self".to_string()];1971            all.extend(stub_params(cx, &args, here));1972            out.push_str(&format!("    def __init__({}) -> None:\n", all.join(", ")));1973            docstring(out, 2, &summary(&f.docs));1974        } else if f.self_kind.is_some() {1975            stub_fn(1976                cx,1977                out,1978                1,1979                &export,1980                Place::Method(cx.ty("core::Rng")?),1981                here,1982            )?;1983        }1984    }1985    out.push_str("    def choice(self, seq: Any) -> Any:\n        \"\"\"Draws one item of the sequence, the same draw as Rust's choice.\"\"\"\n");1986    out.push_str("    def shuffle(self, seq: list[Any]) -> None:\n        \"\"\"Shuffles the list in place, the same permutation as Rust's shuffle.\"\"\"\n");1987    out.push('\n');1988    Ok(())1989}19901991fn stub_file(cx: &Cx, node: &Node) -> Result<String> {1992    let here = node.path.as_str();1993    let mut out = String::new();1994    let mut body = String::new();1995    for c in &node.consts {1996        body.push_str(&format!("{}: {}\n", c.name, py_type(cx, &c.ty, here)));1997    }1998    if !node.consts.is_empty() {1999        body.push('\n');2000    }2001    if here == "core" {2002        stub_rng(cx, &mut body, here)?;2003    }2004    for class in &node.classes {2005        stub_class(cx, &mut body, class, here)?;2006    }2007    for holder in &node.holders {2008        stub_holder(cx, &mut body, holder, here)?;2009    }2010    for export in &node.exports {2011        stub_fn(cx, &mut body, 0, export, Place::Free, here)?;2012        body.push('\n');2013    }2014    let mut packages: Vec<String> = Vec::new();2015    for word in body.split(|c: char| !(c.is_alphanumeric() || c == '_' || c == '.')) {2016        if let Some(rest) = word.strip_prefix("mrlypy.") {2017            let module = rest.rsplit_once('.').map(|(m, _)| m).unwrap_or(rest);2018            let pkg = format!("mrlypy.{module}");2019            if !packages.contains(&pkg) {2020                packages.push(pkg);2021            }2022        }2023    }2024    packages.sort();2025    out.push_str("from typing import Any, Literal\n\nfrom numpy.typing import NDArray\n");2026    for pkg in &packages {2027        out.push_str(&format!("import {pkg}\n"));2028    }2029    if !node.children.is_empty() {2030        let children: Vec<&str> = node.children.keys().map(String::as_str).collect();2031        out.push_str(&format!("from . import {}\n", children.join(", ")));2032    }2033    out.push('\n');2034    out.push_str(body.trim_end());2035    out.push('\n');2036    Ok(out)2037}20382039fn init_file(node: &Node) -> String {2040    let mut out = String::new();2041    out.push_str(&format!("from {NATIVE}.{} import *\n", dotted(&node.path)));2042    if !node.children.is_empty() {2043        let children: Vec<&str> = node.children.keys().map(String::as_str).collect();2044        out.push_str(&format!("from . import {}\n", children.join(", ")));2045    }2046    let mut names = node.item_names();2047    names.extend(node.children.keys().cloned());2048    let listed: Vec<String> = names.iter().map(|n| py_str(n)).collect();2049    out.push_str(&format!("\n__all__ = [{}]\n", listed.join(", ")));2050    out2051}20522053fn python_files(cx: &Cx, root: &Node, python: &Path) -> Result<()> {2054    let children: Vec<&str> = root.children.keys().map(String::as_str).collect();2055    let listed: Vec<String> = children.iter().map(|n| py_str(n)).collect();2056    let init = format!(2057        "from mrlypy import _mrlypy\nfrom mrlypy import {}\n\n__version__ = _mrlypy.__version__\n__all__ = [{}]\n",2058        children.join(", "),2059        listed.join(", ")2060    );2061    save(&python.join("__init__.py"), &init)?;2062    let stub = format!(2063        "from . import {}\n\n__version__: str\n",2064        children.join(", ")2065    );2066    save(&python.join("__init__.pyi"), &stub)?;2067    let mut nodes = Vec::new();2068    for child in root.children.values() {2069        child.walk(&mut nodes);2070    }2071    for node in nodes {2072        let dir = python.join(node.path.replace("::", "/"));2073        save(&dir.join("__init__.py"), &init_file(node))?;2074        save(&dir.join("__init__.pyi"), &stub_file(cx, node)?)?;2075    }2076    Ok(())2077}20782079// TEST20802081fn test_file(cx: &Cx) -> String {2082    let ok = cx2083        .manifest2084        .functions2085        .iter()2086        .filter(|f| f.cross == Cross::Ok)2087        .count();2088    format!(2089        r#"import importlib2090import json2091import keyword2092import pathlib20932094import mrlypy20952096MANIFEST = pathlib.Path(__file__).resolve().parents[2] / "bridge" / "manifest.json"209720982099def load():2100    return json.loads(MANIFEST.read_text())210121022103def name_of(word):2104    return word + "_" if keyword.iskeyword(word) else word210521062107def locate(fn, kinds):2108    module = importlib.import_module("mrlypy." + fn["module"].replace("::", "."))2109    exported = name_of(fn["path"].rsplit("::", 1)[-1])2110    owner = fn["owner"]2111    if owner and kinds[owner] in ("class", "plain", "enum"):2112        return getattr(getattr(module, owner.rsplit("::", 1)[-1]), exported)2113    return getattr(module, exported)211421152116def test_every_ok_function_is_callable_under_its_rust_doc():2117    manifest = load()2118    kinds = {{t["path"]: t["cross"]["kind"] for t in manifest["types"]}}2119    count = 02120    for fn in manifest["functions"]:2121        if fn["cross"]["status"] != "ok":2122            continue2123        target = locate(fn, kinds)2124        assert callable(target), fn["path"]2125        doc = target.__doc__ or ""2126        if fn["docs"]:2127            if fn["dims"]:2128                assert fn["docs"][0] in doc, fn["path"]2129            else:2130                assert doc.startswith(fn["docs"][0]), fn["path"]2131        count += 12132    assert count == {ok}2133"#2134    )2135}