Merge pull request #2621 from rtfeldman/wasm-runtime-error

Wasm RuntimeError
This commit is contained in:
Brian Carroll 2022-03-08 11:25:32 +00:00 committed by GitHub
commit c79fc18ba8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 595 additions and 217 deletions

View file

@ -29,11 +29,6 @@ use crate::{
PTR_SIZE, PTR_TYPE, STACK_POINTER_GLOBAL_ID, STACK_POINTER_NAME, TARGET_INFO,
};
/// The memory address where the constants data will be loaded during module instantiation.
/// We avoid address zero and anywhere near it. They're valid addresses but maybe bug-prone.
/// Follow Emscripten's example by leaving 1kB unused (though 4 bytes would probably do!)
const CONST_SEGMENT_BASE_ADDR: u32 = 1024;
pub struct WasmBackend<'a> {
pub env: &'a Env<'a>,
interns: &'a mut Interns,
@ -57,7 +52,6 @@ pub struct WasmBackend<'a> {
}
impl<'a> WasmBackend<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
env: &'a Env<'a>,
interns: &'a mut Interns,
@ -78,6 +72,11 @@ impl<'a> WasmBackend<'a> {
index: STACK_POINTER_GLOBAL_ID,
});
// The preloaded binary has a global to tell us where its data section ends
// Note: We need this to account for zero data (.bss), which doesn't have an explicit DataSegment!
let data_end_idx = module.export.globals_lookup["__data_end".as_bytes()];
let next_constant_addr = module.global.parse_u32_at_index(data_end_idx);
WasmBackend {
env,
interns,
@ -86,7 +85,7 @@ impl<'a> WasmBackend<'a> {
module,
layout_ids,
next_constant_addr: CONST_SEGMENT_BASE_ADDR,
next_constant_addr,
fn_index_offset,
called_preload_fns: Vec::with_capacity_in(2, env.arena),
proc_lookup,
@ -530,7 +529,23 @@ impl<'a> WasmBackend<'a> {
}
fn stmt_runtime_error(&mut self, msg: &'a str) {
todo!("RuntimeError {:?}", msg)
// Create a zero-terminated version of the message string
let mut bytes = Vec::with_capacity_in(msg.len() + 1, self.env.arena);
bytes.extend_from_slice(msg.as_bytes());
bytes.push(0);
// Store it in the app's data section
let sym = self.create_symbol(msg);
let (linker_sym_index, elements_addr) = self.store_bytes_in_data_section(&bytes, sym);
// Pass its address to roc_panic
let tag_id = 0;
self.code_builder
.i32_const_mem_addr(elements_addr, linker_sym_index);
self.code_builder.i32_const(tag_id);
self.call_zig_builtin_after_loading_args("roc_panic", 2, false);
self.code_builder.unreachable_();
}
/**********************************************************
@ -541,7 +556,7 @@ impl<'a> WasmBackend<'a> {
fn expr(&mut self, sym: Symbol, expr: &Expr<'a>, layout: &Layout<'a>, storage: &StoredValue) {
match expr {
Expr::Literal(lit) => self.expr_literal(lit, storage, sym, layout),
Expr::Literal(lit) => self.expr_literal(lit, storage, sym),
Expr::Call(roc_mono::ir::Call {
call_type,
@ -579,7 +594,9 @@ impl<'a> WasmBackend<'a> {
index,
} => self.expr_union_at_index(*structure, *tag_id, union_layout, *index, sym),
_ => todo!("Expression `{}`", expr.to_pretty(100)),
Expr::Reuse { .. } | Expr::Reset { .. } | Expr::RuntimeErrorFunction(_) => {
todo!("Expression `{}`", expr.to_pretty(100))
}
}
}
@ -587,13 +604,7 @@ impl<'a> WasmBackend<'a> {
* Literals
*******************************************************************/
fn expr_literal(
&mut self,
lit: &Literal<'a>,
storage: &StoredValue,
sym: Symbol,
layout: &Layout<'a>,
) {
fn expr_literal(&mut self, lit: &Literal<'a>, storage: &StoredValue, sym: Symbol) {
let invalid_error =
|| internal_error!("Literal value {:?} has invalid storage {:?}", lit, storage);
@ -655,8 +666,9 @@ impl<'a> WasmBackend<'a> {
self.code_builder.i64_const(str_as_int);
self.code_builder.i64_store(Align::Bytes4, offset);
} else {
let bytes = string.as_bytes();
let (linker_sym_index, elements_addr) =
self.expr_literal_big_str(string, sym, layout);
self.store_bytes_in_data_section(bytes, sym);
self.code_builder.get_local(local_id);
self.code_builder
@ -678,16 +690,11 @@ impl<'a> WasmBackend<'a> {
/// Create a string constant in the module data section
/// Return the data we need for code gen: linker symbol index and memory address
fn expr_literal_big_str(
&mut self,
string: &'a str,
sym: Symbol,
layout: &Layout<'a>,
) -> (u32, u32) {
fn store_bytes_in_data_section(&mut self, bytes: &[u8], sym: Symbol) -> (u32, u32) {
// Place the segment at a 4-byte aligned offset
let segment_addr = round_up_to_alignment!(self.next_constant_addr, PTR_SIZE);
let elements_addr = segment_addr + PTR_SIZE;
let length_with_refcount = 4 + string.len();
let length_with_refcount = 4 + bytes.len();
self.next_constant_addr = segment_addr + length_with_refcount as u32;
let mut segment = DataSegment {
@ -698,14 +705,14 @@ impl<'a> WasmBackend<'a> {
// Prefix the string bytes with "infinite" refcount
let refcount_max_bytes: [u8; 4] = (REFCOUNT_MAX as i32).to_le_bytes();
segment.init.extend_from_slice(&refcount_max_bytes);
segment.init.extend_from_slice(string.as_bytes());
segment.init.extend_from_slice(bytes);
let segment_index = self.module.data.append_segment(segment);
// Generate linker symbol
let name = self
.layout_ids
.get(sym, layout)
.get(sym, &Layout::Builtin(Builtin::Str))
.to_symbol_string(sym, self.interns);
let linker_symbol = SymInfo::Data(DataSymbol::Defined {
@ -713,7 +720,7 @@ impl<'a> WasmBackend<'a> {
name: name.clone(),
segment_index,
segment_offset: 4,
size: string.len() as u32,
size: bytes.len() as u32,
});
// Ensure the linker keeps the segment aligned when relocating it

View file

@ -216,7 +216,7 @@ macro_rules! round_up_to_alignment {
if $alignment_bytes <= 1 {
$unaligned
} else if $alignment_bytes.count_ones() != 1 {
panic!(
internal_error!(
"Cannot align to {} bytes. Not a power of 2.",
$alignment_bytes
);

View file

@ -39,16 +39,16 @@ pub struct PreloadsCallGraph<'a> {
}
impl<'a> PreloadsCallGraph<'a> {
pub fn new(arena: &'a Bump, import_fn_count: u32, fn_count: u32) -> Self {
let num_preloads = (import_fn_count + fn_count) as usize;
pub fn new(arena: &'a Bump, import_fn_count: usize, fn_count: usize) -> Self {
let num_preloads = import_fn_count + fn_count;
let mut code_offsets = Vec::with_capacity_in(num_preloads, arena);
let calls = Vec::with_capacity_in(2 * num_preloads, arena);
let mut calls_offsets = Vec::with_capacity_in(1 + num_preloads, arena);
// Imported functions have zero code length and no calls
code_offsets.extend(std::iter::repeat(0).take(import_fn_count as usize));
calls_offsets.extend(std::iter::repeat(0).take(import_fn_count as usize));
code_offsets.extend(std::iter::repeat(0).take(import_fn_count));
calls_offsets.extend(std::iter::repeat(0).take(import_fn_count));
PreloadsCallGraph {
num_preloads,
@ -65,11 +65,24 @@ impl<'a> PreloadsCallGraph<'a> {
/// use this backend without a linker.
pub fn parse_preloads_call_graph<'a>(
arena: &'a Bump,
fn_count: u32,
code_section_body: &[u8],
import_fn_count: u32,
imported_fn_signatures: &[u32],
defined_fn_signatures: &[u32],
indirect_callees: &[u32],
) -> PreloadsCallGraph<'a> {
let mut call_graph = PreloadsCallGraph::new(arena, import_fn_count, fn_count);
let mut call_graph = PreloadsCallGraph::new(
arena,
imported_fn_signatures.len(),
defined_fn_signatures.len(),
);
// Function type signatures, used for indirect calls
let mut signatures = Vec::with_capacity_in(
imported_fn_signatures.len() + defined_fn_signatures.len(),
arena,
);
signatures.extend_from_slice(imported_fn_signatures);
signatures.extend_from_slice(defined_fn_signatures);
// Iterate over the bytes of the Code section
let mut cursor: usize = 0;
@ -88,13 +101,23 @@ pub fn parse_preloads_call_graph<'a>(
cursor += 1; // ValueType
}
// Parse `call` instructions and skip over all other instructions
// Parse `call` and `call_indirect` instructions, skip over everything else
while cursor < func_end {
let opcode_byte: u8 = code_section_body[cursor];
if opcode_byte == OpCode::CALL as u8 {
cursor += 1;
let call_index = parse_u32_or_panic(code_section_body, &mut cursor);
call_graph.calls.push(call_index as u32);
} else if opcode_byte == OpCode::CALLINDIRECT as u8 {
cursor += 1;
// Insert all indirect callees with a matching type signature
let sig = parse_u32_or_panic(code_section_body, &mut cursor);
call_graph.calls.extend(
indirect_callees
.iter()
.filter(|f| signatures[**f as usize] == sig),
);
u32::skip_bytes(code_section_body, &mut cursor); // table_idx
} else {
OpCode::skip_bytes(code_section_body, &mut cursor);
}
@ -193,11 +216,13 @@ pub fn copy_preloads_shrinking_dead_fns<'a, T: SerialBuffer>(
live_preload_indices.sort_unstable();
live_preload_indices.dedup();
let mut live_iter = live_preload_indices.iter();
let mut live_iter = live_preload_indices
.into_iter()
.skip_while(|f| (*f as usize) < preload_idx_start);
let mut next_live_idx = live_iter.next();
for i in preload_idx_start..call_graph.num_preloads {
match next_live_idx {
Some(live) if *live as usize == i => {
Some(live) if live as usize == i => {
next_live_idx = live_iter.next();
let live_body_start = call_graph.code_offsets[i] as usize;
let live_body_end = call_graph.code_offsets[i + 1] as usize;

View file

@ -11,12 +11,10 @@ pub use linking::SymInfo;
use roc_error_macros::internal_error;
pub use sections::{ConstExpr, Export, ExportType, Global, GlobalType, Signature};
use crate::wasm_module::serialize::SkipBytes;
use self::linking::{LinkingSection, RelocationSection};
use self::sections::{
CodeSection, DataSection, ExportSection, FunctionSection, GlobalSection, ImportSection,
MemorySection, NameSection, OpaqueSection, Section, SectionId, TypeSection,
CodeSection, DataSection, ElementSection, ExportSection, FunctionSection, GlobalSection,
ImportSection, MemorySection, NameSection, OpaqueSection, Section, SectionId, TypeSection,
};
use self::serialize::{SerialBuffer, Serialize};
@ -32,7 +30,7 @@ pub struct WasmModule<'a> {
pub global: GlobalSection<'a>,
pub export: ExportSection<'a>,
pub start: OpaqueSection<'a>,
pub element: OpaqueSection<'a>,
pub element: ElementSection<'a>,
pub code: CodeSection<'a>,
pub data: DataSection<'a>,
pub names: NameSection<'a>,
@ -66,6 +64,7 @@ impl<'a> WasmModule<'a> {
self.element.serialize(buffer);
self.code.serialize(buffer);
self.data.serialize(buffer);
self.names.serialize(buffer);
}
/// Serialize the module to bytes
@ -118,6 +117,7 @@ impl<'a> WasmModule<'a> {
+ self.element.size()
+ self.code.size()
+ self.data.size()
+ self.names.size()
}
pub fn preload(arena: &'a Bump, bytes: &[u8]) -> Self {
@ -132,18 +132,33 @@ impl<'a> WasmModule<'a> {
let mut types = TypeSection::preload(arena, bytes, &mut cursor);
types.parse_offsets();
let import = ImportSection::preload(arena, bytes, &mut cursor);
let mut import = ImportSection::preload(arena, bytes, &mut cursor);
let imported_fn_signatures = import.parse(arena);
let function = FunctionSection::preload(arena, bytes, &mut cursor);
let defined_fn_signatures = function.parse(arena);
let table = OpaqueSection::preload(SectionId::Table, arena, bytes, &mut cursor);
let memory = MemorySection::preload(arena, bytes, &mut cursor);
let global = GlobalSection::preload(arena, bytes, &mut cursor);
ExportSection::skip_bytes(bytes, &mut cursor);
let export = ExportSection::empty(arena);
let export = ExportSection::preload_globals(arena, bytes, &mut cursor);
let start = OpaqueSection::preload(SectionId::Start, arena, bytes, &mut cursor);
let element = OpaqueSection::preload(SectionId::Element, arena, bytes, &mut cursor);
let code = CodeSection::preload(arena, bytes, &mut cursor, import.function_count);
let element = ElementSection::preload(arena, bytes, &mut cursor);
let indirect_callees = element.indirect_callees(arena);
let code = CodeSection::preload(
arena,
bytes,
&mut cursor,
&imported_fn_signatures,
&defined_fn_signatures,
&indirect_callees,
);
let data = DataSection::preload(arena, bytes, &mut cursor);

View file

@ -1,3 +1,5 @@
use std::fmt::Debug;
use bumpalo::collections::vec::Vec;
use bumpalo::Bump;
use roc_collections::all::MutMap;
@ -10,7 +12,8 @@ use super::dead_code::{
use super::linking::RelocationEntry;
use super::opcodes::OpCode;
use super::serialize::{
parse_u32_or_panic, SerialBuffer, Serialize, SkipBytes, MAX_SIZE_ENCODED_U32,
parse_string_bytes, parse_u32_or_panic, SerialBuffer, Serialize, SkipBytes,
MAX_SIZE_ENCODED_U32,
};
use super::{CodeBuilder, ValueType};
@ -377,20 +380,19 @@ impl<'a> ImportSection<'a> {
self.count += 1;
}
fn update_function_count(&mut self) {
let mut f_count = 0;
pub fn parse(&mut self, arena: &'a Bump) -> Vec<'a, u32> {
let mut fn_signatures = bumpalo::vec![in arena];
let mut cursor = 0;
while cursor < self.bytes.len() {
String::skip_bytes(&self.bytes, &mut cursor);
String::skip_bytes(&self.bytes, &mut cursor);
String::skip_bytes(&self.bytes, &mut cursor); // import namespace
String::skip_bytes(&self.bytes, &mut cursor); // import name
let type_id = ImportTypeId::from(self.bytes[cursor]);
cursor += 1;
match type_id {
ImportTypeId::Func => {
f_count += 1;
u32::skip_bytes(&self.bytes, &mut cursor);
fn_signatures.push(parse_u32_or_panic(&self.bytes, &mut cursor));
}
ImportTypeId::Table => {
TableType::skip_bytes(&self.bytes, &mut cursor);
@ -404,17 +406,16 @@ impl<'a> ImportSection<'a> {
}
}
self.function_count = f_count;
self.function_count = fn_signatures.len() as u32;
fn_signatures
}
pub fn from_count_and_bytes(count: u32, bytes: Vec<'a, u8>) -> Self {
let mut created = ImportSection {
ImportSection {
bytes,
count,
function_count: 0,
};
created.update_function_count();
created
}
}
}
@ -442,6 +443,16 @@ impl<'a> FunctionSection<'a> {
self.bytes.encode_u32(sig_id);
self.count += 1;
}
pub fn parse(&self, arena: &'a Bump) -> Vec<'a, u32> {
let count = self.count as usize;
let mut signatures = Vec::with_capacity_in(count, arena);
let mut cursor = 0;
for _ in 0..count {
signatures.push(parse_u32_or_panic(&self.bytes, &mut cursor));
}
signatures
}
}
section_impl!(FunctionSection, SectionId::Function);
@ -555,6 +566,26 @@ pub enum ConstExpr {
F64(f64),
}
impl ConstExpr {
fn parse_u32(bytes: &[u8], cursor: &mut usize) -> u32 {
let err = || internal_error!("Invalid ConstExpr. Expected i32.");
if bytes[*cursor] != OpCode::I32CONST as u8 {
err();
}
*cursor += 1;
let value = parse_u32_or_panic(bytes, cursor);
if bytes[*cursor] != OpCode::END as u8 {
err();
}
*cursor += 1;
value
}
}
impl Serialize for ConstExpr {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
match self {
@ -579,6 +610,15 @@ impl Serialize for ConstExpr {
}
}
impl SkipBytes for ConstExpr {
fn skip_bytes(bytes: &[u8], cursor: &mut usize) {
while bytes[*cursor] != OpCode::END as u8 {
OpCode::skip_bytes(bytes, cursor);
}
*cursor += 1;
}
}
#[derive(Debug)]
pub struct Global {
/// Type and mutability of the global
@ -601,16 +641,14 @@ pub struct GlobalSection<'a> {
}
impl<'a> GlobalSection<'a> {
pub fn new(arena: &'a Bump, globals: &[Global]) -> Self {
let capacity = 13 * globals.len();
let mut bytes = Vec::with_capacity_in(capacity, arena);
for global in globals {
global.serialize(&mut bytes);
}
GlobalSection {
count: globals.len() as u32,
bytes,
pub fn parse_u32_at_index(&self, index: u32) -> u32 {
let mut cursor = 0;
for _ in 0..index {
GlobalType::skip_bytes(&self.bytes, &mut cursor);
ConstExpr::skip_bytes(&self.bytes, &mut cursor);
}
GlobalType::skip_bytes(&self.bytes, &mut cursor);
ConstExpr::parse_u32(&self.bytes, &mut cursor)
}
pub fn append(&mut self, global: Global) {
@ -636,6 +674,18 @@ pub enum ExportType {
Global = 3,
}
impl From<u8> for ExportType {
fn from(x: u8) -> Self {
match x {
0 => Self::Func,
1 => Self::Table,
2 => Self::Mem,
3 => Self::Global,
_ => internal_error!("invalid ExportType {:2x?}", x),
}
}
}
#[derive(Debug)]
pub struct Export<'a> {
pub name: &'a [u8],
@ -643,6 +693,19 @@ pub struct Export<'a> {
pub index: u32,
}
impl<'a> Export<'a> {
fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Self {
let name = parse_string_bytes(arena, bytes, cursor);
let ty = ExportType::from(bytes[*cursor]);
*cursor += 1;
let index = parse_u32_or_panic(bytes, cursor);
Export { name, ty, index }
}
}
impl Serialize for Export<'_> {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
self.name.serialize(buffer);
@ -655,7 +718,10 @@ impl Serialize for Export<'_> {
pub struct ExportSection<'a> {
pub count: u32,
pub bytes: Vec<'a, u8>,
/// List of exported functions to keep during dead-code-elimination
pub function_indices: Vec<'a, u32>,
/// name -> index
pub globals_lookup: MutMap<&'a [u8], u32>,
}
impl<'a> ExportSection<'a> {
@ -673,18 +739,36 @@ impl<'a> ExportSection<'a> {
section_size(&self.bytes)
}
pub fn empty(arena: &'a Bump) -> Self {
fn empty(arena: &'a Bump) -> Self {
ExportSection {
count: 0,
bytes: Vec::with_capacity_in(256, arena),
function_indices: Vec::with_capacity_in(4, arena),
globals_lookup: MutMap::default(),
}
}
}
impl SkipBytes for ExportSection<'_> {
fn skip_bytes(bytes: &[u8], cursor: &mut usize) {
parse_section(Self::ID, bytes, cursor);
/// Preload from object file. Keep only the Global exports, ignore the rest.
pub fn preload_globals(arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize) -> Self {
let (num_exports, body_bytes) = parse_section(Self::ID, module_bytes, cursor);
let mut export_section = ExportSection::empty(arena);
let mut body_cursor = 0;
for _ in 0..num_exports {
let export_start = body_cursor;
let export = Export::parse(arena, body_bytes, &mut body_cursor);
if matches!(export.ty, ExportType::Global) {
let global_bytes = &body_bytes[export_start..body_cursor];
export_section.bytes.extend_from_slice(global_bytes);
export_section.count += 1;
export_section
.globals_lookup
.insert(export.name, export.index);
}
}
export_section
}
}
@ -699,6 +783,122 @@ impl<'a> Serialize for ExportSection<'a> {
}
}
/*******************************************************************
*
* Element section
*
* Elements are entries in tables (see Table section)
* For example, Wasm uses a function table instead of function pointers,
* and each entry in that function table is an element.
* The call_indirect instruction uses element indices to refer to functions.
* This section therefore enumerates all indirectly-called functions.
*
*******************************************************************/
#[repr(u8)]
enum ElementSegmentFormatId {
/// Currently only supporting the original Wasm MVP format since it's the only one in wide use.
/// There are newer formats for other table types, with complex encodings to preserve backward compatibility
/// (Already going down the same path as x86!)
ActiveImplicitTableIndex = 0x00,
}
#[derive(Debug)]
struct ElementSegment<'a> {
offset: ConstExpr,
fn_indices: Vec<'a, u32>,
}
impl<'a> ElementSegment<'a> {
fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Self {
// In practice we only need the original MVP format
let format_id = bytes[*cursor];
debug_assert!(format_id == ElementSegmentFormatId::ActiveImplicitTableIndex as u8);
*cursor += 1;
// The table index offset is encoded as a ConstExpr, but only I32 makes sense
let const_expr_opcode = bytes[*cursor];
debug_assert!(const_expr_opcode == OpCode::I32CONST as u8);
*cursor += 1;
let offset = parse_u32_or_panic(bytes, cursor);
debug_assert!(bytes[*cursor] == OpCode::END as u8);
*cursor += 1;
let num_elems = parse_u32_or_panic(bytes, cursor);
let mut fn_indices = Vec::with_capacity_in(num_elems as usize, arena);
for _ in 0..num_elems {
let fn_idx = parse_u32_or_panic(bytes, cursor);
fn_indices.push(fn_idx);
}
ElementSegment {
offset: ConstExpr::I32(offset as i32),
fn_indices,
}
}
fn size(&self) -> usize {
let variant_id = 1;
let constexpr_opcode = 1;
let constexpr_value = MAX_SIZE_ENCODED_U32;
let vec_len = MAX_SIZE_ENCODED_U32;
let vec_contents = MAX_SIZE_ENCODED_U32 * self.fn_indices.len();
variant_id + constexpr_opcode + constexpr_value + vec_len + vec_contents
}
}
impl<'a> Serialize for ElementSegment<'a> {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
buffer.append_u8(ElementSegmentFormatId::ActiveImplicitTableIndex as u8);
self.offset.serialize(buffer);
self.fn_indices.serialize(buffer);
}
}
#[derive(Debug)]
pub struct ElementSection<'a> {
segments: Vec<'a, ElementSegment<'a>>,
}
impl<'a> ElementSection<'a> {
const ID: SectionId = SectionId::Element;
pub fn preload(arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize) -> Self {
let (num_segments, body_bytes) = parse_section(Self::ID, module_bytes, cursor);
let mut segments = Vec::with_capacity_in(num_segments as usize, arena);
let mut body_cursor = 0;
for _ in 0..num_segments {
let seg = ElementSegment::parse(arena, body_bytes, &mut body_cursor);
segments.push(seg);
}
ElementSection { segments }
}
pub fn size(&self) -> usize {
self.segments.iter().map(|seg| seg.size()).sum()
}
pub fn indirect_callees(&self, arena: &'a Bump) -> Vec<'a, u32> {
let mut result = bumpalo::vec![in arena];
for segment in self.segments.iter() {
result.extend_from_slice(&segment.fn_indices);
}
result
}
}
impl<'a> Serialize for ElementSection<'a> {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
let header_indices = write_section_header(buffer, Self::ID);
self.segments.serialize(buffer);
update_section_size(buffer, header_indices);
}
}
/*******************************************************************
*
* Code section (see also code_builder.rs)
@ -742,14 +942,21 @@ impl<'a> CodeSection<'a> {
arena: &'a Bump,
module_bytes: &[u8],
cursor: &mut usize,
import_fn_count: u32,
import_signatures: &[u32],
function_signatures: &[u32],
indirect_callees: &[u32],
) -> Self {
let (preloaded_count, initial_bytes) = parse_section(SectionId::Code, module_bytes, cursor);
let preloaded_bytes = arena.alloc_slice_copy(initial_bytes);
// TODO: Try to move this call_graph preparation to platform build time
let dead_code_metadata =
parse_preloads_call_graph(arena, preloaded_count, initial_bytes, import_fn_count);
let dead_code_metadata = parse_preloads_call_graph(
arena,
initial_bytes,
import_signatures,
function_signatures,
indirect_callees,
);
CodeSection {
preloaded_count,
@ -850,7 +1057,7 @@ impl Serialize for DataSegment<'_> {
#[derive(Debug)]
pub struct DataSection<'a> {
count: u32,
bytes: Vec<'a, u8>,
pub bytes: Vec<'a, u8>, // public so backend.rs can calculate addr of first string
}
impl<'a> DataSection<'a> {
@ -928,8 +1135,8 @@ enum NameSubSections {
LocalNames = 2,
}
#[derive(Debug, Default)]
pub struct NameSection<'a> {
pub bytes: Vec<'a, u8>,
pub functions: MutMap<&'a [u8], u32>,
}
@ -938,13 +1145,6 @@ impl<'a> NameSection<'a> {
const NAME: &'static str = "name";
pub fn parse(arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize) -> Self {
let functions = MutMap::default();
let mut section = NameSection { functions };
section.parse_help(arena, module_bytes, cursor);
section
}
fn parse_help(&mut self, arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize) {
// Custom section ID
let section_id_byte = module_bytes[*cursor];
if section_id_byte != Self::ID as u8 {
@ -958,21 +1158,37 @@ impl<'a> NameSection<'a> {
*cursor += 1;
// Section size
let section_size = parse_u32_or_panic(module_bytes, cursor);
let section_end = *cursor + section_size as usize;
let section_size = parse_u32_or_panic(module_bytes, cursor) as usize;
let section_end = *cursor + section_size;
// Custom section name
let section_name_len = parse_u32_or_panic(module_bytes, cursor);
let section_name_end = *cursor + section_name_len as usize;
let section_name = &module_bytes[*cursor..section_name_end];
let mut bytes = Vec::with_capacity_in(section_size, arena);
bytes.extend_from_slice(&module_bytes[*cursor..section_end]);
let functions = MutMap::default();
let mut section = NameSection { bytes, functions };
section.parse_body(arena, module_bytes, cursor, section_end);
section
}
pub fn size(&self) -> usize {
self.bytes.len()
}
fn parse_body(
&mut self,
arena: &'a Bump,
module_bytes: &[u8],
cursor: &mut usize,
section_end: usize,
) {
let section_name = parse_string_bytes(arena, module_bytes, cursor);
if section_name != Self::NAME.as_bytes() {
internal_error!(
"Expected Custon section {:?}, found {:?}",
"Expected Custom section {:?}, found {:?}",
Self::NAME,
std::str::from_utf8(section_name)
);
}
*cursor = section_name_end;
// Find function names subsection
let mut found_function_names = false;
@ -997,10 +1213,7 @@ impl<'a> NameSection<'a> {
let num_entries = parse_u32_or_panic(module_bytes, cursor) as usize;
for _ in 0..num_entries {
let fn_index = parse_u32_or_panic(module_bytes, cursor);
let name_len = parse_u32_or_panic(module_bytes, cursor);
let name_end = *cursor + name_len as usize;
let name_bytes: &[u8] = &module_bytes[*cursor..name_end];
*cursor = name_end;
let name_bytes = parse_string_bytes(arena, module_bytes, cursor);
self.functions
.insert(arena.alloc_slice_copy(name_bytes), fn_index);
@ -1008,6 +1221,37 @@ impl<'a> NameSection<'a> {
}
}
impl<'a> Serialize for NameSection<'a> {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
if !self.bytes.is_empty() {
let header_indices = write_section_header(buffer, Self::ID);
buffer.append_slice(&self.bytes);
update_section_size(buffer, header_indices);
}
}
}
impl<'a> Debug for NameSection<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "NameSection")?;
// We want to display index->name because it matches the binary format and looks nicer.
// But our hashmap is name->index because that's what code gen wants to look up.
let mut by_index = std::vec::Vec::with_capacity(self.functions.len());
for (name, index) in self.functions.iter() {
by_index.push((*index, name));
}
by_index.sort_unstable();
for (index, name) in by_index.iter() {
let name_str = unsafe { std::str::from_utf8_unchecked(name) };
writeln!(f, " {:4}: {}", index, name_str)?;
}
Ok(())
}
}
/*******************************************************************
*
* Unit tests

View file

@ -1,6 +1,6 @@
use std::{fmt::Debug, iter::FromIterator};
use bumpalo::collections::vec::Vec;
use bumpalo::{collections::vec::Vec, Bump};
use roc_error_macros::internal_error;
/// In the WebAssembly binary format, all integers are variable-length encoded (using LEB-128)
@ -262,6 +262,15 @@ pub fn parse_u32_or_panic(bytes: &[u8], cursor: &mut usize) -> u32 {
value
}
pub fn parse_string_bytes<'a>(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> &'a [u8] {
let len = parse_u32_or_panic(bytes, cursor);
let end = *cursor + len as usize;
let bytes: &[u8] = &bytes[*cursor..end];
let copy = arena.alloc_slice_copy(bytes);
*cursor = end;
copy
}
/// Skip over serialized bytes for a type
/// This may, or may not, require looking at the byte values
pub trait SkipBytes {

View file

@ -3,8 +3,6 @@ use crate::helpers::llvm::assert_evals_to;
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_expect_failed;
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_llvm_evals_to;
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_non_opt_evals_to;
#[cfg(feature = "gen-dev")]
@ -2470,10 +2468,10 @@ fn function_malformed_pattern() {
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[should_panic(expected = "Hit an erroneous type when creating a layout for")]
fn call_invalid_layout() {
assert_llvm_evals_to!(
assert_evals_to!(
indoc!(
r#"
f : I64 -> I64

View file

@ -1,14 +1,11 @@
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_evals_to;
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::expect_runtime_error_panic;
use crate::helpers::llvm::{assert_evals_to, expect_runtime_error_panic};
#[cfg(feature = "gen-dev")]
use crate::helpers::dev::assert_evals_to;
#[cfg(feature = "gen-wasm")]
use crate::helpers::wasm::assert_evals_to;
use crate::helpers::wasm::{assert_evals_to, expect_runtime_error_panic};
// use crate::assert_wasm_evals_to as assert_evals_to;
use indoc::indoc;
@ -1044,6 +1041,7 @@ fn different_proc_types_specialized_to_same_layout() {
#[cfg(any(feature = "gen-llvm"))]
#[should_panic(
// TODO: something upstream is escaping the '
// NOTE: Are we sure it's upstream? It's not escaped in gen-wasm version below!
expected = r#"Roc failed with message: "Can\'t create record with improper layout""#
)]
fn call_with_bad_record_runtime_error() {
@ -1059,6 +1057,22 @@ fn call_with_bad_record_runtime_error() {
))
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
#[should_panic(expected = r#"Can't create record with improper layout"#)]
fn call_with_bad_record_runtime_error() {
expect_runtime_error_panic!(indoc!(
r#"
app "test" provides [ main ] to "./platform"
main =
get : {a: Bool} -> Bool
get = \{a} -> a
get {b: ""}
"#
))
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn generalized_accessor() {

View file

@ -1,5 +1,5 @@
#[cfg(feature = "gen-wasm")]
use crate::helpers::wasm::assert_refcounts;
use crate::helpers::{wasm::assert_refcounts, RefCount::*};
#[allow(unused_imports)]
use indoc::indoc;
@ -25,8 +25,8 @@ fn str_inc() {
),
RocList<RocStr>,
&[
3, // s
1 // result
Live(3), // s
Live(1) // result
]
);
}
@ -43,7 +43,7 @@ fn str_dealloc() {
"#
),
bool,
&[0]
&[Deallocated]
);
}
@ -60,10 +60,10 @@ fn list_int_inc() {
RocList<RocList<i64>>,
&[
// TODO be smarter about coalescing polymorphic list values
1, // list0
1, // list1
1, // list2
1 // result
Live(1), // list0
Live(1), // list1
Live(1), // list2
Live(1) // result
]
);
}
@ -81,10 +81,10 @@ fn list_int_dealloc() {
usize,
&[
// TODO be smarter about coalescing polymorphic list values
0, // list0
0, // list1
0, // list2
0 // result
Deallocated, // list0
Deallocated, // list1
Deallocated, // list2
Deallocated // result
]
);
}
@ -102,9 +102,9 @@ fn list_str_inc() {
),
RocList<RocList<RocStr>>,
&[
6, // s
2, // list
1 // result
Live(6), // s
Live(2), // list
Live(1) // result
]
);
}
@ -122,9 +122,9 @@ fn list_str_dealloc() {
),
usize,
&[
0, // s
0, // list
0 // result
Deallocated, // s
Deallocated, // list
Deallocated // result
]
);
}
@ -142,7 +142,7 @@ fn struct_inc() {
"#
),
[(i64, RocStr, RocStr); 2],
&[4] // s
&[Live(4)] // s
);
}
@ -160,7 +160,7 @@ fn struct_dealloc() {
"#
),
i64,
&[0] // s
&[Deallocated] // s
);
}
@ -186,7 +186,7 @@ fn union_nonrecursive_inc() {
"#
),
(TwoStr, TwoStr, i64),
&[4]
&[Live(4)]
);
}
@ -209,7 +209,7 @@ fn union_nonrecursive_dec() {
"#
),
RocStr,
&[1] // s
&[Live(1)] // s
);
}
@ -234,9 +234,9 @@ fn union_recursive_inc() {
),
(Pointer, Pointer),
&[
4, // s
4, // sym
2, // e
Live(4), // s
Live(4), // sym
Live(2), // e
]
);
}
@ -264,9 +264,9 @@ fn union_recursive_dec() {
),
Pointer,
&[
1, // s
1, // sym
0 // e
Live(1), // s
Live(1), // sym
Deallocated // e
]
);
}
@ -300,13 +300,13 @@ fn refcount_different_rosetrees_inc() {
),
(Pointer, Pointer),
&[
2, // s
3, // i1
2, // s1
1, // [i1, i1]
1, // i2
1, // [s1, s1]
1 // s2
Live(2), // s
Live(3), // i1
Live(2), // s1
Live(1), // [i1, i1]
Live(1), // i2
Live(1), // [s1, s1]
Live(1) // s2
]
);
}
@ -341,13 +341,13 @@ fn refcount_different_rosetrees_dec() {
),
i64,
&[
0, // s
0, // i1
0, // s1
0, // [i1, i1]
0, // i2
0, // [s1, s1]
0, // s2
Deallocated, // s
Deallocated, // i1
Deallocated, // s1
Deallocated, // [i1, i1]
Deallocated, // i2
Deallocated, // [s1, s1]
Deallocated, // s2
]
);
}
@ -370,10 +370,10 @@ fn union_linked_list_inc() {
),
(Pointer, Pointer),
&[
6, // s
2, // Cons
2, // Cons
2, // Cons
Live(6), // s
Live(2), // Cons
Live(2), // Cons
Live(2), // Cons
]
);
}
@ -398,10 +398,10 @@ fn union_linked_list_dec() {
),
RocStr,
&[
1, // s
0, // Cons
0, // Cons
0, // Cons
Live(1), // s
Deallocated, // Cons
Deallocated, // Cons
Deallocated, // Cons
]
);
}
@ -434,6 +434,6 @@ fn union_linked_list_long_dec() {
"#
),
i64,
&[0; 1_000]
&[Deallocated; 1_000]
);
}

View file

@ -1222,7 +1222,7 @@ fn applied_tag_function_linked_list() {
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[should_panic(expected = "")]
#[should_panic(expected = "")] // TODO: this only panics because it returns 0 instead of 1!
fn tag_must_be_its_own_type() {
assert_evals_to!(
indoc!(

View file

@ -258,6 +258,7 @@
function fd_write(fd, iovs_ptr, iovs_len, nwritten_mut_ptr) {
let string_buffer = "";
let nwritten = 0;
const STDOUT = 1;
for (let i = 0; i < iovs_len; i++) {
const index32 = iovs_ptr >> 2;
@ -282,17 +283,19 @@
}
wasiLinkObject.memory32[nwritten_mut_ptr >> 2] = nwritten;
if (string_buffer) {
if (fd === STDOUT) {
console.log(string_buffer);
} else {
console.error(string_buffer);
}
}
return 0;
}
// proc_exit : (i32) -> nil
function proc_exit(exit_code) {
if (exit_code) {
throw new Error(`Wasm exited with code ${exit_code}`);
}
}
// Signatures from wasm_test_platform.o
const sig2 = (i32) => {};

View file

@ -579,19 +579,31 @@ macro_rules! assert_llvm_evals_to {
#[allow(unused_macros)]
macro_rules! assert_evals_to {
($src:expr, $expected:expr, $ty:ty) => {{
assert_evals_to!($src, $expected, $ty, $crate::helpers::llvm::identity);
assert_evals_to!($src, $expected, $ty, $crate::helpers::llvm::identity, false);
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {{
// same as above, except with an additional transformation argument.
{
assert_evals_to!($src, $expected, $ty, $transform, false);
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems: expr) => {{
// same as above, except with ignore_problems.
#[cfg(feature = "wasm-cli-run")]
$crate::helpers::llvm::assert_wasm_evals_to!(
$src, $expected, $ty, $transform, false, false
$src,
$expected,
$ty,
$transform,
$ignore_problems
);
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false);
}
};
$crate::helpers::llvm::assert_llvm_evals_to!(
$src,
$expected,
$ty,
$transform,
$ignore_problems
);
}};
}
#[allow(unused_macros)]

View file

@ -55,3 +55,10 @@ where
{
run_test()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RefCount {
Live(u32),
Deallocated,
Constant,
}

View file

@ -6,6 +6,7 @@ use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use wasmer::{Memory, WasmPtr};
use super::RefCount;
use crate::helpers::from_wasmer_memory::FromWasmerMemory;
use roc_collections::all::{MutMap, MutSet};
use roc_gen_wasm::wasm32_result::Wasm32Result;
@ -17,6 +18,7 @@ const OUT_DIR_VAR: &str = "TEST_GEN_OUT";
const TEST_WRAPPER_NAME: &str = "test_wrapper";
const INIT_REFCOUNT_NAME: &str = "init_refcount_test";
const PANIC_MSG_NAME: &str = "panic_msg";
fn promote_expr_to_module(src: &str) -> String {
let mut buffer = String::from("app \"test\" provides [ main ] to \"./platform\"\n\nmain =\n");
@ -176,7 +178,7 @@ fn load_bytes_into_runtime(bytes: Vec<u8>) -> wasmer::Instance {
}
#[allow(dead_code)]
pub fn assert_wasm_evals_to_help<T>(src: &str, phantom: PhantomData<T>) -> Result<T, String>
pub fn assert_evals_to_help<T>(src: &str, phantom: PhantomData<T>) -> Result<T, String>
where
T: FromWasmerMemory + Wasm32Result,
{
@ -192,7 +194,13 @@ where
let test_wrapper = instance.exports.get_function(TEST_WRAPPER_NAME).unwrap();
match test_wrapper.call(&[]) {
Err(e) => Err(format!("{:?}", e)),
Err(e) => {
if let Some(msg) = get_roc_panic_msg(&instance, memory) {
Err(msg)
} else {
Err(e.to_string())
}
}
Ok(result) => {
let address = result[0].unwrap_i32();
@ -213,12 +221,37 @@ where
}
}
/// Our test roc_panic stores a pointer to its message in a global variable so we can find it.
fn get_roc_panic_msg(instance: &wasmer::Instance, memory: &Memory) -> Option<String> {
let memory_bytes = unsafe { memory.data_unchecked() };
// We need to dereference twice!
// The Wasm Global only points at the memory location of the C global value
let panic_msg_global = instance.exports.get_global(PANIC_MSG_NAME).unwrap();
let global_addr = panic_msg_global.get().unwrap_i32() as usize;
let global_ptr = memory_bytes[global_addr..].as_ptr() as *const u32;
// Dereference again to find the bytes of the message string
let msg_addr = unsafe { *global_ptr };
if msg_addr == 0 {
return None;
}
let msg_index = msg_addr as usize;
let msg_len = memory_bytes[msg_index..]
.iter()
.position(|c| *c == 0)
.unwrap();
let msg_bytes = memory_bytes[msg_index..][..msg_len].to_vec();
let msg = unsafe { String::from_utf8_unchecked(msg_bytes) };
Some(msg)
}
#[allow(dead_code)]
pub fn assert_wasm_refcounts_help<T>(
src: &str,
phantom: PhantomData<T>,
num_refcounts: usize,
) -> Result<Vec<u32>, String>
) -> Result<Vec<RefCount>, String>
where
T: FromWasmerMemory + Wasm32Result,
{
@ -264,12 +297,15 @@ where
for i in 0..num_refcounts {
let rc_ptr = refcount_ptrs[i].get();
let rc = if rc_ptr.offset() == 0 {
// RC pointer has been set to null, which means the value has been freed.
// In tests, we simply represent this as zero refcount.
0
RefCount::Deallocated
} else {
let rc_encoded = rc_ptr.deref(memory).unwrap().get();
(rc_encoded - i32::MIN + 1) as u32
let rc_encoded: i32 = rc_ptr.deref(memory).unwrap().get();
if rc_encoded == 0 {
RefCount::Constant
} else {
let rc = rc_encoded - i32::MIN + 1;
RefCount::Live(rc as u32)
}
};
refcounts.push(rc);
}
@ -307,42 +343,44 @@ pub fn debug_memory_hex(memory: &Memory, address: i32, size: usize) {
}
#[allow(unused_macros)]
macro_rules! assert_wasm_evals_to {
macro_rules! assert_evals_to {
($src:expr, $expected:expr, $ty:ty) => {
$crate::helpers::wasm::assert_evals_to!(
$src,
$expected,
$ty,
$crate::helpers::wasm::identity,
false
)
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
$crate::helpers::wasm::assert_evals_to!($src, $expected, $ty, $transform, false);
};
($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems: expr) => {{
let phantom = std::marker::PhantomData;
match $crate::helpers::wasm::assert_wasm_evals_to_help::<$ty>($src, phantom) {
Err(msg) => panic!("{:?}", msg),
let _ = $ignore_problems; // Always ignore "problems"! One backend (LLVM) is enough to cover them.
match $crate::helpers::wasm::assert_evals_to_help::<$ty>($src, phantom) {
Err(msg) => panic!("{}", msg),
Ok(actual) => {
assert_eq!($transform(actual), $expected)
}
}
};
($src:expr, $expected:expr, $ty:ty) => {
$crate::helpers::wasm::assert_wasm_evals_to!(
$src,
$expected,
$ty,
$crate::helpers::wasm::identity
);
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
$crate::helpers::wasm::assert_wasm_evals_to!($src, $expected, $ty, $transform);
};
}};
}
#[allow(unused_macros)]
macro_rules! assert_evals_to {
($src:expr, $expected:expr, $ty:ty) => {{
assert_evals_to!($src, $expected, $ty, $crate::helpers::wasm::identity);
macro_rules! expect_runtime_error_panic {
($src:expr) => {{
$crate::helpers::wasm::assert_evals_to!(
$src,
false, // fake value/type for eval
bool,
$crate::helpers::wasm::identity,
true // ignore problems
);
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
// Same as above, except with an additional transformation argument.
{
$crate::helpers::wasm::assert_wasm_evals_to!($src, $expected, $ty, $transform);
}
};
}
#[allow(dead_code)]
@ -371,8 +409,9 @@ macro_rules! assert_refcounts {
#[allow(unused_imports)]
pub(crate) use assert_evals_to;
#[allow(unused_imports)]
pub(crate) use assert_wasm_evals_to;
pub(crate) use expect_runtime_error_panic;
#[allow(unused_imports)]
pub(crate) use assert_refcounts;

View file

@ -1,4 +1,5 @@
#include <stdio.h>
#include <stdlib.h>
// Makes test runs take 50% longer, due to linking
#define ENABLE_PRINTF 0
@ -121,14 +122,18 @@ void roc_dealloc(void *ptr, unsigned int alignment)
//--------------------------
void roc_panic(void *ptr, unsigned int alignment)
// Allow the test to probe the panic message
char* panic_msg;
void roc_panic(char *msg, unsigned int tag_id)
{
#if ENABLE_PRINTF
char *msg = (char *)ptr;
fprintf(stderr,
"Application crashed with message\n\n %s\n\nShutting down\n", msg);
#endif
abort();
panic_msg = msg;
// Note: no dynamic string formatting
fputs("Application crashed with message\n\n ", stderr);
fputs(msg, stderr);
fputs("\n\nShutting down\n", stderr);
exit(101);
}
//--------------------------