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}