js.rs
83.9 kB · rust · 2327 lines
1use crate::disk::save;2use crate::model::{3 Const, Cross, Function, Manifest, Result, SelfKind, Ty, Type, TypeCross, TypeKind,4};5use std::collections::{BTreeMap, BTreeSet};6use std::fmt::Write as _;7use std::path::Path;89const TYPED: &[&str] = &[10 "u8", "u16", "u32", "i8", "i16", "i32", "f32", "f64", "usize", "u64", "i64",11];1213const NATIVE_ITEM: &[&str] = &[14 "u8", "u16", "u32", "i8", "i16", "i32", "f32", "f64", "usize",15];1617const WORDS: &[&str] = &[18 "void",19 "new",20 "class",21 "function",22 "default",23 "delete",24 "in",25 "var",26 "let",27 "const",28 "export",29 "import",30 "switch",31 "case",32 "for",33 "if",34 "else",35 "this",36 "super",37 "with",38 "yield",39 "enum",40 "await",41 "typeof",42 "instanceof",43 "return",44 "try",45 "catch",46 "finally",47 "throw",48 "while",49 "do",50 "break",51 "continue",52 "debugger",53 "static",54 "interface",55 "package",56 "private",57 "protected",58 "public",59 "implements",60 "arguments",61 "eval",62 "null",63 "true",64 "false",65 "extends",66 "as",67 "crate",68 "extern",69 "fn",70 "impl",71 "loop",72 "match",73 "mod",74 "move",75 "mut",76 "pub",77 "ref",78 "self",79 "struct",80 "trait",81 "type",82 "unsafe",83 "use",84 "where",85 "async",86 "dyn",87 "box",88 "macro",89 "gen",90];9192const RNG_METHODS: &[&str] = &[93 "new",94 "unit",95 "below",96 "range",97 "boolean",98 "chance",99 "sample_indices",100 "choice",101 "shuffle",102 "__state",103 "__restore",104];105106const JS_RESERVED: &[&str] = &[107 "new",108 "void",109 "class",110 "function",111 "default",112 "delete",113 "in",114 "var",115 "let",116 "const",117 "export",118 "import",119 "switch",120 "case",121 "for",122 "if",123 "else",124 "this",125 "super",126 "with",127 "yield",128 "enum",129 "await",130 "typeof",131 "instanceof",132 "return",133 "try",134 "catch",135 "finally",136 "throw",137 "while",138 "do",139 "break",140 "continue",141 "debugger",142 "static",143 "interface",144 "package",145 "private",146 "protected",147 "public",148 "implements",149 "null",150 "true",151 "false",152 "extends",153];154155const HAND_RS: &str =156 "#[path = \"../../../hand/hand.rs\"]\nmod crossing;\n\npub use crossing::*;\n";157158const ALLOWS: &str = "#![allow(non_camel_case_types, non_snake_case, clippy::too_many_arguments)]";159160// FILES161162pub fn write(manifest: &Manifest, root: &Path) -> Result<()> {163 let names =164 std::fs::read_to_string(root.join("pkgs/bridge/units.txt")).map_err(|e| e.to_string())?;165 let names: Vec<&str> = names166 .lines()167 .map(str::trim)168 .filter(|l| !l.is_empty())169 .collect();170 let js = root.join("pkgs/mrlyjs");171 for name in &names {172 let unit = Unit::new(manifest, name);173 let dir = js.join("units").join(name);174 save(175 &dir.join("Cargo.toml"),176 &cargo_toml(name, &manifest.version),177 )?;178 save(&dir.join("src/hand.rs"), HAND_RS)?;179 save(&dir.join("src/lib.rs"), &unit.lib_rs()?)?;180 save(&js.join(format!("{name}.js")), &unit.wrapper_js())?;181 save(&js.join(format!("{name}.d.ts")), &unit.types_dts()?)?;182 }183 save(&js.join("manifest.test.js"), MANIFEST_TEST)?;184 Ok(())185}186187fn cargo_toml(name: &str, version: &str) -> String {188 let what = if name == "all" {189 "every module of mrlyrs".to_string()190 } else {191 format!("the {name} module of mrlyrs")192 };193 format!(194 "[package]\nname = \"mrlyjs_{name}\"\nversion = \"{version}\"\nedition.workspace = true\nlicense.workspace = true\nrepository.workspace = true\ndescription = \"The {name} wasm unit of mrlyjs: {what} in a browser.\"\npublish = false\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nmrlyrs.workspace = true\nwasm-bindgen = \"0.2\"\nserde-wasm-bindgen = \"0.6\"\njs-sys = \"0.3\"\nserde = {{ version = \"1\", features = [\"derive\"] }}\nserde_json = \"1\"\n\n[package.metadata.wasm-pack.profile.release]\nwasm-opt = false\n"195 )196}197198// UNIT199200struct Unit<'a> {201 name: &'a str,202 manifest: &'a Manifest,203 types: BTreeMap<&'a str, &'a Type>,204 reserved: BTreeSet<String>,205}206207#[derive(Clone, Copy)]208enum Owner<'a> {209 Free,210 Hand(&'a Type),211 Class(&'a Type),212 Holder(&'a Type),213}214215#[derive(Clone, Copy)]216struct Cx<'a> {217 dim: Option<u8>,218 fname: &'a str,219}220221struct ParamPlan {222 name: String,223 sig: String,224 pre: Vec<String>,225 arg: String,226 post: Vec<String>,227}228229struct RetPlan {230 sig: String,231 done: String,232}233234impl<'a> Unit<'a> {235 fn new(manifest: &'a Manifest, name: &'a str) -> Unit<'a> {236 let mut unit = Unit {237 name,238 manifest,239 types: manifest240 .types241 .iter()242 .map(|t| (t.path.as_str(), t))243 .collect(),244 reserved: BTreeSet::new(),245 };246 unit.reserved = unit.symbols();247 unit248 }249250 fn symbols(&self) -> BTreeSet<String> {251 let mut out = BTreeSet::new();252 for method in RNG_METHODS {253 out.insert(format!("rng_{method}"));254 }255 for t in self.classes().into_iter().chain(self.holders()) {256 let lower = self.local(&t.path).to_lowercase();257 for f in self.functions() {258 if f.owner.as_deref() == Some(t.path.as_str()) {259 out.insert(format!("{lower}_{}", f.name));260 }261 }262 for extra in ["from", "toJSON", "new"] {263 out.insert(format!("{lower}_{extra}"));264 }265 }266 out267 }268269 fn export(&self, name: &str) -> String {270 let mut out = name.to_string();271 while self.reserved.contains(&out) {272 out.push('_');273 }274 out275 }276277 fn holds(&self, path: &str) -> bool {278 self.name == "all" || path.split("::").next() == Some(self.name)279 }280281 fn rel<'p>(&self, path: &'p str) -> &'p str {282 if self.name == "all" {283 return path;284 }285 match path.strip_prefix(self.name) {286 Some("") => "",287 Some(rest) => rest.strip_prefix("::").unwrap_or(path),288 None => path,289 }290 }291292 fn local(&self, path: &str) -> String {293 self.rel(path).replace("::", "_")294 }295296 fn ty(&self, path: &str) -> Option<&'a Type> {297 self.types.get(path).copied()298 }299300 fn functions(&self) -> Vec<&'a Function> {301 self.manifest302 .functions303 .iter()304 .filter(|f| f.cross == Cross::Ok && self.holds(&f.module))305 .collect()306 }307308 fn groups(&self) -> Vec<Vec<&'a Function>> {309 let mut out: Vec<Vec<&'a Function>> = vec![];310 for f in self.functions() {311 match out.last_mut() {312 Some(group) if group[0].path == f.path => group.push(f),313 _ => out.push(vec![f]),314 }315 }316 out317 }318319 fn consts(&self) -> Vec<&'a Const> {320 self.manifest321 .consts322 .iter()323 .filter(|c| c.cross == Cross::Ok && self.holds(&c.path))324 .collect()325 }326327 fn classes(&self) -> Vec<&'a Type> {328 self.manifest329 .types330 .iter()331 .filter(|t| t.cross == TypeCross::Class && self.holds(&t.path))332 .collect()333 }334335 fn holders(&self) -> Vec<&'a Type> {336 let owners: BTreeSet<&str> = self337 .functions()338 .iter()339 .filter_map(|f| f.owner.as_deref())340 .collect();341 self.manifest342 .types343 .iter()344 .filter(|t| {345 matches!(t.cross, TypeCross::Plain | TypeCross::Enum { .. })346 && self.holds(&t.path)347 && owners.contains(t.path.as_str())348 })349 .collect()350 }351352 fn owner_of(&self, f: &Function) -> Owner<'a> {353 let Some(path) = f.owner.as_deref() else {354 return Owner::Free;355 };356 match self.ty(path).map(|t| (t, &t.cross)) {357 Some((t, TypeCross::Hand { .. })) => Owner::Hand(t),358 Some((t, TypeCross::Class)) => Owner::Class(t),359 Some((t, TypeCross::Plain | TypeCross::Enum { .. })) => Owner::Holder(t),360 _ => Owner::Free,361 }362 }363364 fn hand_name(&self, t: &Type) -> &'static str {365 match &t.cross {366 TypeCross::Hand { name } => match name.as_str() {367 "Tensor" => "Tensor",368 "Cell" => "Cell",369 "CellNd" => "CellNd",370 "Cell6d" => "Cell6d",371 "Image" => "Image",372 "Color" => "Color",373 "Code" => "Code",374 _ => "Rng",375 },376 _ => "Rng",377 }378 }379380 // RUST TYPES381382 fn rust_ty(&self, ty: &Ty, cx: Cx) -> Result<String> {383 Ok(match ty {384 Ty::Unit => "()".into(),385 Ty::Scalar { name } => name.clone(),386 Ty::Str | Ty::String => "String".into(),387 Ty::U128 => "u128".into(),388 Ty::I128 => "i128".into(),389 Ty::Code => "mrlyrs::math::bang::Code".into(),390 Ty::Json => "mrlyrs::core::Json".into(),391 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => {392 format!("Vec<{}>", self.rust_ty(item, cx)?)393 }394 Ty::Option { item } => format!("Option<{}>", self.rust_ty(item, cx)?),395 Ty::Tuple { items } => {396 let parts: Result<Vec<String>> =397 items.iter().map(|t| self.rust_ty(t, cx)).collect();398 format!("({})", parts?.join(", "))399 }400 Ty::Array { item, len } => format!("[{}; {len}]", self.rust_ty(item, cx)?),401 Ty::Ref { item, .. } | Ty::Result { item } => self.rust_ty(item, cx)?,402 Ty::Map { key, value } => format!(403 "std::collections::HashMap<{}, {}>",404 self.rust_ty(key, cx)?,405 self.rust_ty(value, cx)?406 ),407 Ty::Hand { name, dim } => match name.as_str() {408 "Tensor" => "mrlyrs::core::Tensor".into(),409 "Cell" => "mrlyrs::core::Cell".into(),410 "CellNd" => match dim.or(cx.dim) {411 Some(2) => "mrlyrs::math::two::Cell2d".into(),412 Some(3) => "mrlyrs::math::three::Cell3d".into(),413 _ => return Err(format!("{}: a cell without a dimension", cx.fname)),414 },415 "Cell6d" => "mrlyrs::math::six::Cell6d".into(),416 "Image" => "mrlyrs::core::Image".into(),417 "Color" => "mrlyrs::core::Color".into(),418 "Code" => "mrlyrs::math::bang::Code".into(),419 "Rng" => "mrlyrs::core::Rng".into(),420 other => return Err(format!("{}: unknown hand type {other}", cx.fname)),421 },422 Ty::Plain { path } => {423 let generic = self.ty(path).is_some_and(|t| t.const_generic);424 if generic {425 format!("mrlyrs::{path}<{}>", suffix_dim(cx.fname))426 } else {427 format!("mrlyrs::{path}")428 }429 }430 Ty::Enum { path } | Ty::Class { path, .. } => format!("mrlyrs::{path}"),431 Ty::Opaque { path } => return Err(format!("{}: {path} cannot cross", cx.fname)),432 Ty::Unknown { text } => return Err(format!("{}: {text} cannot cross", cx.fname)),433 })434 }435436 fn is_copy(&self, ty: &Ty) -> bool {437 match ty {438 Ty::Unit | Ty::Scalar { .. } | Ty::U128 | Ty::I128 | Ty::Code => true,439 Ty::Hand { name, .. } => name == "Color" || name == "Code",440 Ty::Tuple { items } => items.iter().all(|t| self.is_copy(t)),441 Ty::Array { item, .. } | Ty::Option { item } => self.is_copy(item),442 Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } => {443 self.ty(path).is_some_and(|t| derives(t, "Copy"))444 }445 _ => false,446 }447 }448449 // CROSSINGS IN450451 fn cross_in(&self, ty: &Ty, e: &str, cx: Cx, depth: usize) -> Result<String> {452 if pure_in(ty) {453 return Ok(format!("hand::from_js::<{}>({e})?", self.rust_ty(ty, cx)?));454 }455 let x = format!("x{depth}");456 Ok(match ty {457 Ty::Scalar { name } => match name.as_str() {458 "u64" => format!("hand::u64_from_js({e})?"),459 "i64" => format!("hand::i64_from_js({e})?"),460 "bool" => format!("hand::bool_from_js({e})?"),461 "char" => format!("hand::char_from_js({e})?"),462 other => format!("(hand::number_from_js({e})? as {other})"),463 },464 Ty::Str | Ty::String => format!("hand::string_from_js({e})?"),465 Ty::U128 => format!("hand::u128_from_js({e})?"),466 Ty::I128 => format!("hand::i128_from_js({e})?"),467 Ty::Code => format!("hand::code_from_js({e})?"),468 Ty::Json => format!("hand::json_from_js({e})?"),469 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => format!(470 "hand::list_from_js({e}, {})?",471 closure(&x, ok(self.cross_in(item, &x, cx, depth + 1)?))472 ),473 Ty::Option { item } => format!(474 "hand::option_from_js({e}, {})?",475 closure(&x, ok(self.cross_in(item, &x, cx, depth + 1)?))476 ),477 Ty::Tuple { items } => {478 let mut parts = vec![];479 for (i, t) in items.iter().enumerate() {480 parts.push(self.cross_in(481 t,482 &format!("&hand::item({e}, {i})?"),483 cx,484 depth + 1,485 )?);486 }487 format!("({})", parts.join(", "))488 }489 Ty::Array { item, len } => format!(490 "hand::array_from_js::<_, {len}>({e}, {})?",491 closure(&x, ok(self.cross_in(item, &x, cx, depth + 1)?))492 ),493 Ty::Ref { item, .. } | Ty::Result { item } => self.cross_in(item, e, cx, depth)?,494 Ty::Map { key, value } => format!(495 "hand::map_from_js::<{}, _>({e}, {})?",496 self.rust_ty(key, cx)?,497 closure(&x, ok(self.cross_in(value, &x, cx, depth + 1)?))498 ),499 Ty::Hand { name, dim } => match name.as_str() {500 "Tensor" => format!("hand::tensor_from_js({e})?"),501 "Cell" => format!("hand::cell_from_js({e})?"),502 "CellNd" => match dim.or(cx.dim) {503 Some(2) => format!("hand::cell2d_from_js({e})?"),504 Some(3) => format!("hand::cell3d_from_js({e})?"),505 _ => return Err(format!("{}: a cell without a dimension", cx.fname)),506 },507 "Cell6d" => format!("hand::cell6d_from_js({e})?"),508 "Image" => format!("hand::image_from_js({e})?"),509 "Color" => format!("hand::color_from_js({e})?"),510 "Code" => format!("hand::code_from_js({e})?"),511 other => return Err(format!("{}: {other} cannot cross in by value", cx.fname)),512 },513 Ty::Class { .. } => format!(514 "hand::from_js::<{}>(&hand::plain({e})?)?",515 self.rust_ty(ty, cx)?516 ),517 Ty::Unit | Ty::Plain { .. } | Ty::Enum { .. } => {518 format!("hand::from_js::<{}>({e})?", self.rust_ty(ty, cx)?)519 }520 Ty::Opaque { path } => return Err(format!("{}: {path} cannot cross", cx.fname)),521 Ty::Unknown { text } => return Err(format!("{}: {text} cannot cross", cx.fname)),522 })523 }524525 // CROSSINGS OUT526527 fn cross_out(&self, ty: &Ty, e: &str, cx: Cx, depth: usize) -> Result<String> {528 if pure_out(ty) {529 return Ok(format!("hand::to_js({e})?"));530 }531 let x = format!("x{depth}");532 Ok(match ty {533 Ty::Unit => "JsValue::UNDEFINED".into(),534 Ty::Scalar { name } => match name.as_str() {535 "u64" | "i64" => format!("JsValue::from({})", deref(e)),536 "bool" => format!("JsValue::from_bool({})", deref(e)),537 "char" => format!("JsValue::from_str(&{}.to_string())", place(e)),538 _ => format!("JsValue::from_f64({} as f64)", deref(e)),539 },540 Ty::Str | Ty::String => format!("JsValue::from_str({e})"),541 Ty::U128 | Ty::I128 => format!("JsValue::from_str(&{}.to_string())", place(e)),542 Ty::Code => format!("JsValue::from_str(&hand::code_to_js({}))", deref(e)),543 Ty::Json => format!("hand::json_to_js({e})?"),544 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Array { item, .. }545 if typed_scalar(item) =>546 {547 format!("hand::typed(&({})[..])", deref(e))548 }549 Ty::Vec { item }550 | Ty::Slice { item, .. }551 | Ty::Array { item, .. }552 | Ty::Set { item } => {553 format!(554 "hand::list_to_js({e}, {})?",555 closure(&x, ok(self.cross_out(item, &x, cx, depth + 1)?))556 )557 }558 Ty::Option { item } => match &**item {559 Ty::Ref { item: inner, .. } => format!(560 "hand::option_to_js({}, {})?",561 deref(e),562 closure(&x, ok(self.cross_out(inner, &x, cx, depth + 1)?))563 ),564 _ => format!(565 "hand::option_to_js({}.as_ref(), {})?",566 place(e),567 closure(&x, ok(self.cross_out(item, &x, cx, depth + 1)?))568 ),569 },570 Ty::Tuple { items } => {571 let mut parts = vec![];572 for (i, t) in items.iter().enumerate() {573 parts.push(self.cross_out(t, &format!("&{}.{i}", place(e)), cx, depth + 1)?);574 }575 format!("hand::tuple_to_js(&[{}])", parts.join(", "))576 }577 Ty::Ref { item, .. } | Ty::Result { item } => {578 self.cross_out(item, &format!("({})", deref(e)), cx, depth)?579 }580 Ty::Map { value, .. } => format!(581 "hand::map_to_js({e}, {})?",582 closure(&x, ok(self.cross_out(value, &x, cx, depth + 1)?))583 ),584 Ty::Hand { name, dim } => match name.as_str() {585 "Tensor" => format!("hand::tensor_to_js({e})?"),586 "Cell" => format!("hand::cell_to_js({e})?"),587 "CellNd" => match dim.or(cx.dim) {588 Some(2) => format!("hand::cell2d_to_js({e})?"),589 Some(3) => format!("hand::cell3d_to_js({e})?"),590 _ => return Err(format!("{}: a cell without a dimension", cx.fname)),591 },592 "Cell6d" => format!("hand::cell6d_to_js({e})?"),593 "Image" => format!("hand::image_to_js({e})?"),594 "Color" => format!("hand::color_to_js({})", deref(e)),595 "Code" => format!("JsValue::from_str(&hand::code_to_js({}))", deref(e)),596 _ => format!("JsValue::from(hand::Rng::wrap({}.clone()))", place(e)),597 },598 Ty::Class { path, .. } if self.holds(path) => {599 let inner = if self.is_copy(ty) {600 deref(e)601 } else {602 format!("{}.clone()", place(e))603 };604 format!("JsValue::from({} {{ inner: {inner} }})", self.local(path))605 }606 Ty::Class { .. } | Ty::Plain { .. } | Ty::Enum { .. } => format!("hand::to_js({e})?"),607 Ty::Opaque { path } => return Err(format!("{}: {path} cannot cross", cx.fname)),608 Ty::Unknown { text } => return Err(format!("{}: {text} cannot cross", cx.fname)),609 })610 }611612 fn ret_plan(&self, ty: &Ty, cx: Cx) -> Result<RetPlan> {613 let plan = |sig: &str, done: String| RetPlan {614 sig: sig.to_string(),615 done,616 };617 Ok(match ty {618 Ty::Unit => plan("()", "Ok(())".into()),619 Ty::Result { item } => self.ret_plan(item, cx)?,620 Ty::Scalar { name } => plan(name, "Ok(value)".into()),621 Ty::String => plan("String", "Ok(value)".into()),622 Ty::Str => plan("String", "Ok(value.to_string())".into()),623 Ty::Vec { item } if typed_scalar(item) => plan(624 &format!("Vec<{}>", self.rust_ty(item, cx)?),625 "Ok(value)".into(),626 ),627 Ty::Slice { item, .. } | Ty::Array { item, .. } if typed_scalar(item) => plan(628 &format!("Vec<{}>", self.rust_ty(item, cx)?),629 "Ok(value.to_vec())".into(),630 ),631 Ty::Class { path, .. } if self.holds(path) => {632 let ident = self.local(path);633 plan(&ident, format!("Ok({ident} {{ inner: value }})"))634 }635 Ty::Option { item } => match &**item {636 Ty::Class { path, .. } if self.holds(path) => {637 let ident = self.local(path);638 plan(639 &format!("Option<{ident}>"),640 format!("Ok(value.map(|inner| {ident} {{ inner }}))"),641 )642 }643 _ => plan("JsValue", ok(self.cross_out(ty, "&value", cx, 1)?)),644 },645 Ty::Hand { name, .. } if name == "Rng" => {646 plan("hand::Rng", "Ok(hand::Rng::wrap(value))".into())647 }648 Ty::Ref { item, .. } => match &**item {649 Ty::Slice { item: inner, .. } | Ty::Array { item: inner, .. }650 if typed_scalar(inner) =>651 {652 plan(653 &format!("Vec<{}>", self.rust_ty(inner, cx)?),654 "Ok(value.to_vec())".into(),655 )656 }657 Ty::Str => plan("String", "Ok(value.to_string())".into()),658 _ => plan("JsValue", ok(self.cross_out(item, "value", cx, 1)?)),659 },660 Ty::Slice { .. } => plan("JsValue", ok(self.cross_out(ty, "value", cx, 1)?)),661 _ => plan("JsValue", ok(self.cross_out(ty, "&value", cx, 1)?)),662 })663 }664665 // PARAMETERS666667 fn plan_param(&self, name: &str, ty: &Ty, cx: Cx, extra: bool) -> Result<ParamPlan> {668 let n = ident(name);669 let js = |pre: Vec<String>, arg: String, post: Vec<String>| ParamPlan {670 name: n.clone(),671 sig: "JsValue".into(),672 pre,673 arg,674 post,675 };676 let native = |sig: String, arg: String| ParamPlan {677 name: n.clone(),678 sig,679 pre: vec![],680 arg,681 post: vec![],682 };683 let read = |inner: &Ty| -> Result<String> {684 let converted = self.cross_in(inner, &format!("&{n}"), cx, 1)?;685 Ok(if extra {686 let want = format!("a {}d cell wants {name}.", cx.dim.unwrap_or(0));687 format!("let {n} = if {n}.is_undefined() {{ return Err(hand::refuse(\"{want}\")); }} else {{ {converted} }};")688 } else {689 format!("let {n} = {converted};")690 })691 };692 let owned = |inner: &Ty, arg: String| -> Result<ParamPlan> {693 Ok(js(vec![read(inner)?], arg, vec![]))694 };695 if !extra {696 match ty {697 Ty::Scalar { name } if !is_wide(name) => {698 return Ok(native(name.clone(), n.clone()))699 }700 Ty::Str => return Ok(native("&str".into(), n.clone())),701 Ty::String => return Ok(native("String".into(), n.clone())),702 Ty::Slice { item, .. } if native_item(item) => {703 return Ok(native(format!("&[{}]", self.rust_ty(item, cx)?), n.clone()))704 }705 Ty::Vec { item } if native_item(item) => {706 return Ok(native(707 format!("Vec<{}>", self.rust_ty(item, cx)?),708 n.clone(),709 ))710 }711 Ty::Class { path, .. } if self.holds(path) => {712 let taken = if self.is_copy(ty) {713 format!("{n}.inner")714 } else {715 format!("{n}.inner.clone()")716 };717 return Ok(native(format!("&{}", self.local(path)), taken));718 }719 Ty::Ref { mutable, item, .. } => match &**item {720 Ty::Class { path, .. } if self.holds(path) => {721 let ident = self.local(path);722 return Ok(if *mutable {723 native(format!("&mut {ident}"), format!("&mut {n}.inner"))724 } else {725 native(format!("&{ident}"), format!("&{n}.inner"))726 });727 }728 Ty::Hand { name, .. } if name == "Rng" && *mutable => {729 return Ok(native("&mut hand::Rng".into(), format!("{n}.stream()")))730 }731 _ => {}732 },733 _ => {}734 }735 }736 Ok(match ty {737 Ty::Ref {738 mutable: true,739 item,740 ..741 } => match &**item {742 Ty::Hand { name, .. } if name == "Tensor" || name == "Cell" => {743 let back = if name == "Tensor" {744 "tensor_into_js"745 } else {746 "cell_into_js"747 };748 let converted = self.cross_in(item, &format!("&{n}"), cx, 1)?;749 js(750 vec![format!("let mut {n}_value = {converted};")],751 format!("&mut {n}_value"),752 vec![format!("hand::{back}(&{n}, &{n}_value)?;")],753 )754 }755 _ => {756 return Err(format!(757 "{}: {name} mutates a value that cannot cross",758 cx.fname759 ))760 }761 },762 Ty::Ref { item, .. } => owned(item, format!("&{n}"))?,763 Ty::Slice { item, .. } => match self.view(item, &n, cx)? {764 Some(seen) => {765 let mut pre = vec![read(ty)?];766 pre.push(seen);767 js(pre, format!("&{n}_view"), vec![])768 }769 None => owned(ty, format!("&{n}"))?,770 },771 Ty::Option { item } => match &**item {772 Ty::Ref {773 mutable: true,774 item: inner,775 ..776 } if is_rng(inner) => js(777 vec![format!("let mut {n}_stream = hand::stream_from_js(&{n})?;")],778 format!("{n}_stream.as_mut()"),779 vec![format!(780 "if let Some(stream) = &{n}_stream {{ hand::stream_to_js(&{n}, stream)?; }}"781 )],782 ),783 Ty::Ref { item: inner, .. } if matches!(**inner, Ty::Slice { .. }) => {784 owned(ty, format!("{n}.as_deref()"))?785 }786 Ty::Ref { .. } => owned(ty, format!("{n}.as_ref()"))?,787 Ty::Slice { .. } => owned(ty, format!("{n}.as_deref()"))?,788 _ => owned(ty, n.clone())?,789 },790 _ => owned(ty, n.clone())?,791 })792 }793794 fn view(&self, item: &Ty, n: &str, cx: Cx) -> Result<Option<String>> {795 Ok(match item {796 Ty::Str => Some(format!(797 "let {n}_view: Vec<&str> = {n}.iter().map(String::as_str).collect();"798 )),799 Ty::Slice { item: inner, .. } => Some(format!(800 "let {n}_view: Vec<&[{}]> = {n}.iter().map(Vec::as_slice).collect();",801 self.rust_ty(inner, cx)?802 )),803 Ty::Tuple { items } if items.iter().any(|t| matches!(t, Ty::Str)) => {804 let names: Vec<String> = (0..items.len()).map(|i| format!("a{i}")).collect();805 let parts: Vec<String> = items806 .iter()807 .zip(&names)808 .map(|(t, a)| match t {809 Ty::Str => format!("{a}.as_str()"),810 Ty::Scalar { .. } => format!("*{a}"),811 _ => format!("{a}.clone()"),812 })813 .collect();814 let mut tys = vec![];815 for t in items {816 tys.push(match t {817 Ty::Str => "&str".to_string(),818 other => self.rust_ty(other, cx)?,819 });820 }821 Some(format!(822 "let {n}_view: Vec<({})> = {n}.iter().map(|({})| ({})).collect();",823 tys.join(", "),824 names.join(", "),825 parts.join(", ")826 ))827 }828 _ => None,829 })830 }831832 fn self_param(&self, f: &Function, owner: &Type) -> Result<Option<(String, Ty)>> {833 let Some(kind) = f.self_kind else {834 return Ok(None);835 };836 let name = self.hand_name(owner);837 let inner = Ty::Hand {838 name: name.to_string(),839 dim: None,840 };841 let taken = f.params.iter().any(|p| p.name == hand_self(name));842 let label = if taken {843 "self_".to_string()844 } else {845 hand_self(name).to_string()846 };847 let ty = match (kind, name) {848 (SelfKind::Mut, _) => Ty::Ref {849 mutable: true,850 lifetime: None,851 item: Box::new(inner),852 },853 (_, "Rng") => return Err(format!("{}: an Rng self must be mutable", f.path)),854 _ => inner,855 };856 Ok(Some((label, ty)))857 }858859 // FUNCTIONS860861 fn rank_source(&self, params: &[(String, Ty, bool)]) -> Option<String> {862 fn cell(t: &Ty) -> bool {863 matches!(t, Ty::Hand { name, dim: None } if name == "CellNd")864 }865 fn shaped(t: &Ty) -> bool {866 matches!(t, Ty::Hand { name, .. } if name == "Tensor" || name == "Cell" || name == "CellNd")867 }868 for test in [cell, shaped] {869 for (name, ty, _) in params {870 let n = ident(name);871 match ty {872 t if test(t) => return Some(format!("hand::cell_rank(&{n})?")),873 Ty::Ref { item, .. } if test(item) => {874 return Some(format!("hand::cell_rank(&{n})?"))875 }876 Ty::Slice { item, .. } | Ty::Vec { item } if test(item) => {877 return Some(format!("hand::cell_rank(&hand::first(&{n})?)?"))878 }879 _ => {}880 }881 }882 }883 None884 }885886 fn emit_group(&self, out: &mut String, group: &[&Function], owner: Owner) -> Result<()> {887 let head = group[0];888 let dispatch = group.len() > 1 || !head.dims.is_empty();889 let name = match owner {890 Owner::Free | Owner::Hand(_) => self.local(&head.path),891 _ => head.name.clone(),892 };893 let mut merged: Vec<(String, Ty, bool)> = vec![];894 for f in group {895 if let Owner::Hand(t) = owner {896 if let Some((label, ty)) = self.self_param(f, t)? {897 if !merged.iter().any(|(n, _, _)| *n == label) {898 merged.push((label, ty, false));899 }900 }901 }902 if let Owner::Holder(t) = owner {903 if f.self_kind.is_some() {904 let label = t.name.to_lowercase();905 let label = if f.params.iter().any(|p| p.name == label) {906 "self_".to_string()907 } else {908 label909 };910 if !merged.iter().any(|(n, _, _)| *n == label) {911 merged.push((label, plain_ty(t), false));912 }913 }914 }915 for p in &f.params {916 if !merged.iter().any(|(n, _, _)| *n == p.name) {917 merged.push((p.name.clone(), p.ty.clone(), false));918 }919 }920 }921 let shared: Vec<String> = merged922 .iter()923 .filter(|(n, _, _)| group.iter().all(|f| self.mentions(f, owner, n)))924 .map(|(n, _, _)| n.clone())925 .collect();926 for slot in &mut merged {927 slot.2 = !shared.contains(&slot.0);928 }929 let cx = Cx {930 dim: head.dims.first().copied(),931 fname: &head.path,932 };933 let ret = self.ret_plan(&head.ret, cx)?;934 let mut sigs: Vec<String> = vec![];935 let mut plans: Vec<ParamPlan> = vec![];936 for (n, ty, extra) in &merged {937 let plan = self.plan_param(n, ty, cx, *extra)?;938 sigs.push(format!("{}: {}", plan.name, plan.sig));939 plans.push(plan);940 }941 if let Owner::Class(_) = owner {942 let receiver = match head.self_kind {943 Some(SelfKind::Mut) => Some("&mut self"),944 Some(_) => Some("&self"),945 None => None,946 };947 if let Some(receiver) = receiver {948 sigs.insert(0, receiver.to_string());949 }950 }951 let doc = doc_line(&head.docs);952 let indent = if matches!(owner, Owner::Class(_) | Owner::Holder(_)) {953 " "954 } else {955 ""956 };957 let constructor =958 matches!(owner, Owner::Class(_)) && head.name == "new" && head.self_kind.is_none();959 let renamed = match owner {960 Owner::Holder(_) => head.name == "new" || head.name == "default",961 Owner::Class(_) => head.name == "default",962 Owner::Free | Owner::Hand(_) => false,963 };964 if !doc.is_empty() {965 writeln!(out, "{indent}/// {doc}").ok();966 }967 if constructor {968 writeln!(out, "{indent}#[wasm_bindgen(constructor)]").ok();969 } else if renamed {970 writeln!(out, "{indent}#[wasm_bindgen(js_name = \"{}\")]", head.name).ok();971 } else if indent.is_empty() {972 writeln!(out, "#[wasm_bindgen]").ok();973 }974 let rust_name = if renamed {975 format!("{}_", head.name)976 } else if indent.is_empty() {977 self.export(&name)978 } else {979 name.clone()980 };981 writeln!(982 out,983 "{indent}pub fn {rust_name}({}) -> Result<{}, JsValue> {{",984 sigs.join(", "),985 ret.sig986 )987 .ok();988 if dispatch {989 let rank = self990 .rank_source(&merged)991 .ok_or_else(|| format!("{}: no cell to dispatch on", head.path))?;992 writeln!(out, "{indent} match {rank} {{").ok();993 let mut dims: Vec<u8> = group.iter().flat_map(|f| f.dims.iter().copied()).collect();994 dims.sort();995 dims.dedup();996 for dim in dims {997 let f = group998 .iter()999 .find(|f| f.dims.contains(&dim))1000 .ok_or_else(|| format!("{}: no entry for {dim}d", head.path))?;1001 let cx = Cx {1002 dim: Some(dim),1003 fname: &head.path,1004 };1005 let ret = self.ret_plan(&f.ret, cx)?;1006 let mut plans: Vec<ParamPlan> = vec![];1007 for (n, ty, extra) in &merged {1008 if !self.mentions(f, owner, n) {1009 continue;1010 }1011 plans.push(self.plan_param(n, ty, cx, *extra)?);1012 }1013 writeln!(out, "{indent} {dim} => {{").ok();1014 out.push_str(&self.emit_body(1015 f,1016 owner,1017 &plans,1018 &ret,1019 cx,1020 &format!("{indent} "),1021 )?);1022 writeln!(out, "{indent} }}").ok();1023 }1024 writeln!(1025 out,1026 "{indent} rank => Err(hand::refuse(&format!(\"a 2d or 3d cell was wanted, not {{rank}}d.\"))),"1027 )1028 .ok();1029 writeln!(out, "{indent} }}").ok();1030 } else {1031 out.push_str(&self.emit_body(1032 head,1033 owner,1034 &plans,1035 &ret,1036 cx,1037 &format!("{indent} "),1038 )?);1039 }1040 writeln!(out, "{indent}}}").ok();1041 Ok(())1042 }10431044 fn mentions(&self, f: &Function, owner: Owner, name: &str) -> bool {1045 if f.params.iter().any(|p| p.name == name) {1046 return true;1047 }1048 match owner {1049 Owner::Hand(t) => {1050 f.self_kind.is_some() && {1051 let label = hand_self(self.hand_name(t));1052 let taken = f.params.iter().any(|p| p.name == label);1053 (if taken { "self_" } else { label }) == name1054 }1055 }1056 Owner::Holder(t) => {1057 f.self_kind.is_some() && {1058 let label = t.name.to_lowercase();1059 let taken = f.params.iter().any(|p| p.name == label);1060 (if taken { "self_".to_string() } else { label }) == name1061 }1062 }1063 _ => false,1064 }1065 }10661067 fn emit_body(1068 &self,1069 f: &Function,1070 owner: Owner,1071 plans: &[ParamPlan],1072 ret: &RetPlan,1073 cx: Cx,1074 pad: &str,1075 ) -> Result<String> {1076 let mut out = String::new();1077 for plan in plans {1078 for line in &plan.pre {1079 writeln!(out, "{pad}{line}").ok();1080 }1081 }1082 let (receiver, rest): (Option<&ParamPlan>, &[ParamPlan]) = match owner {1083 Owner::Hand(_) | Owner::Holder(_) if f.self_kind.is_some() => {1084 (plans.first(), &plans[1..])1085 }1086 _ => (None, plans),1087 };1088 let args: Vec<String> = rest.iter().map(|p| p.arg.clone()).collect();1089 let args = args.join(", ");1090 let turbo = if f.dims.is_empty() {1091 String::new()1092 } else {1093 format!("::<{}>", cx.dim.unwrap_or(2))1094 };1095 let call = match owner {1096 Owner::Free => format!("mrlyrs::{}{turbo}({args})", f.path),1097 Owner::Hand(t) => match receiver {1098 Some(r) => {1099 let name = match r.post.is_empty() {1100 true => r.arg.trim_start_matches('&').to_string(),1101 false => format!("{}_value", r.name),1102 };1103 let name = if self.hand_name(t) == "Rng" {1104 r.arg.clone()1105 } else {1106 name1107 };1108 format!("{name}.{}({args})", f.name)1109 }1110 None => {1111 let base = self.rust_ty(1112 &Ty::Hand {1113 name: self.hand_name(t).into(),1114 dim: cx.dim,1115 },1116 cx,1117 )?;1118 let base = if self.hand_name(t) == "CellNd" {1119 format!(1120 "mrlyrs::math::cell::models::CellNd::<{}>",1121 cx.dim.unwrap_or(2)1122 )1123 } else {1124 base1125 };1126 format!("{base}::{}({args})", f.name)1127 }1128 },1129 Owner::Class(t) => {1130 let base = format!("mrlyrs::{}", t.path);1131 let copied = self.ty(&t.path).is_some_and(|t| derives(t, "Copy"));1132 let me = match f.self_kind {1133 Some(SelfKind::Value) if copied => "self.inner".to_string(),1134 Some(SelfKind::Value) => "self.inner.clone()".to_string(),1135 Some(SelfKind::Ref) => "&self.inner".to_string(),1136 Some(SelfKind::Mut) => "&mut self.inner".to_string(),1137 None => String::new(),1138 };1139 match (&f.via, f.self_kind) {1140 (Some(via), None) => format!("<{base} as mrlyrs::{via}>::{}({args})", f.name),1141 (Some(via), Some(_)) => {1142 let all = if args.is_empty() {1143 me1144 } else {1145 format!("{me}, {args}")1146 };1147 format!("<{base} as mrlyrs::{via}>::{}({all})", f.name)1148 }1149 (None, None) => format!("{base}::{}({args})", f.name),1150 (None, Some(SelfKind::Value)) => format!("{me}.{}({args})", f.name),1151 (None, Some(_)) => format!("self.inner.{}({args})", f.name),1152 }1153 }1154 Owner::Holder(t) => match receiver {1155 Some(r) => format!("{}.{}({args})", r.arg, f.name),1156 None => format!("mrlyrs::{}::{}({args})", t.path, f.name),1157 },1158 };1159 let fallible = matches!(f.ret, Ty::Result { .. });1160 let unit = match &f.ret {1161 Ty::Unit => true,1162 Ty::Result { item } => matches!(**item, Ty::Unit),1163 _ => false,1164 };1165 match (unit, fallible) {1166 (true, true) => writeln!(out, "{pad}{call}.map_err(hand::throw)?;").ok(),1167 (true, false) => writeln!(out, "{pad}{call};").ok(),1168 (false, true) => writeln!(out, "{pad}let value = {call}.map_err(hand::throw)?;").ok(),1169 (false, false) => writeln!(out, "{pad}let value = {call};").ok(),1170 };1171 for plan in plans {1172 for line in &plan.post {1173 writeln!(out, "{pad}{line}").ok();1174 }1175 }1176 writeln!(out, "{pad}{}", ret.done).ok();1177 Ok(out)1178 }11791180 fn emit_const(&self, out: &mut String, c: &Const) -> Result<()> {1181 let cx = Cx {1182 dim: None,1183 fname: &c.path,1184 };1185 let ret = self.ret_plan(&c.ty, cx)?;1186 let doc = doc_line(&c.docs);1187 if !doc.is_empty() {1188 writeln!(out, "/// {doc}").ok();1189 }1190 let name = self.export(&self.local(&c.path));1191 writeln!(out, "#[wasm_bindgen]").ok();1192 writeln!(out, "pub fn {name}() -> Result<{}, JsValue> {{", ret.sig).ok();1193 writeln!(out, " let value = mrlyrs::{};", c.path).ok();1194 writeln!(out, " {}", ret.done).ok();1195 writeln!(out, "}}").ok();1196 Ok(())1197 }11981199 fn getter_ok(&self, ty: &Ty) -> bool {1200 let mut ok = true;1201 ty.walk(&mut |t| match t {1202 Ty::Unknown { .. } | Ty::Opaque { .. } => ok = false,1203 Ty::Class { path, .. } if !self.holds(path) => ok = false,1204 Ty::Ref {1205 lifetime: Some(l),1206 item,1207 ..1208 } if l == "'static" && !matches!(**item, Ty::Str) => ok = false,1209 _ => {}1210 });1211 ok1212 }12131214 fn emit_class(&self, out: &mut String, t: &Type, groups: &[Vec<&Function>]) -> Result<()> {1215 let ident = self.local(&t.path);1216 let cx = Cx {1217 dim: None,1218 fname: &t.path,1219 };1220 let doc = doc_line(&t.docs);1221 if !doc.is_empty() {1222 writeln!(out, "/// {doc}").ok();1223 }1224 writeln!(out, "#[wasm_bindgen]").ok();1225 writeln!(1226 out,1227 "pub struct {ident} {{\n inner: mrlyrs::{},\n}}\n",1228 t.path1229 )1230 .ok();1231 writeln!(out, "#[wasm_bindgen]\nimpl {ident} {{").ok();1232 if derives(t, "Deserialize") {1233 writeln!(out, " /// Reads the {} from its plain data.", t.name).ok();1234 writeln!(out, " #[wasm_bindgen(js_name = \"from\")]").ok();1235 writeln!(1236 out,1237 " pub fn from_plain(data: JsValue) -> Result<{ident}, JsValue> {{"1238 )1239 .ok();1240 writeln!(1241 out,1242 " Ok({ident} {{ inner: hand::from_js(&data)? }})"1243 )1244 .ok();1245 writeln!(out, " }}").ok();1246 }1247 if derives(t, "Serialize") {1248 writeln!(out, " /// Writes the {} as plain data.", t.name).ok();1249 writeln!(out, " #[wasm_bindgen(js_name = \"toJSON\")]").ok();1250 writeln!(1251 out,1252 " pub fn to_plain(&self) -> Result<JsValue, JsValue> {{"1253 )1254 .ok();1255 writeln!(out, " hand::to_js(&self.inner)").ok();1256 writeln!(out, " }}").ok();1257 }1258 let methods: BTreeSet<&str> = groups.iter().map(|g| g[0].name.as_str()).collect();1259 for field in t.fields.iter().filter(|f| f.public) {1260 if !self.getter_ok(&field.ty) || methods.contains(field.name.as_str()) {1261 continue;1262 }1263 let ret = self.ret_plan(&field.ty, cx)?;1264 let doc = doc_line(&field.docs);1265 if !doc.is_empty() {1266 writeln!(out, " /// {doc}").ok();1267 }1268 writeln!(out, " #[wasm_bindgen(getter)]").ok();1269 writeln!(1270 out,1271 " pub fn {}(&self) -> Result<{}, JsValue> {{",1272 field.name, ret.sig1273 )1274 .ok();1275 let value = match field.ty {1276 Ty::Ref { .. } => format!("self.inner.{}", field.name),1277 _ if self.is_copy(&field.ty) => format!("self.inner.{}", field.name),1278 _ => format!("self.inner.{}.clone()", field.name),1279 };1280 writeln!(out, " let value = {value};").ok();1281 writeln!(out, " {}", ret.done).ok();1282 writeln!(out, " }}").ok();1283 if field.ty.settable() {1284 let plan = self.plan_param("value", &field.ty, cx, false)?;1285 writeln!(out, " #[wasm_bindgen(setter)]").ok();1286 writeln!(1287 out,1288 " pub fn set_{}(&mut self, {}: {}) -> Result<(), JsValue> {{",1289 field.name, plan.name, plan.sig1290 )1291 .ok();1292 for line in &plan.pre {1293 writeln!(out, " {line}").ok();1294 }1295 writeln!(out, " self.inner.{} = {};", field.name, plan.arg).ok();1296 for line in &plan.post {1297 writeln!(out, " {line}").ok();1298 }1299 writeln!(out, " Ok(())").ok();1300 writeln!(out, " }}").ok();1301 }1302 }1303 for group in groups {1304 self.emit_group(out, group, Owner::Class(t))?;1305 }1306 writeln!(out, "}}").ok();1307 Ok(())1308 }13091310 fn emit_holder(&self, out: &mut String, t: &Type, groups: &[Vec<&Function>]) -> Result<()> {1311 let ident = self.local(&t.path);1312 let doc = doc_line(&t.docs);1313 if !doc.is_empty() {1314 writeln!(out, "/// {doc}").ok();1315 }1316 writeln!(out, "#[wasm_bindgen]").ok();1317 writeln!(out, "pub struct {ident} {{}}\n").ok();1318 writeln!(out, "#[wasm_bindgen]\nimpl {ident} {{").ok();1319 for group in groups {1320 self.emit_group(out, group, Owner::Holder(t))?;1321 }1322 writeln!(out, "}}").ok();1323 Ok(())1324 }13251326 fn lib_rs(&self) -> Result<String> {1327 let mut out = String::new();1328 writeln!(1329 out,1330 "{ALLOWS}\n\nmod hand;\n\nuse wasm_bindgen::prelude::*;\n"1331 )1332 .ok();1333 let groups = self.groups();1334 let mut owned: BTreeMap<&str, Vec<Vec<&Function>>> = BTreeMap::new();1335 for group in &groups {1336 match self.owner_of(group[0]) {1337 Owner::Free | Owner::Hand(_) => {1338 self.emit_group(&mut out, group, self.owner_of(group[0]))?;1339 out.push('\n');1340 }1341 Owner::Class(t) | Owner::Holder(t) => {1342 owned1343 .entry(t.path.as_str())1344 .or_default()1345 .push(group.clone());1346 }1347 }1348 }1349 for c in self.consts() {1350 self.emit_const(&mut out, c)?;1351 out.push('\n');1352 }1353 for t in self.classes() {1354 let groups = owned.get(t.path.as_str()).cloned().unwrap_or_default();1355 self.emit_class(&mut out, t, &groups)?;1356 out.push('\n');1357 }1358 for t in self.holders() {1359 let groups = owned.get(t.path.as_str()).cloned().unwrap_or_default();1360 self.emit_holder(&mut out, t, &groups)?;1361 out.push('\n');1362 }1363 while out.ends_with("\n\n") {1364 out.pop();1365 }1366 Ok(out)1367 }13681369 // WRAPPER13701371 fn tree(&self) -> Tree {1372 let mut tree = Tree::default();1373 let mut seen: BTreeSet<String> = BTreeSet::new();1374 for f in self.functions() {1375 let (home, leaf) = match self.owner_of(f) {1376 Owner::Free | Owner::Hand(_) => (f.module.clone(), leaf(&f.path)),1377 Owner::Class(t) | Owner::Holder(t) => (parent(&t.path), t.name.clone()),1378 };1379 let key = format!("{home}::{leaf}");1380 if seen.insert(key) {1381 let export = match self.owner_of(f) {1382 Owner::Free | Owner::Hand(_) => self.export(&self.local(&f.path)),1383 Owner::Class(t) | Owner::Holder(t) => self.local(&t.path),1384 };1385 if JS_RESERVED.contains(&leaf.as_str()) {1386 tree.insert(self.rel(&home), format!("{leaf}_"), export.clone());1387 }1388 tree.insert(self.rel(&home), leaf, export);1389 }1390 }1391 for t in self.classes() {1392 let key = format!("{}::{}", parent(&t.path), t.name);1393 if seen.insert(key) {1394 tree.insert(1395 self.rel(&parent(&t.path)),1396 t.name.clone(),1397 self.local(&t.path),1398 );1399 }1400 }1401 for c in self.consts() {1402 let export = self.export(&self.local(&c.path));1403 tree.insert(self.rel(&parent(&c.path)), c.name.clone(), export);1404 }1405 tree1406 }14071408 fn wrapper_js(&self) -> String {1409 let name = self.name;1410 let mut out = String::new();1411 writeln!(1412 out,1413 "import * as wasm from \"./pkg/{name}/mrlyjs_{name}.js\";\n"1414 )1415 .ok();1416 writeln!(1417 out,1418 "export {{ default, initSync }} from \"./pkg/{name}/mrlyjs_{name}.js\";"1419 )1420 .ok();1421 writeln!(out, "export const Rng = wasm.Rng;").ok();1422 let tree = self.tree();1423 for (leaf, export) in &tree.leaves {1424 writeln!(out, "export const {leaf} = wasm.{export};").ok();1425 }1426 for (seg, child) in &tree.children {1427 writeln!(out, "export const {seg} = {};", child.literal(1)).ok();1428 }1429 out1430 }14311432 // TYPES14331434 fn ts_ref(&self, path: &str) -> String {1435 self.rel(path).replace("::", ".")1436 }14371438 fn ts_direct(&self, ty: &Ty, ret: bool) -> String {1439 match ty {1440 Ty::Unit => "void".into(),1441 Ty::Scalar { name } => match name.as_str() {1442 "bool" => "boolean".into(),1443 "char" => "string".into(),1444 "u64" | "i64" if ret => "bigint".into(),1445 "u64" | "i64" => "number | bigint".into(),1446 _ => "number".into(),1447 },1448 Ty::Str | Ty::String => "string".into(),1449 Ty::U128 | Ty::I128 | Ty::Code if ret => "string".into(),1450 Ty::U128 | Ty::I128 | Ty::Code => "string | number | bigint".into(),1451 Ty::Json => "any".into(),1452 Ty::Vec { item }1453 | Ty::Slice { item, .. }1454 | Ty::Array { item, .. }1455 | Ty::Set { item } => match &**item {1456 Ty::Scalar { name } if TYPED.contains(&name.as_str()) => {1457 if ret {1458 typed_array(name).into()1459 } else if is_wide(name) {1460 "ArrayLike<number | bigint>".into()1461 } else {1462 "ArrayLike<number>".into()1463 }1464 }1465 _ => format!("{}[]", self.ts_wrap(&self.ts_direct(item, ret))),1466 },1467 Ty::Option { item } => format!("{} | undefined", self.ts_direct(item, ret)),1468 Ty::Tuple { items } => format!(1469 "[{}]",1470 items1471 .iter()1472 .map(|t| self.ts_direct(t, ret))1473 .collect::<Vec<_>>()1474 .join(", ")1475 ),1476 Ty::Ref { item, .. } | Ty::Result { item } => self.ts_direct(item, ret),1477 Ty::Map { value, .. } => format!("Record<string, {}>", self.ts_direct(value, ret)),1478 Ty::Hand { name, .. } => match name.as_str() {1479 "Tensor" => "Tensor".into(),1480 "Cell" | "CellNd" => "Cell".into(),1481 "Cell6d" => "Cell6d".into(),1482 "Image" => "Image".into(),1483 "Color" => "Color".into(),1484 "Code" if ret => "string".into(),1485 "Code" => "string | number | bigint".into(),1486 _ => "Rng".into(),1487 },1488 Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } => self.ts_ref(path),1489 Ty::Opaque { .. } | Ty::Unknown { .. } => "unknown".into(),1490 }1491 }14921493 fn ts_wrap(&self, text: &str) -> String {1494 if text.contains(" | ") {1495 format!("({text})")1496 } else {1497 text.to_string()1498 }1499 }15001501 fn ts_serde(&self, ty: &Ty) -> String {1502 match ty {1503 Ty::Unit => "null".into(),1504 Ty::Scalar { name } => match name.as_str() {1505 "bool" => "boolean".into(),1506 "char" => "string".into(),1507 _ => "number".into(),1508 },1509 Ty::Str | Ty::String => "string".into(),1510 Ty::U128 | Ty::I128 | Ty::Code => "bigint".into(),1511 Ty::Json => "any".into(),1512 Ty::Vec { item }1513 | Ty::Slice { item, .. }1514 | Ty::Array { item, .. }1515 | Ty::Set { item } => {1516 format!("{}[]", self.ts_wrap(&self.ts_serde(item)))1517 }1518 Ty::Option { item } => format!("{} | undefined", self.ts_serde(item)),1519 Ty::Tuple { items } => format!(1520 "[{}]",1521 items1522 .iter()1523 .map(|t| self.ts_serde(t))1524 .collect::<Vec<_>>()1525 .join(", ")1526 ),1527 Ty::Ref { item, .. } | Ty::Result { item } => self.ts_serde(item),1528 Ty::Map { value, .. } => format!("Record<string, {}>", self.ts_serde(value)),1529 Ty::Hand { name, .. } => match name.as_str() {1530 "Tensor" => "TensorData".into(),1531 "Cell" => "CellData".into(),1532 "CellNd" => "{ cell: CellData }".into(),1533 "Cell6d" => "Cell6dData".into(),1534 "Image" => "{ width: number; height: number; colors: number[][] }".into(),1535 "Color" => "ColorData".into(),1536 "Code" => "bigint".into(),1537 _ => "unknown".into(),1538 },1539 Ty::Plain { path } | Ty::Enum { path } => self.ts_ref(path),1540 Ty::Class { path, .. } => format!("{}Data", self.ts_ref(path)),1541 Ty::Opaque { .. } | Ty::Unknown { .. } => "string".into(),1542 }1543 }15441545 fn ts_params(&self, params: &[(String, Ty, bool)]) -> String {1546 let last_required = params1547 .iter()1548 .rposition(|(_, ty, extra)| !matches!(ty, Ty::Option { .. }) && !extra)1549 .map(|i| i + 1)1550 .unwrap_or(0);1551 params1552 .iter()1553 .enumerate()1554 .map(|(i, (name, ty, extra))| {1555 let n = ident(name);1556 let inner = match ty {1557 Ty::Option { item } => item,1558 _ => ty,1559 };1560 if *extra || (i >= last_required && matches!(ty, Ty::Option { .. })) {1561 format!("{n}?: {}", self.ts_direct(inner, false))1562 } else {1563 format!("{n}: {}", self.ts_direct(ty, false))1564 }1565 })1566 .collect::<Vec<_>>()1567 .join(", ")1568 }15691570 fn ts_function(&self, group: &[&Function], owner: Owner) -> (String, String, String) {1571 let head = group[0];1572 let mut merged: Vec<(String, Ty, bool)> = vec![];1573 for f in group {1574 match owner {1575 Owner::Hand(t) => {1576 if let Ok(Some((label, ty))) = self.self_param(f, t) {1577 if !merged.iter().any(|(n, _, _)| *n == label) {1578 merged.push((label, ty, false));1579 }1580 }1581 }1582 Owner::Holder(t) if f.self_kind.is_some() => {1583 let label = t.name.to_lowercase();1584 let label = if f.params.iter().any(|p| p.name == label) {1585 "self_".to_string()1586 } else {1587 label1588 };1589 if !merged.iter().any(|(n, _, _)| *n == label) {1590 merged.push((label, plain_ty(t), false));1591 }1592 }1593 _ => {}1594 }1595 for p in &f.params {1596 if !merged.iter().any(|(n, _, _)| *n == p.name) {1597 merged.push((p.name.clone(), p.ty.clone(), false));1598 }1599 }1600 }1601 for slot in &mut merged {1602 slot.2 = !group.iter().all(|f| self.mentions(f, owner, &slot.0));1603 }1604 let ret = match &head.ret {1605 Ty::Result { item } => self.ts_direct(item, true),1606 other => self.ts_direct(other, true),1607 };1608 (doc_line(&head.docs), self.ts_params(&merged), ret)1609 }16101611 fn referenced(&self) -> BTreeSet<String> {1612 let mut out: BTreeSet<String> = BTreeSet::new();1613 let mut queue: Vec<String> = vec![];1614 let note = |ty: &Ty, queue: &mut Vec<String>| {1615 ty.walk(&mut |t| {1616 if let Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } = t {1617 queue.push(path.clone());1618 }1619 })1620 };1621 for f in self.functions() {1622 for p in &f.params {1623 note(&p.ty, &mut queue);1624 }1625 note(&f.ret, &mut queue);1626 }1627 for c in self.consts() {1628 note(&c.ty, &mut queue);1629 }1630 for t in self.classes() {1631 queue.push(t.path.clone());1632 }1633 for t in self.holders() {1634 queue.push(t.path.clone());1635 }1636 while let Some(path) = queue.pop() {1637 if !out.insert(path.clone()) {1638 continue;1639 }1640 let Some(t) = self.ty(&path) else { continue };1641 for field in &t.fields {1642 note(&field.ty, &mut queue);1643 }1644 for v in &t.variants {1645 for field in &v.fields {1646 note(&field.ty, &mut queue);1647 }1648 }1649 }1650 out1651 }16521653 fn ts_type_decl(&self, t: &Type, groups: &[Vec<&Function>], pad: &str) -> Result<String> {1654 let mut out = String::new();1655 let doc = doc_line(&t.docs);1656 let name = &t.name;1657 match &t.cross {1658 TypeCross::Enum { .. } => {1659 let words: Vec<String> = t1660 .variants1661 .iter()1662 .map(|v| enum_word(t, &v.name, &v.serde))1663 .collect();1664 let union = words1665 .iter()1666 .map(|w| format!("{w:?}"))1667 .collect::<Vec<_>>()1668 .join(" | ");1669 if !doc.is_empty() {1670 writeln!(out, "{pad}/** {doc} */").ok();1671 }1672 writeln!(out, "{pad}export type {name} = {union};").ok();1673 }1674 TypeCross::Plain if t.kind == TypeKind::Alias => {1675 if let Some(alias) = &t.alias {1676 writeln!(out, "{pad}export type {name} = {};", self.ts_serde(alias)).ok();1677 }1678 }1679 TypeCross::Plain if t.kind == TypeKind::Enum => {1680 if !doc.is_empty() {1681 writeln!(out, "{pad}/** {doc} */").ok();1682 }1683 writeln!(out, "{pad}export type {name} = {};", self.ts_union(t)).ok();1684 }1685 TypeCross::Plain => {1686 if !doc.is_empty() {1687 writeln!(out, "{pad}/** {doc} */").ok();1688 }1689 writeln!(out, "{pad}export interface {name} {{").ok();1690 self.ts_fields(&mut out, t, &format!("{pad} "), true);1691 writeln!(out, "{pad}}}").ok();1692 }1693 TypeCross::Class => {1694 let all_public = t.fields.iter().all(|f| f.public);1695 let data = format!("{name}Data");1696 if t.kind == TypeKind::Enum {1697 writeln!(out, "{pad}export type {data} = {};", self.ts_union(t)).ok();1698 } else if all_public {1699 writeln!(out, "{pad}export interface {data} {{").ok();1700 self.ts_fields(&mut out, t, &format!("{pad} "), false);1701 writeln!(out, "{pad}}}").ok();1702 } else {1703 writeln!(out, "{pad}export type {data} = Record<string, unknown>;").ok();1704 }1705 if !doc.is_empty() {1706 writeln!(out, "{pad}/** {doc} */").ok();1707 }1708 writeln!(out, "{pad}export class {name} {{").ok();1709 let inner = format!("{pad} ");1710 let constructor = groups1711 .iter()1712 .find(|g| g[0].name == "new" && g[0].self_kind.is_none());1713 match constructor {1714 Some(g) => {1715 let (d, params, _) = self.ts_function(g, Owner::Class(t));1716 if !d.is_empty() {1717 writeln!(out, "{inner}/** {d} */").ok();1718 }1719 writeln!(out, "{inner}constructor({params});").ok();1720 }1721 None => {1722 writeln!(out, "{inner}private constructor();").ok();1723 }1724 }1725 writeln!(out, "{inner}free(): void;").ok();1726 if derives(t, "Deserialize") {1727 writeln!(out, "{inner}/** Reads the {name} from its plain data. */").ok();1728 writeln!(out, "{inner}static from(data: {data}): {name};").ok();1729 }1730 if derives(t, "Serialize") {1731 writeln!(out, "{inner}/** Writes the {name} as plain data. */").ok();1732 writeln!(out, "{inner}toJSON(): {data};").ok();1733 }1734 let methods: BTreeSet<&str> = groups.iter().map(|g| g[0].name.as_str()).collect();1735 for field in t.fields.iter().filter(|f| f.public) {1736 if !self.getter_ok(&field.ty) || methods.contains(field.name.as_str()) {1737 continue;1738 }1739 let d = doc_line(&field.docs);1740 if !d.is_empty() {1741 writeln!(out, "{inner}/** {d} */").ok();1742 }1743 let out_ty = self.ts_direct(&field.ty, true);1744 if field.ty.settable() {1745 writeln!(out, "{inner}get {}(): {out_ty};", field.name).ok();1746 writeln!(1747 out,1748 "{inner}set {}(value: {});",1749 field.name,1750 self.ts_direct(&field.ty, false)1751 )1752 .ok();1753 } else {1754 writeln!(out, "{inner}readonly {}: {out_ty};", ident(&field.name)).ok();1755 }1756 }1757 for g in groups {1758 let f = g[0];1759 if f.name == "new" && f.self_kind.is_none() {1760 continue;1761 }1762 let (d, params, ret) = self.ts_function(g, Owner::Class(t));1763 if !d.is_empty() {1764 writeln!(out, "{inner}/** {d} */").ok();1765 }1766 let prefix = if f.self_kind.is_none() { "static " } else { "" };1767 writeln!(out, "{inner}{prefix}{}({params}): {ret};", f.name).ok();1768 }1769 writeln!(out, "{pad}}}").ok();1770 }1771 TypeCross::Hand { .. } | TypeCross::Uncrossable { .. } => {}1772 }1773 if matches!(t.cross, TypeCross::Plain | TypeCross::Enum { .. }) && !groups.is_empty() {1774 writeln!(out, "{pad}export const {name}: {{").ok();1775 for g in groups {1776 let f = g[0];1777 let (d, params, ret) = self.ts_function(g, Owner::Holder(t));1778 if !d.is_empty() {1779 writeln!(out, "{pad} /** {d} */").ok();1780 }1781 let label = if f.name == "new" {1782 "\"new\"".to_string()1783 } else {1784 f.name.clone()1785 };1786 writeln!(out, "{pad} {label}({params}): {ret};").ok();1787 }1788 writeln!(out, "{pad}}};").ok();1789 }1790 Ok(out)1791 }17921793 fn ts_union(&self, t: &Type) -> String {1794 let untagged = t.serde.iter().any(|s| s == "untagged");1795 let mut arms = vec![];1796 for v in &t.variants {1797 let word = enum_word(t, &v.name, &v.serde);1798 let body = if v.fields.is_empty() {1799 None1800 } else if v.fields.iter().all(|f| f.name.parse::<usize>().is_ok()) {1801 Some(if v.fields.len() == 1 {1802 self.ts_serde(&v.fields[0].ty)1803 } else {1804 format!(1805 "[{}]",1806 v.fields1807 .iter()1808 .map(|f| self.ts_serde(&f.ty))1809 .collect::<Vec<_>>()1810 .join(", ")1811 )1812 })1813 } else {1814 Some(format!(1815 "{{ {} }}",1816 v.fields1817 .iter()1818 .map(|f| format!("{}: {}", f.name, self.ts_serde(&f.ty)))1819 .collect::<Vec<_>>()1820 .join("; ")1821 ))1822 };1823 arms.push(match (untagged, body) {1824 (true, None) => "null".to_string(),1825 (false, None) => format!("{word:?}"),1826 (true, Some(body)) => body,1827 (false, Some(body)) => format!("{{ {word}: {body} }}"),1828 });1829 }1830 arms.join(" | ")1831 }18321833 fn ts_fields(&self, out: &mut String, t: &Type, pad: &str, all: bool) {1834 for field in t1835 .fields1836 .iter()1837 .filter(|f| (all || f.public) && !f.serde_skip)1838 {1839 let d = doc_line(&field.docs);1840 if !d.is_empty() {1841 writeln!(out, "{pad}/** {d} */").ok();1842 }1843 let (name, ty) = match &field.ty {1844 Ty::Option { item } => (format!("{}?", ident(&field.name)), self.ts_serde(item)),1845 other => (ident(&field.name), self.ts_serde(other)),1846 };1847 writeln!(out, "{pad}{name}: {ty};").ok();1848 }1849 }18501851 fn types_dts(&self) -> Result<String> {1852 let name = self.name;1853 let mut out = String::new();1854 writeln!(1855 out,1856 "export {{ default, initSync }} from \"./pkg/{name}/mrlyjs_{name}.js\";\n"1857 )1858 .ok();1859 let words = |path: &str| -> String {1860 self.ty(path)1861 .map(|t| {1862 t.variants1863 .iter()1864 .map(|v| enum_word(t, &v.name, &v.serde))1865 .collect::<Vec<String>>()1866 })1867 .unwrap_or_default()1868 .iter()1869 .map(|w| format!("{w:?}"))1870 .collect::<Vec<_>>()1871 .join(" | ")1872 };1873 let projection = words("math::six::Projection");1874 let orientation = words("math::six::Orientation");1875 writeln!(out, "/** An rgba color as four bytes. */").ok();1876 writeln!(out, "export type Color = [number, number, number, number];").ok();1877 writeln!(1878 out,1879 "/** A tensor: its shape and its flat data as a typed array of its dtype. */"1880 )1881 .ok();1882 writeln!(out, "export interface Tensor {{\n shape: number[];\n data: Uint8Array | Uint16Array | Uint32Array | Int32Array;\n}}").ok();1883 writeln!(out, "/** A cell: the shape, the type bytes, and the flat rgba colors and the tags when present. */").ok();1884 writeln!(out, "export interface Cell {{\n shape: number[];\n types: Uint8Array | Uint16Array | Uint32Array | Int32Array;\n colors?: Uint8Array;\n tags?: Uint8Array | Uint16Array | Uint32Array | Int32Array;\n}}").ok();1885 writeln!(1886 out,1887 "/** A hex cell: a flat cell with its projection, orientation and start row. */"1888 )1889 .ok();1890 writeln!(out, "export interface Cell6d {{\n cell: Cell;\n projection: {projection};\n orientation: {orientation};\n start: number;\n}}").ok();1891 writeln!(1892 out,1893 "/** An image: its height and width, and its flat rgba colors row by row. */"1894 )1895 .ok();1896 writeln!(1897 out,1898 "export interface Image {{\n shape: [number, number];\n colors: Uint8Array;\n}}"1899 )1900 .ok();1901 writeln!(out, "/** A color inside plain data, serde's form. */").ok();1902 writeln!(out, "export interface ColorData {{\n r: number;\n g: number;\n b: number;\n a: number;\n}}").ok();1903 writeln!(out, "/** A tensor inside plain data, serde's form. */").ok();1904 writeln!(out, "export interface TensorData {{\n shape: number[];\n data: {{ U8: number[] }} | {{ U16: number[] }} | {{ U32: number[] }} | {{ I32: number[] }};\n}}").ok();1905 writeln!(out, "/** A cell inside plain data, serde's form. */").ok();1906 writeln!(out, "export interface CellData {{\n types: TensorData;\n colors?: number[][];\n tags?: TensorData;\n}}").ok();1907 writeln!(out, "/** A hex cell inside plain data, serde's form. */").ok();1908 writeln!(out, "export interface Cell6dData {{\n cell: {{ cell: CellData }};\n projection: {projection};\n orientation: {orientation};\n start: number;\n}}").ok();1909 writeln!(1910 out,1911 "/** A seeded random stream, opened from a number or a bigint seed. */"1912 )1913 .ok();1914 writeln!(out, "export class Rng {{\n constructor(seed: number | bigint | string);\n free(): void;\n /** Draws a float at or above zero and below one. */\n unit(): number;\n /** Draws an integer below n, or zero when n is zero. */\n below(n: number): number;\n /** Draws an integer between lo and hi inclusive, or lo when hi is not above lo. */\n range(lo: number, hi: number): number;\n /** Draws a fair coin flip. */\n boolean(): boolean;\n /** Returns true with probability p. */\n chance(p: number): boolean;\n /** Draws amount distinct indices below length, or every index when amount is larger. */\n sample_indices(length: number, amount: number): Uint32Array;\n /** Draws one item of the array, the same draw as Rust's choice. */\n choice<T>(items: ArrayLike<T>): T;\n /** Shuffles the array in place, the same permutation as Rust's shuffle. */\n shuffle<T>(items: T[]): void;\n}}").ok();1915 let mut tree = DeclTree::default();1916 let groups = self.groups();1917 let mut owned: BTreeMap<&str, Vec<Vec<&Function>>> = BTreeMap::new();1918 for group in &groups {1919 match self.owner_of(group[0]) {1920 Owner::Free | Owner::Hand(_) => {1921 let (d, params, ret) = self.ts_function(group, self.owner_of(group[0]));1922 let mut text = String::new();1923 if !d.is_empty() {1924 text.push_str(&format!("/** {d} */\n"));1925 }1926 text.push_str(&format!(1927 "export function {}({params}): {ret};\n",1928 ts_leaf(&leaf(&group[0].path))1929 ));1930 tree.insert(self.rel(&group[0].module), text);1931 }1932 Owner::Class(t) | Owner::Holder(t) => {1933 owned1934 .entry(t.path.as_str())1935 .or_default()1936 .push(group.clone());1937 }1938 }1939 }1940 for c in self.consts() {1941 let mut text = String::new();1942 let d = doc_line(&c.docs);1943 if !d.is_empty() {1944 text.push_str(&format!("/** {d} */\n"));1945 }1946 text.push_str(&format!(1947 "export function {}(): {};\n",1948 c.name,1949 self.ts_direct(&c.ty, true)1950 ));1951 tree.insert(self.rel(&parent(&c.path)), text);1952 }1953 for path in self.referenced() {1954 let Some(t) = self.ty(&path) else { continue };1955 if matches!(1956 t.cross,1957 TypeCross::Hand { .. } | TypeCross::Uncrossable { .. }1958 ) {1959 continue;1960 }1961 let groups = owned.get(path.as_str()).cloned().unwrap_or_default();1962 let text = self.ts_type_decl(t, &groups, "")?;1963 tree.insert(self.rel(&parent(&t.path)), text);1964 }1965 out.push_str(&tree.render(0));1966 while out.ends_with("\n\n") {1967 out.pop();1968 }1969 Ok(out)1970 }1971}19721973// TREES19741975#[derive(Default)]1976struct Tree {1977 leaves: BTreeMap<String, String>,1978 children: BTreeMap<String, Tree>,1979}19801981impl Tree {1982 fn insert(&mut self, module: &str, leaf: String, export: String) {1983 let mut node = self;1984 for seg in module.split("::").filter(|s| !s.is_empty()) {1985 node = node.children.entry(seg.to_string()).or_default();1986 }1987 node.leaves.insert(leaf, export);1988 }19891990 fn literal(&self, depth: usize) -> String {1991 let pad = " ".repeat(depth);1992 let mut out = String::from("{\n");1993 for (leaf, export) in &self.leaves {1994 out.push_str(&format!("{pad}{leaf}: wasm.{export},\n"));1995 }1996 for (seg, child) in &self.children {1997 out.push_str(&format!("{pad}{seg}: {},\n", child.literal(depth + 1)));1998 }1999 out.push_str(&" ".repeat(depth - 1));2000 out.push('}');2001 out2002 }2003}20042005#[derive(Default)]2006struct DeclTree {2007 texts: Vec<String>,2008 children: BTreeMap<String, DeclTree>,2009}20102011impl DeclTree {2012 fn insert(&mut self, module: &str, text: String) {2013 let mut node = self;2014 for seg in module.split("::").filter(|s| !s.is_empty()) {2015 node = node.children.entry(seg.to_string()).or_default();2016 }2017 node.texts.push(text);2018 }20192020 fn render(&self, depth: usize) -> String {2021 let pad = " ".repeat(depth);2022 let mut out = String::new();2023 for text in &self.texts {2024 for line in text.lines() {2025 out.push_str(&pad);2026 out.push_str(line);2027 out.push('\n');2028 }2029 }2030 for (seg, child) in &self.children {2031 let keyword = if depth == 0 {2032 "export declare namespace"2033 } else {2034 "export namespace"2035 };2036 out.push_str(&format!("{pad}{keyword} {seg} {{\n"));2037 out.push_str(&child.render(depth + 1));2038 out.push_str(&format!("{pad}}}\n"));2039 }2040 out2041 }2042}20432044// RULES20452046fn deref(e: &str) -> String {2047 match e.strip_prefix('&') {2048 Some(inner) => inner.to_string(),2049 None => format!("*{e}"),2050 }2051}20522053fn place(e: &str) -> String {2054 e.strip_prefix('&').unwrap_or(e).to_string()2055}20562057fn closure(x: &str, body: String) -> String {2058 let tail = format!("({x})");2059 match body.strip_suffix(&tail) {2060 Some(path)2061 if !path.contains(x) && !path.contains(' ') || path.starts_with("hand::from_js::<") =>2062 {2063 path.to_string()2064 }2065 _ => format!("|{x}| {body}"),2066 }2067}20682069fn ok(expr: String) -> String {2070 match expr.strip_suffix('?') {2071 Some(inner) => inner.to_string(),2072 None => format!("Ok({expr})"),2073 }2074}20752076fn is_wide(name: &str) -> bool {2077 name == "u64" || name == "i64"2078}20792080fn is_rng(ty: &Ty) -> bool {2081 matches!(ty, Ty::Hand { name, .. } if name == "Rng")2082}20832084fn typed_scalar(ty: &Ty) -> bool {2085 matches!(ty, Ty::Scalar { name } if TYPED.contains(&name.as_str()))2086}20872088fn native_item(ty: &Ty) -> bool {2089 matches!(ty, Ty::Scalar { name } if NATIVE_ITEM.contains(&name.as_str()))2090}20912092fn typed_array(name: &str) -> &'static str {2093 match name {2094 "u8" => "Uint8Array",2095 "u16" => "Uint16Array",2096 "u32" | "usize" => "Uint32Array",2097 "i8" => "Int8Array",2098 "i16" => "Int16Array",2099 "i32" => "Int32Array",2100 "f32" => "Float32Array",2101 "f64" => "Float64Array",2102 "u64" => "BigUint64Array",2103 _ => "BigInt64Array",2104 }2105}21062107fn pure_in(ty: &Ty) -> bool {2108 match ty {2109 Ty::Unit | Ty::Str | Ty::String | Ty::Json | Ty::Plain { .. } | Ty::Enum { .. } => true,2110 Ty::Scalar { name } => !is_wide(name),2111 Ty::Vec { item }2112 | Ty::Slice { item, .. }2113 | Ty::Option { item }2114 | Ty::Array { item, .. } => pure_in(item),2115 Ty::Ref {2116 mutable: false,2117 item,2118 ..2119 } => pure_in(item),2120 Ty::Tuple { items } => items.iter().all(pure_in),2121 _ => false,2122 }2123}21242125fn pure_out(ty: &Ty) -> bool {2126 match ty {2127 Ty::Unit | Ty::Str | Ty::String | Ty::Json | Ty::Plain { .. } | Ty::Enum { .. } => true,2128 Ty::Scalar { name } => !is_wide(name),2129 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Array { item, .. } => {2130 !typed_scalar(item) && pure_out(item)2131 }2132 Ty::Option { item } => pure_out(item),2133 Ty::Tuple { items } => items.iter().all(pure_out),2134 _ => false,2135 }2136}21372138fn plain_ty(t: &Type) -> Ty {2139 match t.cross {2140 TypeCross::Enum { .. } => Ty::Enum {2141 path: t.path.clone(),2142 },2143 _ => Ty::Plain {2144 path: t.path.clone(),2145 },2146 }2147}21482149fn hand_self(name: &str) -> &'static str {2150 match name {2151 "Tensor" => "tensor",2152 "Image" => "image",2153 "Color" => "color",2154 "Code" => "code",2155 "Rng" => "rng",2156 _ => "cell",2157 }2158}21592160fn suffix_dim(fname: &str) -> u8 {2161 if fname.ends_with("_3d") || fname.ends_with("_6d") {2162 32163 } else {2164 22165 }2166}21672168fn ident(name: &str) -> String {2169 if WORDS.contains(&name) {2170 format!("{name}_")2171 } else {2172 name.to_string()2173 }2174}21752176fn derives(t: &Type, what: &str) -> bool {2177 t.derives.iter().any(|d| d == what)2178}21792180fn doc_line(docs: &[String]) -> String {2181 docs.first()2182 .map(|d| d.trim().to_string())2183 .unwrap_or_default()2184}21852186fn ts_leaf(name: &str) -> String {2187 if JS_RESERVED.contains(&name) {2188 format!("{name}_")2189 } else {2190 name.to_string()2191 }2192}21932194fn leaf(path: &str) -> String {2195 path.rsplit("::").next().unwrap_or(path).to_string()2196}21972198fn parent(path: &str) -> String {2199 match path.rfind("::") {2200 Some(at) => path[..at].to_string(),2201 None => String::new(),2202 }2203}22042205fn enum_word(t: &Type, variant: &str, serde: &[String]) -> String {2206 for attr in serde {2207 if let Some(rest) = attr.trim().strip_prefix("rename = ") {2208 return rest.trim().trim_matches('"').to_string();2209 }2210 }2211 for attr in &t.serde {2212 if let Some(rest) = attr.trim().strip_prefix("rename_all = ") {2213 let rule = rest.trim().trim_matches('"');2214 return rename_all(variant, rule);2215 }2216 }2217 variant.to_string()2218}22192220fn rename_all(name: &str, rule: &str) -> String {2221 let words: Vec<String> = {2222 let mut out: Vec<String> = vec![];2223 for c in name.chars() {2224 if c.is_uppercase() || out.is_empty() {2225 out.push(String::new());2226 }2227 out.last_mut().unwrap().push(c);2228 }2229 out.into_iter().map(|w| w.to_lowercase()).collect()2230 };2231 match rule {2232 "lowercase" => name.to_lowercase(),2233 "UPPERCASE" => name.to_uppercase(),2234 "snake_case" => words.join("_"),2235 "SCREAMING_SNAKE_CASE" => words.join("_").to_uppercase(),2236 "kebab-case" => words.join("-"),2237 "SCREAMING-KEBAB-CASE" => words.join("-").to_uppercase(),2238 "camelCase" => {2239 let mut c = name.chars();2240 match c.next() {2241 Some(first) => first.to_lowercase().collect::<String>() + c.as_str(),2242 None => String::new(),2243 }2244 }2245 _ => name.to_string(),2246 }2247}22482249// TEST22502251const MANIFEST_TEST: &str = r#"import { expect, test } from "bun:test";22522253const manifest = await Bun.file(new URL("../bridge/manifest.json", import.meta.url)).json();2254const units = (await Bun.file(new URL("../bridge/units.txt", import.meta.url)).text())2255 .split("\n")2256 .map((line) => line.trim())2257 .filter(Boolean);2258const pkg = await Bun.file(new URL("./package.json", import.meta.url)).json();2259const kinds = new Map(manifest.types.map((t) => [t.path, t.cross.kind]));22602261function parent(path) {2262 const at = path.lastIndexOf("::");2263 return at < 0 ? "" : path.slice(0, at);2264}22652266function relative(unit, path) {2267 return unit === "all" ? path : path.slice(unit.length + 2);2268}22692270function walk(mod, path) {2271 let node = mod;2272 for (const seg of path.split("::").filter(Boolean)) {2273 if (node === undefined || node === null) return undefined;2274 node = node[seg];2275 }2276 return node;2277}22782279function holds(unit, path) {2280 return unit === "all" || path.split("::")[0] === unit;2281}22822283function check(mod, unit, f) {2284 const kind = f.owner ? kinds.get(f.owner) : undefined;2285 if (kind === "class") {2286 const cls = walk(mod, relative(unit, f.owner));2287 if (typeof cls !== "function") return false;2288 if (f.self_kind === null && f.name === "new") return true;2289 if (f.self_kind === null) return typeof cls[f.name] === "function";2290 return typeof cls.prototype[f.name] === "function";2291 }2292 if (kind === "plain" || kind === "enum") {2293 const holder = walk(mod, relative(unit, f.owner));2294 return typeof holder === "function" && typeof holder[f.name] === "function";2295 }2296 return typeof walk(mod, relative(unit, f.path)) === "function";2297}22982299for (const unit of units) {2300 test(`${unit} exports every ok name`, async () => {2301 expect(pkg.exports[unit === "all" ? "." : `./${unit}`]).toBeDefined();2302 const mod = await import(`./${unit}.js`);2303 const bytes = await Bun.file(new URL(`./pkg/${unit}/mrlyjs_${unit}_bg.wasm`, import.meta.url)).arrayBuffer();2304 mod.initSync({ module: bytes });2305 expect(typeof mod.Rng).toBe("function");2306 const missing = [];2307 const names = new Set();2308 for (const f of manifest.functions) {2309 if (f.cross.status !== "ok" || !holds(unit, f.module)) continue;2310 names.add(f.path);2311 if (!check(mod, unit, f)) missing.push(f.path);2312 }2313 for (const c of manifest.consts) {2314 if (c.cross.status !== "ok" || !holds(unit, c.path)) continue;2315 names.add(c.path);2316 if (typeof walk(mod, relative(unit, c.path)) !== "function") missing.push(c.path);2317 }2318 for (const t of manifest.types) {2319 if (t.cross.kind !== "class" || !holds(unit, t.path)) continue;2320 names.add(t.path);2321 if (typeof walk(mod, relative(unit, t.path)) !== "function") missing.push(t.path);2322 }2323 expect(missing).toEqual([]);2324 console.log(`${unit}: ${names.size} names`);2325 });2326}2327"#;