From 0ec8ff22eef6621f5aeaa94e038098a00d56d3b7 Mon Sep 17 00:00:00 2001 From: David Ellis Date: Sat, 21 Sep 2024 18:02:49 -0500 Subject: [PATCH] Implement js methods, Property{T}, and other odds-and-ends (#912) * Implement js methods, Property{T}, and other odds-and-ends * Apparently parse doesn't understand numbers with underscores, even though Rust's own parser does... --- alan/src/compile/integration_tests.rs | 33 ++++++++-------- alan/src/lntojs/function.rs | 53 ++++++++++++++++++-------- alan/src/program/ctype.rs | 54 +++++++++++++++++++++++++++ alan/src/program/scope.rs | 6 +-- alan/src/std/root.ln | 53 ++++++++++++++++++++------ 5 files changed, 153 insertions(+), 46 deletions(-) diff --git a/alan/src/compile/integration_tests.rs b/alan/src/compile/integration_tests.rs index f3bdcd2e..94c05107 100644 --- a/alan/src/compile/integration_tests.rs +++ b/alan/src/compile/integration_tests.rs @@ -267,13 +267,13 @@ test_full!(passing_ints_to_function => r#" stdout "I got a number! 5\n"; status 0; ); -test!(underscores_in_numbers => r#" +test_full!(underscores_in_numbers => r#" export fn main = print(1_000_000 * 2); "#; stdout "2000000\n"; status 0; ); -test!(other_integer_syntaxes => r#" +test_full!(other_integer_syntaxes => r#" export fn main { print(0b10 == 2); print(0o10 == 8); @@ -283,7 +283,7 @@ test!(other_integer_syntaxes => r#" "#; stdout "true\ntrue\ntrue\ntrue\n"; ); -test!(scientific_notation => r#" +test_full!(scientific_notation => r#" export fn main { print(15.0 == 1.5e1); print(-5.0 == -5e0); @@ -783,7 +783,7 @@ test_full!(f64_neg => r#" stdout "-3\n"; ); -test!(grouping => r#" +test_full!(grouping => r#" export fn main { print(2 / (3)); print(3 / (1 + 2)); @@ -791,13 +791,13 @@ test!(grouping => r#" stdout "0\n1\n"; ); -test!(string_min => r#" +test_full!(string_min => r#" export fn main { min(3.string, 5.string).print; }"#; stdout "3\n"; ); -test!(string_max => r#" +test_full!(string_max => r#" export fn main { max(3.string, 5.string).print; }"#; @@ -1084,7 +1084,7 @@ true // String Manipulation -test!(string_ops => r#" +test_full!(string_ops => r#" export fn main { concat('Hello, ', 'World!').print; @@ -1116,14 +1116,14 @@ Hello Hello "#; ); -test!(string_const_vs_computed_equality => r#" +test_full!(string_const_vs_computed_equality => r#" export fn main { const foo = 'foo'; print(foo.trim == foo); }"#; stdout "true\n"; ); -test!(string_chars_direct => r#" +test_full!(string_chars_direct => r#" export fn main { const foo = 'foo'; print(#foo); @@ -1600,7 +1600,7 @@ test!(bitshifting => r#" // Functions and Custom Operators -test!(basic_function_usage => r#" +test_full!(basic_function_usage => r#" fn foo() = print('foo'); fn bar(s: string) = s.concat("bar"); @@ -1614,7 +1614,7 @@ foobar "#; ); -test!(functions_and_custom_operators => r#" +test_full!(functions_and_custom_operators => r#" fn foo() { print('foo'); } @@ -1654,6 +1654,7 @@ to barto bar3 "#; ); +// TODO: Need to figure out how to get this working in JS-land test!(mutable_functions => r#" fn addeq (a: Mut{i64}, b: i64) { a = a.clone() + b; @@ -1672,7 +1673,7 @@ test!(mutable_functions => r#" // Conditionals -test!(if_fn => r#" +test_full!(if_fn => r#" export fn main { if(1 == 0, fn = print('What!?'), fn = print('Math is sane...')); if(1 == 2, fn = 'Uhh...').print; @@ -1778,7 +1779,7 @@ test_ignore!(conditional_let_assignment => r#" stdout "1\n"; ); -test!(conditional_compilation => r#" +test_full!(conditional_compilation => r#" type{true} foo = string; type{false} foo = i64; @@ -1805,7 +1806,7 @@ test!(conditional_compilation => r#" }"#; stdout "Hello, World!\n9\n9\ntrue\n"; ); -test!(library_testing => r#" +test_full!(library_testing => r#" export fn add1(a: i64) -> i64 = a + 1; export postfix add1 as ++ precedence 5; @@ -1828,7 +1829,7 @@ test_compile_error!(object_constructor_compiler_checks => r#" }"#; error "Could not find a function with a call signature of Foo(f64)"; ); -test!(array_literals => r#" +test_full!(array_literals => r#" export fn main { const test3 = [ 1, 2, 4, 8, 16, 32, 64 ]; print(test3[0]); @@ -1837,7 +1838,7 @@ test!(array_literals => r#" }"#; stdout "1\n2\n4\n"; ); -test!(object_literals => r#" +test_full!(object_literals => r#" type MyType = foo: string, bar: bool; diff --git a/alan/src/lntojs/function.rs b/alan/src/lntojs/function.rs index 47f7a3d9..23d19877 100644 --- a/alan/src/lntojs/function.rs +++ b/alan/src/lntojs/function.rs @@ -21,13 +21,10 @@ pub fn from_microstatement( name: _, kind, typen: _, - } => { - if let ArgKind::Ref = kind { - Ok(("".to_string(), out, deps)) - } else { - Err("Targeting JS does not allow for Mut{T}, Own{T}, or Deref{T}".into()) - } - } + } => match kind { + ArgKind::Ref | ArgKind::Mut => Ok(("".to_string(), out, deps)), + _ => Err("Targeting JS does not allow for Own{T} or Deref{T}".into()), + }, Microstatement::Assignment { name, value, @@ -36,10 +33,15 @@ pub fn from_microstatement( let (val, o, d) = from_microstatement(value, scope, out, deps)?; out = o; deps = d; + let n = if name.as_str() == "var" { + "__var__" + } else { + name.as_str() + }; if *mutable { - Ok((format!("let {} = {}", name, val), out, deps)) + Ok((format!("let {} = {}", n, val), out, deps)) } else { - Ok((format!("const {} = {}", name, val), out, deps)) + Ok((format!("const {} = {}", n, val), out, deps)) } } Microstatement::Closure { function } => { @@ -70,7 +72,18 @@ pub fn from_microstatement( representation, } => match &typen { CType::Type(n, _) if n == "string" => { - Ok((representation.replace("\n", "\\n"), out, deps)) + if representation.starts_with("\"") { + Ok((representation.replace("\n", "\\n"), out, deps)) + } else { + Ok((representation.clone(), out, deps)) + } + } + CType::Type(n, _) if n == "i64" || n == "u64" => { + if representation.replace("_", "").parse::().is_ok() { + Ok((format!("{}n", representation), out, deps)) + } else { + Ok((representation.clone(), out, deps)) + } } CType::Binds(n, _) => match &**n { CType::TString(_) => Ok((representation.clone(), out, deps)), @@ -116,7 +129,7 @@ pub fn from_microstatement( Ok((representation.clone(), out, deps)) } otherwise => CType::fail(&format!( - "1. Bound types must be strings or node.js imports: {:?}", + "Bound types must be strings or node.js imports: {:?}", otherwise )), }, @@ -228,7 +241,13 @@ pub fn from_microstatement( }, } } - _ => Ok((representation.clone(), out, deps)), + _ => { + if representation.as_str() == "var" { + Ok(("__var__".to_string(), out, deps)) + } else { + Ok((representation.clone(), out, deps)) + } + } }, Microstatement::Array { vals, .. } => { let mut val_representations = Vec::new(); @@ -278,7 +297,11 @@ pub fn from_microstatement( let (a, o, d) = from_microstatement(arg, scope, out, deps)?; out = o; deps = d; - argstrs.push(a); + if a.as_str() == "var" { + argstrs.push("__var__".to_string()); + } else { + argstrs.push(a); + } } if let FnKind::External(d) = &function.kind { match &*d { @@ -543,7 +566,7 @@ pub fn from_microstatement( if function.name == "Error" { return Ok(( format!( - "({} instanceof AlanError ? {} : null)", + "({} instanceof alan_std.AlanError ? {} : null)", argstrs[0], argstrs[0] ), out, @@ -552,7 +575,7 @@ pub fn from_microstatement( } else { return Ok(( format!( - "({} instanceof AlanError ? null : {})", + "(!({} instanceof alan_std.AlanError) ? {} : null)", argstrs[0], argstrs[0] ), out, diff --git a/alan/src/program/ctype.rs b/alan/src/program/ctype.rs index 4c4b1ab7..4f627be7 100644 --- a/alan/src/program/ctype.rs +++ b/alan/src/program/ctype.rs @@ -28,6 +28,7 @@ pub enum CType { Infix(Box), Prefix(Box), Method(Box), + Property(Box), Cast(Box), Own(Box), Deref(Box), @@ -207,6 +208,7 @@ impl CType { CType::Infix(o) => format!("Infix{{{}}}", o.to_strict_string(strict)), CType::Prefix(o) => format!("Prefix{{{}}}", o.to_strict_string(strict)), CType::Method(f) => format!("Method{{{}}}", f.to_strict_string(strict)), + CType::Property(p) => format!("Property{{{}}}", p.to_strict_string(strict)), CType::Cast(t) => format!("Cast{{{}}}", t.to_strict_string(strict)), CType::Own(t) => { if strict { @@ -471,6 +473,7 @@ impl CType { CType::Infix(o) => format!("Infix{{{}}}", o.to_functional_string()), CType::Prefix(o) => format!("Prefix{{{}}}", o.to_functional_string()), CType::Method(f) => format!("Method{{{}}}", f.to_functional_string()), + CType::Property(p) => format!("Property{{{}}}", p.to_functional_string()), CType::Cast(t) => format!("Cast{{{}}}", t.to_functional_string()), CType::Own(t) => format!("Own{{{}}}", t.to_functional_string()), CType::Deref(t) => format!("Deref{{{}}}", t.to_functional_string()), @@ -733,6 +736,7 @@ impl CType { CType::Infix(o) => CType::Infix(Box::new((*o).degroup())), CType::Prefix(o) => CType::Prefix(Box::new((*o).degroup())), CType::Method(f) => CType::Method(Box::new((*f).degroup())), + CType::Property(p) => CType::Property(Box::new((*p).degroup())), CType::Cast(t) => CType::Cast(Box::new((*t).degroup())), CType::Own(t) => CType::Own(Box::new((*t).degroup())), CType::Deref(t) => CType::Deref(Box::new((*t).degroup())), @@ -947,6 +951,10 @@ impl CType { arg.push(f1); input.push(f2); } + (Some(CType::Property(p1)), Some(CType::Property(p2))) => { + arg.push(p1); + input.push(p2); + } (Some(CType::Cast(t1)), Some(CType::Cast(t2))) => { arg.push(t1); input.push(t2); @@ -1936,6 +1944,50 @@ impl CType { otherwise )), }, + CType::Property(p) => match &**p { + CType::TString(s) => { + if args.len() > 1 { + CType::fail(&format!("Property bindings may only have one argument, the value the property is accessed from. Not {:?}", args)) + } else { + let arg_car = args[0].clone(); + microstatements.push(Microstatement::Return { + value: Some(Box::new(Microstatement::Value { + typen: rettype.clone(), + representation: format!( + "{}.{}", + match &arg_car.2 { + CType::Int(i) => { + trimmed_args = true; + format!("{}", i) + } + CType::Float(f) => { + trimmed_args = true; + format!("{}", f) + } + CType::Bool(b) => { + trimmed_args = true; + match b { + true => "true".to_string(), + false => "false".to_string(), + } + } + CType::TString(s) => { + trimmed_args = true; + format!("\"{}\"", s.replace("\"", "\\\"")) + } + _ => arg_car.0.clone(), + }, + s, + ), + })), + }); + } + } + otherwise => CType::fail(&format!( + "Unsupported native method declaration {:?}", + otherwise + )), + }, CType::Cast(t) => match &**t { CType::TString(s) => { if args.len() != 1 { @@ -2657,6 +2709,7 @@ impl CType { CType::Infix(o) => CType::Infix(Box::new(o.swap_subtype(old_type, new_type))), CType::Prefix(o) => CType::Prefix(Box::new(o.swap_subtype(old_type, new_type))), CType::Method(f) => CType::Method(Box::new(f.swap_subtype(old_type, new_type))), + CType::Property(p) => CType::Property(Box::new(p.swap_subtype(old_type, new_type))), CType::Cast(t) => CType::Cast(Box::new(t.swap_subtype(old_type, new_type))), CType::Own(t) => CType::Own(Box::new(t.swap_subtype(old_type, new_type))), CType::Deref(t) => CType::Deref(Box::new(t.swap_subtype(old_type, new_type))), @@ -3962,6 +4015,7 @@ pub fn typebaselist_to_ctype( "Infix" => CType::Infix(Box::new(args[0].clone())), "Prefix" => CType::Prefix(Box::new(args[0].clone())), "Method" => CType::Method(Box::new(args[0].clone())), + "Property" => CType::Property(Box::new(args[0].clone())), "Cast" => CType::Cast(Box::new(args[0].clone())), "Own" => CType::Own(Box::new(args[0].clone())), "Deref" => CType::Deref(Box::new(args[0].clone())), diff --git a/alan/src/program/scope.rs b/alan/src/program/scope.rs index f91cd0b9..26987ba1 100644 --- a/alan/src/program/scope.rs +++ b/alan/src/program/scope.rs @@ -92,9 +92,9 @@ impl<'a> Scope<'a> { "Type" | "Generic" | "Int" | "Float" | "Bool" | "String" => { /* Do nothing for the 'structural' types */ } - g @ ("Group" | "Infix" | "Prefix" | "Method" | "Cast" | "Own" - | "Deref" | "Mut" | "Rust" | "Node" | "From" | "Array" | "Fail" - | "Neg" | "Len" | "Size" | "FileStr" | "Env" | "EnvExists" + g @ ("Group" | "Infix" | "Prefix" | "Method" | "Property" | "Cast" + | "Own" | "Deref" | "Mut" | "Rust" | "Node" | "From" | "Array" + | "Fail" | "Neg" | "Len" | "Size" | "FileStr" | "Env" | "EnvExists" | "Not") => s = CType::from_generic(s, g, 1), g @ ("Function" | "Call" | "Dependency" | "Import" | "Field" | "Prop" | "Buffer" | "Add" | "Sub" | "Mul" | "Div" | "Mod" diff --git a/alan/src/std/root.ln b/alan/src/std/root.ln index 1e55426e..5953a758 100644 --- a/alan/src/std/root.ln +++ b/alan/src/std/root.ln @@ -19,6 +19,7 @@ export ctype Call{N, F}; // A reference to a function call (or method, etc) in t export ctype Infix{O}; // A reference to a native infix operation in the platform language export ctype Prefix{O}; // A reference to a native prefix operation in the platform language export ctype Method{F}; // A reference to a native method in the platform language +export ctype Property{P}; // A reference to a native property in the platform language export ctype Cast{T}; // A reference to a native type in the platform language to cast to export ctype Own{T}; // Specifying that the native code needs an owned version of the type export ctype Deref{T}; // Specifying that the native code needs an owned version of the type and it is safe to dereference the reference @@ -192,7 +193,7 @@ export fn{Rs} hash "alan_std::hashstring" <- RootBacking :: string -> i64; export fn void{T}(v: T) -> void {} export fn void() -> void {} export fn{Rs} store{T} (a: Mut{T}, b: T) -> T = {"std::mem::replace" :: (Mut{T}, Own{T}) -> T}(a, b); -export fn{Js} store{T} (a: Mut{T}, b: T) -> T = {Infix{"="} :: (T, T) -> T}(a, b); +export fn{Js} store{T} (a: Mut{T}, b: T) -> T = {Infix{"="} :: (Mut{T}, T) -> T}(a, b); /// Fallible, Maybe, and Either functions export fn{Rs} Maybe{T}(v: T!) = {Method{"ok"} :: T! -> T?}(v); @@ -201,14 +202,18 @@ export fn{Rs} getOr{T, U}(v: U, d: T) = {T}(v).getOr(d); export fn{Rs} getOr{T} (v: T?, d: T) = {Method{"unwrap_or"} :: (Own{T?}, Own{T}) -> T}(v, d); export fn{Rs} getOr{T} (v: T!, d: T) = {Method{"unwrap_or"} :: (Own{T!}, Own{T}) -> T}(v, d); export fn{Rs} getOrExit{T} (v: T!) = {Method{"unwrap"} :: Own{T!} -> T}(v); +export fn{Js} getOrExit{T} (v: T!) = {"((a) => a)" :: T! -> T}(v); // TODO: How to really represent this? export fn{Rs} getOrExit{T} (v: T?) = {Method{"unwrap"} :: Own{T?} -> T}(v); +export fn{Js} getOrExit{T} (v: T?) = {"((a) => a)" :: T? -> T}(v); // TODO: How to really represent this? export fn{Rs} Error{T} (e: string) = {"Err" :: Own{Error} -> T!}( {Method{"into"} :: Own{string} -> Error}(e)); export fn{Rs} Error (e: string) = {Method{"into"} :: Own{string} -> Error}(e); +export fn{Js} Error{T} "new alan_std.AlanError" <- RootBacking :: string -> T!; export fn{Js} Error "new alan_std.AlanError" <- RootBacking :: string -> Error; export fn{Rs} exists{T} (m: T?) = {Method{"is_some"} :: T? -> bool}(m); +export fn{Js} exists{T} (m: T?) = {"((a) => a !== null)" :: T? -> bool}(m); export fn{Rs} string(e: Error) = {"format!" :: ("{}", Error) -> string}(e); -export fn{Js} string Method{"message"} :: Error -> string; +export fn{Js} string Property{"message"} :: Error -> string; /// Primitive type casting functions export fn i8(i: i8) = i; @@ -962,31 +967,52 @@ export fn{Js} concat Infix{"+"} :: (string, string) -> string; export fn{Rs} repeat (a: string, n: i64) = {Method{"repeat"} :: (string, Deref{Binds{"usize"}}) -> string}( a, {Cast{"usize"} :: Deref{i64} -> Binds{"usize"}}(n)); -export fn{Rs} replace Method{"replace"} :: (string, string, string) -> string; +export fn{Js} repeat (a: string, n: i64) = {Method{"repeat"} :: (string, i32) -> string}(a, n.i32); +export fn replace Method{"replace"} :: (string, string, string) -> string; export fn{Rs} split "alan_std::splitstring" <- RootBacking :: (string, string) -> string[]; +export fn{Js} split Method{"split"} :: (string, string) -> string[]; export fn{Rs} len (s: string) = {Cast{"i64"} :: Deref{Binds{"usize"}} -> i64}( {Method{"len"} :: Array{Binds{"char"}} -> Binds{"usize"}}( {Method{"collect::>"} :: Own{Binds{"std::str::Chars"}} -> Array{Binds{"char"}}}( {Method{"chars"} :: string -> Binds{"std::str::Chars"}}(s)))); +export fn{Js} len (s: string) = {Property{"length"} :: string -> i32}(s).i64; export fn{Rs} get "alan_std::getstring" <- RootBacking :: (string, i64) -> string!; -export fn{Rs} trim Method{"trim"} :: string -> string; +export fn{Js} get (s: string, i: i64) = if(i.gte(0).and(i.lt(s.len)), + fn = Fallible{string}({Method{"at"} :: (string, i32) -> string}(s, i.i32)), + fn = Error{string}("Index ".concat(i.string).concat(" is out-of-bounds for a string length of ").concat(s.len.string))); +export fn trim Method{"trim"} :: string -> string; export fn{Rs} index "alan_std::indexstring" <- RootBacking :: (string, string) -> i64!; +export fn{Js} index (a: string, b: string) -> i64! { + let i = {Method{"indexOf"} :: (string, string) -> i64}(a, b); + return if(i.lt(0), + fn = Error{i64}("Could not find ".concat(b).concat(" in ").concat(a)), + fn = Fallible{i64}(i)); +} // TODO: Optimize this by using `as_str` inline, but need a `Str` type, which I *really* don't want to terrorize the user with export fn{Rs} eq Infix{"=="} :: (Own{string}, Own{string}) -> bool; +export fn{Js} eq Infix{"=="} :: (string, string) -> bool; export fn{Rs} neq Infix{"!="} :: (Own{string}, Own{string}) -> bool; +export fn{Js} neq Infix{"!="} :: (string, string) -> bool; export fn{Rs} lt Infix{"<"} :: (Own{string}, Own{string}) -> bool; +export fn{Js} lt Infix{"<"} :: (string, string) -> bool; export fn{Rs} lte Infix{"<="} :: (Own{string}, Own{string}) -> bool; +export fn{Js} lte Infix{"<="} :: (string, string) -> bool; export fn{Rs} gt Infix{">"} :: (Own{string}, Own{string}) -> bool; +export fn{Js} gt Infix{">"} :: (string, string) -> bool; export fn{Rs} gte Infix{">="} :: (Own{string}, Own{string}) -> bool; -export fn{Rs} min (a: string, b: string) = if(a.lte(b), fn () = a.clone(), fn () = b.clone()); -export fn{Rs} max (a: string, b: string) = if(a.gte(b), fn () = a.clone(), fn () = b.clone()); -export fn{Rs} join Method{"join"} :: (string[], string) -> string; +export fn{Js} gte Infix{">="} :: (string, string) -> bool; +export fn min (a: string, b: string) = if(a.lte(b), fn () = a.clone(), fn () = b.clone()); +export fn max (a: string, b: string) = if(a.gte(b), fn () = a.clone(), fn () = b.clone()); +export fn join Method{"join"} :: (string[], string) -> string; export fn{Rs} join{S}(a: string[S], s: string) = {Method{"join"} :: (string[S], string) -> string}(a, s); /// Array related bindings -export fn get{T} "alan_std::getarray" <- RootBacking :: (T[], i64) -> T?; -export fn len{T} (a: T[]) -> i64 = {Cast{"i64"} :: Deref{Binds{"usize"}} -> i64}( +export fn{Rs} len{T} (a: T[]) = {Cast{"i64"} :: Deref{Binds{"usize"}} -> i64}( {Method{"len"} :: T[] -> Binds{"usize"}}(a)); +export fn{Js} len{T} (a: T[]) = {Property{"length"} :: T[] -> i32}(a).i64; +export fn{Rs} get{T} "alan_std::getarray" <- RootBacking :: (T[], i64) -> T?; +export fn{Js} get{T} (a: T[], i: i64) = if(i.gte(0).and(i.lt(a.len)), + fn = {Method{"at"} :: (T[], i32) -> T}(a, i.i32)); export fn push{T} (a: Mut{T[]}, v: T) = {Method{"push"} :: (Mut{T[]}, Own{T})}(a, v); export fn pop{T} (a: Mut{T[]}) -> T? = {Method{"pop"} :: Mut{T[]} -> T?}(a); export fn map{T, U} "alan_std::map_onearg" <- RootBacking :: (T[], T -> U) -> U[]; @@ -4164,7 +4190,10 @@ export fn ExitCode(e: i64) = ExitCode(e.u8); /// Stdout/stderr-related bindings // TODO: Rework this to just print anything that can be converted to `string` via interfaces export fn{Rs} print{T}(v: T) = {"println!" :: ("{}", string)}(v.string); -export fn{Js} print{T}(v: T) = {"console.log" :: string}(v.string); +export fn{Js} print{T}(v: T) = {"console.log" :: T}(v); +export fn{Js} print (i: i64) = i.string.print; +export fn{Js} print (u: u64) = u.string.print; +export fn{Js} print (e: Error) = "Error: ".concat(e.string).print; export fn{Rs} print (d: Duration) = {"println!" :: ("{}.{:0>9}", u64, u32)}( {Method{"as_secs"} :: Duration -> u64}(d), {Method{"subsec_nanos"} :: Duration -> u32}(d)); @@ -4173,10 +4202,10 @@ export fn{Js} print(v: void) = {"console.log" :: "void"}(); export fn{Rs} print(s: string) = {"println!" :: ("{}", string)}(s); export fn{Rs} print{T, N}(a: T[N]) = "[".concat(a.map(fn (v: T) = v.string).join(", ")).concat("]").print; export fn{Rs} print{T}(a: T[]) = "[".concat(a.map(fn (v: T) = v.string).join(", ")).concat("]").print; -export fn{Rs} print{T}(v: T?) = if(v.exists, +export fn print{T}(v: T?) = if(v.exists, fn = v.getOrExit.print, fn = print("void")); -export fn{Rs} print{T}(v: T!) = if({T}(v).exists, +export fn print{T}(v: T!) = if({T}(v).exists, fn = v.getOrExit.print, fn = v.Error.getOrExit.print); export fn{Rs} eprint{T}(v: T) = {"eprintln!" :: ("{}", string)}(v.string);