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}