js.rs
79.9 kB · rust · 2002 lines
1use crate::model::{Const, Cross, Function, Manifest, Result, SelfKind, Ty, Type, TypeCross, TypeKind};2use std::collections::{BTreeMap, BTreeSet};3use std::fmt::Write as _;4use std::path::Path;56const TYPED: &[&str] = &[7 "u8", "u16", "u32", "i8", "i16", "i32", "f32", "f64", "usize", "u64", "i64",8];910const NATIVE_ITEM: &[&str] = &["u8", "u16", "u32", "i8", "i16", "i32", "f32", "f64", "usize"];1112const WORDS: &[&str] = &[13 "void", "new", "class", "function", "default", "delete", "in", "var", "let", "const", "export",14 "import", "switch", "case", "for", "if", "else", "this", "super", "with", "yield", "enum",15 "await", "typeof", "instanceof", "return", "try", "catch", "finally", "throw", "while", "do",16 "break", "continue", "debugger", "static", "interface", "package", "private", "protected",17 "public", "implements", "arguments", "eval", "null", "true", "false", "extends", "as", "crate",18 "extern", "fn", "impl", "loop", "match", "mod", "move", "mut", "pub", "ref", "self", "struct",19 "trait", "type", "unsafe", "use", "where", "async", "dyn", "box", "macro", "gen",20];2122const RNG_METHODS: &[&str] = &[23 "new", "unit", "below", "range", "boolean", "chance", "sample_indices", "choice", "shuffle",24 "__state", "__restore",25];2627const JS_RESERVED: &[&str] = &[28 "new", "void", "class", "function", "default", "delete", "in", "var", "let", "const", "export",29 "import", "switch", "case", "for", "if", "else", "this", "super", "with", "yield", "enum",30 "await", "typeof", "instanceof", "return", "try", "catch", "finally", "throw", "while", "do",31 "break", "continue", "debugger", "static", "interface", "package", "private", "protected",32 "public", "implements", "null", "true", "false", "extends",33];3435const HAND_RS: &str = "#[path = \"../../../hand/hand.rs\"]\nmod crossing;\n\npub use crossing::*;\n";3637const ALLOWS: &str = "#![allow(non_camel_case_types, non_snake_case, clippy::too_many_arguments)]";3839// FILES4041pub fn write(manifest: &Manifest, root: &Path) -> Result<()> {42 let names = std::fs::read_to_string(root.join("bridge/units.txt")).map_err(|e| e.to_string())?;43 let names: Vec<&str> = names44 .lines()45 .map(str::trim)46 .filter(|l| !l.is_empty())47 .collect();48 let js = root.join("pkgs/mrlyjs");49 for name in &names {50 let unit = Unit::new(manifest, name);51 let dir = js.join("units").join(name);52 save(&dir.join("Cargo.toml"), &cargo_toml(name))?;53 save(&dir.join("src/hand.rs"), HAND_RS)?;54 save(&dir.join("src/lib.rs"), &unit.lib_rs()?)?;55 save(&js.join(format!("{name}.js")), &unit.wrapper_js())?;56 save(&js.join(format!("{name}.d.ts")), &unit.types_dts()?)?;57 }58 save(&js.join("manifest.test.js"), MANIFEST_TEST)?;59 Ok(())60}6162fn save(path: &Path, text: &str) -> Result<()> {63 if let Some(parent) = path.parent() {64 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;65 }66 if std::fs::read_to_string(path).ok().as_deref() == Some(text) {67 return Ok(());68 }69 std::fs::write(path, text).map_err(|e| format!("{}: {e}", path.display()))70}7172fn cargo_toml(name: &str) -> String {73 let what = if name == "all" {74 "every module of mrlyrs".to_string()75 } else {76 format!("the {name} module of mrlyrs")77 };78 format!(79 "[package]\nname = \"mrlyjs_{name}\"\nversion = \"0.2.0\"\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"80 )81}8283// UNIT8485struct Unit<'a> {86 name: &'a str,87 manifest: &'a Manifest,88 types: BTreeMap<&'a str, &'a Type>,89 reserved: BTreeSet<String>,90}9192#[derive(Clone, Copy)]93enum Owner<'a> {94 Free,95 Hand(&'a Type),96 Class(&'a Type),97 Holder(&'a Type),98}99100#[derive(Clone, Copy)]101struct Cx<'a> {102 dim: Option<u8>,103 fname: &'a str,104}105106struct ParamPlan {107 name: String,108 sig: String,109 pre: Vec<String>,110 arg: String,111 post: Vec<String>,112}113114struct RetPlan {115 sig: String,116 done: String,117}118119impl<'a> Unit<'a> {120 fn new(manifest: &'a Manifest, name: &'a str) -> Unit<'a> {121 let mut unit = Unit {122 name,123 manifest,124 types: manifest.types.iter().map(|t| (t.path.as_str(), t)).collect(),125 reserved: BTreeSet::new(),126 };127 unit.reserved = unit.symbols();128 unit129 }130131 fn symbols(&self) -> BTreeSet<String> {132 let mut out = BTreeSet::new();133 for method in RNG_METHODS {134 out.insert(format!("rng_{method}"));135 }136 for t in self.classes().into_iter().chain(self.holders()) {137 let lower = self.local(&t.path).to_lowercase();138 for f in self.functions() {139 if f.owner.as_deref() == Some(t.path.as_str()) {140 out.insert(format!("{lower}_{}", f.name));141 }142 }143 for extra in ["from", "toJSON", "new"] {144 out.insert(format!("{lower}_{extra}"));145 }146 }147 out148 }149150 fn export(&self, name: &str) -> String {151 let mut out = name.to_string();152 while self.reserved.contains(&out) {153 out.push('_');154 }155 out156 }157158 fn holds(&self, path: &str) -> bool {159 self.name == "all" || path.split("::").next() == Some(self.name)160 }161162 fn rel<'p>(&self, path: &'p str) -> &'p str {163 if self.name == "all" {164 return path;165 }166 match path.strip_prefix(self.name) {167 Some("") => "",168 Some(rest) => rest.strip_prefix("::").unwrap_or(path),169 None => path,170 }171 }172173 fn local(&self, path: &str) -> String {174 self.rel(path).replace("::", "_")175 }176177 fn ty(&self, path: &str) -> Option<&'a Type> {178 self.types.get(path).copied()179 }180181 fn functions(&self) -> Vec<&'a Function> {182 self.manifest183 .functions184 .iter()185 .filter(|f| f.cross == Cross::Ok && self.holds(&f.module))186 .collect()187 }188189 fn groups(&self) -> Vec<Vec<&'a Function>> {190 let mut out: Vec<Vec<&'a Function>> = vec![];191 for f in self.functions() {192 match out.last_mut() {193 Some(group) if group[0].path == f.path => group.push(f),194 _ => out.push(vec![f]),195 }196 }197 out198 }199200 fn consts(&self) -> Vec<&'a Const> {201 self.manifest202 .consts203 .iter()204 .filter(|c| c.cross == Cross::Ok && self.holds(&c.path))205 .collect()206 }207208 fn classes(&self) -> Vec<&'a Type> {209 self.manifest210 .types211 .iter()212 .filter(|t| t.cross == TypeCross::Class && self.holds(&t.path))213 .collect()214 }215216 fn holders(&self) -> Vec<&'a Type> {217 let owners: BTreeSet<&str> = self218 .functions()219 .iter()220 .filter_map(|f| f.owner.as_deref())221 .collect();222 self.manifest223 .types224 .iter()225 .filter(|t| {226 matches!(t.cross, TypeCross::Plain | TypeCross::Enum { .. })227 && self.holds(&t.path)228 && owners.contains(t.path.as_str())229 })230 .collect()231 }232233 fn owner_of(&self, f: &Function) -> Owner<'a> {234 let Some(path) = f.owner.as_deref() else {235 return Owner::Free;236 };237 match self.ty(path).map(|t| (t, &t.cross)) {238 Some((t, TypeCross::Hand { .. })) => Owner::Hand(t),239 Some((t, TypeCross::Class)) => Owner::Class(t),240 Some((t, TypeCross::Plain | TypeCross::Enum { .. })) => Owner::Holder(t),241 _ => Owner::Free,242 }243 }244245 fn hand_name(&self, t: &Type) -> &'static str {246 match &t.cross {247 TypeCross::Hand { name } => match name.as_str() {248 "Tensor" => "Tensor",249 "Cell" => "Cell",250 "CellNd" => "CellNd",251 "Cell6d" => "Cell6d",252 "Color" => "Color",253 "Code" => "Code",254 _ => "Rng",255 },256 _ => "Rng",257 }258 }259260 // RUST TYPES261262 fn rust_ty(&self, ty: &Ty, cx: Cx) -> Result<String> {263 Ok(match ty {264 Ty::Unit => "()".into(),265 Ty::Scalar { name } => name.clone(),266 Ty::Str | Ty::String => "String".into(),267 Ty::U128 => "u128".into(),268 Ty::I128 => "i128".into(),269 Ty::Code => "mrlyrs::math::bang::Code".into(),270 Ty::Json => "mrlyrs::core::Json".into(),271 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => {272 format!("Vec<{}>", self.rust_ty(item, cx)?)273 }274 Ty::Option { item } => format!("Option<{}>", self.rust_ty(item, cx)?),275 Ty::Tuple { items } => {276 let parts: Result<Vec<String>> =277 items.iter().map(|t| self.rust_ty(t, cx)).collect();278 format!("({})", parts?.join(", "))279 }280 Ty::Array { item, len } => format!("[{}; {len}]", self.rust_ty(item, cx)?),281 Ty::Ref { item, .. } | Ty::Result { item } => self.rust_ty(item, cx)?,282 Ty::Map { key, value } => format!(283 "std::collections::HashMap<{}, {}>",284 self.rust_ty(key, cx)?,285 self.rust_ty(value, cx)?286 ),287 Ty::Hand { name, dim } => match name.as_str() {288 "Tensor" => "mrlyrs::core::Tensor".into(),289 "Cell" => "mrlyrs::core::Cell".into(),290 "CellNd" => match dim.or(cx.dim) {291 Some(2) => "mrlyrs::math::two::Cell2d".into(),292 Some(3) => "mrlyrs::math::three::Cell3d".into(),293 _ => return Err(format!("{}: a cell without a dimension", cx.fname)),294 },295 "Cell6d" => "mrlyrs::math::six::Cell6d".into(),296 "Color" => "mrlyrs::core::Color".into(),297 "Code" => "mrlyrs::math::bang::Code".into(),298 "Rng" => "mrlyrs::core::Rng".into(),299 other => return Err(format!("{}: unknown hand type {other}", cx.fname)),300 },301 Ty::Plain { path } => {302 let generic = self.ty(path).is_some_and(|t| t.const_generic);303 if generic {304 format!("mrlyrs::{path}<{}>", suffix_dim(cx.fname))305 } else {306 format!("mrlyrs::{path}")307 }308 }309 Ty::Enum { path } | Ty::Class { path, .. } => format!("mrlyrs::{path}"),310 Ty::Opaque { path } => return Err(format!("{}: {path} cannot cross", cx.fname)),311 Ty::Unknown { text } => return Err(format!("{}: {text} cannot cross", cx.fname)),312 })313 }314315 fn is_copy(&self, ty: &Ty) -> bool {316 match ty {317 Ty::Unit | Ty::Scalar { .. } | Ty::U128 | Ty::I128 | Ty::Code => true,318 Ty::Hand { name, .. } => name == "Color" || name == "Code",319 Ty::Tuple { items } => items.iter().all(|t| self.is_copy(t)),320 Ty::Array { item, .. } | Ty::Option { item } => self.is_copy(item),321 Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } => {322 self.ty(path).is_some_and(|t| derives(t, "Copy"))323 }324 _ => false,325 }326 }327328 // CROSSINGS IN329330 fn cross_in(&self, ty: &Ty, e: &str, cx: Cx, depth: usize) -> Result<String> {331 if pure_in(ty) {332 return Ok(format!("hand::from_js::<{}>({e})?", self.rust_ty(ty, cx)?));333 }334 let x = format!("x{depth}");335 Ok(match ty {336 Ty::Scalar { name } => match name.as_str() {337 "u64" => format!("hand::u64_from_js({e})?"),338 "i64" => format!("hand::i64_from_js({e})?"),339 "bool" => format!("hand::bool_from_js({e})?"),340 "char" => format!("hand::char_from_js({e})?"),341 other => format!("(hand::number_from_js({e})? as {other})"),342 },343 Ty::Str | Ty::String => format!("hand::string_from_js({e})?"),344 Ty::U128 => format!("hand::u128_from_js({e})?"),345 Ty::I128 => format!("hand::i128_from_js({e})?"),346 Ty::Code => format!("hand::code_from_js({e})?"),347 Ty::Json => format!("hand::json_from_js({e})?"),348 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Set { item } => format!(349 "hand::list_from_js({e}, {})?",350 closure(&x, ok(self.cross_in(item, &x, cx, depth + 1)?))351 ),352 Ty::Option { item } => format!(353 "hand::option_from_js({e}, {})?",354 closure(&x, ok(self.cross_in(item, &x, cx, depth + 1)?))355 ),356 Ty::Tuple { items } => {357 let mut parts = vec![];358 for (i, t) in items.iter().enumerate() {359 parts.push(self.cross_in(t, &format!("&hand::item({e}, {i})?"), cx, depth + 1)?);360 }361 format!("({})", parts.join(", "))362 }363 Ty::Array { item, len } => format!(364 "hand::array_from_js::<_, {len}>({e}, {})?",365 closure(&x, ok(self.cross_in(item, &x, cx, depth + 1)?))366 ),367 Ty::Ref { item, .. } | Ty::Result { item } => self.cross_in(item, e, cx, depth)?,368 Ty::Map { key, value } => format!(369 "hand::map_from_js::<{}, _>({e}, {})?",370 self.rust_ty(key, cx)?,371 closure(&x, ok(self.cross_in(value, &x, cx, depth + 1)?))372 ),373 Ty::Hand { name, dim } => match name.as_str() {374 "Tensor" => format!("hand::tensor_from_js({e})?"),375 "Cell" => format!("hand::cell_from_js({e})?"),376 "CellNd" => match dim.or(cx.dim) {377 Some(2) => format!("hand::cell2d_from_js({e})?"),378 Some(3) => format!("hand::cell3d_from_js({e})?"),379 _ => return Err(format!("{}: a cell without a dimension", cx.fname)),380 },381 "Cell6d" => format!("hand::cell6d_from_js({e})?"),382 "Color" => format!("hand::color_from_js({e})?"),383 "Code" => format!("hand::code_from_js({e})?"),384 other => return Err(format!("{}: {other} cannot cross in by value", cx.fname)),385 },386 Ty::Class { .. } => format!(387 "hand::from_js::<{}>(&hand::plain({e})?)?",388 self.rust_ty(ty, cx)?389 ),390 Ty::Unit | Ty::Plain { .. } | Ty::Enum { .. } => {391 format!("hand::from_js::<{}>({e})?", self.rust_ty(ty, cx)?)392 }393 Ty::Opaque { path } => return Err(format!("{}: {path} cannot cross", cx.fname)),394 Ty::Unknown { text } => return Err(format!("{}: {text} cannot cross", cx.fname)),395 })396 }397398 // CROSSINGS OUT399400 fn cross_out(&self, ty: &Ty, e: &str, cx: Cx, depth: usize) -> Result<String> {401 if pure_out(ty) {402 return Ok(format!("hand::to_js({e})?"));403 }404 let x = format!("x{depth}");405 Ok(match ty {406 Ty::Unit => "JsValue::UNDEFINED".into(),407 Ty::Scalar { name } => match name.as_str() {408 "u64" | "i64" => format!("JsValue::from({})", deref(e)),409 "bool" => format!("JsValue::from_bool({})", deref(e)),410 "char" => format!("JsValue::from_str(&{}.to_string())", place(e)),411 _ => format!("JsValue::from_f64({} as f64)", deref(e)),412 },413 Ty::Str | Ty::String => format!("JsValue::from_str({e})"),414 Ty::U128 | Ty::I128 => format!("JsValue::from_str(&{}.to_string())", place(e)),415 Ty::Code => format!("JsValue::from_str(&hand::code_to_js({}))", deref(e)),416 Ty::Json => format!("hand::json_to_js({e})?"),417 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Array { item, .. }418 if typed_scalar(item) =>419 {420 format!("hand::typed(&({})[..])", deref(e))421 }422 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Array { item, .. } | Ty::Set { item } => {423 format!(424 "hand::list_to_js({e}, {})?",425 closure(&x, ok(self.cross_out(item, &x, cx, depth + 1)?))426 )427 }428 Ty::Option { item } => match &**item {429 Ty::Ref { item: inner, .. } => format!(430 "hand::option_to_js({}, {})?",431 deref(e),432 closure(&x, ok(self.cross_out(inner, &x, cx, depth + 1)?))433 ),434 _ => format!(435 "hand::option_to_js({}.as_ref(), {})?",436 place(e),437 closure(&x, ok(self.cross_out(item, &x, cx, depth + 1)?))438 ),439 },440 Ty::Tuple { items } => {441 let mut parts = vec![];442 for (i, t) in items.iter().enumerate() {443 parts.push(self.cross_out(t, &format!("&{}.{i}", place(e)), cx, depth + 1)?);444 }445 format!("hand::tuple_to_js(&[{}])", parts.join(", "))446 }447 Ty::Ref { item, .. } | Ty::Result { item } => {448 self.cross_out(item, &format!("({})", deref(e)), cx, depth)?449 }450 Ty::Map { value, .. } => format!(451 "hand::map_to_js({e}, {})?",452 closure(&x, ok(self.cross_out(value, &x, cx, depth + 1)?))453 ),454 Ty::Hand { name, dim } => match name.as_str() {455 "Tensor" => format!("hand::tensor_to_js({e})?"),456 "Cell" => format!("hand::cell_to_js({e})?"),457 "CellNd" => match dim.or(cx.dim) {458 Some(2) => format!("hand::cell2d_to_js({e})?"),459 Some(3) => format!("hand::cell3d_to_js({e})?"),460 _ => return Err(format!("{}: a cell without a dimension", cx.fname)),461 },462 "Cell6d" => format!("hand::cell6d_to_js({e})?"),463 "Color" => format!("hand::color_to_js({})", deref(e)),464 "Code" => format!("JsValue::from_str(&hand::code_to_js({}))", deref(e)),465 _ => format!("JsValue::from(hand::Rng::wrap({}.clone()))", place(e)),466 },467 Ty::Class { path, .. } if self.holds(path) => {468 let inner = if self.is_copy(ty) { deref(e) } else { format!("{}.clone()", place(e)) };469 format!("JsValue::from({} {{ inner: {inner} }})", self.local(path))470 }471 Ty::Class { .. } | Ty::Plain { .. } | Ty::Enum { .. } => format!("hand::to_js({e})?"),472 Ty::Opaque { path } => return Err(format!("{}: {path} cannot cross", cx.fname)),473 Ty::Unknown { text } => return Err(format!("{}: {text} cannot cross", cx.fname)),474 })475 }476477 fn ret_plan(&self, ty: &Ty, cx: Cx) -> Result<RetPlan> {478 let plan = |sig: &str, done: String| RetPlan {479 sig: sig.to_string(),480 done,481 };482 Ok(match ty {483 Ty::Unit => plan("()", "Ok(())".into()),484 Ty::Result { item } => self.ret_plan(item, cx)?,485 Ty::Scalar { name } => plan(name, "Ok(value)".into()),486 Ty::String => plan("String", "Ok(value)".into()),487 Ty::Str => plan("String", "Ok(value.to_string())".into()),488 Ty::Vec { item } if typed_scalar(item) => {489 plan(&format!("Vec<{}>", self.rust_ty(item, cx)?), "Ok(value)".into())490 }491 Ty::Slice { item, .. } | Ty::Array { item, .. } if typed_scalar(item) => plan(492 &format!("Vec<{}>", self.rust_ty(item, cx)?),493 "Ok(value.to_vec())".into(),494 ),495 Ty::Class { path, .. } if self.holds(path) => {496 let ident = self.local(path);497 plan(&ident, format!("Ok({ident} {{ inner: value }})"))498 }499 Ty::Option { item } => match &**item {500 Ty::Class { path, .. } if self.holds(path) => {501 let ident = self.local(path);502 plan(503 &format!("Option<{ident}>"),504 format!("Ok(value.map(|inner| {ident} {{ inner }}))"),505 )506 }507 _ => plan("JsValue", ok(self.cross_out(ty, "&value", cx, 1)?)),508 },509 Ty::Hand { name, .. } if name == "Rng" => {510 plan("hand::Rng", "Ok(hand::Rng::wrap(value))".into())511 }512 Ty::Ref { item, .. } => match &**item {513 Ty::Slice { item: inner, .. } | Ty::Array { item: inner, .. }514 if typed_scalar(inner) =>515 {516 plan(517 &format!("Vec<{}>", self.rust_ty(inner, cx)?),518 "Ok(value.to_vec())".into(),519 )520 }521 Ty::Str => plan("String", "Ok(value.to_string())".into()),522 _ => plan("JsValue", ok(self.cross_out(item, "value", cx, 1)?)),523 },524 Ty::Slice { .. } => plan("JsValue", ok(self.cross_out(ty, "value", cx, 1)?)),525 _ => plan("JsValue", ok(self.cross_out(ty, "&value", cx, 1)?)),526 })527 }528529 // PARAMETERS530531 fn plan_param(&self, name: &str, ty: &Ty, cx: Cx, extra: bool) -> Result<ParamPlan> {532 let n = ident(name);533 let js = |pre: Vec<String>, arg: String, post: Vec<String>| ParamPlan {534 name: n.clone(),535 sig: "JsValue".into(),536 pre,537 arg,538 post,539 };540 let native = |sig: String, arg: String| ParamPlan {541 name: n.clone(),542 sig,543 pre: vec![],544 arg,545 post: vec![],546 };547 let read = |inner: &Ty| -> Result<String> {548 let converted = self.cross_in(inner, &format!("&{n}"), cx, 1)?;549 Ok(if extra {550 let want = format!("a {}d cell wants {name}.", cx.dim.unwrap_or(0));551 format!("let {n} = if {n}.is_undefined() {{ return Err(hand::refuse(\"{want}\")); }} else {{ {converted} }};")552 } else {553 format!("let {n} = {converted};")554 })555 };556 let owned = |inner: &Ty, arg: String| -> Result<ParamPlan> {557 Ok(js(vec![read(inner)?], arg, vec![]))558 };559 if !extra {560 match ty {561 Ty::Scalar { name } if !is_wide(name) => return Ok(native(name.clone(), n.clone())),562 Ty::Str => return Ok(native("&str".into(), n.clone())),563 Ty::String => return Ok(native("String".into(), n.clone())),564 Ty::Slice { item, .. } if native_item(item) => {565 return Ok(native(format!("&[{}]", self.rust_ty(item, cx)?), n.clone()))566 }567 Ty::Vec { item } if native_item(item) => {568 return Ok(native(format!("Vec<{}>", self.rust_ty(item, cx)?), n.clone()))569 }570 Ty::Class { path, .. } if self.holds(path) => {571 let taken = if self.is_copy(ty) { format!("{n}.inner") } else { format!("{n}.inner.clone()") };572 return Ok(native(format!("&{}", self.local(path)), taken));573 }574 Ty::Ref { mutable, item, .. } => match &**item {575 Ty::Class { path, .. } if self.holds(path) => {576 let ident = self.local(path);577 return Ok(if *mutable {578 native(format!("&mut {ident}"), format!("&mut {n}.inner"))579 } else {580 native(format!("&{ident}"), format!("&{n}.inner"))581 });582 }583 Ty::Hand { name, .. } if name == "Rng" && *mutable => {584 return Ok(native("&mut hand::Rng".into(), format!("{n}.stream()")))585 }586 _ => {}587 },588 _ => {}589 }590 }591 Ok(match ty {592 Ty::Ref { mutable: true, item, .. } => match &**item {593 Ty::Hand { name, .. } if name == "Tensor" || name == "Cell" => {594 let back = if name == "Tensor" { "tensor_into_js" } else { "cell_into_js" };595 let converted = self.cross_in(item, &format!("&{n}"), cx, 1)?;596 js(597 vec![format!("let mut {n}_value = {converted};")],598 format!("&mut {n}_value"),599 vec![format!("hand::{back}(&{n}, &{n}_value)?;")],600 )601 }602 _ => return Err(format!("{}: {name} mutates a value that cannot cross", cx.fname)),603 },604 Ty::Ref { item, .. } => owned(item, format!("&{n}"))?,605 Ty::Slice { item, .. } => match self.view(item, &n, cx)? {606 Some(seen) => {607 let mut pre = vec![read(ty)?];608 pre.push(seen);609 js(pre, format!("&{n}_view"), vec![])610 }611 None => owned(ty, format!("&{n}"))?,612 },613 Ty::Option { item } => match &**item {614 Ty::Ref { mutable: true, item: inner, .. } if is_rng(inner) => js(615 vec![format!("let mut {n}_stream = hand::stream_from_js(&{n})?;")],616 format!("{n}_stream.as_mut()"),617 vec![format!(618 "if let Some(stream) = &{n}_stream {{ hand::stream_to_js(&{n}, stream)?; }}"619 )],620 ),621 Ty::Ref { item: inner, .. } if matches!(**inner, Ty::Slice { .. }) => {622 owned(ty, format!("{n}.as_deref()"))?623 }624 Ty::Ref { .. } => owned(ty, format!("{n}.as_ref()"))?,625 Ty::Slice { .. } => owned(ty, format!("{n}.as_deref()"))?,626 _ => owned(ty, n.clone())?,627 },628 _ => owned(ty, n.clone())?,629 })630 }631632 fn view(&self, item: &Ty, n: &str, cx: Cx) -> Result<Option<String>> {633 Ok(match item {634 Ty::Str => Some(format!(635 "let {n}_view: Vec<&str> = {n}.iter().map(String::as_str).collect();"636 )),637 Ty::Slice { item: inner, .. } => Some(format!(638 "let {n}_view: Vec<&[{}]> = {n}.iter().map(Vec::as_slice).collect();",639 self.rust_ty(inner, cx)?640 )),641 Ty::Tuple { items } if items.iter().any(|t| matches!(t, Ty::Str)) => {642 let names: Vec<String> = (0..items.len()).map(|i| format!("a{i}")).collect();643 let parts: Vec<String> = items644 .iter()645 .zip(&names)646 .map(|(t, a)| match t {647 Ty::Str => format!("{a}.as_str()"),648 Ty::Scalar { .. } => format!("*{a}"),649 _ => format!("{a}.clone()"),650 })651 .collect();652 let mut tys = vec![];653 for t in items {654 tys.push(match t {655 Ty::Str => "&str".to_string(),656 other => self.rust_ty(other, cx)?,657 });658 }659 Some(format!(660 "let {n}_view: Vec<({})> = {n}.iter().map(|({})| ({})).collect();",661 tys.join(", "),662 names.join(", "),663 parts.join(", ")664 ))665 }666 _ => None,667 })668 }669670 fn self_param(&self, f: &Function, owner: &Type) -> Result<Option<(String, Ty)>> {671 let Some(kind) = f.self_kind else {672 return Ok(None);673 };674 let name = self.hand_name(owner);675 let inner = Ty::Hand {676 name: name.to_string(),677 dim: None,678 };679 let taken = f.params.iter().any(|p| p.name == hand_self(name));680 let label = if taken { "self_".to_string() } else { hand_self(name).to_string() };681 let ty = match (kind, name) {682 (SelfKind::Mut, _) => Ty::Ref {683 mutable: true,684 lifetime: None,685 item: Box::new(inner),686 },687 (_, "Rng") => return Err(format!("{}: an Rng self must be mutable", f.path)),688 _ => inner,689 };690 Ok(Some((label, ty)))691 }692693 // FUNCTIONS694695 fn rank_source(&self, params: &[(String, Ty, bool)]) -> Option<String> {696 fn cell(t: &Ty) -> bool {697 matches!(t, Ty::Hand { name, dim: None } if name == "CellNd")698 }699 fn shaped(t: &Ty) -> bool {700 matches!(t, Ty::Hand { name, .. } if name == "Tensor" || name == "Cell" || name == "CellNd")701 }702 for test in [cell, shaped] {703 for (name, ty, _) in params {704 let n = ident(name);705 match ty {706 t if test(t) => return Some(format!("hand::cell_rank(&{n})?")),707 Ty::Ref { item, .. } if test(item) => {708 return Some(format!("hand::cell_rank(&{n})?"))709 }710 Ty::Slice { item, .. } | Ty::Vec { item } if test(item) => {711 return Some(format!("hand::cell_rank(&hand::first(&{n})?)?"))712 }713 _ => {}714 }715 }716 }717 None718 }719720 fn emit_group(&self, out: &mut String, group: &[&Function], owner: Owner) -> Result<()> {721 let head = group[0];722 let dispatch = group.len() > 1 || !head.dims.is_empty();723 let name = match owner {724 Owner::Free | Owner::Hand(_) => self.local(&head.path),725 _ => head.name.clone(),726 };727 let mut merged: Vec<(String, Ty, bool)> = vec![];728 for f in group {729 if let Owner::Hand(t) = owner {730 if let Some((label, ty)) = self.self_param(f, t)? {731 if !merged.iter().any(|(n, _, _)| *n == label) {732 merged.push((label, ty, false));733 }734 }735 }736 if let Owner::Holder(t) = owner {737 if f.self_kind.is_some() {738 let label = t.name.to_lowercase();739 let label = if f.params.iter().any(|p| p.name == label) {740 "self_".to_string()741 } else {742 label743 };744 if !merged.iter().any(|(n, _, _)| *n == label) {745 merged.push((label, plain_ty(t), false));746 }747 }748 }749 for p in &f.params {750 if !merged.iter().any(|(n, _, _)| *n == p.name) {751 merged.push((p.name.clone(), p.ty.clone(), false));752 }753 }754 }755 let shared: Vec<String> = merged756 .iter()757 .filter(|(n, _, _)| group.iter().all(|f| self.mentions(f, owner, n)))758 .map(|(n, _, _)| n.clone())759 .collect();760 for slot in &mut merged {761 slot.2 = !shared.contains(&slot.0);762 }763 let cx = Cx {764 dim: head.dims.first().copied(),765 fname: &head.path,766 };767 let ret = self.ret_plan(&head.ret, cx)?;768 let mut sigs: Vec<String> = vec![];769 let mut plans: Vec<ParamPlan> = vec![];770 for (n, ty, extra) in &merged {771 let plan = self.plan_param(n, ty, cx, *extra)?;772 sigs.push(format!("{}: {}", plan.name, plan.sig));773 plans.push(plan);774 }775 if let Owner::Class(_) = owner {776 let receiver = match head.self_kind {777 Some(SelfKind::Mut) => Some("&mut self"),778 Some(_) => Some("&self"),779 None => None,780 };781 if let Some(receiver) = receiver {782 sigs.insert(0, receiver.to_string());783 }784 }785 let doc = doc_line(&head.docs);786 let indent = if matches!(owner, Owner::Class(_) | Owner::Holder(_)) { " " } else { "" };787 let constructor = matches!(owner, Owner::Class(_)) && head.name == "new" && head.self_kind.is_none();788 let renamed = match owner {789 Owner::Holder(_) => head.name == "new" || head.name == "default",790 Owner::Class(_) => head.name == "default",791 Owner::Free | Owner::Hand(_) => false,792 };793 if !doc.is_empty() {794 writeln!(out, "{indent}/// {doc}").ok();795 }796 if constructor {797 writeln!(out, "{indent}#[wasm_bindgen(constructor)]").ok();798 } else if renamed {799 writeln!(out, "{indent}#[wasm_bindgen(js_name = \"{}\")]", head.name).ok();800 } else if indent.is_empty() {801 writeln!(out, "#[wasm_bindgen]").ok();802 }803 let rust_name = if renamed {804 format!("{}_", head.name)805 } else if indent.is_empty() {806 self.export(&name)807 } else {808 name.clone()809 };810 writeln!(811 out,812 "{indent}pub fn {rust_name}({}) -> Result<{}, JsValue> {{",813 sigs.join(", "),814 ret.sig815 )816 .ok();817 if dispatch {818 let rank = self819 .rank_source(&merged)820 .ok_or_else(|| format!("{}: no cell to dispatch on", head.path))?;821 writeln!(out, "{indent} match {rank} {{").ok();822 let mut dims: Vec<u8> = group.iter().flat_map(|f| f.dims.iter().copied()).collect();823 dims.sort();824 dims.dedup();825 for dim in dims {826 let f = group827 .iter()828 .find(|f| f.dims.contains(&dim))829 .ok_or_else(|| format!("{}: no entry for {dim}d", head.path))?;830 let cx = Cx {831 dim: Some(dim),832 fname: &head.path,833 };834 let ret = self.ret_plan(&f.ret, cx)?;835 let mut plans: Vec<ParamPlan> = vec![];836 for (n, ty, extra) in &merged {837 if !self.mentions(f, owner, n) {838 continue;839 }840 plans.push(self.plan_param(n, ty, cx, *extra)?);841 }842 writeln!(out, "{indent} {dim} => {{").ok();843 out.push_str(&self.emit_body(f, owner, &plans, &ret, cx, &format!("{indent} "))?);844 writeln!(out, "{indent} }}").ok();845 }846 writeln!(847 out,848 "{indent} rank => Err(hand::refuse(&format!(\"a 2d or 3d cell was wanted, not {{rank}}d.\"))),"849 )850 .ok();851 writeln!(out, "{indent} }}").ok();852 } else {853 out.push_str(&self.emit_body(head, owner, &plans, &ret, cx, &format!("{indent} "))?);854 }855 writeln!(out, "{indent}}}").ok();856 Ok(())857 }858859 fn mentions(&self, f: &Function, owner: Owner, name: &str) -> bool {860 if f.params.iter().any(|p| p.name == name) {861 return true;862 }863 match owner {864 Owner::Hand(t) => f.self_kind.is_some() && {865 let label = hand_self(self.hand_name(t));866 let taken = f.params.iter().any(|p| p.name == label);867 (if taken { "self_" } else { label }) == name868 },869 Owner::Holder(t) => f.self_kind.is_some() && {870 let label = t.name.to_lowercase();871 let taken = f.params.iter().any(|p| p.name == label);872 (if taken { "self_".to_string() } else { label }) == name873 },874 _ => false,875 }876 }877878 fn emit_body(879 &self,880 f: &Function,881 owner: Owner,882 plans: &[ParamPlan],883 ret: &RetPlan,884 cx: Cx,885 pad: &str,886 ) -> Result<String> {887 let mut out = String::new();888 for plan in plans {889 for line in &plan.pre {890 writeln!(out, "{pad}{line}").ok();891 }892 }893 let (receiver, rest): (Option<&ParamPlan>, &[ParamPlan]) = match owner {894 Owner::Hand(_) | Owner::Holder(_) if f.self_kind.is_some() => {895 (plans.first(), &plans[1..])896 }897 _ => (None, plans),898 };899 let args: Vec<String> = rest.iter().map(|p| p.arg.clone()).collect();900 let args = args.join(", ");901 let turbo = if f.dims.is_empty() { String::new() } else { format!("::<{}>", cx.dim.unwrap_or(2)) };902 let call = match owner {903 Owner::Free => format!("mrlyrs::{}{turbo}({args})", f.path),904 Owner::Hand(t) => match receiver {905 Some(r) => {906 let name = match r.post.is_empty() {907 true => r.arg.trim_start_matches('&').to_string(),908 false => format!("{}_value", r.name),909 };910 let name = if self.hand_name(t) == "Rng" { r.arg.clone() } else { name };911 format!("{name}.{}({args})", f.name)912 }913 None => {914 let base = self.rust_ty(&Ty::Hand { name: self.hand_name(t).into(), dim: cx.dim }, cx)?;915 let base = if self.hand_name(t) == "CellNd" {916 format!("mrlyrs::math::cell::models::CellNd::<{}>", cx.dim.unwrap_or(2))917 } else {918 base919 };920 format!("{base}::{}({args})", f.name)921 }922 },923 Owner::Class(t) => {924 let base = format!("mrlyrs::{}", t.path);925 let copied = self.ty(&t.path).is_some_and(|t| derives(t, "Copy"));926 let me = match f.self_kind {927 Some(SelfKind::Value) if copied => "self.inner".to_string(),928 Some(SelfKind::Value) => "self.inner.clone()".to_string(),929 Some(SelfKind::Ref) => "&self.inner".to_string(),930 Some(SelfKind::Mut) => "&mut self.inner".to_string(),931 None => String::new(),932 };933 match (&f.via, f.self_kind) {934 (Some(via), None) => format!("<{base} as mrlyrs::{via}>::{}({args})", f.name),935 (Some(via), Some(_)) => {936 let all = if args.is_empty() { me } else { format!("{me}, {args}") };937 format!("<{base} as mrlyrs::{via}>::{}({all})", f.name)938 }939 (None, None) => format!("{base}::{}({args})", f.name),940 (None, Some(SelfKind::Value)) => format!("{me}.{}({args})", f.name),941 (None, Some(_)) => format!("self.inner.{}({args})", f.name),942 }943 }944 Owner::Holder(t) => match receiver {945 Some(r) => format!("{}.{}({args})", r.arg, f.name),946 None => format!("mrlyrs::{}::{}({args})", t.path, f.name),947 },948 };949 let fallible = matches!(f.ret, Ty::Result { .. });950 let unit = match &f.ret {951 Ty::Unit => true,952 Ty::Result { item } => matches!(**item, Ty::Unit),953 _ => false,954 };955 match (unit, fallible) {956 (true, true) => writeln!(out, "{pad}{call}.map_err(hand::throw)?;").ok(),957 (true, false) => writeln!(out, "{pad}{call};").ok(),958 (false, true) => writeln!(out, "{pad}let value = {call}.map_err(hand::throw)?;").ok(),959 (false, false) => writeln!(out, "{pad}let value = {call};").ok(),960 };961 for plan in plans {962 for line in &plan.post {963 writeln!(out, "{pad}{line}").ok();964 }965 }966 writeln!(out, "{pad}{}", ret.done).ok();967 Ok(out)968 }969970 fn emit_const(&self, out: &mut String, c: &Const) -> Result<()> {971 let cx = Cx {972 dim: None,973 fname: &c.path,974 };975 let ret = self.ret_plan(&c.ty, cx)?;976 let doc = doc_line(&c.docs);977 if !doc.is_empty() {978 writeln!(out, "/// {doc}").ok();979 }980 let name = self.export(&self.local(&c.path));981 writeln!(out, "#[wasm_bindgen]").ok();982 writeln!(out, "pub fn {name}() -> Result<{}, JsValue> {{", ret.sig).ok();983 writeln!(out, " let value = mrlyrs::{};", c.path).ok();984 writeln!(out, " {}", ret.done).ok();985 writeln!(out, "}}").ok();986 Ok(())987 }988989 fn getter_ok(&self, ty: &Ty) -> bool {990 let mut ok = true;991 ty.walk(&mut |t| match t {992 Ty::Unknown { .. } | Ty::Opaque { .. } => ok = false,993 Ty::Class { path, .. } if !self.holds(path) => ok = false,994 Ty::Ref { lifetime: Some(l), item, .. } if l == "'static" && !matches!(**item, Ty::Str) => ok = false,995 _ => {}996 });997 ok998 }9991000 fn emit_class(&self, out: &mut String, t: &Type, groups: &[Vec<&Function>]) -> Result<()> {1001 let ident = self.local(&t.path);1002 let cx = Cx {1003 dim: None,1004 fname: &t.path,1005 };1006 let doc = doc_line(&t.docs);1007 if !doc.is_empty() {1008 writeln!(out, "/// {doc}").ok();1009 }1010 writeln!(out, "#[wasm_bindgen]").ok();1011 writeln!(out, "pub struct {ident} {{\n inner: mrlyrs::{},\n}}\n", t.path).ok();1012 writeln!(out, "#[wasm_bindgen]\nimpl {ident} {{").ok();1013 if derives(t, "Deserialize") {1014 writeln!(out, " /// Reads the {} from its plain data.", t.name).ok();1015 writeln!(out, " #[wasm_bindgen(js_name = \"from\")]").ok();1016 writeln!(out, " pub fn from_plain(data: JsValue) -> Result<{ident}, JsValue> {{").ok();1017 writeln!(out, " Ok({ident} {{ inner: hand::from_js(&data)? }})").ok();1018 writeln!(out, " }}").ok();1019 }1020 if derives(t, "Serialize") {1021 writeln!(out, " /// Writes the {} as plain data.", t.name).ok();1022 writeln!(out, " #[wasm_bindgen(js_name = \"toJSON\")]").ok();1023 writeln!(out, " pub fn to_plain(&self) -> Result<JsValue, JsValue> {{").ok();1024 writeln!(out, " hand::to_js(&self.inner)").ok();1025 writeln!(out, " }}").ok();1026 }1027 let methods: BTreeSet<&str> = groups.iter().map(|g| g[0].name.as_str()).collect();1028 for field in t.fields.iter().filter(|f| f.public) {1029 if !self.getter_ok(&field.ty) || methods.contains(field.name.as_str()) {1030 continue;1031 }1032 let ret = self.ret_plan(&field.ty, cx)?;1033 let doc = doc_line(&field.docs);1034 if !doc.is_empty() {1035 writeln!(out, " /// {doc}").ok();1036 }1037 writeln!(out, " #[wasm_bindgen(getter)]").ok();1038 writeln!(1039 out,1040 " pub fn {}(&self) -> Result<{}, JsValue> {{",1041 field.name,1042 ret.sig1043 )1044 .ok();1045 let value = match field.ty {1046 Ty::Ref { .. } => format!("self.inner.{}", field.name),1047 _ if self.is_copy(&field.ty) => format!("self.inner.{}", field.name),1048 _ => format!("self.inner.{}.clone()", field.name),1049 };1050 writeln!(out, " let value = {value};").ok();1051 writeln!(out, " {}", ret.done).ok();1052 writeln!(out, " }}").ok();1053 if field.ty.settable() {1054 let plan = self.plan_param("value", &field.ty, cx, false)?;1055 writeln!(out, " #[wasm_bindgen(setter)]").ok();1056 writeln!(1057 out,1058 " pub fn set_{}(&mut self, {}: {}) -> Result<(), JsValue> {{",1059 field.name, plan.name, plan.sig1060 )1061 .ok();1062 for line in &plan.pre {1063 writeln!(out, " {line}").ok();1064 }1065 writeln!(out, " self.inner.{} = {};", field.name, plan.arg).ok();1066 for line in &plan.post {1067 writeln!(out, " {line}").ok();1068 }1069 writeln!(out, " Ok(())").ok();1070 writeln!(out, " }}").ok();1071 }1072 }1073 for group in groups {1074 self.emit_group(out, group, Owner::Class(t))?;1075 }1076 writeln!(out, "}}").ok();1077 Ok(())1078 }10791080 fn emit_holder(&self, out: &mut String, t: &Type, groups: &[Vec<&Function>]) -> Result<()> {1081 let ident = self.local(&t.path);1082 let doc = doc_line(&t.docs);1083 if !doc.is_empty() {1084 writeln!(out, "/// {doc}").ok();1085 }1086 writeln!(out, "#[wasm_bindgen]").ok();1087 writeln!(out, "pub struct {ident} {{}}\n").ok();1088 writeln!(out, "#[wasm_bindgen]\nimpl {ident} {{").ok();1089 for group in groups {1090 self.emit_group(out, group, Owner::Holder(t))?;1091 }1092 writeln!(out, "}}").ok();1093 Ok(())1094 }10951096 fn lib_rs(&self) -> Result<String> {1097 let mut out = String::new();1098 writeln!(out, "{ALLOWS}\n\nmod hand;\n\nuse wasm_bindgen::prelude::*;\n").ok();1099 let groups = self.groups();1100 let mut owned: BTreeMap<&str, Vec<Vec<&Function>>> = BTreeMap::new();1101 for group in &groups {1102 match self.owner_of(group[0]) {1103 Owner::Free | Owner::Hand(_) => {1104 self.emit_group(&mut out, group, self.owner_of(group[0]))?;1105 out.push('\n');1106 }1107 Owner::Class(t) | Owner::Holder(t) => {1108 owned.entry(t.path.as_str()).or_default().push(group.clone());1109 }1110 }1111 }1112 for c in self.consts() {1113 self.emit_const(&mut out, c)?;1114 out.push('\n');1115 }1116 for t in self.classes() {1117 let groups = owned.get(t.path.as_str()).cloned().unwrap_or_default();1118 self.emit_class(&mut out, t, &groups)?;1119 out.push('\n');1120 }1121 for t in self.holders() {1122 let groups = owned.get(t.path.as_str()).cloned().unwrap_or_default();1123 self.emit_holder(&mut out, t, &groups)?;1124 out.push('\n');1125 }1126 while out.ends_with("\n\n") {1127 out.pop();1128 }1129 Ok(out)1130 }11311132 // WRAPPER11331134 fn tree(&self) -> Tree {1135 let mut tree = Tree::default();1136 let mut seen: BTreeSet<String> = BTreeSet::new();1137 for f in self.functions() {1138 let (home, leaf) = match self.owner_of(f) {1139 Owner::Free | Owner::Hand(_) => (f.module.clone(), leaf(&f.path)),1140 Owner::Class(t) | Owner::Holder(t) => (parent(&t.path), t.name.clone()),1141 };1142 let key = format!("{home}::{leaf}");1143 if seen.insert(key) {1144 let export = match self.owner_of(f) {1145 Owner::Free | Owner::Hand(_) => self.export(&self.local(&f.path)),1146 Owner::Class(t) | Owner::Holder(t) => self.local(&t.path),1147 };1148 if JS_RESERVED.contains(&leaf.as_str()) {1149 tree.insert(self.rel(&home), format!("{leaf}_"), export.clone());1150 }1151 tree.insert(self.rel(&home), leaf, export);1152 }1153 }1154 for t in self.classes() {1155 let key = format!("{}::{}", parent(&t.path), t.name);1156 if seen.insert(key) {1157 tree.insert(self.rel(&parent(&t.path)), t.name.clone(), self.local(&t.path));1158 }1159 }1160 for c in self.consts() {1161 let export = self.export(&self.local(&c.path));1162 tree.insert(self.rel(&parent(&c.path)), c.name.clone(), export);1163 }1164 tree1165 }11661167 fn wrapper_js(&self) -> String {1168 let name = self.name;1169 let mut out = String::new();1170 writeln!(out, "import * as wasm from \"./pkg/{name}/mrlyjs_{name}.js\";\n").ok();1171 writeln!(out, "export {{ default, initSync }} from \"./pkg/{name}/mrlyjs_{name}.js\";").ok();1172 writeln!(out, "export const Rng = wasm.Rng;").ok();1173 let tree = self.tree();1174 for (leaf, export) in &tree.leaves {1175 writeln!(out, "export const {leaf} = wasm.{export};").ok();1176 }1177 for (seg, child) in &tree.children {1178 writeln!(out, "export const {seg} = {};", child.literal(1)).ok();1179 }1180 out1181 }11821183 // TYPES11841185 fn ts_ref(&self, path: &str) -> String {1186 self.rel(path).replace("::", ".")1187 }11881189 fn ts_direct(&self, ty: &Ty, ret: bool) -> String {1190 match ty {1191 Ty::Unit => "void".into(),1192 Ty::Scalar { name } => match name.as_str() {1193 "bool" => "boolean".into(),1194 "char" => "string".into(),1195 "u64" | "i64" if ret => "bigint".into(),1196 "u64" | "i64" => "number | bigint".into(),1197 _ => "number".into(),1198 },1199 Ty::Str | Ty::String => "string".into(),1200 Ty::U128 | Ty::I128 | Ty::Code if ret => "string".into(),1201 Ty::U128 | Ty::I128 | Ty::Code => "string | number | bigint".into(),1202 Ty::Json => "any".into(),1203 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Array { item, .. } | Ty::Set { item } => {1204 match &**item {1205 Ty::Scalar { name } if TYPED.contains(&name.as_str()) => {1206 if ret {1207 typed_array(name).into()1208 } else if is_wide(name) {1209 "ArrayLike<number | bigint>".into()1210 } else {1211 "ArrayLike<number>".into()1212 }1213 }1214 _ => format!("{}[]", self.ts_wrap(&self.ts_direct(item, ret))),1215 }1216 }1217 Ty::Option { item } => format!("{} | undefined", self.ts_direct(item, ret)),1218 Ty::Tuple { items } => format!(1219 "[{}]",1220 items1221 .iter()1222 .map(|t| self.ts_direct(t, ret))1223 .collect::<Vec<_>>()1224 .join(", ")1225 ),1226 Ty::Ref { item, .. } | Ty::Result { item } => self.ts_direct(item, ret),1227 Ty::Map { value, .. } => format!("Record<string, {}>", self.ts_direct(value, ret)),1228 Ty::Hand { name, .. } => match name.as_str() {1229 "Tensor" => "Tensor".into(),1230 "Cell" | "CellNd" => "Cell".into(),1231 "Cell6d" => "Cell6d".into(),1232 "Color" => "Color".into(),1233 "Code" if ret => "string".into(),1234 "Code" => "string | number | bigint".into(),1235 _ => "Rng".into(),1236 },1237 Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } => self.ts_ref(path),1238 Ty::Opaque { .. } | Ty::Unknown { .. } => "unknown".into(),1239 }1240 }12411242 fn ts_wrap(&self, text: &str) -> String {1243 if text.contains(" | ") {1244 format!("({text})")1245 } else {1246 text.to_string()1247 }1248 }12491250 fn ts_serde(&self, ty: &Ty) -> String {1251 match ty {1252 Ty::Unit => "null".into(),1253 Ty::Scalar { name } => match name.as_str() {1254 "bool" => "boolean".into(),1255 "char" => "string".into(),1256 _ => "number".into(),1257 },1258 Ty::Str | Ty::String => "string".into(),1259 Ty::U128 | Ty::I128 | Ty::Code => "bigint".into(),1260 Ty::Json => "any".into(),1261 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Array { item, .. } | Ty::Set { item } => {1262 format!("{}[]", self.ts_wrap(&self.ts_serde(item)))1263 }1264 Ty::Option { item } => format!("{} | undefined", self.ts_serde(item)),1265 Ty::Tuple { items } => format!(1266 "[{}]",1267 items.iter().map(|t| self.ts_serde(t)).collect::<Vec<_>>().join(", ")1268 ),1269 Ty::Ref { item, .. } | Ty::Result { item } => self.ts_serde(item),1270 Ty::Map { value, .. } => format!("Record<string, {}>", self.ts_serde(value)),1271 Ty::Hand { name, .. } => match name.as_str() {1272 "Tensor" => "TensorData".into(),1273 "Cell" => "CellData".into(),1274 "CellNd" => "{ cell: CellData }".into(),1275 "Cell6d" => "Cell6dData".into(),1276 "Color" => "ColorData".into(),1277 "Code" => "bigint".into(),1278 _ => "unknown".into(),1279 },1280 Ty::Plain { path } | Ty::Enum { path } => self.ts_ref(path),1281 Ty::Class { path, .. } => format!("{}Data", self.ts_ref(path)),1282 Ty::Opaque { .. } | Ty::Unknown { .. } => "string".into(),1283 }1284 }12851286 fn ts_params(&self, params: &[(String, Ty, bool)]) -> String {1287 let last_required = params1288 .iter()1289 .rposition(|(_, ty, extra)| !matches!(ty, Ty::Option { .. }) && !extra)1290 .map(|i| i + 1)1291 .unwrap_or(0);1292 params1293 .iter()1294 .enumerate()1295 .map(|(i, (name, ty, extra))| {1296 let n = ident(name);1297 let inner = match ty {1298 Ty::Option { item } => item,1299 _ => ty,1300 };1301 if *extra || (i >= last_required && matches!(ty, Ty::Option { .. })) {1302 format!("{n}?: {}", self.ts_direct(inner, false))1303 } else {1304 format!("{n}: {}", self.ts_direct(ty, false))1305 }1306 })1307 .collect::<Vec<_>>()1308 .join(", ")1309 }13101311 fn ts_function(&self, group: &[&Function], owner: Owner) -> (String, String, String) {1312 let head = group[0];1313 let mut merged: Vec<(String, Ty, bool)> = vec![];1314 for f in group {1315 match owner {1316 Owner::Hand(t) => {1317 if let Ok(Some((label, ty))) = self.self_param(f, t) {1318 if !merged.iter().any(|(n, _, _)| *n == label) {1319 merged.push((label, ty, false));1320 }1321 }1322 }1323 Owner::Holder(t) if f.self_kind.is_some() => {1324 let label = t.name.to_lowercase();1325 let label = if f.params.iter().any(|p| p.name == label) {1326 "self_".to_string()1327 } else {1328 label1329 };1330 if !merged.iter().any(|(n, _, _)| *n == label) {1331 merged.push((label, plain_ty(t), false));1332 }1333 }1334 _ => {}1335 }1336 for p in &f.params {1337 if !merged.iter().any(|(n, _, _)| *n == p.name) {1338 merged.push((p.name.clone(), p.ty.clone(), false));1339 }1340 }1341 }1342 for slot in &mut merged {1343 slot.2 = !group.iter().all(|f| self.mentions(f, owner, &slot.0));1344 }1345 let ret = match &head.ret {1346 Ty::Result { item } => self.ts_direct(item, true),1347 other => self.ts_direct(other, true),1348 };1349 (doc_line(&head.docs), self.ts_params(&merged), ret)1350 }13511352 fn referenced(&self) -> BTreeSet<String> {1353 let mut out: BTreeSet<String> = BTreeSet::new();1354 let mut queue: Vec<String> = vec![];1355 let note = |ty: &Ty, queue: &mut Vec<String>| {1356 ty.walk(&mut |t| {1357 if let Ty::Plain { path } | Ty::Enum { path } | Ty::Class { path, .. } = t {1358 queue.push(path.clone());1359 }1360 })1361 };1362 for f in self.functions() {1363 for p in &f.params {1364 note(&p.ty, &mut queue);1365 }1366 note(&f.ret, &mut queue);1367 }1368 for c in self.consts() {1369 note(&c.ty, &mut queue);1370 }1371 for t in self.classes() {1372 queue.push(t.path.clone());1373 }1374 for t in self.holders() {1375 queue.push(t.path.clone());1376 }1377 while let Some(path) = queue.pop() {1378 if !out.insert(path.clone()) {1379 continue;1380 }1381 let Some(t) = self.ty(&path) else { continue };1382 for field in &t.fields {1383 note(&field.ty, &mut queue);1384 }1385 for v in &t.variants {1386 for field in &v.fields {1387 note(&field.ty, &mut queue);1388 }1389 }1390 }1391 out1392 }13931394 fn ts_type_decl(&self, t: &Type, groups: &[Vec<&Function>], pad: &str) -> Result<String> {1395 let mut out = String::new();1396 let doc = doc_line(&t.docs);1397 let name = &t.name;1398 match &t.cross {1399 TypeCross::Enum { .. } => {1400 let words: Vec<String> =1401 t.variants.iter().map(|v| enum_word(t, &v.name, &v.serde)).collect();1402 let union = words.iter().map(|w| format!("{w:?}")).collect::<Vec<_>>().join(" | ");1403 if !doc.is_empty() {1404 writeln!(out, "{pad}/** {doc} */").ok();1405 }1406 writeln!(out, "{pad}export type {name} = {union};").ok();1407 }1408 TypeCross::Plain if t.kind == TypeKind::Alias => {1409 if let Some(alias) = &t.alias {1410 writeln!(out, "{pad}export type {name} = {};", self.ts_serde(alias)).ok();1411 }1412 }1413 TypeCross::Plain if t.kind == TypeKind::Enum => {1414 if !doc.is_empty() {1415 writeln!(out, "{pad}/** {doc} */").ok();1416 }1417 writeln!(out, "{pad}export type {name} = {};", self.ts_union(t)).ok();1418 }1419 TypeCross::Plain => {1420 if !doc.is_empty() {1421 writeln!(out, "{pad}/** {doc} */").ok();1422 }1423 writeln!(out, "{pad}export interface {name} {{").ok();1424 self.ts_fields(&mut out, t, &format!("{pad} "), true);1425 writeln!(out, "{pad}}}").ok();1426 }1427 TypeCross::Class => {1428 let all_public = t.fields.iter().all(|f| f.public);1429 let data = format!("{name}Data");1430 if t.kind == TypeKind::Enum {1431 writeln!(out, "{pad}export type {data} = {};", self.ts_union(t)).ok();1432 } else if all_public {1433 writeln!(out, "{pad}export interface {data} {{").ok();1434 self.ts_fields(&mut out, t, &format!("{pad} "), false);1435 writeln!(out, "{pad}}}").ok();1436 } else {1437 writeln!(out, "{pad}export type {data} = Record<string, unknown>;").ok();1438 }1439 if !doc.is_empty() {1440 writeln!(out, "{pad}/** {doc} */").ok();1441 }1442 writeln!(out, "{pad}export class {name} {{").ok();1443 let inner = format!("{pad} ");1444 let constructor = groups.iter().find(|g| g[0].name == "new" && g[0].self_kind.is_none());1445 match constructor {1446 Some(g) => {1447 let (d, params, _) = self.ts_function(g, Owner::Class(t));1448 if !d.is_empty() {1449 writeln!(out, "{inner}/** {d} */").ok();1450 }1451 writeln!(out, "{inner}constructor({params});").ok();1452 }1453 None => {1454 writeln!(out, "{inner}private constructor();").ok();1455 }1456 }1457 writeln!(out, "{inner}free(): void;").ok();1458 if derives(t, "Deserialize") {1459 writeln!(out, "{inner}/** Reads the {name} from its plain data. */").ok();1460 writeln!(out, "{inner}static from(data: {data}): {name};").ok();1461 }1462 if derives(t, "Serialize") {1463 writeln!(out, "{inner}/** Writes the {name} as plain data. */").ok();1464 writeln!(out, "{inner}toJSON(): {data};").ok();1465 }1466 let methods: BTreeSet<&str> = groups.iter().map(|g| g[0].name.as_str()).collect();1467 for field in t.fields.iter().filter(|f| f.public) {1468 if !self.getter_ok(&field.ty) || methods.contains(field.name.as_str()) {1469 continue;1470 }1471 let d = doc_line(&field.docs);1472 if !d.is_empty() {1473 writeln!(out, "{inner}/** {d} */").ok();1474 }1475 let out_ty = self.ts_direct(&field.ty, true);1476 if field.ty.settable() {1477 writeln!(out, "{inner}get {}(): {out_ty};", field.name).ok();1478 writeln!(1479 out,1480 "{inner}set {}(value: {});",1481 field.name,1482 self.ts_direct(&field.ty, false)1483 )1484 .ok();1485 } else {1486 writeln!(out, "{inner}readonly {}: {out_ty};", ident(&field.name)).ok();1487 }1488 }1489 for g in groups {1490 let f = g[0];1491 if f.name == "new" && f.self_kind.is_none() {1492 continue;1493 }1494 let (d, params, ret) = self.ts_function(g, Owner::Class(t));1495 if !d.is_empty() {1496 writeln!(out, "{inner}/** {d} */").ok();1497 }1498 let prefix = if f.self_kind.is_none() { "static " } else { "" };1499 writeln!(out, "{inner}{prefix}{}({params}): {ret};", f.name).ok();1500 }1501 writeln!(out, "{pad}}}").ok();1502 }1503 TypeCross::Hand { .. } | TypeCross::Uncrossable { .. } => {}1504 }1505 if matches!(t.cross, TypeCross::Plain | TypeCross::Enum { .. }) && !groups.is_empty() {1506 writeln!(out, "{pad}export const {name}: {{").ok();1507 for g in groups {1508 let f = g[0];1509 let (d, params, ret) = self.ts_function(g, Owner::Holder(t));1510 if !d.is_empty() {1511 writeln!(out, "{pad} /** {d} */").ok();1512 }1513 let label = if f.name == "new" { "\"new\"".to_string() } else { f.name.clone() };1514 writeln!(out, "{pad} {label}({params}): {ret};").ok();1515 }1516 writeln!(out, "{pad}}};").ok();1517 }1518 Ok(out)1519 }15201521 fn ts_union(&self, t: &Type) -> String {1522 let untagged = t.serde.iter().any(|s| s == "untagged");1523 let mut arms = vec![];1524 for v in &t.variants {1525 let word = enum_word(t, &v.name, &v.serde);1526 let body = if v.fields.is_empty() {1527 None1528 } else if v.fields.iter().all(|f| f.name.parse::<usize>().is_ok()) {1529 Some(if v.fields.len() == 1 {1530 self.ts_serde(&v.fields[0].ty)1531 } else {1532 format!(1533 "[{}]",1534 v.fields.iter().map(|f| self.ts_serde(&f.ty)).collect::<Vec<_>>().join(", ")1535 )1536 })1537 } else {1538 Some(format!(1539 "{{ {} }}",1540 v.fields1541 .iter()1542 .map(|f| format!("{}: {}", f.name, self.ts_serde(&f.ty)))1543 .collect::<Vec<_>>()1544 .join("; ")1545 ))1546 };1547 arms.push(match (untagged, body) {1548 (true, None) => "null".to_string(),1549 (false, None) => format!("{word:?}"),1550 (true, Some(body)) => body,1551 (false, Some(body)) => format!("{{ {word}: {body} }}"),1552 });1553 }1554 arms.join(" | ")1555 }15561557 fn ts_fields(&self, out: &mut String, t: &Type, pad: &str, all: bool) {1558 for field in t.fields.iter().filter(|f| (all || f.public) && !f.serde_skip) {1559 let d = doc_line(&field.docs);1560 if !d.is_empty() {1561 writeln!(out, "{pad}/** {d} */").ok();1562 }1563 let (name, ty) = match &field.ty {1564 Ty::Option { item } => (format!("{}?", ident(&field.name)), self.ts_serde(item)),1565 other => (ident(&field.name), self.ts_serde(other)),1566 };1567 writeln!(out, "{pad}{name}: {ty};").ok();1568 }1569 }15701571 fn types_dts(&self) -> Result<String> {1572 let name = self.name;1573 let mut out = String::new();1574 writeln!(out, "export {{ default, initSync }} from \"./pkg/{name}/mrlyjs_{name}.js\";\n").ok();1575 let words = |path: &str| -> String {1576 self.ty(path)1577 .map(|t| {1578 t.variants1579 .iter()1580 .map(|v| enum_word(t, &v.name, &v.serde))1581 .collect::<Vec<String>>()1582 })1583 .unwrap_or_default()1584 .iter()1585 .map(|w| format!("{w:?}"))1586 .collect::<Vec<_>>()1587 .join(" | ")1588 };1589 let projection = words("math::six::Projection");1590 let orientation = words("math::six::Orientation");1591 writeln!(out, "/** An rgba color as four bytes. */").ok();1592 writeln!(out, "export type Color = [number, number, number, number];").ok();1593 writeln!(out, "/** A tensor: its shape and its flat data as a typed array of its dtype. */").ok();1594 writeln!(out, "export interface Tensor {{\n shape: number[];\n data: Uint8Array | Uint16Array | Uint32Array | Int32Array;\n}}").ok();1595 writeln!(out, "/** A cell: the shape, the type bytes, and the flat rgba colors and the tags when present. */").ok();1596 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();1597 writeln!(out, "/** A hex cell: a flat cell with its projection, orientation and start row. */").ok();1598 writeln!(out, "export interface Cell6d {{\n cell: Cell;\n projection: {projection};\n orientation: {orientation};\n start: number;\n}}").ok();1599 writeln!(out, "/** A color inside plain data, serde's form. */").ok();1600 writeln!(out, "export interface ColorData {{\n r: number;\n g: number;\n b: number;\n a: number;\n}}").ok();1601 writeln!(out, "/** A tensor inside plain data, serde's form. */").ok();1602 writeln!(out, "export interface TensorData {{\n shape: number[];\n data: {{ U8: number[] }} | {{ U16: number[] }} | {{ U32: number[] }} | {{ I32: number[] }};\n}}").ok();1603 writeln!(out, "/** A cell inside plain data, serde's form. */").ok();1604 writeln!(out, "export interface CellData {{\n types: TensorData;\n colors?: number[][];\n tags?: TensorData;\n}}").ok();1605 writeln!(out, "/** A hex cell inside plain data, serde's form. */").ok();1606 writeln!(out, "export interface Cell6dData {{\n cell: {{ cell: CellData }};\n projection: {projection};\n orientation: {orientation};\n start: number;\n}}").ok();1607 writeln!(out, "/** A seeded random stream, opened from a number or a bigint seed. */").ok();1608 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();1609 let mut tree = DeclTree::default();1610 let groups = self.groups();1611 let mut owned: BTreeMap<&str, Vec<Vec<&Function>>> = BTreeMap::new();1612 for group in &groups {1613 match self.owner_of(group[0]) {1614 Owner::Free | Owner::Hand(_) => {1615 let (d, params, ret) = self.ts_function(group, self.owner_of(group[0]));1616 let mut text = String::new();1617 if !d.is_empty() {1618 text.push_str(&format!("/** {d} */\n"));1619 }1620 text.push_str(&format!(1621 "export function {}({params}): {ret};\n",1622 ts_leaf(&leaf(&group[0].path))1623 ));1624 tree.insert(self.rel(&group[0].module), text);1625 }1626 Owner::Class(t) | Owner::Holder(t) => {1627 owned.entry(t.path.as_str()).or_default().push(group.clone());1628 }1629 }1630 }1631 for c in self.consts() {1632 let mut text = String::new();1633 let d = doc_line(&c.docs);1634 if !d.is_empty() {1635 text.push_str(&format!("/** {d} */\n"));1636 }1637 text.push_str(&format!(1638 "export function {}(): {};\n",1639 c.name,1640 self.ts_direct(&c.ty, true)1641 ));1642 tree.insert(self.rel(&parent(&c.path)), text);1643 }1644 for path in self.referenced() {1645 let Some(t) = self.ty(&path) else { continue };1646 if matches!(t.cross, TypeCross::Hand { .. } | TypeCross::Uncrossable { .. }) {1647 continue;1648 }1649 let groups = owned.get(path.as_str()).cloned().unwrap_or_default();1650 let text = self.ts_type_decl(t, &groups, "")?;1651 tree.insert(self.rel(&parent(&t.path)), text);1652 }1653 out.push_str(&tree.render(0));1654 while out.ends_with("\n\n") {1655 out.pop();1656 }1657 Ok(out)1658 }1659}16601661// TREES16621663#[derive(Default)]1664struct Tree {1665 leaves: BTreeMap<String, String>,1666 children: BTreeMap<String, Tree>,1667}16681669impl Tree {1670 fn insert(&mut self, module: &str, leaf: String, export: String) {1671 let mut node = self;1672 for seg in module.split("::").filter(|s| !s.is_empty()) {1673 node = node.children.entry(seg.to_string()).or_default();1674 }1675 node.leaves.insert(leaf, export);1676 }16771678 fn literal(&self, depth: usize) -> String {1679 let pad = " ".repeat(depth);1680 let mut out = String::from("{\n");1681 for (leaf, export) in &self.leaves {1682 out.push_str(&format!("{pad}{leaf}: wasm.{export},\n"));1683 }1684 for (seg, child) in &self.children {1685 out.push_str(&format!("{pad}{seg}: {},\n", child.literal(depth + 1)));1686 }1687 out.push_str(&" ".repeat(depth - 1));1688 out.push('}');1689 out1690 }1691}16921693#[derive(Default)]1694struct DeclTree {1695 texts: Vec<String>,1696 children: BTreeMap<String, DeclTree>,1697}16981699impl DeclTree {1700 fn insert(&mut self, module: &str, text: String) {1701 let mut node = self;1702 for seg in module.split("::").filter(|s| !s.is_empty()) {1703 node = node.children.entry(seg.to_string()).or_default();1704 }1705 node.texts.push(text);1706 }17071708 fn render(&self, depth: usize) -> String {1709 let pad = " ".repeat(depth);1710 let mut out = String::new();1711 for text in &self.texts {1712 for line in text.lines() {1713 out.push_str(&pad);1714 out.push_str(line);1715 out.push('\n');1716 }1717 }1718 for (seg, child) in &self.children {1719 let keyword = if depth == 0 { "export declare namespace" } else { "export namespace" };1720 out.push_str(&format!("{pad}{keyword} {seg} {{\n"));1721 out.push_str(&child.render(depth + 1));1722 out.push_str(&format!("{pad}}}\n"));1723 }1724 out1725 }1726}17271728// RULES17291730fn deref(e: &str) -> String {1731 match e.strip_prefix('&') {1732 Some(inner) => inner.to_string(),1733 None => format!("*{e}"),1734 }1735}17361737fn place(e: &str) -> String {1738 e.strip_prefix('&').unwrap_or(e).to_string()1739}17401741fn closure(x: &str, body: String) -> String {1742 let tail = format!("({x})");1743 match body.strip_suffix(&tail) {1744 Some(path) if !path.contains(x) && !path.contains(' ') || path.starts_with("hand::from_js::<") => {1745 path.to_string()1746 }1747 _ => format!("|{x}| {body}"),1748 }1749}17501751fn ok(expr: String) -> String {1752 match expr.strip_suffix('?') {1753 Some(inner) => inner.to_string(),1754 None => format!("Ok({expr})"),1755 }1756}17571758fn is_wide(name: &str) -> bool {1759 name == "u64" || name == "i64"1760}17611762fn is_rng(ty: &Ty) -> bool {1763 matches!(ty, Ty::Hand { name, .. } if name == "Rng")1764}17651766fn typed_scalar(ty: &Ty) -> bool {1767 matches!(ty, Ty::Scalar { name } if TYPED.contains(&name.as_str()))1768}17691770fn native_item(ty: &Ty) -> bool {1771 matches!(ty, Ty::Scalar { name } if NATIVE_ITEM.contains(&name.as_str()))1772}17731774fn typed_array(name: &str) -> &'static str {1775 match name {1776 "u8" => "Uint8Array",1777 "u16" => "Uint16Array",1778 "u32" | "usize" => "Uint32Array",1779 "i8" => "Int8Array",1780 "i16" => "Int16Array",1781 "i32" => "Int32Array",1782 "f32" => "Float32Array",1783 "f64" => "Float64Array",1784 "u64" => "BigUint64Array",1785 _ => "BigInt64Array",1786 }1787}17881789fn pure_in(ty: &Ty) -> bool {1790 match ty {1791 Ty::Unit | Ty::Str | Ty::String | Ty::Json | Ty::Plain { .. } | Ty::Enum { .. } => true,1792 Ty::Scalar { name } => !is_wide(name),1793 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Option { item } | Ty::Array { item, .. } => {1794 pure_in(item)1795 }1796 Ty::Ref { mutable: false, item, .. } => pure_in(item),1797 Ty::Tuple { items } => items.iter().all(pure_in),1798 _ => false,1799 }1800}18011802fn pure_out(ty: &Ty) -> bool {1803 match ty {1804 Ty::Unit | Ty::Str | Ty::String | Ty::Json | Ty::Plain { .. } | Ty::Enum { .. } => true,1805 Ty::Scalar { name } => !is_wide(name),1806 Ty::Vec { item } | Ty::Slice { item, .. } | Ty::Array { item, .. } => {1807 !typed_scalar(item) && pure_out(item)1808 }1809 Ty::Option { item } => pure_out(item),1810 Ty::Tuple { items } => items.iter().all(pure_out),1811 _ => false,1812 }1813}18141815fn plain_ty(t: &Type) -> Ty {1816 match t.cross {1817 TypeCross::Enum { .. } => Ty::Enum {1818 path: t.path.clone(),1819 },1820 _ => Ty::Plain {1821 path: t.path.clone(),1822 },1823 }1824}18251826fn hand_self(name: &str) -> &'static str {1827 match name {1828 "Tensor" => "tensor",1829 "Color" => "color",1830 "Code" => "code",1831 "Rng" => "rng",1832 _ => "cell",1833 }1834}18351836fn suffix_dim(fname: &str) -> u8 {1837 if fname.ends_with("_3d") || fname.ends_with("_6d") {1838 31839 } else {1840 21841 }1842}18431844fn ident(name: &str) -> String {1845 if WORDS.contains(&name) {1846 format!("{name}_")1847 } else {1848 name.to_string()1849 }1850}185118521853fn derives(t: &Type, what: &str) -> bool {1854 t.derives.iter().any(|d| d == what)1855}18561857fn doc_line(docs: &[String]) -> String {1858 docs.first().map(|d| d.trim().to_string()).unwrap_or_default()1859}18601861fn ts_leaf(name: &str) -> String {1862 if JS_RESERVED.contains(&name) {1863 format!("{name}_")1864 } else {1865 name.to_string()1866 }1867}18681869fn leaf(path: &str) -> String {1870 path.rsplit("::").next().unwrap_or(path).to_string()1871}18721873fn parent(path: &str) -> String {1874 match path.rfind("::") {1875 Some(at) => path[..at].to_string(),1876 None => String::new(),1877 }1878}18791880fn enum_word(t: &Type, variant: &str, serde: &[String]) -> String {1881 for attr in serde {1882 if let Some(rest) = attr.trim().strip_prefix("rename = ") {1883 return rest.trim().trim_matches('"').to_string();1884 }1885 }1886 for attr in &t.serde {1887 if let Some(rest) = attr.trim().strip_prefix("rename_all = ") {1888 let rule = rest.trim().trim_matches('"');1889 return rename_all(variant, rule);1890 }1891 }1892 variant.to_string()1893}18941895fn rename_all(name: &str, rule: &str) -> String {1896 let words: Vec<String> = {1897 let mut out: Vec<String> = vec![];1898 for c in name.chars() {1899 if c.is_uppercase() || out.is_empty() {1900 out.push(String::new());1901 }1902 out.last_mut().unwrap().push(c);1903 }1904 out.into_iter().map(|w| w.to_lowercase()).collect()1905 };1906 match rule {1907 "lowercase" => name.to_lowercase(),1908 "UPPERCASE" => name.to_uppercase(),1909 "snake_case" => words.join("_"),1910 "SCREAMING_SNAKE_CASE" => words.join("_").to_uppercase(),1911 "kebab-case" => words.join("-"),1912 "SCREAMING-KEBAB-CASE" => words.join("-").to_uppercase(),1913 "camelCase" => {1914 let mut c = name.chars();1915 match c.next() {1916 Some(first) => first.to_lowercase().collect::<String>() + c.as_str(),1917 None => String::new(),1918 }1919 }1920 _ => name.to_string(),1921 }1922}19231924// TEST19251926const MANIFEST_TEST: &str = r#"import { expect, test } from "bun:test";19271928const manifest = await Bun.file(new URL("../../bridge/manifest.json", import.meta.url)).json();1929const units = (await Bun.file(new URL("../../bridge/units.txt", import.meta.url)).text())1930 .split("\n")1931 .map((line) => line.trim())1932 .filter(Boolean);1933const pkg = await Bun.file(new URL("./package.json", import.meta.url)).json();1934const kinds = new Map(manifest.types.map((t) => [t.path, t.cross.kind]));19351936function parent(path) {1937 const at = path.lastIndexOf("::");1938 return at < 0 ? "" : path.slice(0, at);1939}19401941function relative(unit, path) {1942 return unit === "all" ? path : path.slice(unit.length + 2);1943}19441945function walk(mod, path) {1946 let node = mod;1947 for (const seg of path.split("::").filter(Boolean)) {1948 if (node === undefined || node === null) return undefined;1949 node = node[seg];1950 }1951 return node;1952}19531954function holds(unit, path) {1955 return unit === "all" || path.split("::")[0] === unit;1956}19571958function check(mod, unit, f) {1959 const kind = f.owner ? kinds.get(f.owner) : undefined;1960 if (kind === "class") {1961 const cls = walk(mod, relative(unit, f.owner));1962 if (typeof cls !== "function") return false;1963 if (f.self_kind === null && f.name === "new") return true;1964 if (f.self_kind === null) return typeof cls[f.name] === "function";1965 return typeof cls.prototype[f.name] === "function";1966 }1967 if (kind === "plain" || kind === "enum") {1968 const holder = walk(mod, relative(unit, f.owner));1969 return typeof holder === "function" && typeof holder[f.name] === "function";1970 }1971 return typeof walk(mod, relative(unit, f.path)) === "function";1972}19731974for (const unit of units) {1975 test(`${unit} exports every ok name`, async () => {1976 expect(pkg.exports[unit === "all" ? "." : `./${unit}`]).toBeDefined();1977 const mod = await import(`./${unit}.js`);1978 const bytes = await Bun.file(new URL(`./pkg/${unit}/mrlyjs_${unit}_bg.wasm`, import.meta.url)).arrayBuffer();1979 mod.initSync({ module: bytes });1980 expect(typeof mod.Rng).toBe("function");1981 const missing = [];1982 const names = new Set();1983 for (const f of manifest.functions) {1984 if (f.cross.status !== "ok" || !holds(unit, f.module)) continue;1985 names.add(f.path);1986 if (!check(mod, unit, f)) missing.push(f.path);1987 }1988 for (const c of manifest.consts) {1989 if (c.cross.status !== "ok" || !holds(unit, c.path)) continue;1990 names.add(c.path);1991 if (typeof walk(mod, relative(unit, c.path)) !== "function") missing.push(c.path);1992 }1993 for (const t of manifest.types) {1994 if (t.cross.kind !== "class" || !holds(unit, t.path)) continue;1995 names.add(t.path);1996 if (typeof walk(mod, relative(unit, t.path)) !== "function") missing.push(t.path);1997 }1998 expect(missing).toEqual([]);1999 console.log(`${unit}: ${names.size} names`);2000 });2001}2002"#;