Merge pull request #2071 from rtfeldman/refcount-mono-ir

Start generating refcounting code as mono IR
This commit is contained in:
Brian Carroll 2021-12-01 12:59:16 +00:00 committed by GitHub
commit a3827d6636
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 726 additions and 209 deletions

View file

@ -491,11 +491,12 @@ fn gen_from_mono_module_dev_wasm32(
loaded: MonomorphizedModule,
app_o_file: &Path,
) -> CodeGenTiming {
let mut procedures = MutMap::default();
for (key, proc) in loaded.procedures {
procedures.insert(key, proc);
}
let MonomorphizedModule {
module_id,
procedures,
mut interns,
..
} = loaded;
let exposed_to_host = loaded
.exposed_to_host
@ -505,11 +506,11 @@ fn gen_from_mono_module_dev_wasm32(
let env = roc_gen_wasm::Env {
arena,
interns: loaded.interns,
module_id,
exposed_to_host,
};
let bytes = roc_gen_wasm::build_module(&env, procedures).unwrap();
let bytes = roc_gen_wasm::build_module(&env, &mut interns, procedures).unwrap();
std::fs::write(&app_o_file, &bytes).expect("failed to write object to file");

View file

@ -138,6 +138,7 @@ comptime {
comptime {
exportUtilsFn(utils.test_panic, "test_panic");
exportUtilsFn(utils.increfC, "incref");
exportUtilsFn(utils.decrefC, "decref");
exportUtilsFn(utils.decrefCheckNullC, "decref_check_null");

View file

@ -104,6 +104,12 @@ pub const IntWidth = enum(u8) {
I128 = 9,
};
pub fn increfC(ptr_to_refcount: *isize, amount: isize) callconv(.C) void {
var refcount = ptr_to_refcount.*;
var masked_amount = if (refcount == REFCOUNT_MAX_ISIZE) 0 else amount;
ptr_to_refcount.* = refcount + masked_amount;
}
pub fn decrefC(
bytes_or_null: ?[*]isize,
alignment: u32,
@ -261,3 +267,17 @@ pub const UpdateMode = enum(u8) {
Immutable = 0,
InPlace = 1,
};
test "increfC, refcounted data" {
var mock_rc: isize = REFCOUNT_ONE_ISIZE + 17;
var ptr_to_refcount: *isize = &mock_rc;
increfC(ptr_to_refcount, 2);
try std.testing.expectEqual(mock_rc, REFCOUNT_ONE_ISIZE + 19);
}
test "increfC, static data" {
var mock_rc: isize = REFCOUNT_MAX_ISIZE;
var ptr_to_refcount: *isize = &mock_rc;
increfC(ptr_to_refcount, 2);
try std.testing.expectEqual(mock_rc, REFCOUNT_MAX_ISIZE);
}

View file

@ -309,5 +309,6 @@ pub const DEC_MUL_WITH_OVERFLOW: &str = "roc_builtins.dec.mul_with_overflow";
pub const DEC_DIV: &str = "roc_builtins.dec.div";
pub const UTILS_TEST_PANIC: &str = "roc_builtins.utils.test_panic";
pub const UTILS_INCREF: &str = "roc_builtins.utils.incref";
pub const UTILS_DECREF: &str = "roc_builtins.utils.decref";
pub const UTILS_DECREF_CHECK_NULL: &str = "roc_builtins.utils.decref_check_null";

View file

@ -6038,6 +6038,10 @@ fn run_low_level<'a, 'ctx, 'env>(
| ListAny | ListAll | ListFindUnsafe | DictWalk => {
unreachable!("these are higher order, and are handled elsewhere")
}
RefCountGetPtr | RefCountInc | RefCountDec => {
unreachable!("LLVM backend does not use lowlevels for refcounting");
}
}
}

View file

@ -19,6 +19,8 @@ use roc_module::symbol::Interns;
use roc_module::symbol::Symbol;
use roc_mono::layout::{Builtin, Layout, LayoutIds, UnionLayout};
/// "Infinite" reference count, for static values
/// Ref counts are encoded as negative numbers where isize::MIN represents 1
pub const REFCOUNT_MAX: usize = 0_usize;
pub fn refcount_1(ctx: &Context, ptr_bytes: u32) -> IntValue<'_> {

View file

@ -3,7 +3,8 @@ use bumpalo::{self, collections::Vec};
use code_builder::Align;
use roc_collections::all::MutMap;
use roc_module::low_level::LowLevel;
use roc_module::symbol::Symbol;
use roc_module::symbol::{Interns, Symbol};
use roc_mono::gen_refcount::{RefcountProcGenerator, REFCOUNT_MAX};
use roc_mono::ir::{CallType, Expr, JoinPointId, Literal, Proc, Stmt};
use roc_mono::layout::{Builtin, Layout, LayoutIds};
@ -20,7 +21,7 @@ use crate::wasm_module::sections::{
};
use crate::wasm_module::{
code_builder, BlockType, CodeBuilder, ConstExpr, Export, ExportType, Global, GlobalType,
LocalId, Signature, SymInfo, ValueType,
LinkingSubSection, LocalId, Signature, SymInfo, ValueType,
};
use crate::{
copy_memory, CopyMemoryConfig, Env, BUILTINS_IMPORT_MODULE_NAME, MEMORY_NAME, PTR_SIZE,
@ -37,18 +38,21 @@ const CONST_SEGMENT_INDEX: usize = 0;
pub struct WasmBackend<'a> {
env: &'a Env<'a>,
interns: &'a mut Interns,
// Module-level data
pub module: WasmModule<'a>,
module: WasmModule<'a>,
layout_ids: LayoutIds<'a>,
constant_sym_index_map: MutMap<&'a str, usize>,
builtin_sym_index_map: MutMap<&'a str, usize>,
proc_symbols: Vec<'a, Symbol>,
pub linker_symbols: Vec<'a, SymInfo>,
proc_symbols: Vec<'a, (Symbol, u32)>,
linker_symbols: Vec<'a, SymInfo>,
refcount_proc_gen: RefcountProcGenerator<'a>,
// Function-level data
code_builder: CodeBuilder<'a>,
storage: Storage<'a>,
symbol_layouts: MutMap<Symbol, Layout<'a>>,
/// how many blocks deep are we (used for jumps)
block_depth: u32,
@ -58,10 +62,12 @@ pub struct WasmBackend<'a> {
impl<'a> WasmBackend<'a> {
pub fn new(
env: &'a Env<'a>,
interns: &'a mut Interns,
layout_ids: LayoutIds<'a>,
proc_symbols: Vec<'a, Symbol>,
proc_symbols: Vec<'a, (Symbol, u32)>,
mut linker_symbols: Vec<'a, SymInfo>,
mut exports: Vec<'a, Export>,
refcount_proc_gen: RefcountProcGenerator<'a>,
) -> Self {
const MEMORY_INIT_SIZE: u32 = 1024 * 1024;
let arena = env.arena;
@ -124,6 +130,7 @@ impl<'a> WasmBackend<'a> {
WasmBackend {
env,
interns,
// Module-level data
module,
@ -133,15 +140,47 @@ impl<'a> WasmBackend<'a> {
builtin_sym_index_map: MutMap::default(),
proc_symbols,
linker_symbols,
refcount_proc_gen,
// Function-level data
block_depth: 0,
joinpoint_label_map: MutMap::default(),
code_builder: CodeBuilder::new(arena),
storage: Storage::new(arena),
symbol_layouts: MutMap::default(),
}
}
pub fn generate_refcount_procs(&mut self) -> Vec<'a, Proc<'a>> {
let ident_ids = self
.interns
.all_ident_ids
.get_mut(&self.env.module_id)
.unwrap();
self.refcount_proc_gen
.generate_refcount_procs(self.env.arena, ident_ids)
}
pub fn finalize_module(mut self) -> WasmModule<'a> {
let symbol_table = LinkingSubSection::SymbolTable(self.linker_symbols);
self.module.linking.subsections.push(symbol_table);
self.module
}
/// Register the debug names of Symbols in a global lookup table
/// so that they have meaningful names when you print them.
/// Particularly useful after generating IR for refcount procedures
#[cfg(debug_assertions)]
pub fn register_symbol_debug_names(&self) {
let module_id = self.env.module_id;
let ident_ids = self.interns.all_ident_ids.get(&module_id).unwrap();
self.env.module_id.register_debug_idents(ident_ids);
}
#[cfg(not(debug_assertions))]
pub fn register_symbol_debug_names(&self) {}
/// Reset function-level data
fn reset(&mut self) {
// Push the completed CodeBuilder into the module and swap it for a new empty one
@ -151,6 +190,7 @@ impl<'a> WasmBackend<'a> {
self.storage.clear();
self.joinpoint_label_map.clear();
self.symbol_layouts.clear();
assert_eq!(self.block_depth, 0);
}
@ -160,17 +200,17 @@ impl<'a> WasmBackend<'a> {
***********************************************************/
pub fn build_proc(&mut self, proc: Proc<'a>, _sym: Symbol) -> Result<(), String> {
// println!("\ngenerating procedure {:?}\n", _sym);
pub fn build_proc(&mut self, proc: &Proc<'a>) -> Result<(), String> {
// println!("\ngenerating procedure {:?}\n", proc.name);
self.start_proc(&proc);
self.start_proc(proc);
self.build_stmt(&proc.body, &proc.ret_layout)?;
self.finalize_proc()?;
self.reset();
// println!("\nfinished generating {:?}\n", _sym);
// println!("\nfinished generating {:?}\n", proc.name);
Ok(())
}
@ -243,6 +283,8 @@ impl<'a> WasmBackend<'a> {
Stmt::Let(_, _, _, _) => {
let mut current_stmt = stmt;
while let Stmt::Let(sym, expr, layout, following) = current_stmt {
// println!("let {:?} = {}", sym, expr.to_pretty(200)); // ignore `following`! Too confusing otherwise.
let wasm_layout = WasmLayout::new(layout);
let kind = match following {
@ -268,6 +310,8 @@ impl<'a> WasmBackend<'a> {
);
}
self.symbol_layouts.insert(*sym, *layout);
current_stmt = *following;
}
@ -458,9 +502,46 @@ impl<'a> WasmBackend<'a> {
Ok(())
}
Stmt::Refcounting(_modify, following) => {
// TODO: actually deal with refcounting. For hello world, we just skipped it.
self.build_stmt(following, ret_layout)?;
Stmt::Refcounting(modify, following) => {
let value = modify.get_symbol();
let layout = self.symbol_layouts.get(&value).unwrap();
let ident_ids = self
.interns
.all_ident_ids
.get_mut(&self.env.module_id)
.unwrap();
let (rc_stmt, new_proc_info) = self
.refcount_proc_gen
.expand_refcount_stmt(ident_ids, *layout, modify, *following);
if false {
self.register_symbol_debug_names();
println!("## rc_stmt:\n{}\n{:?}", rc_stmt.to_pretty(200), rc_stmt);
}
// If we're creating a new RC procedure, we need to store its symbol data,
// so that we can correctly generate calls to it.
if let Some((rc_proc_sym, rc_proc_layout)) = new_proc_info {
let wasm_fn_index = self.proc_symbols.len() as u32;
let linker_sym_index = self.linker_symbols.len() as u32;
let name = self
.layout_ids
.get_toplevel(rc_proc_sym, &rc_proc_layout)
.to_symbol_string(rc_proc_sym, self.interns);
self.proc_symbols.push((rc_proc_sym, linker_sym_index));
self.linker_symbols
.push(SymInfo::Function(WasmObjectSymbol::Defined {
flags: 0,
index: wasm_fn_index,
name,
}));
}
self.build_stmt(&rc_stmt, ret_layout)?;
Ok(())
}
@ -504,29 +585,26 @@ impl<'a> WasmBackend<'a> {
CallConv::C,
);
// Index of the called function in the code section. Assumes all functions end up in the binary.
// (We may decide to keep all procs even if calls are inlined, in case platform calls them)
let func_index = match self.proc_symbols.iter().position(|s| s == func_sym) {
Some(i) => i as u32,
None => {
// TODO: actually useful linking! Push a relocation for it.
return Err(format!(
"Not yet supported: calling foreign function {:?}",
func_sym
));
}
};
// Index of the function's name in the symbol table
// Same as the function index since those are the first symbols we add
let symbol_index = func_index;
for (func_index, (ir_sym, linker_sym_index)) in
self.proc_symbols.iter().enumerate()
{
if ir_sym == func_sym {
let num_wasm_args = param_types.len();
let has_return_val = ret_type.is_some();
self.code_builder
.call(func_index, symbol_index, num_wasm_args, has_return_val);
self.code_builder.call(
func_index as u32,
*linker_sym_index,
num_wasm_args,
has_return_val,
);
return Ok(());
}
}
Ok(())
unreachable!(
"Could not find procedure {:?}\nKnown procedures: {:?}",
func_sym, self.proc_symbols
);
}
CallType::LowLevel { op: lowlevel, .. } => {
@ -631,7 +709,7 @@ impl<'a> WasmBackend<'a> {
sym: Symbol,
layout: &Layout<'a>,
) -> Result<(), String> {
let not_supported_error = || Err(format!("Literal value {:?} is not yet implemented", lit));
let not_supported_error = || panic!("Literal value {:?} is not yet implemented", lit);
match storage {
StoredValue::VirtualMachineStack { value_type, .. } => {
@ -675,6 +753,8 @@ impl<'a> WasmBackend<'a> {
stack_mem_bytes[7] = 0x80 | (len as u8);
let str_as_int = i64::from_le_bytes(stack_mem_bytes);
// Write all 8 bytes at once using an i64
// Str is normally two i32's, but in this special case, we can get away with fewer instructions
self.code_builder.get_local(local_id);
self.code_builder.i64_const(str_as_int);
self.code_builder.i64_store(Align::Bytes4, offset);
@ -732,10 +812,13 @@ impl<'a> WasmBackend<'a> {
None => {
let const_segment_bytes = &mut self.module.data.segments[CONST_SEGMENT_INDEX].init;
// Store the string in the data section, to be loaded on module instantiation
// RocStr `elements` field will point to that constant data, not the heap
let segment_offset = const_segment_bytes.len() as u32;
let elements_addr = segment_offset + CONST_SEGMENT_BASE_ADDR;
// Store the string in the data section
// Prefix it with a special refcount value (treated as "infinity")
// The string's `elements` field points at the data after the refcount
let refcount_max_bytes: [u8; 4] = (REFCOUNT_MAX as i32).to_le_bytes();
const_segment_bytes.extend_from_slice(&refcount_max_bytes);
let elements_offset = const_segment_bytes.len() as u32;
let elements_addr = elements_offset + CONST_SEGMENT_BASE_ADDR;
const_segment_bytes.extend_from_slice(string.as_bytes());
// Generate linker info
@ -743,12 +826,12 @@ impl<'a> WasmBackend<'a> {
let name = self
.layout_ids
.get(sym, layout)
.to_symbol_string(sym, &self.env.interns);
.to_symbol_string(sym, self.interns);
let linker_symbol = SymInfo::Data(DataSymbol::Defined {
flags: 0,
name,
segment_index: CONST_SEGMENT_INDEX as u32,
segment_offset,
segment_offset: elements_offset,
size: string.len() as u32,
});
@ -830,10 +913,11 @@ impl<'a> WasmBackend<'a> {
None => {
// Wasm function signature
let signature_index = self.module.types.insert(Signature {
let signature = Signature {
param_types,
ret_type,
});
};
let signature_index = self.module.types.insert(signature);
// Declare it as an import since it comes from a different .o file
let import_index = self.module.import.entries.len() as u32;

View file

@ -39,9 +39,6 @@ pub enum WasmLayout {
alignment_bytes: u32,
format: StackMemoryFormat,
},
// Local pointer to heap memory
HeapMemory,
}
impl WasmLayout {
@ -105,7 +102,7 @@ impl WasmLayout {
| NullableWrapped { .. }
| NullableUnwrapped { .. },
)
| Layout::RecursivePointer => Self::HeapMemory,
| Layout::RecursivePointer => Self::Primitive(PTR_TYPE, PTR_SIZE),
}
}
@ -120,7 +117,6 @@ impl WasmLayout {
Self::Primitive(I64, _) => &[I64],
Self::Primitive(F32, _) => &[F32],
Self::Primitive(F64, _) => &[F64],
Self::HeapMemory => &[I32],
// 1 Roc argument => 0-2 Wasm arguments (depending on size and calling convention)
Self::StackMemory { size, format, .. } => conv.stack_memory_arg_types(*size, *format),
@ -130,7 +126,6 @@ impl WasmLayout {
pub fn return_method(&self) -> ReturnMethod {
match self {
Self::Primitive(ty, _) => ReturnMethod::Primitive(*ty),
Self::HeapMemory => ReturnMethod::Primitive(PTR_TYPE),
Self::StackMemory { size, .. } => {
if *size == 0 {
ReturnMethod::NoReturnValue
@ -145,7 +140,6 @@ impl WasmLayout {
match self {
Self::Primitive(_, size) => *size,
Self::StackMemory { size, .. } => *size,
Self::HeapMemory => PTR_SIZE,
}
}
}

View file

@ -6,16 +6,17 @@ pub mod wasm_module;
use bumpalo::{self, collections::Vec, Bump};
use roc_builtins::bitcode::IntWidth;
use roc_collections::all::{MutMap, MutSet};
use roc_module::low_level::LowLevel;
use roc_module::symbol::{Interns, Symbol};
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_mono::gen_refcount::RefcountProcGenerator;
use roc_mono::ir::{Proc, ProcLayout};
use roc_mono::layout::LayoutIds;
use crate::backend::WasmBackend;
use crate::wasm_module::{
Align, CodeBuilder, Export, ExportType, LinkingSubSection, LocalId, SymInfo, ValueType,
WasmModule,
Align, CodeBuilder, Export, ExportType, LocalId, SymInfo, ValueType, WasmModule,
};
const PTR_SIZE: u32 = 4;
@ -29,27 +30,29 @@ pub const STACK_POINTER_NAME: &str = "__stack_pointer";
pub struct Env<'a> {
pub arena: &'a Bump,
pub interns: Interns,
pub module_id: ModuleId,
pub exposed_to_host: MutSet<Symbol>,
}
pub fn build_module<'a>(
env: &'a Env,
env: &'a Env<'a>,
interns: &'a mut Interns,
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
) -> Result<std::vec::Vec<u8>, String> {
let (mut wasm_module, _) = build_module_help(env, procedures)?;
let (mut wasm_module, _) = build_module_help(env, interns, procedures)?;
let mut buffer = std::vec::Vec::with_capacity(4096);
wasm_module.serialize_mut(&mut buffer);
Ok(buffer)
}
pub fn build_module_help<'a>(
env: &'a Env,
env: &'a Env<'a>,
interns: &'a mut Interns,
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
) -> Result<(WasmModule<'a>, u32), String> {
let mut layout_ids = LayoutIds::default();
let mut generated_procs = Vec::with_capacity_in(procedures.len(), env.arena);
let mut generated_symbols = Vec::with_capacity_in(procedures.len(), env.arena);
let mut procs = Vec::with_capacity_in(procedures.len(), env.arena);
let mut proc_symbols = Vec::with_capacity_in(procedures.len() * 2, env.arena);
let mut linker_symbols = Vec::with_capacity_in(procedures.len() * 2, env.arena);
let mut exports = Vec::with_capacity_in(4, env.arena);
let mut main_fn_index = None;
@ -61,12 +64,11 @@ pub fn build_module_help<'a>(
if LowLevel::from_inlined_wrapper(sym).is_some() {
continue;
}
generated_procs.push(proc);
generated_symbols.push(sym);
procs.push(proc);
let fn_name = layout_ids
.get_toplevel(sym, &layout)
.to_symbol_string(sym, &env.interns);
.to_symbol_string(sym, interns);
if env.exposed_to_host.contains(&sym) {
main_fn_index = Some(fn_index);
@ -78,29 +80,54 @@ pub fn build_module_help<'a>(
}
let linker_sym = SymInfo::for_function(fn_index, fn_name);
proc_symbols.push((sym, linker_symbols.len() as u32));
linker_symbols.push(linker_sym);
fn_index += 1;
}
// Build the Wasm module
let (mut module, linker_symbols) = {
let mut backend = WasmBackend::new(
env,
interns,
layout_ids,
generated_symbols.clone(),
proc_symbols,
linker_symbols,
exports,
RefcountProcGenerator::new(env.arena, IntWidth::I32, env.module_id),
);
for (proc, sym) in generated_procs.into_iter().zip(generated_symbols) {
backend.build_proc(proc, sym)?;
if false {
println!("## procs");
for proc in procs.iter() {
println!("{}", proc.to_pretty(200));
println!("{:#?}", proc);
}
}
(backend.module, backend.linker_symbols)
};
let symbol_table = LinkingSubSection::SymbolTable(linker_symbols);
module.linking.subsections.push(symbol_table);
// Generate procs from user code
for proc in procs.iter() {
backend.build_proc(proc)?;
}
// Generate IR for refcounting procs
let refcount_procs = backend.generate_refcount_procs();
backend.register_symbol_debug_names();
if false {
println!("## refcount_procs");
for proc in refcount_procs.iter() {
println!("{}", proc.to_pretty(200));
println!("{:#?}", proc);
}
}
// Generate Wasm for refcounting procs
for proc in refcount_procs.iter() {
backend.build_proc(proc)?;
}
let module = backend.finalize_module();
Ok((module, main_fn_index.unwrap()))
}

View file

@ -76,7 +76,6 @@ pub fn decode_low_level<'a>(
StackMemoryFormat::Float128 => return NotImplemented,
StackMemoryFormat::Decimal => return BuiltinCall(bitcode::DEC_ADD_WITH_OVERFLOW),
},
WasmLayout::HeapMemory { .. } => return NotImplemented,
},
NumAddWrap => match ret_layout.arg_types(CallConv::Zig)[0] {
I32 => {
@ -371,6 +370,12 @@ pub fn decode_low_level<'a>(
Not => code_builder.i32_eqz(),
Hash => return NotImplemented,
ExpectTrue => return NotImplemented,
RefCountGetPtr => {
code_builder.i32_const(4);
code_builder.i32_sub();
}
RefCountInc => return BuiltinCall(bitcode::UTILS_INCREF),
RefCountDec => return BuiltinCall(bitcode::UTILS_DECREF),
}
Done
}

View file

@ -8,7 +8,7 @@ use crate::layout::{
CallConv, ReturnMethod, StackMemoryFormat, WasmLayout, ZigVersion, BUILTINS_ZIG_VERSION,
};
use crate::wasm_module::{Align, CodeBuilder, LocalId, ValueType, VmSymbolState};
use crate::{copy_memory, round_up_to_alignment, CopyMemoryConfig, PTR_SIZE, PTR_TYPE};
use crate::{copy_memory, round_up_to_alignment, CopyMemoryConfig, PTR_TYPE};
pub enum StoredValueKind {
Parameter,
@ -79,6 +79,7 @@ impl StoredValue {
/// Helper structure for WasmBackend, to keep track of how values are stored,
/// including the VM stack, local variables, and linear memory
#[derive(Debug)]
pub struct Storage<'a> {
pub arg_types: Vec<'a, ValueType>,
pub local_types: Vec<'a, ValueType>,
@ -146,18 +147,6 @@ impl<'a> Storage<'a> {
},
},
WasmLayout::HeapMemory => {
match kind {
StoredValueKind::Parameter => self.arg_types.push(PTR_TYPE),
_ => self.local_types.push(PTR_TYPE),
}
StoredValue::Local {
local_id: next_local_id,
value_type: PTR_TYPE,
size: PTR_SIZE,
}
}
WasmLayout::StackMemory {
size,
alignment_bytes,

View file

@ -234,7 +234,9 @@ impl<'a> CodeBuilder<'a> {
pub fn set_top_symbol(&mut self, sym: Symbol) -> VmSymbolState {
let current_stack = &mut self.vm_block_stack.last_mut().unwrap().value_stack;
let pushed_at = self.code.len();
let top_symbol: &mut Symbol = current_stack.last_mut().unwrap();
let top_symbol: &mut Symbol = current_stack
.last_mut()
.unwrap_or_else(|| unreachable!("Empty stack when trying to set Symbol {:?}", sym));
*top_symbol = sym;
VmSymbolState::Pushed { pushed_at }

View file

@ -1,30 +0,0 @@
#!/bin/bash
if [[ -z "$1" || -z "$2" ]]
then
echo "$0 needs 2 arguments: the directories to compare"
exit 1
fi
OVERHEAD_BYTES=114 # total file size minus generated code size (test wrapper + module headers)
printf "filename \tLHS\tRHS\tchange\n"
printf "======== \t===\t===\t======\n"
for f in `ls $1/wasm`
do
if [[ ! -f "$2/wasm/$f" ]]
then
echo "$f found in $1/wasm but not in $2/wasm"
continue
fi
SIZE1=$(stat --format '%s' "$1/wasm/$f")
SIZE2=$(stat --format '%s' "$2/wasm/$f")
CHANGE=$(( $SIZE2 - $SIZE1 ))
NET_SIZE1=$(( $SIZE1 - $OVERHEAD_BYTES ))
NET_SIZE2=$(( $SIZE2 - $OVERHEAD_BYTES ))
PERCENT_CHANGE=$(( $CHANGE * 100 / $NET_SIZE1 ))
printf "%s\t%d\t%d\t%d\t%d%%\n" $f $NET_SIZE1 $NET_SIZE2 $CHANGE $PERCENT_CHANGE
done

View file

@ -1,24 +0,0 @@
#!/bin/bash
TARGET_DIR=$1
if [[ -z "$TARGET_DIR" ]]
then
echo "$0 needs an argument: target directory for output wasm and wat files"
exit 1
fi
rm -rf output $TARGET_DIR
mkdir -p output $TARGET_DIR $TARGET_DIR/wasm $TARGET_DIR/wat
cargo test -- --test-threads=1 --nocapture
mv output/* $TARGET_DIR/wasm
for f in `ls $TARGET_DIR/wasm`
do
wasm2wat $TARGET_DIR/wasm/$f -o $TARGET_DIR/wat/${f%.wasm}.wat
done
SIZE=$(du -b "$TARGET_DIR/wasm")
echo "Total bytes *.wasm = $SIZE"

View file

@ -116,6 +116,9 @@ pub enum LowLevel {
Not,
Hash,
ExpectTrue,
RefCountGetPtr,
RefCountInc,
RefCountDec,
}
macro_rules! higher_order {

View file

@ -1008,6 +1008,10 @@ pub fn lowlevel_borrow_signature(arena: &Bump, op: LowLevel) -> &[bool] {
SetFromList => arena.alloc_slice_copy(&[owned]),
ExpectTrue => arena.alloc_slice_copy(&[irrelevant]),
RefCountGetPtr | RefCountInc | RefCountDec => {
unreachable!("Refcounting lowlevel calls are inserted *after* borrow checking");
}
}
}

View file

@ -0,0 +1,410 @@
use bumpalo::collections::vec::Vec;
use bumpalo::Bump;
use roc_builtins::bitcode::IntWidth;
use roc_module::ident::Ident;
use roc_module::low_level::LowLevel;
use roc_module::symbol::{IdentIds, ModuleId, Symbol};
use crate::ir::{
BranchInfo, Call, CallSpecId, CallType, Expr, HostExposedLayouts, Literal, ModifyRc, Proc,
ProcLayout, SelfRecursive, Stmt, UpdateModeId,
};
use crate::layout::{Builtin, Layout};
const LAYOUT_BOOL: Layout = Layout::Builtin(Builtin::Bool);
const LAYOUT_UNIT: Layout = Layout::Struct(&[]);
const LAYOUT_PTR: Layout = Layout::RecursivePointer;
const LAYOUT_U32: Layout = Layout::Builtin(Builtin::Int(IntWidth::U32));
/// "Infinite" reference count, for static values
/// Ref counts are encoded as negative numbers where isize::MIN represents 1
pub const REFCOUNT_MAX: usize = 0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RefcountOp {
Inc,
Dec,
DecRef,
}
/// Generate specialized refcounting code in mono IR format
/// -------------------------------------------------------
///
/// Any backend that wants to use this, needs a field of type `RefcountProcGenerator`.
///
/// Whenever the backend sees a `Stmt::Refcounting`, it calls
/// `RefcountProcGenerator::expand_refcount_stmt()`, which returns IR statements
/// to call a refcounting procedure. The backend can then generate target code
/// for those IR statements instead of the original `Refcounting` statement.
///
/// Essentially we are expanding the `Refcounting` statement into a more detailed
/// form that's more suitable for code generation.
///
/// But so far, we've only mentioned _calls_ to the refcounting procedures.
/// The procedures themselves don't exist yet!
///
/// So when the backend has finished with all the `Proc`s from user code,
/// it's time to call `RefcountProcGenerator::generate_refcount_procs()`,
/// which generates the `Procs` for refcounting helpers. The backend can
/// simply generate target code for these `Proc`s just like any other Proc.
///
pub struct RefcountProcGenerator<'a> {
arena: &'a Bump,
home: ModuleId,
ptr_size: u32,
layout_isize: Layout<'a>,
/// List of refcounting procs to generate, specialised by Layout and RefCountOp
/// Order of insertion is preserved, since it is important for Wasm backend
procs_to_generate: Vec<'a, (Layout<'a>, RefcountOp, Symbol)>,
}
impl<'a> RefcountProcGenerator<'a> {
pub fn new(arena: &'a Bump, intwidth_isize: IntWidth, home: ModuleId) -> Self {
RefcountProcGenerator {
arena,
home,
ptr_size: intwidth_isize.stack_size(),
layout_isize: Layout::Builtin(Builtin::Int(intwidth_isize)),
procs_to_generate: Vec::with_capacity_in(16, arena),
}
}
/// Expands the IR node Stmt::Refcounting to a more detailed IR Stmt that calls a helper proc.
/// The helper procs themselves can be generated later by calling `generate_refcount_procs`
pub fn expand_refcount_stmt(
&mut self,
ident_ids: &mut IdentIds,
layout: Layout<'a>,
modify: &ModifyRc,
following: &'a Stmt<'a>,
) -> (Stmt<'a>, Option<(Symbol, ProcLayout<'a>)>) {
match modify {
ModifyRc::Inc(structure, amount) => {
let layout_isize = self.layout_isize;
let (is_existing, proc_name) =
self.get_proc_symbol(ident_ids, layout, RefcountOp::Inc);
// Define a constant for the amount to increment
let amount_sym = self.create_symbol(ident_ids, "amount");
let amount_expr = Expr::Literal(Literal::Int(*amount as i128));
let amount_stmt = |next| Stmt::Let(amount_sym, amount_expr, layout_isize, next);
// Call helper proc, passing the Roc structure and constant amount
let arg_layouts = self.arena.alloc([layout, layout_isize]);
let call_result_empty = self.create_symbol(ident_ids, "call_result_empty");
let call_expr = Expr::Call(Call {
call_type: CallType::ByName {
name: proc_name,
ret_layout: &LAYOUT_UNIT,
arg_layouts,
specialization_id: CallSpecId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([*structure, amount_sym]),
});
let call_stmt = Stmt::Let(call_result_empty, call_expr, LAYOUT_UNIT, following);
let rc_stmt = amount_stmt(self.arena.alloc(call_stmt));
// Create a linker symbol for the helper proc if this is the first usage
let new_proc_info = if is_existing {
None
} else {
Some((
proc_name,
ProcLayout {
arguments: arg_layouts,
result: LAYOUT_UNIT,
},
))
};
(rc_stmt, new_proc_info)
}
ModifyRc::Dec(structure) => {
let (is_existing, proc_name) =
self.get_proc_symbol(ident_ids, layout, RefcountOp::Dec);
// Call helper proc, passing the Roc structure
let arg_layouts = self.arena.alloc([layout, self.layout_isize]);
let call_result_empty = self.create_symbol(ident_ids, "call_result_empty");
let call_expr = Expr::Call(Call {
call_type: CallType::ByName {
name: proc_name,
ret_layout: &LAYOUT_UNIT,
arg_layouts: self.arena.alloc([layout]),
specialization_id: CallSpecId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([*structure]),
});
let rc_stmt = Stmt::Let(call_result_empty, call_expr, LAYOUT_UNIT, following);
// Create a linker symbol for the helper proc if this is the first usage
let new_proc_info = if is_existing {
None
} else {
Some((
proc_name,
ProcLayout {
arguments: arg_layouts,
result: LAYOUT_UNIT,
},
))
};
(rc_stmt, new_proc_info)
}
ModifyRc::DecRef(structure) => {
// No generated procs for DecRef, just lowlevel calls
// Get a pointer to the refcount itself
let rc_ptr_sym = self.create_symbol(ident_ids, "rc_ptr");
let rc_ptr_expr = Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::RefCountGetPtr,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([*structure]),
});
let rc_ptr_stmt = |next| Stmt::Let(rc_ptr_sym, rc_ptr_expr, LAYOUT_PTR, next);
// Pass the refcount pointer to the lowlevel call (see utils.zig)
let call_result_empty = self.create_symbol(ident_ids, "call_result_empty");
let call_expr = Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::RefCountDec,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([rc_ptr_sym]),
});
let call_stmt = Stmt::Let(call_result_empty, call_expr, LAYOUT_UNIT, following);
let rc_stmt = rc_ptr_stmt(self.arena.alloc(call_stmt));
(rc_stmt, None)
}
}
}
/// Generate refcounting helper procs, each specialized to a particular Layout.
/// For example `List (Result { a: Str, b: Int } Str)` would get its own helper
/// to update the refcounts on the List, the Result and the strings.
pub fn generate_refcount_procs(
&mut self,
arena: &'a Bump,
ident_ids: &mut IdentIds,
) -> Vec<'a, Proc<'a>> {
// Move the vector so we can loop over it safely
let mut procs_to_generate = Vec::with_capacity_in(0, arena);
std::mem::swap(&mut self.procs_to_generate, &mut procs_to_generate);
let mut procs = Vec::with_capacity_in(procs_to_generate.len(), arena);
for (layout, op, proc_symbol) in procs_to_generate.drain(0..) {
let proc = match layout {
Layout::Builtin(Builtin::Str) => self.gen_modify_str(ident_ids, op, proc_symbol),
_ => todo!("Refcounting is not yet implemented for Layout {:?}", layout),
};
procs.push(proc);
}
procs
}
/// Find the Symbol of the procedure for this layout and refcount operation,
/// or create one if needed.
fn get_proc_symbol(
&mut self,
ident_ids: &mut IdentIds,
layout: Layout<'a>,
op: RefcountOp,
) -> (bool, Symbol) {
let found = self
.procs_to_generate
.iter()
.find(|(l, o, _)| *l == layout && *o == op);
if let Some((_, _, existing_symbol)) = found {
(true, *existing_symbol)
} else {
let layout_name = layout_debug_name(&layout);
let unique_idx = self.procs_to_generate.len();
let debug_name = format!("#rc{:?}_{}_{}", op, layout_name, unique_idx);
let new_symbol: Symbol = self.create_symbol(ident_ids, &debug_name);
self.procs_to_generate.push((layout, op, new_symbol));
(false, new_symbol)
}
}
fn create_symbol(&mut self, ident_ids: &mut IdentIds, debug_name: &str) -> Symbol {
let ident_id = ident_ids.add(Ident::from(debug_name));
Symbol::new(self.home, ident_id)
}
fn return_unit(&mut self, ident_ids: &mut IdentIds) -> Stmt<'a> {
let unit = self.create_symbol(ident_ids, "unit");
let ret_stmt = self.arena.alloc(Stmt::Ret(unit));
Stmt::Let(unit, Expr::Struct(&[]), LAYOUT_UNIT, ret_stmt)
}
fn gen_args(&mut self, op: RefcountOp, layout: Layout<'a>) -> &'a [(Layout<'a>, Symbol)] {
let roc_value = (layout, Symbol::ARG_1);
match op {
RefcountOp::Inc => {
let inc_amount = (self.layout_isize, Symbol::ARG_2);
self.arena.alloc([roc_value, inc_amount])
}
RefcountOp::Dec | RefcountOp::DecRef => self.arena.alloc([roc_value]),
}
}
/// Generate a procedure to modify the reference count of a Str
fn gen_modify_str(
&mut self,
ident_ids: &mut IdentIds,
op: RefcountOp,
proc_name: Symbol,
) -> Proc<'a> {
let string = Symbol::ARG_1;
let layout_isize = self.layout_isize;
// Get the string length as a signed int
let len = self.create_symbol(ident_ids, "len");
let len_expr = Expr::StructAtIndex {
index: 1,
field_layouts: self.arena.alloc([LAYOUT_PTR, layout_isize]),
structure: string,
};
let len_stmt = |next| Stmt::Let(len, len_expr, layout_isize, next);
// Zero
let zero = self.create_symbol(ident_ids, "zero");
let zero_expr = Expr::Literal(Literal::Int(0));
let zero_stmt = |next| Stmt::Let(zero, zero_expr, layout_isize, next);
// is_big_str = (len >= 0);
// Treat len as isize so that the small string flag is the same as the sign bit
let is_big_str = self.create_symbol(ident_ids, "is_big_str");
let is_big_str_expr = Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::NumGte,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([len, zero]),
});
let is_big_str_stmt = |next| Stmt::Let(is_big_str, is_big_str_expr, LAYOUT_BOOL, next);
// Get the pointer to the string elements
let elements = self.create_symbol(ident_ids, "elements");
let elements_expr = Expr::StructAtIndex {
index: 0,
field_layouts: self.arena.alloc([LAYOUT_PTR, layout_isize]),
structure: string,
};
let elements_stmt = |next| Stmt::Let(elements, elements_expr, LAYOUT_PTR, next);
// Get a pointer to the refcount value, just below the elements pointer
let rc_ptr = self.create_symbol(ident_ids, "rc_ptr");
let rc_ptr_expr = Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::RefCountGetPtr,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([elements]),
});
let rc_ptr_stmt = |next| Stmt::Let(rc_ptr, rc_ptr_expr, LAYOUT_PTR, next);
// Alignment constant
let alignment = self.create_symbol(ident_ids, "alignment");
let alignment_expr = Expr::Literal(Literal::Int(self.ptr_size as i128));
let alignment_stmt = |next| Stmt::Let(alignment, alignment_expr, LAYOUT_U32, next);
// Call the relevant Zig lowlevel to actually modify the refcount
let zig_call_result = self.create_symbol(ident_ids, "zig_call_result");
let zig_call_expr = match op {
RefcountOp::Inc => Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::RefCountInc,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([rc_ptr, Symbol::ARG_2]),
}),
RefcountOp::Dec | RefcountOp::DecRef => Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::RefCountDec,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: self.arena.alloc([rc_ptr, alignment]),
}),
};
let zig_call_stmt = |next| Stmt::Let(zig_call_result, zig_call_expr, LAYOUT_UNIT, next);
// Generate an `if` to skip small strings but modify big strings
let then_branch = elements_stmt(self.arena.alloc(
//
rc_ptr_stmt(self.arena.alloc(
//
alignment_stmt(self.arena.alloc(
//
zig_call_stmt(self.arena.alloc(
//
Stmt::Ret(zig_call_result),
)),
)),
)),
));
let if_stmt = Stmt::Switch {
cond_symbol: is_big_str,
cond_layout: LAYOUT_BOOL,
branches: self.arena.alloc([(1, BranchInfo::None, then_branch)]),
default_branch: (
BranchInfo::None,
self.arena.alloc(self.return_unit(ident_ids)),
),
ret_layout: LAYOUT_UNIT,
};
// Combine the statements in sequence
let body = len_stmt(self.arena.alloc(
//
zero_stmt(self.arena.alloc(
//
is_big_str_stmt(self.arena.alloc(
//
if_stmt,
)),
)),
));
let args = self.gen_args(op, Layout::Builtin(Builtin::Str));
Proc {
name: proc_name,
args,
body,
closure_data_layout: None,
ret_layout: LAYOUT_UNIT,
is_self_recursive: SelfRecursive::NotSelfRecursive,
must_own_arguments: false,
host_exposed_layouts: HostExposedLayouts::NotHostExposed,
}
}
}
/// Helper to derive a debug function name from a layout
fn layout_debug_name<'a>(layout: &Layout<'a>) -> &'static str {
match layout {
Layout::Builtin(Builtin::List(_)) => "list",
Layout::Builtin(Builtin::Set(_)) => "set",
Layout::Builtin(Builtin::Dict(_, _)) => "dict",
Layout::Builtin(Builtin::Str) => "str",
Layout::Builtin(builtin) => {
debug_assert!(!builtin.is_refcounted());
unreachable!("Builtin {:?} is not refcounted", builtin);
}
Layout::Struct(_) => "struct",
Layout::Union(_) => "union",
Layout::LambdaSet(_) => "lambdaset",
Layout::RecursivePointer => "recursive_pointer",
}
}

View file

@ -1289,6 +1289,10 @@ impl CallSpecId {
pub fn to_bytes(self) -> [u8; 4] {
self.id.to_ne_bytes()
}
/// Dummy value for generating refcount helper procs in the backends
/// This happens *after* specialization so it's safe
pub const BACKEND_DUMMY: Self = Self { id: 0 };
}
#[derive(Clone, Copy, Debug, PartialEq)]
@ -1300,6 +1304,10 @@ impl UpdateModeId {
pub fn to_bytes(self) -> [u8; 4] {
self.id.to_ne_bytes()
}
/// Dummy value for generating refcount helper procs in the backends
/// This happens *after* alias analysis so it's safe
pub const BACKEND_DUMMY: Self = Self { id: 0 };
}
#[derive(Clone, Copy, Debug, PartialEq)]
@ -1587,6 +1595,17 @@ impl<'a> Expr<'a> {
.append(symbol_to_doc(alloc, *structure)),
}
}
pub fn to_pretty(&self, width: usize) -> String {
let allocator = BoxAllocator;
let mut w = std::vec::Vec::new();
self.to_doc::<_, ()>(&allocator)
.1
.render(width, &mut w)
.unwrap();
w.push(b'\n');
String::from_utf8(w).unwrap()
}
}
impl<'a> Stmt<'a> {

View file

@ -4,6 +4,7 @@
pub mod alias_analysis;
pub mod borrow;
pub mod gen_refcount;
pub mod inc_dec;
pub mod ir;
pub mod layout;

View file

@ -79,8 +79,9 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
use roc_load::file::MonomorphizedModule;
let MonomorphizedModule {
module_id,
procedures,
interns,
mut interns,
exposed_to_host,
..
} = loaded;
@ -114,12 +115,12 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
let env = roc_gen_wasm::Env {
arena,
interns,
module_id,
exposed_to_host,
};
let (mut wasm_module, main_fn_index) =
roc_gen_wasm::build_module_help(&env, procedures).unwrap();
roc_gen_wasm::build_module_help(&env, &mut interns, procedures).unwrap();
T::insert_test_wrapper(arena, &mut wasm_module, TEST_WRAPPER_NAME, main_fn_index);
@ -136,7 +137,7 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
let store = Store::default();
// Keep the final .wasm file for debugging with wasm-objdump or wasm2wat
const DEBUG_WASM_FILE: bool = true;
const DEBUG_WASM_FILE: bool = false;
let wasmer_module = {
let tmp_dir: TempDir; // directory for normal test runs, deleted when dropped
@ -166,8 +167,7 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
// write the module to a file so the linker can access it
std::fs::write(&app_o_file, &module_bytes).unwrap();
let _linker_output = std::process::Command::new("zig")
.args(&[
let args = &[
"wasm-ld",
// input files
app_o_file.to_str().unwrap(),
@ -188,11 +188,21 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
"test_wrapper",
"--export",
"#UserApp_main_1",
])
];
let linker_output = std::process::Command::new("zig")
.args(args)
.output()
.unwrap();
// dbg!(_linker_output);
if !linker_output.status.success() {
print!("\nLINKER FAILED\n");
for arg in args {
print!("{} ", arg);
}
println!("\n{}", std::str::from_utf8(&linker_output.stdout).unwrap());
println!("{}", std::str::from_utf8(&linker_output.stderr).unwrap());
}
Module::from_file(&store, &final_wasm_file).unwrap()
};

View file

@ -865,8 +865,8 @@ fn str_starts_with_false_small_str() {
#[test]
fn str_repeat_small() {
assert_evals_to!(
indoc!(r#"Str.repeat "Roc" 3"#),
RocStr::from("RocRocRoc"),
indoc!(r#"Str.repeat "Roc" 2"#),
RocStr::from("RocRoc"),
RocStr
);
}
@ -903,8 +903,8 @@ fn str_trim_small_blank_string() {
#[test]
fn str_trim_small_to_small() {
assert_evals_to!(
indoc!(r#"Str.trim " hello world ""#),
RocStr::from("hello world"),
indoc!(r#"Str.trim " hello ""#),
RocStr::from("hello"),
RocStr
);
}
@ -921,8 +921,8 @@ fn str_trim_large_to_large_unique() {
#[test]
fn str_trim_large_to_small_unique() {
assert_evals_to!(
indoc!(r#"Str.trim (Str.concat " " "hello world ")"#),
RocStr::from("hello world"),
indoc!(r#"Str.trim (Str.concat " " "hello ")"#),
RocStr::from("hello"),
RocStr
);
}
@ -952,15 +952,12 @@ fn str_trim_large_to_small_shared() {
indoc!(
r#"
original : Str
original = " hello world "
original = " hello "
{ trimmed: Str.trim original, original: original }
"#
),
(
RocStr::from(" hello world "),
RocStr::from("hello world"),
),
(RocStr::from(" hello "), RocStr::from("hello"),),
(RocStr, RocStr)
);
}
@ -971,12 +968,12 @@ fn str_trim_small_to_small_shared() {
indoc!(
r#"
original : Str
original = " hello world "
original = " hello "
{ trimmed: Str.trim original, original: original }
"#
),
(RocStr::from(" hello world "), RocStr::from("hello world"),),
(RocStr::from(" hello "), RocStr::from("hello"),),
(RocStr, RocStr)
);
}
@ -989,8 +986,8 @@ fn str_trim_left_small_blank_string() {
#[test]
fn str_trim_left_small_to_small() {
assert_evals_to!(
indoc!(r#"Str.trimLeft " hello world ""#),
RocStr::from("hello world "),
indoc!(r#"Str.trimLeft " hello ""#),
RocStr::from("hello "),
RocStr
);
}
@ -1007,8 +1004,8 @@ fn str_trim_left_large_to_large_unique() {
#[test]
fn str_trim_left_large_to_small_unique() {
assert_evals_to!(
indoc!(r#"Str.trimLeft (Str.concat " " "hello world ")"#),
RocStr::from("hello world "),
indoc!(r#"Str.trimLeft (Str.concat " " "hello ")"#),
RocStr::from("hello "),
RocStr
);
}
@ -1021,8 +1018,8 @@ fn str_trim_right_small_blank_string() {
#[test]
fn str_trim_right_small_to_small() {
assert_evals_to!(
indoc!(r#"Str.trimRight " hello world ""#),
RocStr::from(" hello world"),
indoc!(r#"Str.trimRight " hello ""#),
RocStr::from(" hello"),
RocStr
);
}
@ -1039,8 +1036,8 @@ fn str_trim_right_large_to_large_unique() {
#[test]
fn str_trim_right_large_to_small_unique() {
assert_evals_to!(
indoc!(r#"Str.trimRight (Str.concat " hello world" " ")"#),
RocStr::from(" hello world"),
indoc!(r#"Str.trimRight (Str.concat " hello" " ")"#),
RocStr::from(" hello"),
RocStr
);
}
@ -1070,15 +1067,12 @@ fn str_trim_right_large_to_small_shared() {
indoc!(
r#"
original : Str
original = " hello world "
original = " hello "
{ trimmed: Str.trimRight original, original: original }
"#
),
(
RocStr::from(" hello world "),
RocStr::from(" hello world"),
),
(RocStr::from(" hello "), RocStr::from(" hello"),),
(RocStr, RocStr)
);
}
@ -1089,12 +1083,12 @@ fn str_trim_right_small_to_small_shared() {
indoc!(
r#"
original : Str
original = " hello world "
original = " hello "
{ trimmed: Str.trimRight original, original: original }
"#
),
(RocStr::from(" hello world "), RocStr::from(" hello world"),),
(RocStr::from(" hello "), RocStr::from(" hello"),),
(RocStr, RocStr)
);
}