diff --git a/cranelift-filetests/src/function_runner.rs b/cranelift-filetests/src/function_runner.rs index 32c2bf5f5..4b73c3579 100644 --- a/cranelift-filetests/src/function_runner.rs +++ b/cranelift-filetests/src/function_runner.rs @@ -1,34 +1,260 @@ +use core::fmt::{self, Display, Formatter}; use core::mem; +use core::str::FromStr; use cranelift_codegen::binemit::{NullRelocSink, NullStackmapSink, NullTrapSink}; -use cranelift_codegen::ir::Function; +use cranelift_codegen::ir::{types, AbiParam, ArgumentPurpose, ConstantData, Function}; use cranelift_codegen::isa::TargetIsa; use cranelift_codegen::{settings, Context}; use cranelift_native::builder as host_isa_builder; -use memmap::MmapMut; +use cranelift_reader::Parser; +use memmap::{Mmap, MmapMut}; /// Run a function on a host pub struct FunctionRunner { function: Function, isa: Box, + action: FunctionRunnerAction, +} + +/// Build a function runner +pub struct FunctionRunnerBuilder { + function: Function, + isa: Option>, + action: FunctionRunnerAction, +} + +#[derive(Clone, Debug)] +pub enum FunctionRunnerAction { + Print, + Test, + TestWithResult { operator: Operator, expected: Value }, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Operator { + EQ, + NEQ, +} + +impl Display for Operator { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str(match self { + Self::EQ => "==", + Self::NEQ => "!=", + }) + } +} + +impl FromStr for Operator { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "==" => Ok(Self::EQ), + "!=" => Ok(Self::NEQ), + _ => Err(format!("Unsupported operator {}", s)), + } + } +} + +macro_rules! try_into_int { + ($type: ty, $name: ident) => { + struct $name {} + impl $name { + fn try_into(d: ConstantData) -> Result<$type, String> { + if d.len() == (mem::size_of::<$type>()) { + let v = d.into_vec().into_iter().rev(); + let mut r: $type = 0; + for b in v { + r = r << 8 | b as $type; + } + Ok(r) + } else { + Err(format!( + "Incorrect vector size: {}, expected {}", + d.len(), + mem::size_of::<$type>() + )) + } + } + } + }; +} + +try_into_int!(u16, ConstantDataU16); +try_into_int!(u32, ConstantDataU32); +try_into_int!(u64, ConstantDataU64); +try_into_int!(u128, ConstantDataU128); + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Value { + Bool(bool), + F32(f32), + F64(f64), + I8(u8), + I16(u16), + I32(u32), + I64(u64), + I128(u128), +} + +impl Value { + fn parse_as(s: &str, ty: types::Type) -> Result { + if ty.is_vector() { + return Err(format!("Expected a non-vector type, not {}", ty)); + } + + let mut p = Parser::new(s); + + let constant_data = match ty { + types::I8 => { + let data = p.match_uimm8("Expected a 8-bit unsigned integer").unwrap(); + Self::I8(data.into()) + } + types::I16 => { + let data = p.match_constant_data(ty).unwrap(); + Self::I16(ConstantDataU16::try_into(data).unwrap()) + } + types::I32 => { + let data = p + .match_uimm32("Expected a 32-bit unsigned integer") + .unwrap(); + Self::I32(data.into()) + } + types::I64 => { + let data = p + .match_uimm64("Expected a 64-bit unsigned integer") + .unwrap(); + Self::I64(data.into()) + } + types::I128 => { + let data = p.match_constant_data(ty).unwrap(); + let data = ConstantDataU128::try_into(data).unwrap(); + Self::I128(data.rotate_left(64)) + } + types::F32 => { + let data = ConstantData::default() + .append(p.match_ieee32("Expected a 32-bit float").unwrap()); + let data = ConstantDataU32::try_into(data).unwrap(); + Self::F32(f32::from_bits(data)) + } + types::F64 => { + let data = ConstantData::default() + .append(p.match_ieee64("Expected a 64-bit float").unwrap()); + let data = ConstantDataU64::try_into(data).unwrap(); + Self::F64(f64::from_bits(data)) + } + b if b.is_bool() => Self::Bool(p.match_bool("Expected a boolean").unwrap()), + _ => { + return Err(format!( + "Expected a type of: float, int or bool, found {}.", + ty + )) + } + }; + Ok(constant_data) + } } impl FunctionRunner { - /// Build a function runner from a function and the ISA to run on (must be the host machine's ISA) - pub fn new(function: Function, isa: Box) -> Self { - Self { function, isa } + /// Build a function runner from an action, a function and the ISA to run on (must be the host machine's ISA). + pub fn new(function: Function, isa: Box, action: FunctionRunnerAction) -> Self { + Self { + function, + isa, + action, + } } - /// Build a function runner using the host machine's ISA and the passed flags - pub fn with_host_isa(function: Function, flags: settings::Flags) -> Self { - let builder = host_isa_builder().expect("Unable to build a TargetIsa for the current host"); - let isa = builder.finish(flags); - Self::new(function, isa) + unsafe fn invoke(code_page: &Mmap) -> T { + let callable_fn: fn() -> T = mem::transmute(code_page.as_ptr()); + callable_fn() } - /// Build a function runner using the host machine's ISA and the default flags for this ISA - pub fn with_default_host_isa(function: Function) -> Self { - let flags = settings::Flags::new(settings::builder()); - Self::with_host_isa(function, flags) + fn check_assertion(value: T, operator: Operator, expected: T) -> Result<(), String> + where + T: std::fmt::Display, + T: std::cmp::PartialEq, + T: Copy, + { + let ok = match operator { + Operator::EQ => value == expected, + Operator::NEQ => value != expected, + }; + + if ok { + return Ok(()); + } + Err(format!( + "Check failed, expected {} {} {}", + expected, operator, value, + )) + } + + fn check( + code_page: memmap::Mmap, + ret_value_type: types::Type, + operator: Operator, + expected: Value, + ) -> Result<(), String> { + macro_rules! gen_match { + ($x:ident, $($y: path => $z: ty, $t: path), +) => { + match $x { + $($y => { + if let $t(v) = expected { + Self::check_assertion(unsafe { Self::invoke::<$z>(&code_page) }, operator, v) + } else { + panic!("Expected an enum member of Value, found {}", stringify!($t)) + } + }),+ , + _ => Err(format!("Unknown return type for check: {}", $x)), + }; + }; + } + + gen_match!(ret_value_type, + types::I8 => u8, Value::I8, + types::I16 => u16, Value::I16, + types::I32 => u32, Value::I32, + types::I64 => u64, Value::I64, + types::I128 => u128, Value::I128, + types::B1 => bool, Value::Bool, + types::B8 => bool, Value::Bool, + types::B16 => bool, Value::Bool, + types::B32 => bool, Value::Bool, + types::B64 => bool, Value::Bool, + types::B128 => bool, Value::Bool, + types::F32 => f32, Value::F32, + types::F64 => f64, Value::F64 + ) + } + + fn print(code_page: memmap::Mmap, ret_value_type: types::Type) -> Result<(), String> { + macro_rules! gen_match { + ($x:ident, $($y: path => $z: ty), +) => { + match $x { + $($y => { + Err(format!("Return value is: {}", unsafe { Self::invoke::<$z>(&code_page) })) + }),+ , + _ => Err(format!("Unknown return type for check: {}", $x)), + }; + }; + } + + gen_match!(ret_value_type, + types::I8 => u8, + types::I16 => u16, + types::I32 => u32, + types::I64 => u64, + types::B1 => bool, + types::B8 => bool, + types::B16 => bool, + types::B32 => bool, + types::B64 => bool, + types::B128 => bool, + types::F32 => f32, + types::F64 => f64 + ) } /// Compile and execute a single function, expecting a boolean to be returned; a 'true' value is @@ -36,18 +262,14 @@ impl FunctionRunner { /// interpreted as a failed test and mapped to Err. pub fn run(&self) -> Result<(), String> { let func = self.function.clone(); - if !(func.signature.params.is_empty() - && func.signature.returns.len() == 1 - && func.signature.returns.first().unwrap().value_type.is_bool()) - { - return Err(String::from( - "Functions must have a signature like: () -> boolean", - )); - } + + // Check return type and merge two i64 to i128 if necessary. + let ret_value_type = return_type(&func)?; if func.signature.call_conv != self.isa.default_call_conv() { - return Err(String::from( - "Functions only run on the host's default calling convention; remove the specified calling convention in the function signature to use the host's default.", + return Err(format!( + "Functions only run on the host's default calling convention; remove the specified calling convention in the function signature to use the host's default, in {}", + func.name )); } @@ -76,17 +298,161 @@ impl FunctionRunner { }; let code_page = code_page.make_exec().map_err(|e| e.to_string())?; - let callable_fn: fn() -> bool = unsafe { mem::transmute(code_page.as_ptr()) }; - // execute - if callable_fn() { - Ok(()) - } else { - Err(format!("Failed: {}", context.func.name.to_string())) + match &self.action { + FunctionRunnerAction::Test => { + if !ret_value_type.is_bool() { + return Err(format!( + "Function {} return type is not boolean", + context.func.name + )); + } + if unsafe { Self::invoke::(&code_page) } { + Ok(()) + } else { + Err(format!("Failed: {}", context.func.name)) + } + } + FunctionRunnerAction::Print => Self::print(code_page, ret_value_type), + FunctionRunnerAction::TestWithResult { operator, expected } => { + Self::check(code_page, ret_value_type, *operator, *expected) + } } } } +fn return_type(func: &Function) -> Result { + if !(func.signature.params.is_empty() && func.signature.returns.len() == 1) { + return Err(format!( + "function {} must have a signature like: () -> ty, found {}", + func.name, func.signature + )); + } + + let normal_rets: Vec<&AbiParam> = func + .signature + .returns + .iter() + .filter(|r| r.purpose == ArgumentPurpose::Normal) + .collect(); + if normal_rets.len() > 2 || normal_rets.len() == 0 { + return Err(String::from("Number of return values not supported")); + } + if normal_rets.len() == 2 { + // Only two i64 return arguments supported and they are evaluated as a i128. + if !(normal_rets[0].value_type == types::I64 && normal_rets[1].value_type == types::I64) { + return Err(String::from( + "Function return type with two parameters can only return i64, i64.", + )); + } + + return Ok(types::I128); + } + Ok(normal_rets[0].value_type) +} + +impl FunctionRunnerBuilder { + /// Create a new function runner builder with the specified function. + pub fn new(function: Function) -> Self { + Self { + action: FunctionRunnerAction::Test, + function: function, + isa: None, + } + } + + /// Use the specified action. + pub fn with_action(mut self, action: FunctionRunnerAction) -> Self { + self.action = action; + self + } + + /// Parse and use the specified action. + pub fn with_action_text(mut self, action: &str) -> Self { + self.action = Self::parse_action(action, return_type(&self.function).unwrap()); + self + } + + /// Use the specified ISA (must be the host machine's ISA). + pub fn with_isa(mut self, isa: Box) -> Self { + self.isa = Some(isa); + self + } + + /// Use the host ISA with the specified flags. + pub fn with_host_isa(self, flags: settings::Flags) -> Self { + let builder = host_isa_builder().expect("Unable to build a TargetIsa for the current host"); + self.with_isa(builder.finish(flags)) + } + + /// Use the default host ISA. + pub fn with_default_host_isa(self) -> Self { + let flags = settings::Flags::new(settings::builder()); + self.with_host_isa(flags) + } + + /// Build a function runner from an action, a function and the ISA to run on. + pub fn finish(self) -> FunctionRunner { + FunctionRunner::new(self.function, self.isa.unwrap(), self.action) + } + + /// Parse a function runner action from a comment string such as "; run print". + fn parse_action(comment: &str, ty: types::Type) -> FunctionRunnerAction { + let comment = comment.trim(); + let comment = comment.trim_start_matches(";"); + let comment = comment.trim_start(); + let comment = comment.trim_start_matches("run"); + let comment = comment.trim_start_matches(":"); + let comment = comment.trim_start(); + + if comment.len() == 0 { + // Don't print test return value. + return FunctionRunnerAction::Test; + } + + if comment == "print" { + // Print test return value, if return type is not b1, always fail the test. + return FunctionRunnerAction::Print; + } + + // "; run: %fn() > 42 " is now "%fn() > 42". + let mut parts: Vec<&str> = comment.split_ascii_whitespace().collect(); + + if parts.len() < 3 { + panic!("Invalid syntax for test assertion, expected \"%fn() \", found \"{}\"", comment); + } + + // Last part must be a constant. + let expected = Value::parse_as(parts[parts.len() - 1], ty).unwrap(); + parts.drain(parts.len() - 1..); + + let operator: Operator = parts[parts.len() - 1].parse().unwrap(); + parts.drain(parts.len() - 1..); + + if parts.len() != 1 { + // Arguments not supported. + panic!("Specifying arguments is not supported. \"%fn()\" \", found \"{}\"", parts.join(" ")); + } + + let invocation = parts[0].trim(); + let invocation = invocation.trim_start_matches("%fn("); + let invocation = invocation.trim_end_matches(")"); + let invocation = invocation.trim(); + + if invocation.len() == 0 { + return FunctionRunnerAction::TestWithResult { + operator: operator, + expected: expected, + }; + } + + panic!( + "Expected \"%fn()\" \", found \"{}\"", + parts.join(" ") + ); + } +} + #[cfg(test)] mod test { use super::*; @@ -111,7 +477,9 @@ mod test { let function = test_file.functions[0].0.clone(); // execute function - let runner = FunctionRunner::with_default_host_isa(function); + let runner = FunctionRunnerBuilder::new(function) + .with_default_host_isa() + .finish(); runner.run().unwrap() // will panic if execution fails } } diff --git a/cranelift-filetests/src/lib.rs b/cranelift-filetests/src/lib.rs index ded3acd16..762355ce5 100644 --- a/cranelift-filetests/src/lib.rs +++ b/cranelift-filetests/src/lib.rs @@ -24,6 +24,7 @@ )] pub use crate::function_runner::FunctionRunner; +pub use crate::function_runner::FunctionRunnerBuilder; use crate::runner::TestRunner; use cranelift_codegen::timing; use cranelift_reader::TestCommand; diff --git a/cranelift-filetests/src/test_run.rs b/cranelift-filetests/src/test_run.rs index 6e34bfebf..09f9b9f7f 100644 --- a/cranelift-filetests/src/test_run.rs +++ b/cranelift-filetests/src/test_run.rs @@ -2,7 +2,7 @@ //! //! The `run` test command compiles each function on the host machine and executes it -use crate::function_runner::FunctionRunner; +use crate::function_runner::FunctionRunnerBuilder; use crate::subtest::{Context, SubTest, SubtestResult}; use cranelift_codegen; use cranelift_codegen::ir; @@ -36,8 +36,10 @@ impl SubTest for TestRun { fn run(&self, func: Cow, context: &Context) -> SubtestResult<()> { for comment in context.details.comments.iter() { if comment.text.contains("run") { - let runner = - FunctionRunner::with_host_isa(func.clone().into_owned(), context.flags.clone()); + let runner = FunctionRunnerBuilder::new(func.clone().into_owned()) + .with_host_isa(context.flags.clone()) + .with_action_text(comment.text) + .finish(); runner.run()? } } diff --git a/cranelift-reader/src/lib.rs b/cranelift-reader/src/lib.rs index f0922bf88..47779e955 100644 --- a/cranelift-reader/src/lib.rs +++ b/cranelift-reader/src/lib.rs @@ -28,7 +28,7 @@ pub use crate::error::{Location, ParseError, ParseResult}; pub use crate::isaspec::{parse_options, IsaSpec}; -pub use crate::parser::{parse_functions, parse_test, ParseOptions}; +pub use crate::parser::{parse_functions, parse_test, ParseOptions, Parser}; pub use crate::sourcemap::SourceMap; pub use crate::testcommand::{TestCommand, TestOption}; pub use crate::testfile::{Comment, Details, Feature, TestFile}; diff --git a/cranelift-reader/src/parser.rs b/cranelift-reader/src/parser.rs index 8b603c787..cd58f9392 100644 --- a/cranelift-reader/src/parser.rs +++ b/cranelift-reader/src/parser.rs @@ -110,6 +110,7 @@ pub fn parse_test<'a>(text: &'a str, options: ParseOptions<'a>) -> ParseResult { lex: Lexer<'a>, @@ -609,8 +610,8 @@ impl<'a> Parser<'a> { } } - // Match and consume either a hexadecimal Uimm128 immediate (e.g. 0x000102...) or its literal list form (e.g. [0 1 2...]) - fn match_constant_data(&mut self, controlling_type: Type) -> ParseResult { + /// Match and consume either a hexadecimal Uimm128 immediate (e.g. 0x000102...) or its literal list form (e.g. [0 1 2...]) + pub fn match_constant_data(&mut self, controlling_type: Type) -> ParseResult { let expected_size = controlling_type.bytes() as usize; let constant_data = if self.optional(Token::LBracket) { // parse using a list of values, e.g. vconst.i32x4 [0 1 2 3] @@ -634,8 +635,8 @@ impl<'a> Parser<'a> { } } - // Match and consume a Uimm64 immediate. - fn match_uimm64(&mut self, err_msg: &str) -> ParseResult { + /// Match and consume a Uimm64 immediate. + pub fn match_uimm64(&mut self, err_msg: &str) -> ParseResult { if let Some(Token::Integer(text)) = self.token() { self.consume(); // Lexer just gives us raw text that looks like an integer. @@ -647,8 +648,8 @@ impl<'a> Parser<'a> { } } - // Match and consume a Uimm32 immediate. - fn match_uimm32(&mut self, err_msg: &str) -> ParseResult { + /// Match and consume a Uimm32 immediate. + pub fn match_uimm32(&mut self, err_msg: &str) -> ParseResult { if let Some(Token::Integer(text)) = self.token() { self.consume(); // Lexer just gives us raw text that looks like an integer. @@ -659,9 +660,9 @@ impl<'a> Parser<'a> { } } - // Match and consume a u8 immediate. // This is used for lane numbers in SIMD vectors. - fn match_uimm8(&mut self, err_msg: &str) -> ParseResult { + /// Match and consume a u8 immediate. + pub fn match_uimm8(&mut self, err_msg: &str) -> ParseResult { if let Some(Token::Integer(text)) = self.token() { self.consume(); // Lexer just gives us raw text that looks like an integer. @@ -740,8 +741,8 @@ impl<'a> Parser<'a> { Ok(Imm64::new(0)) } - // Match and consume an Ieee32 immediate. - fn match_ieee32(&mut self, err_msg: &str) -> ParseResult { + /// Match and consume an Ieee32 immediate. + pub fn match_ieee32(&mut self, err_msg: &str) -> ParseResult { if let Some(Token::Float(text)) = self.token() { self.consume(); // Lexer just gives us raw text that looks like a float. @@ -752,8 +753,8 @@ impl<'a> Parser<'a> { } } - // Match and consume an Ieee64 immediate. - fn match_ieee64(&mut self, err_msg: &str) -> ParseResult { + /// Match and consume an Ieee64 immediate. + pub fn match_ieee64(&mut self, err_msg: &str) -> ParseResult { if let Some(Token::Float(text)) = self.token() { self.consume(); // Lexer just gives us raw text that looks like a float. @@ -764,8 +765,8 @@ impl<'a> Parser<'a> { } } - // Match and consume a boolean immediate. - fn match_bool(&mut self, err_msg: &str) -> ParseResult { + /// Match and consume a boolean immediate. + pub fn match_bool(&mut self, err_msg: &str) -> ParseResult { if let Some(Token::Identifier(text)) = self.token() { self.consume(); match text { diff --git a/docs/testing.rst b/docs/testing.rst index b0d3361dc..c494c503c 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -397,3 +397,31 @@ Example:: return v0 } ; run + +To print the return value of the function and fail, add a ``; run: print`` directive +after each function. This is for debugging only. + +Example:: + + test run + + function %trivial_test() -> i16 { + ebb0: + v0 = iconst.i16 42 + return v0 + } + ; run: print + +To assert on the return value of the function, add a ``; run: %fn() `` directive +after a function. Equality "==" and inequality "!=" operators are supported. + +Example:: + + test run + + function %trivial_test() -> i16 { + ebb0: + v0 = iconst.i16 42 + return v0 + } + ; run: %fn() == 0x2a diff --git a/filetests/function_runner/basic.clif b/filetests/function_runner/basic.clif new file mode 100644 index 000000000..886b20053 --- /dev/null +++ b/filetests/function_runner/basic.clif @@ -0,0 +1,18 @@ +test run +target x86_64 + +function %test_assert_equal() -> i64 { +ebb0: + v0 = iconst.i64 42 + return v0 +} + +; run: %fn() == 0x2a + +function %test_assert_inequal() -> i64 { +ebb0: + v0 = iconst.i64 0 + return v0 +} + +; run: %fn() != 0x2a diff --git a/filetests/function_runner/types.clif b/filetests/function_runner/types.clif new file mode 100644 index 000000000..3810abc85 --- /dev/null +++ b/filetests/function_runner/types.clif @@ -0,0 +1,89 @@ +test run +target x86_64 + +function %test_i8() -> i8 { +ebb0: + v0 = iconst.i8 42 + return v0 +} +; run: %fn() == 0x2a + +function %test_i16() -> i16 { +ebb0: + v0 = iconst.i16 42 + return v0 +} +; run: %fn() == 0x2a + +function %test_i32() -> i32 { +ebb0: + v0 = iconst.i32 42 + return v0 +} +; run: %fn() == 0x2a + +function %test_i64() -> i64 { +ebb0: + v0 = iconst.i64 0x4242_3535_1234_5678 + return v0 +} +; run: %fn() == 0x4242_3535_1234_5678 + +; Multi-value return does not work on Windows ABI, for details see #1279. +; function %test_i128() -> i128 { +; ebb0: +; v0 = iconst.i64 0x4242_3535_1234_5678 +; v1 = iconst.i64 0xabcd_ef01_beef_b411 +; v2 = iconcat v0, v1 +; return v2 +; } +; ; r-u-n: %fn() == 0x4242_3535_1234_5678_abcd_ef01_beef_b411 + +function %test_b1() -> b1 { +ebb0: + v0 = bconst.b1 true + return v0 +} +; run: %fn() == true + +function %test_b8() -> b8 { +ebb0: + v0 = bconst.b8 true + return v0 +} +; run: %fn() == true + +function %test_b16() -> b16 { +ebb0: + v0 = bconst.b16 true + return v0 +} +; run: %fn() == true + +function %test_b32() -> b32 { +ebb0: + v0 = bconst.b32 true + return v0 +} +; run: %fn() == true + +function %test_b64() -> b64 { +ebb0: + v0 = bconst.b64 true + return v0 +} +; run: %fn() == true + +function %test_f32() -> f32 { +ebb0: + v0 = f32const 0x1.23 + return v0 +} +; run: %fn() == 0x1.23 + +function %test_f64() -> f64 { +ebb0: + v0 = f64const 0x1000.0001 + return v0 +} +; run: %fn() == 0x1000.0001 diff --git a/src/run.rs b/src/run.rs index 701104fcf..eb525753d 100644 --- a/src/run.rs +++ b/src/run.rs @@ -2,7 +2,7 @@ use crate::utils::read_to_string; use cranelift_codegen::isa::{CallConv, TargetIsa}; -use cranelift_filetests::FunctionRunner; +use cranelift_filetests::FunctionRunnerBuilder; use cranelift_native::builder as host_isa_builder; use cranelift_reader::{parse_test, Details, IsaSpec, ParseOptions}; use std::path::PathBuf; @@ -81,9 +81,14 @@ fn run_file_contents(file_contents: String) -> Result<(), String> { }; let test_file = parse_test(&file_contents, options).map_err(|e| e.to_string())?; for (func, Details { comments, .. }) in test_file.functions { - if comments.iter().any(|c| c.text.contains("run")) { + let comment = comments.iter().find(|c| c.text.contains("run")); + if let Some(c) = comment { let isa = create_target_isa(&test_file.isa_spec)?; - FunctionRunner::new(func, isa).run()? + let runner = FunctionRunnerBuilder::new(func) + .with_isa(isa) + .with_action_text(c.text) + .finish(); + runner.run()? } } Ok(())