Start adding support for classes

This commit is contained in:
Alex Crichton
2017-12-18 12:39:14 -08:00
parent 2225942000
commit 7c510a8a7e
7 changed files with 504 additions and 171 deletions

View File

@ -0,0 +1,159 @@
use shared;
#[derive(Default)]
pub struct Js {
pub expose_global_memory: bool,
pub expose_global_exports: bool,
pub exports: Vec<(String, String)>,
pub nodejs: bool,
}
impl Js {
pub fn generate_program(&mut self, program: &shared::Program) {
for f in program.free_functions.iter() {
self.generate_free_function(f);
}
}
pub fn generate_free_function(&mut self, func: &shared::Function) {
let simple = func.arguments.iter().all(|t| t.is_number()) &&
func.ret.as_ref().map(|t| t.is_number()).unwrap_or(true);
if simple {
self.exports.push((
func.name.clone(),
format!("obj.instance.exports.{}", func.name),
));
return
}
let mut dst = format!("function {}(", func.name);
let mut passed_args = String::new();
let mut arg_conversions = String::new();
let mut destructors = String::new();
for (i, arg) in func.arguments.iter().enumerate() {
let name = format!("arg{}", i);
if i > 0 {
dst.push_str(", ");
}
dst.push_str(&name);
let mut pass = |arg: &str| {
if passed_args.len() > 0 {
passed_args.push_str(", ");
}
passed_args.push_str(arg);
};
match *arg {
shared::Type::Number => pass(&name),
shared::Type::BorrowedStr |
shared::Type::String => {
if self.nodejs {
arg_conversions.push_str(&format!("\
const buf{i} = Buffer.from({arg});
const len{i} = buf{i}.length;
const ptr{i} = exports.__wbindgen_malloc(len{i});
let memory{i} = new Uint8Array(memory.buffer);
buf{i}.copy(memory{i}, ptr{i});
", i = i, arg = name));
pass(&format!("ptr{}", i));
pass(&format!("len{}", i));
if let shared::Type::BorrowedStr = *arg {
destructors.push_str(&format!("\n\
exports.__wbindgen_free(ptr{i}, len{i});\n\
", i = i));
}
} else {
panic!("strings not implemented for browser");
}
}
shared::Type::ByRef(_) |
shared::Type::ByMutRef(_) => {
arg_conversions.push_str(&format!("\
const ptr{i} = {arg}.__wasmPtr;
", i = i, arg = name));
pass(&format!("ptr{}", i));
}
shared::Type::ByValue(_) => {
arg_conversions.push_str(&format!("\
const ptr{i} = {arg}.__wasmPtr;
{arg}.__wasmPtr = 0;
", i = i, arg = name));
pass(&format!("ptr{}", i));
}
}
}
let convert_ret = match func.ret {
None |
Some(shared::Type::Number) => format!("return ret;"),
Some(shared::Type::BorrowedStr) |
Some(shared::Type::ByMutRef(_)) |
Some(shared::Type::ByRef(_)) => panic!(),
Some(shared::Type::ByValue(ref name)) => {
format!("\
return {name}.__wasmWrap(ret);
", name = name)
}
Some(shared::Type::String) => {
if self.nodejs {
format!("\
const mem = new Uint8Array(memory.buffer);
const ptr = exports.__wbindgen_boxed_str_ptr(ret);
const len = exports.__wbindgen_boxed_str_len(ret);
const buf = Buffer.from(mem.slice(ptr, ptr + len));
const realRet = buf.toString();
exports.__wbindgen_boxed_str_free(ret);
return realRet;
")
} else {
panic!("strings not implemented for browser");
}
}
};
dst.push_str(") {\n ");
dst.push_str(&arg_conversions);
dst.push_str(&format!("\
try {{
const ret = exports.{f}({passed});
{convert_ret}
}} finally {{
{destructors}
}}
", f = func.name, passed = passed_args, destructors = destructors,
convert_ret = convert_ret));
dst.push_str("};");
self.exports.push((func.name.clone(), dst));
}
pub fn to_string(&self) -> String {
let mut globals = String::new();
if self.expose_global_memory {
globals.push_str("const memory = obj.instance.exports.memory;\n");
}
if self.expose_global_exports {
globals.push_str("const exports = obj.instance.exports;\n");
}
let mut exports = String::new();
for &(ref name, ref body) in self.exports.iter() {
exports.push_str("obj.");
exports.push_str(name);
exports.push_str(" = ");
exports.push_str(body);
exports.push_str(";\n");
}
format!("
const function xform(obj) {{
{}
{}
return obj;
}}
export const function instantiate(bytes, imports) {{
return WebAssembly.instantiate(bytes, imports).then(xform);
}}
", globals, exports)
}
}

View File

@ -11,6 +11,8 @@ use std::io::Write;
use failure::{Error, ResultExt};
use parity_wasm::elements::*;
mod js;
pub struct Bindgen {
path: Option<PathBuf>,
nodejs: bool,
@ -18,7 +20,7 @@ pub struct Bindgen {
pub struct Object {
module: Module,
items: Vec<shared::Function>,
program: shared::Program,
nodejs: bool,
}
@ -48,10 +50,10 @@ impl Bindgen {
let mut module = parity_wasm::deserialize_file(input).map_err(|e| {
format_err!("{:?}", e)
})?;
let items = extract_items(&mut module);
let program = extract_program(&mut module);
Ok(Object {
module,
items,
program,
nodejs: self.nodejs,
})
}
@ -85,117 +87,13 @@ impl Object {
}
pub fn generate_js(&self) -> String {
let mut set_exports = String::new();
for func in self.items.iter() {
self.add_export(func, &mut set_exports);
}
format!("\
const xform = (obj) => {{
const exports = obj.instance.exports;
const memory = obj.instance.exports.memory;
{set_exports}
return obj;
}};
export const instantiate = (bytes, imports) => {{
return WebAssembly.instantiate(bytes, imports).then(xform);
}};
",
set_exports = set_exports,
)
}
fn add_export(&self, func: &shared::Function, dst: &mut String) {
let simple = func.arguments.iter().all(|t| t.is_number()) &&
func.ret.as_ref().map(|t| t.is_number()).unwrap_or(true);
if simple {
dst.push_str(&format!("\n\
obj.{f} = obj.instance.exports.{f};\
", f = func.name));
return
}
dst.push_str(&format!("\n obj.{f} = function {f}(",
f = func.name));
let mut passed_args = String::new();
let mut arg_conversions = String::new();
let mut destructors = String::new();
for (i, arg) in func.arguments.iter().enumerate() {
let name = format!("arg{}", i);
if i > 0 {
dst.push_str(", ");
}
dst.push_str(&name);
let mut pass = |arg: &str| {
if passed_args.len() > 0 {
passed_args.push_str(", ");
}
passed_args.push_str(arg);
};
match *arg {
shared::Type::Number => pass(&name),
shared::Type::BorrowedStr |
shared::Type::String => {
if self.nodejs {
arg_conversions.push_str(&format!("\
const buf{i} = Buffer.from({arg});
const len{i} = buf{i}.length;
const ptr{i} = exports.__wbindgen_malloc(len{i});
let memory{i} = new Uint8Array(memory.buffer);
buf{i}.copy(memory{i}, ptr{i});
", i = i, arg = name));
pass(&format!("ptr{}", i));
pass(&format!("len{}", i));
if let shared::Type::BorrowedStr = *arg {
destructors.push_str(&format!("\n\
exports.__wbindgen_free(ptr{i}, len{i});\n\
", i = i));
}
} else {
panic!("strings not implemented for browser");
}
}
}
}
let convert_ret = match func.ret {
None |
Some(shared::Type::Number) => format!("return ret;"),
Some(shared::Type::BorrowedStr) => panic!(),
Some(shared::Type::String) => {
if self.nodejs {
format!("\
const mem = new Uint8Array(memory.buffer);
const ptr = exports.__wbindgen_boxed_str_ptr(ret);
const len = exports.__wbindgen_boxed_str_len(ret);
const buf = Buffer.from(mem.slice(ptr, ptr + len));
const realRet = buf.toString();
exports.__wbindgen_boxed_str_free(ret);
return realRet;
")
} else {
panic!("strings not implemented for browser");
}
}
};
dst.push_str(") {\n ");
dst.push_str(&arg_conversions);
dst.push_str(&format!("\
try {{
const ret = exports.{f}({passed});
{convert_ret}
}} finally {{
{destructors}
}}
", f = func.name, passed = passed_args, destructors = destructors,
convert_ret = convert_ret));
dst.push_str("};");
let mut js = js::Js::default();
js.generate_program(&self.program);
js.to_string()
}
}
fn extract_items(module: &mut Module) -> Vec<shared::Function> {
fn extract_program(module: &mut Module) -> shared::Program {
let data = module.sections_mut()
.iter_mut()
.filter_map(|s| {
@ -205,12 +103,16 @@ fn extract_items(module: &mut Module) -> Vec<shared::Function> {
}
})
.next();
let mut ret = shared::Program {
structs: Vec::new(),
free_functions: Vec::new(),
};
let data = match data {
Some(data) => data,
None => return Vec::new(),
None => return ret,
};
let mut ret = Vec::new();
for i in (0..data.entries().len()).rev() {
{
let value = data.entries()[i].value();
@ -218,11 +120,13 @@ fn extract_items(module: &mut Module) -> Vec<shared::Function> {
continue
}
let json = &value[4..];
let func: shared::Function = match serde_json::from_slice(json) {
let p = match serde_json::from_slice(json) {
Ok(f) => f,
Err(_) => continue,
};
ret.push(func);
let shared::Program { structs, free_functions } = p;
ret.structs.extend(structs);
ret.free_functions.extend(free_functions);
}
data.entries_mut().remove(i);
}

View File

@ -1,4 +1,4 @@
extern crate wasm_bindgen;
extern crate wasm_bindgen_cli_support;
#[macro_use]
extern crate serde_derive;
extern crate docopt;
@ -6,7 +6,7 @@ extern crate docopt;
use std::path::PathBuf;
use docopt::Docopt;
use wasm_bindgen::Bindgen;
use wasm_bindgen_cli_support::Bindgen;
const USAGE: &'static str = "
Generating JS bindings for a wasm file

View File

@ -1,9 +1,12 @@
use proc_macro2::Literal;
use quote::{ToTokens, Tokens};
use serde_json;
use syn;
use wasm_bindgen_shared as shared;
pub struct Program {
pub structs: Vec<Struct>,
pub free_functions: Vec<Function>,
}
pub struct Function {
pub name: syn::Ident,
pub arguments: Vec<Type>,
@ -14,6 +17,58 @@ pub enum Type {
Integer(syn::Ident),
BorrowedStr,
String,
ByValue(syn::Ident),
ByRef(syn::Ident),
ByMutRef(syn::Ident),
}
pub struct Struct {
pub name: syn::Ident,
pub ctor: Option<Function>,
pub methods: Vec<Method>,
pub functions: Vec<Function>,
}
pub struct Method {
pub mutable: bool,
pub function: Function,
}
impl Program {
pub fn push_impl(&mut self, item: &syn::ItemImpl) {
match item.defaultness {
syn::Defaultness::Final => {}
_ => panic!("default impls are not supported"),
}
match item.unsafety {
syn::Unsafety::Normal => {}
_ => panic!("unsafe impls are not supported"),
}
if item.trait_.is_some() {
panic!("trait impls are not supported");
}
if item.generics.params.len() > 0 {
panic!("generic impls aren't supported");
}
let name = match Type::from(&item.self_ty) {
Type::ByValue(ident) => ident,
_ => panic!("unsupported self type in impl"),
};
let dst = self.structs
.iter_mut()
.find(|s| s.name == name)
.expect(&format!("failed to definition of struct for impl of `{}`", name));
for item in item.items.iter() {
dst.push_item(item);
}
}
pub fn shared(&self) -> shared::Program {
shared::Program {
structs: self.structs.iter().map(|s| s.shared()).collect(),
free_functions: self.free_functions.iter().map(|s| s.shared()).collect(),
}
}
}
impl Function {
@ -36,6 +91,7 @@ impl Function {
if !input.abi.is_none() {
panic!("can only bindgen Rust ABI functions")
}
if input.decl.variadic {
panic!("can't bindgen variadic functions")
}
@ -59,11 +115,7 @@ impl Function {
syn::ReturnType::Type(ref t, _) => Some(Type::from(t)),
};
Function {
name: input.ident,
arguments,
ret,
}
Function { name: input.ident, arguments, ret }
}
pub fn export_name(&self) -> syn::Lit {
@ -79,18 +131,6 @@ impl Function {
syn::Ident::from(generated_name)
}
pub fn generated_static_name(&self) -> syn::Ident {
let generated_name = format!("__WASM_BINDGEN_GENERATED_{}",
self.name.sym.as_str());
syn::Ident::from(generated_name)
}
pub fn generate_static(&self) -> Vec<u8> {
let mut prefix = String::from("wbg:");
prefix.push_str(&serde_json::to_string(&self.shared()).unwrap());
prefix.into_bytes()
}
fn shared(&self) -> shared::Function {
shared::Function {
name: self.name.sym.as_str().to_string(),
@ -120,16 +160,22 @@ impl Type {
if r.lifetime.is_some() {
panic!("can't have lifetimes on references yet");
}
match r.ty.mutability {
syn::Mutability::Immutable => {}
_ => panic!("can't have mutable references yet"),
}
let mutable = match r.ty.mutability {
syn::Mutability::Immutable => false,
syn::Mutability::Mutable(_) => true,
};
match r.ty.ty {
syn::Type::Path(syn::TypePath { qself: None, ref path }) => {
let ident = extract_path_ident(path);
match ident.sym.as_str() {
"str" => Type::BorrowedStr,
_ => panic!("unsupported reference type"),
"str" => {
if mutable {
panic!("mutable strings not allowed");
}
Type::BorrowedStr
}
_ if mutable => Type::ByMutRef(ident),
_ => Type::ByRef(ident),
}
}
_ => panic!("unsupported reference type"),
@ -151,7 +197,7 @@ impl Type {
Type::Integer(ident)
}
"String" => Type::String,
s => panic!("unsupported type: {}", s),
_ => Type::ByValue(ident),
}
}
_ => panic!("unsupported type"),
@ -163,21 +209,108 @@ impl Type {
Type::Integer(_) => shared::Type::Number,
Type::BorrowedStr => shared::Type::BorrowedStr,
Type::String => shared::Type::String,
Type::ByValue(n) => shared::Type::ByValue(n.to_string()),
Type::ByRef(n) => shared::Type::ByRef(n.to_string()),
Type::ByMutRef(n) => shared::Type::ByMutRef(n.to_string()),
}
}
}
impl ToTokens for Type {
fn to_tokens(&self, tokens: &mut Tokens) {
match *self {
Type::Integer(i) => i.to_tokens(tokens),
Type::String => {
syn::Ident::from("String").to_tokens(tokens);
impl Struct {
pub fn from(s: &syn::ItemStruct) -> Struct {
Struct {
name: s.ident,
ctor: None,
methods: Vec::new(),
functions: Vec::new(),
}
Type::BorrowedStr => {
<Token![&]>::default().to_tokens(tokens);
syn::Ident::from("str").to_tokens(tokens);
}
pub fn push_item(&mut self, item: &syn::ImplItem) {
let method = match *item {
syn::ImplItem::Const(_) => panic!("const definitions aren't supported"),
syn::ImplItem::Type(_) => panic!("type definitions in impls aren't supported"),
syn::ImplItem::Method(ref m) => m,
syn::ImplItem::Macro(_) => panic!("macros in impls aren't supported"),
};
match method.vis {
syn::Visibility::Public(_) => {}
_ => return,
}
match method.defaultness {
syn::Defaultness::Final => {}
_ => panic!("default methods are not supported"),
}
match method.sig.constness {
syn::Constness::NotConst => {}
_ => panic!("can only bindgen non-const functions"),
}
match method.sig.unsafety {
syn::Unsafety::Normal => {}
_ => panic!("can only bindgen safe functions"),
}
if method.sig.decl.variadic {
panic!("can't bindgen variadic functions")
}
if method.sig.decl.generics.params.len() > 0 {
panic!("can't bindgen functions with lifetime or type parameters")
}
let mut mutable = None;
let arguments = method.sig.decl.inputs.iter()
.map(|i| i.into_item())
.filter_map(|arg| {
match *arg {
syn::FnArg::Captured(ref c) => Some(c),
syn::FnArg::SelfValue(_) => {
panic!("by-value `self` not yet supported");
}
syn::FnArg::SelfRef(ref a) => {
assert!(mutable.is_none());
mutable = Some(match a.mutbl {
syn::Mutability::Mutable(_) => true,
syn::Mutability::Immutable => false,
});
None
}
_ => panic!("arguments cannot be `self` or ignored"),
}
})
.map(|arg| Type::from(&arg.ty))
.collect::<Vec<_>>();
let ret = match method.sig.decl.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(ref t, _) => Some(Type::from(t)),
};
let function = Function { name: method.sig.ident, arguments, ret };
match mutable {
Some(mutable) => {
self.methods.push(Method { mutable, function });
}
None => {
self.functions.push(function);
}
}
}
pub fn shared(&self) -> shared::Struct {
shared::Struct {
name: self.name.to_string(),
ctor: self.ctor.as_ref().unwrap().shared(),
functions: self.functions.iter().map(|f| f.shared()).collect(),
methods: self.methods.iter().map(|f| f.shared()).collect(),
}
}
}
impl Method {
pub fn shared(&self) -> shared::Method {
shared::Method {
mutable: self.mutable,
function: self.function.shared(),
}
}
}

View File

@ -1,7 +1,6 @@
#![feature(proc_macro)]
extern crate syn;
#[macro_use]
extern crate synom;
#[macro_use]
extern crate quote;
@ -28,25 +27,63 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream {
let mut ret = Tokens::new();
let mut program = ast::Program {
structs: Vec::new(),
free_functions: Vec::new(),
};
for item in file.items.iter() {
item.to_tokens(&mut ret);
match *item {
syn::Item::Fn(ref f) => bindgen_fn(f, &mut ret),
syn::Item::Fn(ref f) => {
program.free_functions.push(ast::Function::from(f));
}
syn::Item::Struct(ref s) => {
let s = ast::Struct::from(s);
if program.structs.iter().any(|a| a.name == s.name) {
panic!("redefinition of struct: {}", s.name);
}
program.structs.push(s);
}
syn::Item::Impl(ref s) => program.push_impl(s),
_ => panic!("unexpected item in bindgen macro"),
}
}
for function in program.free_functions.iter() {
bindgen_fn(function, &mut ret);
}
for s in program.structs.iter() {
bindgen_struct(s, &mut ret);
}
static CNT: AtomicUsize = ATOMIC_USIZE_INIT;
let generated_static_name = format!("__WASM_BINDGEN_GENERATED{}",
CNT.fetch_add(1, Ordering::SeqCst));
let mut generated_static = String::from("wbg:");
generated_static.push_str(&serde_json::to_string(&program.shared()).unwrap());
let generated_static_value = syn::Lit {
value: syn::LitKind::Other(Literal::byte_string(generated_static.as_bytes())),
span: Default::default(),
};
let generated_static_length = generated_static.len();
(quote! {
#[no_mangle]
#[allow(non_upper_case_globals)]
pub static #generated_static_name: [u8; #generated_static_length] =
*#generated_static_value;
}).to_tokens(&mut ret);
ret.into()
}
fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) {
let function = ast::Function::from(input);
fn bindgen_fn(function: &ast::Function, into: &mut Tokens) {
let export_name = function.export_name();
let generated_name = function.rust_symbol();
let mut args = vec![];
let mut arg_conversions = vec![];
let real_name = &input.ident;
let real_name = &function.name;
let mut converted_arguments = vec![];
let ret = syn::Ident::from("_ret");
@ -85,6 +122,32 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) {
};
});
}
ast::Type::ByValue(name) => {
args.push(quote! { #ident: *mut ::std::cell::RefCell<#name> });
arg_conversions.push(quote! {
assert!(!#ident.is_null());
let #ident = unsafe {
(*#ident).borrow_mut();
Box::from_raw(#ident).into_inner()
};
});
}
ast::Type::ByRef(name) => {
args.push(quote! { #ident: *mut ::std::cell::RefCell<#name> });
arg_conversions.push(quote! {
assert!(!#ident.is_null());
let #ident = unsafe { (*#ident).borrow() };
let #ident = &*#ident;
});
}
ast::Type::ByMutRef(name) => {
args.push(quote! { #ident: *mut ::std::cell::RefCell<#name> });
arg_conversions.push(quote! {
assert!(!#ident.is_null());
let mut #ident = unsafe { (*#ident).borrow_mut() };
let #ident = &mut *#ident;
});
}
}
converted_arguments.push(quote! { #ident });
}
@ -96,25 +159,25 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) {
convert_ret = quote! { #ret };
}
Some(ast::Type::BorrowedStr) => panic!("can't return a borrowed string"),
Some(ast::Type::ByRef(_)) => panic!("can't return a borrowed ref"),
Some(ast::Type::ByMutRef(_)) => panic!("can't return a borrowed ref"),
Some(ast::Type::String) => {
boxed_str = !BOXED_STR_GENERATED.swap(true, Ordering::SeqCst);
ret_ty = quote! { -> *mut String };
convert_ret = quote! { Box::into_raw(Box::new(#ret)) };
}
Some(ast::Type::ByValue(name)) => {
ret_ty = quote! { -> *mut ::std::cell::RefCell<#name> };
convert_ret = quote! {
Box::into_raw(Box::new(::std::cell::RefCell<#ret>))
};
}
None => {
ret_ty = quote! {};
convert_ret = quote! {};
}
}
let generated_static_name = function.generated_static_name();
let generated_static = function.generate_static();
let generated_static_value = syn::Lit {
value: syn::LitKind::Other(Literal::byte_string(&generated_static)),
span: Default::default(),
};
let generated_static_length = generated_static.len();
let malloc = if malloc {
quote! {
#[no_mangle]
@ -161,11 +224,6 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) {
#malloc
#boxed_str
#[no_mangle]
#[allow(non_upper_case_globals)]
pub static #generated_static_name: [u8; #generated_static_length] =
*#generated_static_value;
#[no_mangle]
#[export_name = #export_name]
pub extern fn #generated_name(#(#args),*) #ret_ty {
@ -177,3 +235,9 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) {
// println!("{}", tokens);
tokens.to_tokens(into);
}
fn bindgen_struct(s: &ast::Struct, into: &mut Tokens) {
if s.ctor.is_none() {
panic!("struct `{}` needs a `new` function to construct it", s.name);
}
}

View File

@ -1,6 +1,26 @@
#[macro_use]
extern crate serde_derive;
#[derive(Serialize, Deserialize)]
pub struct Program {
pub structs: Vec<Struct>,
pub free_functions: Vec<Function>,
}
#[derive(Serialize, Deserialize)]
pub struct Struct {
pub name: String,
pub ctor: Function,
pub functions: Vec<Function>,
pub methods: Vec<Method>,
}
#[derive(Serialize, Deserialize)]
pub struct Method {
pub mutable: bool,
pub function: Function,
}
#[derive(Serialize, Deserialize)]
pub struct Function {
pub name: String,
@ -13,6 +33,9 @@ pub enum Type {
Number,
BorrowedStr,
String,
ByValue(String),
ByRef(String),
ByMutRef(String),
}
impl Type {

50
tests/classes.rs Normal file
View File

@ -0,0 +1,50 @@
extern crate test_support;
#[test]
fn simple() {
test_support::project()
.file("src/lib.rs", r#"
#![feature(proc_macro)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
wasm_bindgen! {
pub struct Foo {
contents: u32,
}
impl Foo {
pub fn new() -> Foo {
Foo::with_contents(0)
}
pub fn with_contents(a: u32) -> Foo {
Foo::with_contents(a)
}
pub fn add(&mut self, amt: u32) -> u32 {
self.contents += amt;
self.contents
}
}
}
"#)
.file("test.js", r#"
import * as assert from "assert";
export function test(wasm) {
const r = new wasm.Foo();
assert.strictEqual(r.add(0), 0);
assert.strictEqual(r.add(1), 1);
assert.strictEqual(r.add(1), 2);
const r2 = wasm.Foo.with_contents(10);
assert.strictEqual(r.add(1), 11);
assert.strictEqual(r.add(2), 13);
assert.strictEqual(r.add(3), 16);
}
"#)
.test();
}