diff --git a/Cargo.toml b/Cargo.toml index f064d470b..fc931edb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ pretty_env_logger = "0.3.0" file-per-thread-logger = "0.1.2" indicatif = "0.13.0" walkdir = "2.2" +cranelift-interpreter = { path = "cranelift-interpreter", version = "0.1.0" } [features] default = ["disas", "wasm", "cranelift-codegen/all-arch", "basic-blocks"] diff --git a/cranelift-interpreter/Cargo.toml b/cranelift-interpreter/Cargo.toml new file mode 100644 index 000000000..cb5a9ae3a --- /dev/null +++ b/cranelift-interpreter/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "cranelift-interpreter" +version = "0.1.0" +authors = ["Andrew Brown"] +description = "Interpret Cranelift IR" +repository = "https://github.com/CraneStation/cranelift" +documentation = "https://cranelift.readthedocs.io/" +categories = ["no-std"] +license = "Apache-2.0 WITH LLVM-exception" +readme = "README.md" +edition = "2018" + +[dependencies] +cranelift-codegen = { path = "../cranelift-codegen", version = "*", default-features = false } +cranelift-entity = { path = "../cranelift-entity", version = "*" } +cranelift-reader = { path = "../cranelift-reader", version = "*" } +hashbrown = { version = "0.6", optional = true } +log = { version = "0.4.6", default-features = false } +thiserror = "1.0.4" +walkdir = "2.2" +pretty_env_logger = "0.3.1" + +[dev-dependencies] +cranelift-frontend = { path = "../cranelift-frontend", version = "*" } + +[features] +default = ["std"] +std = ["cranelift-codegen/std"] +core = ["hashbrown", "cranelift-codegen/core"] + +[badges] +maintenance = { status = "experimental" } +travis-ci = { repository = "CraneStation/cranelift" } diff --git a/cranelift-interpreter/LICENSE b/cranelift-interpreter/LICENSE new file mode 100644 index 000000000..f9d81955f --- /dev/null +++ b/cranelift-interpreter/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/cranelift-interpreter/README.md b/cranelift-interpreter/README.md new file mode 100644 index 000000000..85389ea0b --- /dev/null +++ b/cranelift-interpreter/README.md @@ -0,0 +1,2 @@ +This crate provides an interpreter for Cranelift IR. It is still a work in progress, as many +instructions are unimplemented and various implementation gaps exist. Use at your own risk. diff --git a/cranelift-interpreter/src/clif/lexer.rs b/cranelift-interpreter/src/clif/lexer.rs new file mode 100644 index 000000000..cbedd4774 --- /dev/null +++ b/cranelift-interpreter/src/clif/lexer.rs @@ -0,0 +1,293 @@ +//! Cranelift annotation lexer. +//! +//! A lexer for the CLIF test language (e.g. `run: fn(1) == 2`). This is loosely based on Alex's +//! work in [wat]. +//! [wat]: https://github.com/bytecodealliance/wat/blob/1b810cb430ee1f1bb11a7641e560a30679e95754/crates/wast/src/lexer.rs +//! +//! ``` +//! use cranelift_interpreter::clif::lexer::{Lexer, Token}; +//! let tokens: Vec = Lexer::new("true false %fn0 ()").collect(); +//! println!("{:?}", tokens); +//! ``` + +use std::iter::Peekable; +use std::path::PathBuf; +use thiserror::Error; + +/// A lexer for the CLIF test language (e.g. `run: fn(1) == 2`). +pub struct Lexer<'a> { + it: Peekable>, + input: &'a str, +} + +impl<'a> Lexer<'a> { + /// Create a new lexer which will lex the `input` source string. + pub fn new(input: &str) -> Lexer<'_> { + Lexer { + it: input.char_indices().peekable(), + input, + } + } + + /// Retrieve the next token from the input. + pub fn token(&mut self) -> Result>, LexerError> { + let token; + loop { + let pos = self.cur(); + token = match self.peek() { + Some(_) if self.has_keyword(pos, "true") => Some(self.keyword("true")?), + Some(_) if self.has_keyword(pos, "false") => Some(self.keyword("false")?), + Some(_) if self.has_keyword(pos, "NaN") => Some(self.keyword("NaN")?), + Some(_) if self.has_keyword(pos, "Inf") => Some(self.keyword("Inf")?), + Some(b) if b.is_numeric() || b == '-' => Some(self.number()?), + Some(b) if b.is_ascii_punctuation() && b != '%' => Some(self.punctuation()?), + Some(b) if b.is_alphabetic() || b == '%' => Some(self.name()?), + Some(_) => { + self.it.next(); + continue; + } // skip unknown characters by repeating the loop + _ => None, + }; + break; + } + Ok(token) + } + + /// Consume characters from the input while the condition is true; returns the start and end + /// indexes of the consumed string. + fn eat_while(&mut self, condition: F) -> (usize, usize) + where + F: Fn(char) -> bool, + { + let start = self.cur(); + while let Some(ch) = self.peek() { + if condition(ch) { + self.it.next(); + } else { + break; + } + } + let end = self.cur(); + (start, end) + } + + /// Tokenize a `Number`. + fn number(&mut self) -> Result, LexerError> { + // FIXME this implementation has various issues to fix: + // - does not yet handle hex values, e.g. 0x... + // - allows multiple - and . in the string + let (start, end) = self.eat_while(|c| c.is_numeric() || c == '-' || c == '.'); + self.expect_span_is_not_empty(start, end)?; + let span = &self.input[start..end]; + Ok(Token::new(TokenKind::Number, span)) + } + + /// Tokenize `Punctuation`. + fn punctuation(&mut self) -> Result, LexerError> { + let start = self.cur(); + if let Some(c) = self.peek() { + if c.is_ascii_punctuation() { + self.it.next(); + return Ok(Token::new( + TokenKind::Punctuation, + &self.input[start..=start], + )); + } + } + Err(self.error(start, LexerErrorKind::NotFound)) + } + + /// Tokenize an `Entity`. + fn name(&mut self) -> Result, LexerError> { + let (start, end) = self.eat_while(|c| c.is_alphanumeric() || c == '_' || c == '%'); + self.expect_span_is_not_empty(start, end)?; + let span = &self.input[start..end]; + Ok(Token::new(TokenKind::Name, span)) + } + + /// Check if the character stream has the keyword `text` at position `pos`. + fn has_keyword(&self, pos: usize, text: &str) -> bool { + self.input[pos..].starts_with(text) + } + + // Tokenize a `Keyword`. + fn keyword(&mut self, text: &str) -> Result, LexerError> { + let span = &self.input[self.cur()..self.cur() + text.len()]; + self.it.nth(text.len() - 1); + Ok(Token::new(TokenKind::Keyword, span)) + } + + /// Raise an error if the span is empty. + fn expect_span_is_not_empty(&self, start: usize, end: usize) -> Result<(), LexerError> { + if start == end { + Err(self.error(start, LexerErrorKind::NotFound)) + } else { + Ok(()) + } + } + + /// Peek at the next character in the input stream; returns None if no character found. + fn peek(&mut self) -> Option { + self.it.peek().map(|(_, c)| *c) + } + + /// Return the current position of our iterator through the input string. + fn cur(&mut self) -> usize { + self.it + .peek() + .map(|p| p.0) + .unwrap_or_else(|| self.input.len()) + } + + /// Return the position `pos` as a 0-based line and column pair. + fn cur_as_linecol(&self, pos: usize) -> (usize, usize) { + let mut cur = 0; + for (i, line) in self.input.split_terminator('\n').enumerate() { + if cur + line.len() + 1 > pos { + return (i, pos - cur); + } + cur += line.len() + 1; + } + (self.input.lines().count(), 0) + } + + /// Create an error at the current position with the specified `kind`. + fn error(&self, pos: usize, kind: LexerErrorKind) -> LexerError { + let (line, col) = self.cur_as_linecol(pos); + let snippet = self.input.lines().nth(line).expect("a line").to_string(); + let text = Text { line, col, snippet }; + LexerError { + text, + kind, + file: None, + } + } +} + +impl<'a> Iterator for Lexer<'a> { + type Item = Token<'a>; + + fn next(&mut self) -> Option { + self.token().expect("a token") + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Token<'a> { + pub kind: TokenKind, + pub text: &'a str, +} + +impl<'a> Token<'a> { + pub fn new(kind: TokenKind, text: &'a str) -> Self { + Self { kind, text } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum TokenKind { + Keyword, + Name, + Number, + Punctuation, +} + +#[derive(Error, Debug)] +#[error("lexer failure")] +pub struct LexerError { + text: Text, + file: Option, + kind: LexerErrorKind, +} + +#[derive(Debug, PartialEq)] +pub enum LexerErrorKind { + NotFound, +} + +#[derive(Debug)] +pub struct Text { + line: usize, + col: usize, + snippet: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lex(s: &str) -> Token<'_> { + Lexer::new(s).token().unwrap().unwrap() + } + fn lex_all(s: &str) -> Vec> { + Lexer::new(s).collect() + } + fn name(text: &str) -> Token { + Token::new(TokenKind::Name, text) + } + fn number(text: &str) -> Token { + Token::new(TokenKind::Number, text) + } + fn punctuation(text: &str) -> Token { + Token::new(TokenKind::Punctuation, text) + } + fn keyword(text: &str) -> Token { + Token::new(TokenKind::Keyword, text) + } + + #[test] + fn lex_name() { + assert_eq!(lex(" fn32 "), name("fn32")); + } + + #[test] + fn lex_parentheses() { + assert_eq!(lex_all(" ()"), vec![punctuation("("), punctuation(")")]); + } + + #[test] + fn lex_integer() { + assert_eq!(lex(" 42 "), number("42")); + } + + #[test] + fn lex_float() { + assert_eq!(lex(" -0.32 "), number("-0.32")); + } + + #[test] + fn lex_boolean() { + assert_eq!( + lex_all(" truefalse "), + vec![keyword("true"), keyword("false")] + ); + } + + #[test] + fn lex_function_invocation() { + assert_eq!( + lex_all(" fn32(42, 43) \n "), + vec![ + name("fn32"), + punctuation("("), + number("42"), + punctuation(","), + number("43"), + punctuation(")") + ] + ); + } + + #[test] + fn not_found() { + let mut lexer = Lexer::new(" "); + let result = lexer.number(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind, LexerErrorKind::NotFound); + } + + #[test] + fn external_name() { + assert_eq!(lex("%fn0"), Token::new(TokenKind::Name, "%fn0")) + } +} diff --git a/cranelift-interpreter/src/clif/mod.rs b/cranelift-interpreter/src/clif/mod.rs new file mode 100644 index 000000000..f341ac3ba --- /dev/null +++ b/cranelift-interpreter/src/clif/mod.rs @@ -0,0 +1,6 @@ +//! Cranelift annotation parser. +//! +//! This module parses CLIF annotation like `; test: %fn0(42) == true`; this allows executing and +//! testing function calls in CLIF files. +pub mod lexer; +pub mod parser; diff --git a/cranelift-interpreter/src/clif/parser.rs b/cranelift-interpreter/src/clif/parser.rs new file mode 100644 index 000000000..641c55998 --- /dev/null +++ b/cranelift-interpreter/src/clif/parser.rs @@ -0,0 +1,302 @@ +//! Cranelift annotation parser. +//! +//! This loosely follows the pattern used by Alex's parser in [wat]. +//! [wat]: https://github.com/bytecodealliance/wat/blob/1b810cb430ee1f1bb11a7641e560a30679e95754/crates/wast/src/parser.rs +//! +//! ``` +//! use cranelift_interpreter::clif::parser::{Parse, ParseBuffer, ClifCommand}; +//! let buffer = ParseBuffer::new("; test: %add1(1) == 2"); +//! let command = ClifCommand::parse(buffer.parser()).unwrap(); +//! println!("{:?}", command); +//! ``` + +use crate::clif::lexer::{Lexer, LexerError, Token, TokenKind}; +use crate::value::Value; +use std::cell::Cell; +use std::path::PathBuf; +use std::str::FromStr; +use thiserror::Error; + +/// Describes the ways the parser may fail. TODO add location of failure within the original text. +#[derive(Error, Debug)] +pub enum ParseFailure { + #[error("lexing failure")] + LexerFailure(#[from] LexerError), + #[error("no token available for parsing")] + NoToken, + #[error("no value found")] + NoValue, + #[error("unexpected token")] + UnexpectedToken, + #[error("unknown command")] + UnknownCommand, + #[error("unknown operator")] + UnknownOperator, +} + +pub trait Parse<'a>: Sized { + fn parse(parser: Parser<'a>) -> Result; +} + +/// The structure owning the lexed tokens and cursor state. +pub struct ParseBuffer<'a> { + file: Option, + tokens: Box<[Token<'a>]>, + cur: Cell, +} + +impl<'a> ParseBuffer<'a> { + /// Construct a parse buffer, immediately tokenizing the text. + pub fn new(text: &'a str) -> Self { + let tokens: Vec> = Lexer::new(text).collect(); + Self { + file: None, + tokens: tokens.into_boxed_slice(), + cur: Cell::new(0), + } + } + + /// Assign a file path to the parse buffer (for better error messages). + pub fn with_file(mut self, file: impl Into) -> Self { + self.file = Some(file.into()); + self + } + + /// Create a parser using a reference to this buffer. + pub fn parser(&self) -> Parser<'_> { + Parser { buffer: self } + } +} + +/// A reference to the buffer; contains the parser implementation. +#[derive(Clone, Copy)] +pub struct Parser<'a> { + buffer: &'a ParseBuffer<'a>, +} + +impl<'a> Parser<'a> { + /// Inspect the next token without consuming it. + pub fn peek(self) -> Option> { + let cursor = self.buffer.cur.get(); + self.buffer.tokens.get(cursor).cloned() + } + + /// Consume the next token. + pub fn next(self) -> Option> { + let cursor = self.buffer.cur.get(); + self.buffer.cur.set(cursor + 1); + self.buffer.tokens.get(cursor).cloned() + } + + /// Consume the next token if it matches the expected `TokenKind`; otherwise fail. + pub fn expect(self, expected: TokenKind) -> Result, ParseFailure> { + match self.next() { + None => Err(ParseFailure::NoToken), + Some(t) => { + if t.kind == expected { + Ok(t) + } else { + Err(ParseFailure::UnexpectedToken) + } + } + } + } + + /// Consume the next token if it matches the expected `Token`; otherwise fail. + pub fn expect_exact(self, expected: Token<'a>) -> Result, ParseFailure> { + match self.next() { + None => Err(ParseFailure::NoToken), + Some(t) => { + if t == expected { + Ok(t) + } else { + Err(ParseFailure::UnexpectedToken) + } + } + } + } + + /// Check if the next token matches the expected `TokenKind`. + pub fn has_next(self, expected: TokenKind) -> bool { + match self.peek() { + Some(ref t) if t.kind == expected => true, + _ => false, + } + } + + /// Check if the next token matches the expected `Token`. + pub fn has_next_exact(self, expected: Token) -> bool { + match self.peek() { + Some(ref t) if t == &expected => true, + _ => false, + } + } +} + +/// The top-level CLIF command; e.g. `test: %fn(0) == 1`. +#[derive(Debug, PartialEq)] +pub enum ClifCommand { + Run(Invoke), + Test(Invoke, Comparison, Vec), +} + +impl<'a> Parse<'a> for ClifCommand { + fn parse(parser: Parser<'a>) -> Result { + // Skip initial comment delimiters, ';'. + while parser.has_next_exact(Token::new(TokenKind::Punctuation, ";")) { + parser.next(); + } + + // Parse command, '[run|test]:' + let command = parser.expect(TokenKind::Name)?; + parser.expect_exact(Token::new(TokenKind::Punctuation, ":"))?; + + // Parse command contents. + match command.text { + "run" => Ok(Self::Run(Invoke::parse(parser)?)), + "test" => Ok(Self::Test( + Invoke::parse(parser)?, + Comparison::parse(parser)?, + Vec::::parse(parser)?, + )), + _ => Err(ParseFailure::UnknownCommand), + } + } +} + +/// A CLIF function invocation; e.g. `%fn(0)`. +#[derive(Debug, PartialEq)] +pub struct Invoke { + pub func: String, + pub arguments: Vec, +} + +impl<'a> Parse<'a> for Invoke { + fn parse(parser: Parser<'a>) -> Result { + let function_name = parser.expect(TokenKind::Name)?.text.to_string(); + + parser.expect_exact(Token::new(TokenKind::Punctuation, "("))?; + let arguments = Vec::::parse(parser)?; + parser.expect_exact(Token::new(TokenKind::Punctuation, ")"))?; + + Ok(Self { + func: function_name, + arguments, + }) + } +} + +/// A CLIF comparison operation; e.g. `==`. +#[derive(Debug, PartialEq)] +pub enum Comparison { + Equals, + NotEquals, +} + +impl<'a> Parse<'a> for Comparison { + fn parse(parser: Parser<'a>) -> Result { + let first = parser.expect(TokenKind::Punctuation)?; + let second = parser.expect(TokenKind::Punctuation)?; + match (first.text, second.text) { + ("=", "=") => Ok(Comparison::Equals), + ("!", "=") => Ok(Comparison::NotEquals), + _ => Err(ParseFailure::UnknownOperator), + } + } +} + +/// To avoid mixing parsing with interpreter functionality, this implementation is included here +/// instead of the `value` module. +impl<'a> Parse<'a> for Value { + fn parse(parser: Parser<'a>) -> Result { + match parser.peek() { + Some(Token { + kind: TokenKind::Number, + text, + }) => { + parser.next(); + let n = i64::from_str(text).expect("a valid integer"); + Ok(Value::Int(n)) + } + Some(Token { + kind: TokenKind::Keyword, + text, + }) => match text { + "true" => { + parser.next(); + Ok(Value::Bool(true)) + } + "false" => { + parser.next(); + Ok(Value::Bool(false)) + } + _ => Err(ParseFailure::NoValue), + }, + _ => Err(ParseFailure::NoValue), + } + } +} + +fn has_next_value(parser: Parser<'_>) -> bool { + if let Some(token) = parser.peek() { + token.kind == TokenKind::Number + || (token.kind == TokenKind::Keyword && (token.text == "true" || token.text == "false")) + } else { + false + } +} + +/// Parse comma-delimited sequences of values; e.g. `42, 43, false` +impl<'a> Parse<'a> for Vec { + fn parse(parser: Parser<'a>) -> Result { + // TODO this likely should distinguish between a single value (e.g. `42`) and multiple + // values (e.g. bracketed as in `[42, 43, 44]`) + let mut values = Vec::new(); + // TODO must parse according to signature; e.g. in `fn(2000)` the `2000` could be either a + // float or integer, or could be out of range for an 8-bit integer + while has_next_value(parser) { + values.push(Value::parse(parser)?); + if !parser.has_next_exact(Token::new(TokenKind::Punctuation, ",")) { + break; + } else { + parser.next(); + } + } + Ok(values) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn run_command() { + let buffer = ParseBuffer::new("run: fn1(2, false)"); + let command = ClifCommand::parse(buffer.parser()).unwrap(); + assert_eq!( + command, + ClifCommand::Run(Invoke { + func: "fn1".to_string(), + arguments: vec![Value::Int(2), Value::Bool(false)] + }) + ); + } + + #[test] + fn test_command() { + let buffer = ParseBuffer::new("; test: fibonacci_recursive(6) == 8"); + let command = ClifCommand::parse(buffer.parser()).unwrap(); + assert_eq!( + command, + ClifCommand::Test( + Invoke { + func: "fibonacci_recursive".to_string(), + arguments: vec![Value::Int(6)] + }, + Comparison::Equals, + vec![Value::Int(8)] + ) + ); + } +} diff --git a/cranelift-interpreter/src/environment.rs b/cranelift-interpreter/src/environment.rs new file mode 100644 index 000000000..b1c892c42 --- /dev/null +++ b/cranelift-interpreter/src/environment.rs @@ -0,0 +1,76 @@ +//! Implements the function environment (e.g. a name-to-function mapping) for interpretation. + +use cranelift_codegen::ir::{FuncRef, Function}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct Environment { + functions: HashMap, + function_name_to_index: Vec, +} + +impl From for Environment { + fn from(f: Function) -> Self { + let mut functions = HashMap::new(); + functions.insert(FuncRef::from_u32(0), f); + Self { + functions, + function_name_to_index: vec![], + } + } +} + +impl Environment { + /// Add a function by name. + pub fn add(&mut self, name: String, function: Function) { + let func_ref = FuncRef::with_number(self.function_name_to_index.len() as u32) + .expect("a valid function reference"); + self.function_name_to_index.push(name); + self.functions.insert(func_ref, function); + } + + /// Retrieve a function's index by name. + pub fn index_of(&self, name: &str) -> Option { + self.function_name_to_index + .iter() + .position(|n| n == name) + .map(|i| FuncRef::with_number(i as u32).expect("a valid function reference")) + // TODO this may be more efficient as a hash map + } + + /// Retrieve a function by its function reference. + pub fn get_by_func_ref(&self, func_ref: FuncRef) -> Option<&Function> { + self.functions.get(&func_ref) + } + + /// Retrieve a function by its name. + pub fn get_by_name(&self, name: &str) -> Option<&Function> { + if let Some(fr) = self.index_of(name) { + if let Some(func) = self.get_by_func_ref(fr) { + return Some(func); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn addition() { + let mut env = Environment::default(); + let a = "a"; + let f = Function::new(); + + env.add(a.to_string(), f); + assert!(env.get_by_name(a).is_some()); + } + + #[test] + fn nonexistence() { + let env = Environment::default(); + assert!(env.get_by_name("a").is_none()); + } +} diff --git a/cranelift-interpreter/src/frame.rs b/cranelift-interpreter/src/frame.rs new file mode 100644 index 000000000..b6fcefea5 --- /dev/null +++ b/cranelift-interpreter/src/frame.rs @@ -0,0 +1,123 @@ +//! Implements a call frame (activation record) for the Cranelift interpreter. + +use crate::value::Value; +use cranelift_codegen::ir::{Function, Value as ValueRef}; +use std::collections::HashMap; + +/// Holds the mutable elements of an interpretation. At some point I thought about using +/// Cell/RefCell to do field-level mutability, thinking that otherwise I would have to +/// pass around a mutable object (for inst and registers) and an immutable one (for function, +/// could be self)--in the end I decided to do exactly that but perhaps one day that will become +/// untenable. +#[derive(Debug)] +pub struct Frame<'a> { + /// The currently executing function. + pub function: &'a Function, + /// The current mapping of SSA value-references to their actual values. + registers: HashMap, +} + +impl<'a> Frame<'a> { + /// Construct a new frame for a function. This allocates a slot in the hash map for each SSA + /// `Value` (renamed as `ValueRef` here) which should mean that no additional allocations are + /// needed while interpreting the frame. + pub fn new(function: &'a Function) -> Self { + Self { + function, + registers: HashMap::with_capacity(function.dfg.num_values()), + } + } + + pub fn with_parameters(mut self, parameters: &[ValueRef], values: &[Value]) -> Self { + for (n, v) in parameters.iter().zip(values) { + self.registers.insert(*n, v.clone()); + } + self + } + + /// Retrieve the actual value associated with an SSA reference. + #[inline] + pub fn get(&self, name: &ValueRef) -> &Value { + self.registers + .get(name) + .unwrap_or_else(|| panic!("unknown value: {}", name)) + } + + /// Retrieve multiple SSA references; see `get`. + pub fn get_all(&self, names: &[ValueRef]) -> Vec { + names.iter().map(|r| self.get(r)).cloned().collect() + } + + /// Assign `value` to the SSA reference `name`. + #[inline] + pub fn set(&mut self, name: ValueRef, value: Value) -> Option { + self.registers.insert(name, value) + } + + /// Assign to multiple SSA references; see `set`. + pub fn set_all(&mut self, names: &[ValueRef], values: Vec) { + assert_eq!(names.len(), values.len()); + for (n, v) in names.iter().zip(values) { + self.set(*n, v); + } + } + + /// Rename all of the SSA references in `old_names` to those in `new_names`. This will remove + /// any old references that are not in `old_names`. TODO This performs an extra allocation that + /// could be removed if we copied the values in the right order (i.e. when modifying in place, + /// we need to avoid changing a value before it is referenced). + pub fn rename(&mut self, old_names: &[ValueRef], new_names: &[ValueRef]) { + let mut registers = HashMap::with_capacity(self.registers.len()); + for (on, nn) in old_names.iter().zip(new_names) { + let v = self.registers.get(on).unwrap().clone(); + registers.insert(*nn, v); + } + self.registers = registers; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cranelift_codegen::ir::InstBuilder; + use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext}; + + /// Build an empty function with a single return. + fn empty_function() -> Function { + let mut func = Function::new(); + let mut context = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut func, &mut context); + let ebb = builder.create_ebb(); + builder.switch_to_block(ebb); + builder.ins().return_(&[]); + func + } + + #[test] + fn construction() { + let func = empty_function(); + // construction should not fail + Frame::new(&func); + } + + #[test] + fn assignment() { + let func = empty_function(); + let mut frame = Frame::new(&func); + + let a = ValueRef::with_number(1).unwrap(); + let fortytwo = Value::Int(42); + frame.set(a, fortytwo.clone()); + assert_eq!(frame.get(&a), &fortytwo); + } + + #[test] + #[should_panic] + fn no_existing_value() { + let func = empty_function(); + let frame = Frame::new(&func); + + let a = ValueRef::with_number(1).unwrap(); + frame.get(&a); + } +} diff --git a/cranelift-interpreter/src/interpreter.rs b/cranelift-interpreter/src/interpreter.rs new file mode 100644 index 000000000..b80d5438e --- /dev/null +++ b/cranelift-interpreter/src/interpreter.rs @@ -0,0 +1,295 @@ +//! Cranelift IR interpreter. +//! +//! This module contains the logic for interpreting Cranelift instructions. + +use crate::environment::Environment; +use crate::frame::Frame; +use crate::value::Value; +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::immediates::Imm64; +use cranelift_codegen::ir::{ + Ebb, FuncRef, Function, Inst, InstructionData, InstructionData::*, Opcode, Opcode::*, + Value as ValueRef, ValueList, +}; +use log::debug; +use std::ops::{Add, Sub}; +use thiserror::Error; + +/// The valid control flow states. +pub enum ControlFlow { + Continue, + ContinueAt(Ebb, Vec), + Return(Vec), +} + +/// The ways interpretation can fail. +#[derive(Error, Debug)] +pub enum Trap { + #[error("unknown trap")] + Unknown, + #[error("invalid type for {1}: expected {0}")] + InvalidType(String, ValueRef), + #[error("reached an unreachable statement")] + Unreachable, + #[error("invalid control flow: {0}")] + InvalidControlFlow(String), + #[error("invalid function reference: {0}")] + InvalidFunctionReference(FuncRef), + #[error("invalid function name: {0}")] + InvalidFunctionName(String), +} + +/// The Cranelift interpreter; it contains immutable elements such as the function environment and +/// implements the Cranelift IR semantics. +#[derive(Default)] +pub struct Interpreter { + pub env: Environment, +} + +impl Interpreter { + pub fn new(env: Environment) -> Self { + Self { env } + } + + pub fn call_by_name(&self, func_name: &str, arguments: &[Value]) -> Result { + let func_ref = self + .env + .index_of(func_name) + .ok_or_else(|| Trap::InvalidFunctionName(func_name.to_string()))?; + self.call_by_index(func_ref, arguments) + } + + pub fn call_by_index( + &self, + func_ref: FuncRef, + arguments: &[Value], + ) -> Result { + match self.env.get_by_func_ref(func_ref) { + None => Err(Trap::InvalidFunctionReference(func_ref)), + Some(func) => self.call(func, arguments), + } + } + + fn call(&self, function: &Function, arguments: &[Value]) -> Result { + debug!("Call: {}({:?})", function.name, arguments); + let first_ebb = function.layout.ebbs().next().expect("to have a first ebb"); + let parameters = function.dfg.ebb_params(first_ebb); + let mut frame = Frame::new(function).with_parameters(parameters, arguments); + let result = self.block(&mut frame, first_ebb); + result + } + + fn block(&self, frame: &mut Frame, ebb: Ebb) -> Result { + debug!("Block: {}", ebb); + for inst in frame.function.layout.ebb_insts(ebb) { + match self.inst(frame, inst)? { + ControlFlow::Continue => continue, + ControlFlow::ContinueAt(ebb, old_names) => { + let new_names = frame.function.dfg.ebb_params(ebb); + frame.rename(&old_names, new_names); + return self.block(frame, ebb); // TODO check that TCO happens + } + ControlFlow::Return(rs) => return Ok(ControlFlow::Return(rs)), + } + } + Err(Trap::Unreachable) + } + + fn binary( + &self, + frame: &mut Frame, + op: fn(Value, Value) -> Value, + a: ValueRef, + b: ValueRef, + r: ValueRef, + ) { + let a = frame.get(&a); + let b = frame.get(&b); + let c = op(a.clone(), b.clone()); + frame.set(r, c); + } + + // TODO refactor to only one `binary` method + fn binary_imm( + &self, + frame: &mut Frame, + op: fn(Value, Value) -> Value, + a: ValueRef, + b: Value, + r: ValueRef, + ) { + let a = frame.get(&a); + let c = op(a.clone(), b); + frame.set(r, c); + } + + fn iconst(&self, frame: &mut Frame, imm: Imm64, r: ValueRef) { + frame.set(r, Value::Int(imm.into())); + } + + fn bconst(&self, frame: &mut Frame, imm: bool, r: ValueRef) { + frame.set(r, Value::Bool(imm)); + } + + // TODO add load/store + fn inst(&self, frame: &mut Frame, inst: Inst) -> Result { + use ControlFlow::{Continue, ContinueAt}; + debug!("Inst: {}", &frame.function.dfg.display_inst(inst, None)); + + let data = &frame.function.dfg[inst]; + match data { + Binary { opcode, args } => match opcode { + Iadd => { + // TODO trap if arguments are of the wrong type; here and below + let res = first_result(frame.function, inst); + self.binary(frame, Add::add, args[0], args[1], res); + Ok(Continue) + } + _ => unimplemented!(), + }, + BinaryImm { opcode, arg, imm } => match opcode { + IrsubImm => { + let res = first_result(frame.function, inst); + let imm = Value::Int((*imm).into()); + self.binary_imm(frame, Sub::sub, *arg, imm, res); + Ok(Continue) + } + _ => unimplemented!(), + }, + Branch { + opcode, + args, + destination, + } => match opcode { + Brnz => { + let mut args = value_refs(frame.function, args); + let first = args.remove(0); + match frame.get(&first) { + Value::Bool(true) => Ok(ContinueAt(*destination, args)), + Value::Bool(false) => Ok(Continue), + _ => Err(Trap::InvalidType("bool".to_string(), args[0])), + } + } + _ => unimplemented!(), + }, + InstructionData::Call { args, func_ref, .. } => { + // Find the function to call. + let func_name = function_name_of_func_ref(*func_ref, frame.function); + + // Call function. + let args = frame.get_all(args.as_slice(&frame.function.dfg.value_lists)); + let result = self.call_by_name(&func_name, &args)?; + + // Save results. + if let ControlFlow::Return(returned_values) = result { + let ssa_values = frame.function.dfg.inst_results(inst); + assert_eq!( + ssa_values.len(), + returned_values.len(), + "expected result length ({}) to match SSA values length ({}): {}", + returned_values.len(), + ssa_values.len(), + frame.function.dfg.display_inst(inst, None) + ); + frame.set_all(ssa_values, returned_values); + Ok(Continue) + } else { + Err(Trap::InvalidControlFlow(format!( + "did not return from: {}", + frame.function.dfg.display_inst(inst, None) + ))) + } + } + InstructionData::Jump { + opcode, + destination, + args, + } => match opcode { + Opcode::Fallthrough => { + Ok(ContinueAt(*destination, value_refs(frame.function, args))) + } + Opcode::Jump => Ok(ContinueAt(*destination, value_refs(frame.function, args))), + _ => unimplemented!(), + }, + IntCompareImm { + opcode, + arg, + cond, + imm, + } => match opcode { + IcmpImm => { + let result = if let Value::Int(arg_value) = *frame.get(arg) { + let imm_value: i64 = (*imm).into(); + match cond { + IntCC::UnsignedLessThanOrEqual => arg_value <= imm_value, + IntCC::Equal => arg_value == imm_value, + _ => unimplemented!(), + } + } else { + return Err(Trap::InvalidType("int".to_string(), *arg)); + }; + let res = first_result(frame.function, inst); + frame.set(res, Value::Bool(result)); + Ok(Continue) + } + _ => unimplemented!(), + }, + MultiAry { opcode, args } => match opcode { + Return => { + let rs: Vec = args + .as_slice(&frame.function.dfg.value_lists) + .iter() + .map(|r| frame.get(r).clone()) + .collect(); + Ok(ControlFlow::Return(rs)) + } + _ => unimplemented!(), + }, + NullAry { opcode } => match opcode { + Nop => Ok(Continue), + _ => unimplemented!(), + }, + UnaryImm { opcode, imm } => match opcode { + Iconst => { + let res = first_result(frame.function, inst); + self.iconst(frame, *imm, res); + Ok(Continue) + } + _ => unimplemented!(), + }, + UnaryBool { opcode, imm } => match opcode { + Bconst => { + let res = first_result(frame.function, inst); + self.bconst(frame, *imm, res); + Ok(Continue) + } + _ => unimplemented!(), + }, + + _ => unimplemented!("{:?}", data), + } + } +} + +fn first_result(function: &Function, inst: Inst) -> ValueRef { + function.dfg.first_result(inst) +} + +fn value_refs(function: &Function, args: &ValueList) -> Vec { + args.as_slice(&function.dfg.value_lists).to_vec() +} + +/// Return the (external) function name of `func_ref` in a local `function`. Note that this may +/// be truncated. +fn function_name_of_func_ref(func_ref: FuncRef, function: &Function) -> String { + function + .dfg + .ext_funcs + .get(func_ref) + .expect("function to exist") + .name + .to_string() +} + +#[cfg(test)] +mod tests {} diff --git a/cranelift-interpreter/src/lib.rs b/cranelift-interpreter/src/lib.rs new file mode 100644 index 000000000..eca6366b0 --- /dev/null +++ b/cranelift-interpreter/src/lib.rs @@ -0,0 +1,10 @@ +//! Cranelift IR interpreter. +//! +//! This module is a project for interpreting Cranelift IR. + +pub mod clif; +pub mod environment; +pub mod frame; +pub mod interpreter; +pub mod runner; +pub mod value; diff --git a/cranelift-interpreter/src/runner.rs b/cranelift-interpreter/src/runner.rs new file mode 100644 index 000000000..c047bbe90 --- /dev/null +++ b/cranelift-interpreter/src/runner.rs @@ -0,0 +1,130 @@ +//! Helper for running the Cranelift IR interpreter on CLIF files with annotations. + +use crate::clif::parser::{ClifCommand, Comparison, Invoke, Parse, ParseBuffer, ParseFailure}; +use crate::environment::Environment; +use crate::interpreter::{ControlFlow, Interpreter, Trap}; +use crate::value::Value; +use cranelift_reader::{parse_test, ParseError, ParseOptions}; +use log::debug; +use std::path::PathBuf; +use std::{fs, io}; +use thiserror::Error; + +/// Contains CLIF code that can be executed with [FileRunner::run]. +pub struct FileRunner { + path: Option, + contents: String, +} + +impl FileRunner { + /// Construct a file runner from a CLIF file path. + pub fn from_path(path: impl Into) -> Result { + let path = path.into(); + debug!("New file runner from path: {}:", path.to_string_lossy()); + let contents = fs::read_to_string(&path)?; + Ok(Self { + path: Some(path), + contents, + }) + } + + /// Construct a file runner from a CLIF code string. + pub fn from_inline_code(contents: String) -> Self { + debug!("New file runner from inline code: {}:", &contents[..20]); + Self { + path: None, + contents, + } + } + + /// Return the path of the file runner or `[inline code]`. + pub fn path(&self) -> String { + match self.path { + None => "[inline code]".to_string(), + Some(ref p) => p.to_string_lossy().to_string(), + } + } + + /// Run the file; this searches for annotations like `; run: %fn0(42)` or + /// `; test: %fn0(42) == 2` and executes them, performing any test comparisons if necessary. + pub fn run(&self) -> Result<(), FileRunnerFailure> { + // parse file + let test = parse_test(&self.contents, ParseOptions::default()) + .map_err(|e| FileRunnerFailure::ParsingClif(self.path(), e))?; + + // collect functions + let mut env = Environment::default(); + let mut comments = vec![]; + comments.append(&mut test.preamble_comments.clone()); + for (func, details) in test.functions.into_iter() { + // FIXME func.name is truncating function name + env.add(func.name.to_string(), func); + comments.append(&mut details.comments.clone()); + } + + // run assertions + let interpreter = Interpreter::new(env); + for comment in comments { + if !(comment.text.starts_with("; run:") || comment.text.starts_with("; test:")) { + continue; + } + let mut parse_buffer = ParseBuffer::new(comment.text); + if let Some(ref p) = self.path { + parse_buffer = parse_buffer.with_file(p); + } + let command = ClifCommand::parse(parse_buffer.parser())?; + match command { + ClifCommand::Run(invoke) => { + let results = invoke_function(&invoke, &interpreter)?; + println!("{:?}", results) + } + ClifCommand::Test(invoke, compare, values) => { + let results = invoke_function(&invoke, &interpreter)?; + let pass = match compare { + Comparison::Equals => values == results, + Comparison::NotEquals => values != results, + }; + if !pass { + let expected = format!("{:?} {:?} {:?}", invoke, compare, values); + let actual = format!("{:?}", results); + return Err(FileRunnerFailure::FailedAssertion(expected, actual)); + } + } + } + } + Ok(()) + } + + // TODO add call(name, args) -> returns +} + +/// Helper for invoking the function. +fn invoke_function( + invocation: &Invoke, + interpreter: &Interpreter, +) -> Result, FileRunnerFailure> { + match interpreter.call_by_name(&invocation.func, &invocation.arguments)? { + ControlFlow::Return(results) => Ok(results), + _ => panic!("Unexpected returned control flow--this is likely a bug."), + } +} + +/// Possible sources of failure in this file. +#[derive(Error, Debug)] +pub enum FileRunnerFailure { + // reading + #[error("failure reading file")] + Io(#[from] io::Error), + #[error("failure parsing file {0}: {1}")] + ParsingClif(String, ParseError), + #[error("failure parsing test annotations: {0}")] + ParsingClifAnnotation(#[from] ParseFailure), + + // executing + #[error("failed interpretation")] + Trap(#[from] Trap), + #[error("Unknown function: {0}")] + UnknownFunction(String), + #[error("expected: {0}, found: {1}")] + FailedAssertion(String, String), +} diff --git a/cranelift-interpreter/src/value.rs b/cranelift-interpreter/src/value.rs new file mode 100644 index 000000000..640093357 --- /dev/null +++ b/cranelift-interpreter/src/value.rs @@ -0,0 +1,45 @@ +//! The values operated on by the interpreter. +//! +//! Each variant of `Value` may implement traits of `std::ops` for use in `interpreter`; e.g., +//! `Value::Int` has an implementation (a match arm) of `Add`. + +use std::ops::{Add, Sub}; + +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + Bool(bool), + Int(i64), + // TODO add floats, vectors +} + +impl Add for Value { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + use Value::*; + match (self, rhs) { + (Int(a), Int(b)) => Int(a + b), + _ => unimplemented!(), + } + } +} + +impl Sub for Value { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + use Value::*; + match (self, rhs) { + (Int(a), Int(b)) => Int(a - b), + _ => unimplemented!(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn value_size() { + assert_eq!(core::mem::size_of::(), 16); + } +} diff --git a/cranelift-interpreter/tests/add.clif b/cranelift-interpreter/tests/add.clif new file mode 100644 index 000000000..739e66d29 --- /dev/null +++ b/cranelift-interpreter/tests/add.clif @@ -0,0 +1,11 @@ +function %add() -> i32 { +ebb0: + v0 = iconst.i32 40 + v1 = iconst.i32 2 + v2 = iadd v0, v1 + jump ebb1(v2) + +ebb1(v3: i32): + return v3 +} +; test: %add() == 42 diff --git a/cranelift-interpreter/tests/fibonacci.clif b/cranelift-interpreter/tests/fibonacci.clif new file mode 100644 index 000000000..9ab43e0e5 --- /dev/null +++ b/cranelift-interpreter/tests/fibonacci.clif @@ -0,0 +1,56 @@ +; A non-recursive fibonacci implementation. +function %fibonacci(i32) -> i32 { +ebb0(v0: i32): + v1 = icmp_imm ule v0, 2 + v2 = iconst.i32 1 + brnz v1, ebb2(v2) ; handle base case, n <= 2 + v3 = iconst.i32 1 + v4 = irsub_imm v0, 2 + fallthrough ebb1(v4, v2, v3) + +ebb1(v10: i32, v11: i32, v12: i32): ; params: n, fib(n-1), fib(n-2) + v13 = iadd v11, v12 + v14 = irsub_imm v10, 1 + v15 = icmp_imm eq v14, 0 + brnz v15, ebb2(v13) + jump ebb1(v14, v13, v11) + +ebb2(v20: i32): ; early return and end of loop + return v20 +} +; test: %fibonacci(0) == 1 +; test: %fibonacci(1) == 1 +; test: %fibonacci(2) == 1 +; test: %fibonacci(3) == 2 +; test: %fibonacci(4) == 3 +; test: %fibonacci(5) == 5 +; test: %fibonacci(6) == 8 +; test: %fibonacci(10) == 55 + + +; A recursive fibonacci implementation. +function %fibonacci_recursive(i32) -> i32 { + fn0 = %fibonacci_recursive(i32) -> i32 + +ebb0(v0: i32): + v1 = icmp_imm ule v0, 2 + brnz v1, ebb1 + v2 = irsub_imm v0, 1 + v3 = call fn0(v2) + v4 = irsub_imm v0, 2 + v5 = call fn0(v4) + v6 = iadd v3, v5 + return v6 + +ebb1: + v20 = iconst.i32 1 + return v20 +} +; test: %fibonacci_recurs(0) == 1 +; test: %fibonacci_recurs(1) == 1 +; test: %fibonacci_recurs(2) == 1 +; test: %fibonacci_recurs(3) == 2 +; test: %fibonacci_recurs(4) == 3 +; test: %fibonacci_recurs(5) == 5 +; test: %fibonacci_recurs(6) == 8 +; test: %fibonacci_recurs(10) == 55 diff --git a/cranelift-interpreter/tests/mod.rs b/cranelift-interpreter/tests/mod.rs new file mode 100644 index 000000000..2aa67414e --- /dev/null +++ b/cranelift-interpreter/tests/mod.rs @@ -0,0 +1,27 @@ +use cranelift_interpreter::runner::FileRunner; +use std::path::PathBuf; +use walkdir::WalkDir; + +#[test] +fn filetests() { + let _ = pretty_env_logger::try_init(); + for path in iterate_files(vec!["tests".to_string()]) { + println!("{}:", path.to_string_lossy()); + FileRunner::from_path(path).unwrap().run().unwrap(); + } +} + +/// Iterate over all of the files passed as arguments, recursively iterating through directories. +fn iterate_files(files: Vec) -> impl Iterator { + files + .into_iter() + .flat_map(WalkDir::new) + .filter(|f| match f { + Ok(d) => d.path().extension().filter(|&e| e.eq("clif")).is_some(), + _ => false, + }) + .map(|f| { + f.expect("This should not happen: we have already filtered out the errors") + .into_path() + }) +} diff --git a/src/clif-util.rs b/src/clif-util.rs index 4066ef0fd..2a8f37664 100755 --- a/src/clif-util.rs +++ b/src/clif-util.rs @@ -24,6 +24,7 @@ mod bugpoint; mod cat; mod compile; mod disasm; +mod interpret; mod print_cfg; mod run; mod utils; @@ -179,6 +180,13 @@ fn main() { .arg(add_input_file_arg()) .arg(add_debug_flag()), ) + .subcommand( + SubCommand::with_name("interpret") + .about("Interpret CLIF code") + .arg(add_verbose_flag()) + .arg(add_input_file_arg()) + .arg(add_debug_flag()), + ) .subcommand( SubCommand::with_name("cat") .about("Outputs .clif file") @@ -239,6 +247,14 @@ fn main() { ) .map(|_time| ()) } + ("interpret", Some(rest_cmd)) => { + handle_debug_flag(rest_cmd.is_present("debug")); + interpret::run( + get_vec(rest_cmd.values_of("file")), + rest_cmd.is_present("verbose"), + ) + .map(|_time| ()) + } ("pass", Some(rest_cmd)) => { handle_debug_flag(rest_cmd.is_present("debug")); diff --git a/src/interpret.rs b/src/interpret.rs new file mode 100644 index 000000000..772de4848 --- /dev/null +++ b/src/interpret.rs @@ -0,0 +1,86 @@ +//! CLI tool to interpret Cranelift IR files. + +use cranelift_interpreter::runner::FileRunner; +use std::path::PathBuf; +use walkdir::WalkDir; + +pub fn run(files: Vec, flag_print: bool) -> Result<(), String> { + let mut total = 0; + let mut errors = 0; + for file in iterate_files(files) { + total += 1; + let runner = FileRunner::from_path(file).map_err(|e| e.to_string())?; + match runner.run() { + Ok(_) => { + if flag_print { + println!("{}", runner.path()); + } + } + Err(e) => { + if flag_print { + println!("{}: {}", runner.path(), e.to_string()); + } + errors += 1; + } + } + } + + if flag_print { + match total { + 0 => println!("0 files"), + 1 => println!("1 file"), + n => println!("{} files", n), + } + } + + match errors { + 0 => Ok(()), + 1 => Err(String::from("1 failure")), + n => Err(format!("{} failures", n)), + } +} + +/// Iterate over all of the files passed as arguments, recursively iterating through directories +fn iterate_files(files: Vec) -> impl Iterator { + files + .into_iter() + .flat_map(WalkDir::new) + .filter(|f| match f { + Ok(d) => { + // filter out hidden files (starting with .) + !d.file_name().to_str().map_or(false, |s| s.starts_with('.')) + // filter out directories + && !d.file_type().is_dir() + } + Err(e) => { + println!("Unable to read file: {}", e); + false + } + }) + .map(|f| { + f.expect("This should not happen: we have already filtered out the errors") + .into_path() + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn nop() { + let code = String::from( + " + function %test() -> b8 { + ebb0: + nop + v1 = bconst.b8 true + v2 = iconst.i8 42 + return v1 + } + ; run + ", + ); + FileRunner::from_inline_code(code).run().unwrap() + } +}