From c87e8e657cf1fa2eb9985add4e4bdf5dc3acef76 Mon Sep 17 00:00:00 2001 From: "Sandeep.C.R" Date: Wed, 25 Oct 2023 18:29:00 +0530 Subject: [PATCH 1/7] MIR: Disable unintentional doctest strings --- contrib/mir/src/ast.rs | 2 +- contrib/mir/src/gas.rs | 2 +- contrib/mir/src/irrefutable_match.rs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/mir/src/ast.rs b/contrib/mir/src/ast.rs index 9d1482e3ca78..0a4c14f30109 100644 --- a/contrib/mir/src/ast.rs +++ b/contrib/mir/src/ast.rs @@ -127,7 +127,7 @@ macro_rules! valuefrom { /// Simple helper for constructing Elt values: /// -/// ``` +/// ```text /// let val: Value = Elt("foo", 3).into() /// ``` pub struct Elt(pub K, pub V); diff --git a/contrib/mir/src/gas.rs b/contrib/mir/src/gas.rs index 716ee13a7428..7d76043168eb 100644 --- a/contrib/mir/src/gas.rs +++ b/contrib/mir/src/gas.rs @@ -67,7 +67,7 @@ impl AsGasCost for checked::Checked { /// A hack to get the integral logarithm base 2, rounded up. /// Rounds up to the nearest power of 2 and counts trailing zeroes. Thus, /// -/// ``` +/// ```text /// log2i(1) = log2i(0b1) = 0; /// log2i(2) = log2i(0b10) = 1; /// log2i(3) = log2i(4) = log2i(0b100) = 2; diff --git a/contrib/mir/src/irrefutable_match.rs b/contrib/mir/src/irrefutable_match.rs index 140cc99918d2..ce300280e669 100644 --- a/contrib/mir/src/irrefutable_match.rs +++ b/contrib/mir/src/irrefutable_match.rs @@ -1,7 +1,7 @@ /// Helper macro emulating forced irrefutable pattern matches, i.e. matches that /// panic at runtime if they fail. This helps avoid the awkward /// -/// ``` +/// ```text /// let x = match y { /// Something(z) => z /// _ => panic!() @@ -15,7 +15,7 @@ /// /// There are three forms of this macro: /// -/// ``` +/// ```text /// let x = irrefutable_match!(expr; Enum::Constructor) /// ``` /// @@ -24,11 +24,11 @@ /// /// this is roughly equivalent to the let-else construct: /// -/// ```ignore +/// ```text /// let Enum::Constructor(x) = expr else { panic!() }; /// ``` /// -/// ``` +/// ```text /// irrefutable_match!(expr; Enum::Constructor, ident, ...) /// ``` /// @@ -40,7 +40,7 @@ /// /// this is roughly equivalent to the let-else construct: /// -/// ```ignore +/// ```text /// let Enum::Constructor(ident,...) = expr else { panic!() }; /// ``` /// @@ -48,7 +48,7 @@ /// /// A third form, /// -/// ``` +/// ```text /// irrefutable_match!(expr;) /// ``` /// -- GitLab From 77ef053c0b3a85c33c28d40128c91cfaf1c4580a Mon Sep 17 00:00:00 2001 From: "Sandeep.C.R" Date: Sat, 21 Oct 2023 09:45:22 +0530 Subject: [PATCH 2/7] MIR: Add a Tzt module with types to represent Tzt test entities --- contrib/mir/src/gas.rs | 2 +- contrib/mir/src/interpreter.rs | 2 +- contrib/mir/src/{main.rs => lib.rs} | 23 ++++---- contrib/mir/src/typechecker.rs | 8 +-- contrib/mir/src/tzt.rs | 84 +++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 18 deletions(-) rename contrib/mir/src/{main.rs => lib.rs} (98%) create mode 100644 contrib/mir/src/tzt.rs diff --git a/contrib/mir/src/gas.rs b/contrib/mir/src/gas.rs index 7d76043168eb..eba90f715f3f 100644 --- a/contrib/mir/src/gas.rs +++ b/contrib/mir/src/gas.rs @@ -10,7 +10,7 @@ pub struct Gas { milligas_amount: Option, } -#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] #[error("out of gas")] pub struct OutOfGas; diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index e1dcd3669ee9..fe6881da37f3 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -10,7 +10,7 @@ use crate::context::Ctx; use crate::gas::{interpret_cost, OutOfGas}; use crate::stack::*; -#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] pub enum InterpretError { #[error(transparent)] OutOfGas(#[from] OutOfGas), diff --git a/contrib/mir/src/main.rs b/contrib/mir/src/lib.rs similarity index 98% rename from contrib/mir/src/main.rs rename to contrib/mir/src/lib.rs index 14f0bc5a3abc..c55b89d4232d 100644 --- a/contrib/mir/src/main.rs +++ b/contrib/mir/src/lib.rs @@ -5,18 +5,17 @@ /* */ /******************************************************************************/ -mod ast; -mod context; -mod gas; -mod interpreter; -mod irrefutable_match; -mod lexer; -mod parser; -mod stack; -mod syntax; -mod typechecker; - -fn main() {} +pub mod ast; +pub mod context; +pub mod gas; +pub mod interpreter; +pub mod irrefutable_match; +pub mod lexer; +pub mod parser; +pub mod stack; +pub mod syntax; +pub mod typechecker; +pub mod tzt; #[cfg(test)] mod tests { diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index 2ccc69bb3b57..87f8c6b48f25 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -17,7 +17,7 @@ use crate::lexer::Prim; use crate::stack::*; /// Typechecker error type. -#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] pub enum TcError { #[error("type stacks not equal: {0:?} != {1:?}")] StacksNotEqual(TypeStack, TypeStack, StacksNotEqualReason), @@ -51,7 +51,7 @@ pub enum TcError { TypeNotPackable(Type), } -#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] pub enum NoMatchingOverloadReason { #[error("stack too short, expected {expected}")] StackTooShort { expected: usize }, @@ -65,7 +65,7 @@ pub enum NoMatchingOverloadReason { TypeNotComparable(Type), } -#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] pub enum StacksNotEqualReason { #[error(transparent)] TypesNotEqual(#[from] TypesNotEqual), @@ -73,7 +73,7 @@ pub enum StacksNotEqualReason { LengthsDiffer(usize, usize), } -#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] #[error("types not equal: {0:?} != {1:?}")] pub struct TypesNotEqual(Type, Type); diff --git a/contrib/mir/src/tzt.rs b/contrib/mir/src/tzt.rs new file mode 100644 index 000000000000..358d2dd162c3 --- /dev/null +++ b/contrib/mir/src/tzt.rs @@ -0,0 +1,84 @@ +/******************************************************************************/ +/* */ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) [2023] Serokell */ +/* */ +/******************************************************************************/ + +use crate::ast::*; +use crate::interpreter::*; +use crate::stack::*; +use crate::typechecker::*; + +pub type TestStack = Vec<(Type, TypedValue)>; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum TztTestError { + StackMismatch( + (FailingTypeStack, Stack), + (FailingTypeStack, Stack), + ), + UnexpectedError(TestError), + UnexpectedSuccess(IStack), + ExpectedDifferentError(ErrorExpectation, TestError), +} + +/// Represent one Tzt test. The output attribute is a Result to include +/// expectation of failure. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct TztTest { + pub code: ParsedInstructionBlock, + pub input: TestStack, + pub output: TestExpectation, + pub amount: Option, +} + +/// This represents possibilities in which the execution of +/// the code in a test can fail. +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] +pub enum TestError { + #[error(transparent)] + TypecheckerError(#[from] TcError), + #[error(transparent)] + InterpreterError(#[from] InterpretError), +} + +/// This represents the outcome that we expect from interpreting +/// the code in a test. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum TestExpectation { + ExpectSuccess(Vec<(Type, Value)>), + ExpectError(ErrorExpectation), +} + +#[allow(dead_code)] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ErrorExpectation { + TypecheckerError(TcError), + InterpreterError(InterpreterErrorExpectation), +} + +#[allow(dead_code)] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum InterpreterErrorExpectation { + GeneralOverflow(i128, i128), + MutezOverflow(i64, i64), + FailedWith(Value), +} + +/// Helper type for use during parsing, represent a single +/// line from the test file. +pub enum TztEntity { + Code(ParsedInstructionBlock), + Input(Vec<(Type, Value)>), + Output(TztOutput), + Amount(i64), +} + +/// Possible values for the "output" field in a Tzt test +pub enum TztOutput { + Success(Vec<(Type, Value)>), + Fail(Value), + MutezOverflow(i64, i64), + GeneralOverflow(i128, i128), +} -- GitLab From f714590e68a8acbdcf2d0808b8addf59a14d19f3 Mon Sep 17 00:00:00 2001 From: "Sandeep.C.R" Date: Sat, 21 Oct 2023 09:52:01 +0530 Subject: [PATCH 3/7] MIR: Amend grammar to parse Tzt file to the new Tzt types --- contrib/mir/src/lexer.rs | 47 +++++++++--- contrib/mir/src/syntax.lalrpop | 131 ++++++++++++++++++++++----------- 2 files changed, 121 insertions(+), 57 deletions(-) diff --git a/contrib/mir/src/lexer.rs b/contrib/mir/src/lexer.rs index 50ba55d476ee..16433978b7db 100644 --- a/contrib/mir/src/lexer.rs +++ b/contrib/mir/src/lexer.rs @@ -11,18 +11,24 @@ use logos::Logos; /// provided, and defines `FromStr` implementation using stringified /// representation of the identifiers. macro_rules! defprim { - ($($prim:ident),* $(,)*) => { + ($ty:ident; $($prim:ident),* $(,)*) => { #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[allow(non_camel_case_types, clippy::upper_case_acronyms)] - pub enum Prim { + pub enum $ty { $($prim),* } - impl std::str::FromStr for Prim { + impl std::fmt::Display for $ty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &self) + } + } + + impl std::str::FromStr for $ty { type Err = PrimError; fn from_str(s: &str) -> Result { match s { - $(stringify!($prim) => Ok(Prim::$prim),)* + $(stringify!($prim) => Ok($ty::$prim),)* _ => Err(PrimError(s.to_owned())) } } @@ -36,6 +42,7 @@ pub struct PrimError(String); // NB: Primitives will be lexed as written, so capitalization matters. defprim! { + Prim; parameter, storage, code, @@ -81,17 +88,28 @@ defprim! { UPDATE, } -impl std::fmt::Display for Prim { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", &self) - } +defprim! { + TztPrim; + Stack_elt, + input, + output, + Failed, + amount, + MutezOverflow, + GeneralOverflow, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PrimWithTzt { + Prim(Prim), + TztPrim(TztPrim), } #[derive(Debug, Clone, PartialEq, Eq, Logos)] #[logos(error = LexerError, skip r"[ \t\r\n\v\f]+")] pub enum Tok { #[regex(r"[A-Za-z_]+", lex_prim)] - Prim(Prim), + Prim(PrimWithTzt), #[regex("([+-]?)[0-9]+", lex_number)] Number(i128), @@ -118,7 +136,8 @@ pub enum Tok { impl std::fmt::Display for Tok { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self { - Tok::Prim(p) => p.fmt(f), + Tok::Prim(PrimWithTzt::Prim(p)) => p.fmt(f), + Tok::Prim(PrimWithTzt::TztPrim(p)) => p.fmt(f), Tok::Number(n) => n.fmt(f), Tok::String(s) => s.fmt(f), Tok::Annotation => write!(f, ""), @@ -153,8 +172,12 @@ impl Default for LexerError { type Lexer<'a> = logos::Lexer<'a, Tok>; -fn lex_prim(lex: &mut Lexer) -> Result { - lex.slice().parse().map_err(LexerError::from) +fn lex_prim(lex: &mut Lexer) -> Result { + lex.slice() + .parse() + .map(PrimWithTzt::Prim) + .or_else(|_| lex.slice().parse().map(PrimWithTzt::TztPrim)) + .map_err(LexerError::from) } fn lex_number(lex: &mut Lexer) -> Result { diff --git a/contrib/mir/src/syntax.lalrpop b/contrib/mir/src/syntax.lalrpop index 2d8a0753aaa9..8b741dbbce52 100644 --- a/contrib/mir/src/syntax.lalrpop +++ b/contrib/mir/src/syntax.lalrpop @@ -11,7 +11,10 @@ use crate::ast::*; use crate::parser::{ParserError, ContractScriptEntity, validate_u10}; -use crate::lexer::{Prim, Tok}; +use crate::lexer::{LexerError, Prim, PrimWithTzt, TztPrim as TzP, Tok}; +use crate::tzt::*; +use PrimWithTzt::*; +use PrimWithTzt as PT; grammar; @@ -20,52 +23,59 @@ extern { type Location = usize; enum Tok { + "stack_elt" => Tok::Prim(TztPrim(TzP::Stack_elt)), + "input" => Tok::Prim(TztPrim(TzP::input)), + "output" => Tok::Prim(TztPrim(TzP::output)), + "failed" => Tok::Prim(TztPrim(TzP::Failed)), + "mutezOverflow" => Tok::Prim(TztPrim(TzP::MutezOverflow)), + "generalOverflow" => Tok::Prim(TztPrim(TzP::GeneralOverflow)), + "amount" => Tok::Prim(TztPrim(TzP::amount)), number => Tok::Number(), string => Tok::String(), ann => Tok::Annotation, - "parameter" => Tok::Prim(Prim::parameter), - "storage" => Tok::Prim(Prim::storage), - "code" => Tok::Prim(Prim::code), - "int" => Tok::Prim(Prim::int), - "nat" => Tok::Prim(Prim::nat), - "bool" => Tok::Prim(Prim::bool), - "mutez" => Tok::Prim(Prim::mutez), - "string" => Tok::Prim(Prim::string), - "unit" => Tok::Prim(Prim::unit), - "operation" => Tok::Prim(Prim::operation), - "pair" => Tok::Prim(Prim::pair), - "option" => Tok::Prim(Prim::option), - "list" => Tok::Prim(Prim::list), - "map" => Tok::Prim(Prim::map), - "True" => Tok::Prim(Prim::True), - "False" => Tok::Prim(Prim::False), - "Unit" => Tok::Prim(Prim::Unit), - "None" => Tok::Prim(Prim::None), - "Pair" => Tok::Prim(Prim::Pair), - "Some" => Tok::Prim(Prim::Some), - "Elt" => Tok::Prim(Prim::Elt), - "PUSH" => Tok::Prim(Prim::PUSH), - "INT" => Tok::Prim(Prim::INT), - "GT" => Tok::Prim(Prim::GT), - "LOOP" => Tok::Prim(Prim::LOOP), - "DIP" => Tok::Prim(Prim::DIP), - "ADD" => Tok::Prim(Prim::ADD), - "DROP" => Tok::Prim(Prim::DROP), - "SWAP" => Tok::Prim(Prim::SWAP), - "IF" => Tok::Prim(Prim::IF), - "DUP" => Tok::Prim(Prim::DUP), - "FAILWITH" => Tok::Prim(Prim::FAILWITH), - "UNIT" => Tok::Prim(Prim::UNIT), - "CAR" => Tok::Prim(Prim::CAR), - "CDR" => Tok::Prim(Prim::CDR), - "PAIR" => Tok::Prim(Prim::PAIR), - "IF_NONE" => Tok::Prim(Prim::IF_NONE), - "SOME" => Tok::Prim(Prim::SOME), - "COMPARE" => Tok::Prim(Prim::COMPARE), - "AMOUNT" => Tok::Prim(Prim::AMOUNT), - "NIL" => Tok::Prim(Prim::NIL), - "GET" => Tok::Prim(Prim::GET), - "UPDATE" => Tok::Prim(Prim::UPDATE), + "parameter" => Tok::Prim(PT::Prim(Prim::parameter)), + "storage" => Tok::Prim(PT::Prim(Prim::storage)), + "code" => Tok::Prim(PT::Prim(Prim::code)), + "int" => Tok::Prim(PT::Prim(Prim::int)), + "nat" => Tok::Prim(PT::Prim(Prim::nat)), + "bool" => Tok::Prim(PT::Prim(Prim::bool)), + "mutez" => Tok::Prim(PT::Prim(Prim::mutez)), + "string" => Tok::Prim(PT::Prim(Prim::string)), + "unit" => Tok::Prim(PT::Prim(Prim::unit)), + "operation" => Tok::Prim(PT::Prim(Prim::operation)), + "pair" => Tok::Prim(PT::Prim(Prim::pair)), + "option" => Tok::Prim(PT::Prim(Prim::option)), + "list" => Tok::Prim(PT::Prim(Prim::list)), + "map" => Tok::Prim(PT::Prim(Prim::map)), + "True" => Tok::Prim(PT::Prim(Prim::True)), + "False" => Tok::Prim(PT::Prim(Prim::False)), + "Unit" => Tok::Prim(PT::Prim(Prim::Unit)), + "None" => Tok::Prim(PT::Prim(Prim::None)), + "Pair" => Tok::Prim(PT::Prim(Prim::Pair)), + "Some" => Tok::Prim(PT::Prim(Prim::Some)), + "Elt" => Tok::Prim(PT::Prim(Prim::Elt)), + "PUSH" => Tok::Prim(PT::Prim(Prim::PUSH)), + "INT" => Tok::Prim(PT::Prim(Prim::INT)), + "GT" => Tok::Prim(PT::Prim(Prim::GT)), + "LOOP" => Tok::Prim(PT::Prim(Prim::LOOP)), + "DIP" => Tok::Prim(PT::Prim(Prim::DIP)), + "ADD" => Tok::Prim(PT::Prim(Prim::ADD)), + "DROP" => Tok::Prim(PT::Prim(Prim::DROP)), + "SWAP" => Tok::Prim(PT::Prim(Prim::SWAP)), + "IF" => Tok::Prim(PT::Prim(Prim::IF)), + "DUP" => Tok::Prim(PT::Prim(Prim::DUP)), + "FAILWITH" => Tok::Prim(PT::Prim(Prim::FAILWITH)), + "UNIT" => Tok::Prim(PT::Prim(Prim::UNIT)), + "CAR" => Tok::Prim(PT::Prim(Prim::CAR)), + "CDR" => Tok::Prim(PT::Prim(Prim::CDR)), + "PAIR" => Tok::Prim(PT::Prim(Prim::PAIR)), + "IF_NONE" => Tok::Prim(PT::Prim(Prim::IF_NONE)), + "SOME" => Tok::Prim(PT::Prim(Prim::SOME)), + "COMPARE" => Tok::Prim(PT::Prim(Prim::COMPARE)), + "AMOUNT" => Tok::Prim(PT::Prim(Prim::AMOUNT)), + "NIL" => Tok::Prim(PT::Prim(Prim::NIL)), + "GET" => Tok::Prim(PT::Prim(Prim::GET)), + "UPDATE" => Tok::Prim(PT::Prim(Prim::UPDATE)), "(" => Tok::LParen, ")" => Tok::RParen, "{" => Tok::LBrace, @@ -159,7 +169,7 @@ atomic_instruction: ParsedInstruction = { "PAIR" => Pair, "SOME" => ISome, "COMPARE" => Compare, - "AMOUNT" => Amount, + "AMOUNT" => Instruction::Amount, "GET" => Get(()), "UPDATE" => Update(()), "DROP" => Drop(None), @@ -226,3 +236,34 @@ pub ContractScript: ContractScript = { ContractScriptEntitySeq =>? <>.try_into().map_err(Into::into), "{" "}" => <>, } + +// For parsing TZT Tests + +use TztEntity::*; +use TztOutput::*; + +tztStackElt : (Type, Value) = { + "stack_elt" => (t, v) +} + +tztStackEltSeq = semicolonSepSeq; + +tztStack : Vec<(Type, Value)> = { + "{" "}" => s +} + +mutezAmount : i64 = + =>? i64::try_from(n) + .map_err(|_| ParserError::LexerError(LexerError::NumericLiteral(n.to_string())).into() ); + +tztEntity : TztEntity = { + "code" => Code(ib), + "input" => Input(s), + "output" => Output(Success(s)), + "output" "(" "failed" ")" => Output(Fail(v)), + "output" "(" "mutezOverflow" ")" => Output(MutezOverflow(a1, a2)), + "output" "(" "generalOverflow" ")" => Output(GeneralOverflow(a1, a2)), + "amount" => TztEntity::Amount(m) +} + +pub tztTestEntities : Vec = semicolonSepSeq; -- GitLab From 3070b9330c30710340cd7fb1d41ea3295e89b439 Mon Sep 17 00:00:00 2001 From: "Sandeep.C.R" Date: Sat, 21 Oct 2023 10:01:14 +0530 Subject: [PATCH 4/7] MIR: Add a function to untype value --- contrib/mir/src/ast.rs | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/contrib/mir/src/ast.rs b/contrib/mir/src/ast.rs index 0a4c14f30109..8f6beeca5592 100644 --- a/contrib/mir/src/ast.rs +++ b/contrib/mir/src/ast.rs @@ -163,6 +163,52 @@ pub enum TypedValue { Map(BTreeMap), } +pub fn typed_value_to_value_optimized(tv: TypedValue) -> Value { + use TypedValue as TV; + use Value as V; + match tv { + TV::Int(i) => V::Number(i), + TV::Nat(u) => V::Number(u.try_into().unwrap()), + TV::Mutez(u) => V::Number(u.try_into().unwrap()), + TV::Bool(b) => V::Boolean(b), + TV::String(s) => V::String(s), + TV::Unit => V::Unit, + // This transformation for pairs deviates from the optimized representation of the + // reference implementation, because reference implementation optimizes the size of combs + // and uses an untyped representation that is the shortest. + TV::Pair(b) => V::new_pair( + typed_value_to_value_optimized(b.0), + typed_value_to_value_optimized(b.1), + ), + TV::List(l) => V::Seq(l.into_iter().map(typed_value_to_value_optimized).collect()), + TV::Map(m) => V::Seq( + m.into_iter() + .map(|(key, val)| { + V::new_elt( + typed_value_to_value_optimized(key), + typed_value_to_value_optimized(val), + ) + }) + .collect(), + ), + TV::Option(None) => V::Option(None), + TV::Option(Some(r)) => V::new_option(Some(typed_value_to_value_optimized(*r))), + } +} + +// Note that there are more than one way to do this conversion. Here we use the optimized untyped +// representation as the target, since that is what the typed to untyped conversion during a +// FAILWITH call does in the reference implementation, and this logic is primarly used in the +// corresponding section of MIR now. +// +// TODO: This implementation will be moved to interpreter in the context of issue, +// https://gitlab.com/tezos/tezos/-/issues/6504 +impl From for Value { + fn from(tv: TypedValue) -> Self { + typed_value_to_value_optimized(tv) + } +} + impl TypedValue { pub fn new_pair(l: Self, r: Self) -> Self { Self::Pair(Box::new((l, r))) -- GitLab From e6291f981bdce6327a0f283c80bccbc5e3429eaa Mon Sep 17 00:00:00 2001 From: "Sandeep.C.R" Date: Sat, 21 Oct 2023 10:03:28 +0530 Subject: [PATCH 5/7] MIR: Add a function to run the parsed Tzt test --- contrib/mir/src/parser.rs | 2 +- contrib/mir/src/typechecker.rs | 2 +- contrib/mir/src/tzt.rs | 118 +++++++++++++++++++++++++++++ contrib/mir/src/tzt/context.rs | 16 ++++ contrib/mir/src/tzt/expectation.rs | 105 +++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 contrib/mir/src/tzt/context.rs create mode 100644 contrib/mir/src/tzt/expectation.rs diff --git a/contrib/mir/src/parser.rs b/contrib/mir/src/parser.rs index 87ac47e14863..4f6464efcda7 100644 --- a/contrib/mir/src/parser.rs +++ b/contrib/mir/src/parser.rs @@ -74,7 +74,7 @@ impl TryFrom> for ContractScript { } } -fn spanned_lexer( +pub fn spanned_lexer( src: &'_ str, ) -> impl Iterator> + '_ { Tok::lexer(src) diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index 87f8c6b48f25..606f3e36adef 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -440,7 +440,7 @@ impl Value { } } -fn typecheck_value(ctx: &mut Ctx, t: &Type, v: Value) -> Result { +pub fn typecheck_value(ctx: &mut Ctx, t: &Type, v: Value) -> Result { use Type::*; use TypedValue as TV; use Value as V; diff --git a/contrib/mir/src/tzt.rs b/contrib/mir/src/tzt.rs index 358d2dd162c3..de3f017a08bd 100644 --- a/contrib/mir/src/tzt.rs +++ b/contrib/mir/src/tzt.rs @@ -5,10 +5,18 @@ /* */ /******************************************************************************/ +mod context; +mod expectation; + use crate::ast::*; +use crate::context::*; use crate::interpreter::*; +use crate::parser::spanned_lexer; use crate::stack::*; +use crate::syntax::tztTestEntitiesParser; use crate::typechecker::*; +use crate::tzt::context::*; +use crate::tzt::expectation::*; pub type TestStack = Vec<(Type, TypedValue)>; @@ -33,6 +41,83 @@ pub struct TztTest { pub amount: Option, } +fn typecheck_stack(stk: Vec<(Type, Value)>) -> Result, TcError> { + stk.into_iter() + .map(|(t, v)| { + let tc_val = typecheck_value(&mut Default::default(), &t, v)?; + Ok((t, tc_val)) + }) + .collect() +} + +#[allow(dead_code)] +pub fn parse_tzt_test(src: &str) -> Result> { + tztTestEntitiesParser::new() + .parse(spanned_lexer(src))? + .try_into() +} + +// Check if the option argument value is none, and raise an error if it is not. +// If it is none, then fill it with the provided value. +fn set_tzt_field(field_name: &str, t: &mut Option, v: T) -> Result<(), String> { + match t { + Some(_) => Err(format!("Duplicate field '{}' in test", field_name)), + None => { + *t = Some(v); + Ok(()) + } + } +} + +use std::error::Error; +impl TryFrom> for TztTest { + type Error = Box; + fn try_from(tzt: Vec) -> Result { + use ErrorExpectation::*; + use TestExpectation::*; + use TztEntity::*; + use TztOutput::*; + let mut m_code: Option = None; + let mut m_input: Option = None; + let mut m_output: Option = None; + let mut m_amount: Option = None; + + for e in tzt { + match e { + Code(ib) => set_tzt_field("code", &mut m_code, ib)?, + Input(stk) => set_tzt_field("input", &mut m_input, typecheck_stack(stk)?)?, + Output(tzt_output) => set_tzt_field( + "output", + &mut m_output, + match tzt_output { + Success(stk) => { + typecheck_stack(stk.clone())?; + ExpectSuccess(stk) + } + Fail(v) => ExpectError(InterpreterError( + InterpreterErrorExpectation::FailedWith(v), + )), + TztOutput::MutezOverflow(v1, v2) => ExpectError(InterpreterError( + InterpreterErrorExpectation::MutezOverflow(v1, v2), + )), + TztOutput::GeneralOverflow(_, _) => { + todo!("General overflow is not implemented!") + } + }, + )?, + Amount(m) => set_tzt_field("amount", &mut m_amount, m)?, + } + } + + Ok(TztTest { + code: m_code.ok_or("code section not found in test")?, + input: m_input.ok_or("input section not found in test")?, + output: m_output.ok_or("output section not found in test")?, + amount: m_amount, + }) + } +} + /// This represents possibilities in which the execution of /// the code in a test can fail. #[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] @@ -82,3 +167,36 @@ pub enum TztOutput { MutezOverflow(i64, i64), GeneralOverflow(i128, i128), } + +fn execute_tzt_test_code( + code: ParsedInstructionBlock, + ctx: &mut Ctx, + input: Vec<(Type, TypedValue)>, +) -> Result<(FailingTypeStack, IStack), TestError> { + // Build initial stacks (type and value) for running the test from the test input + // stack. + let (typs, vals): (Vec, Vec) = input.into_iter().unzip(); + + let mut t_stack: FailingTypeStack = FailingTypeStack::Ok(TopIsFirst::from(typs).0); + + // Run the code and save the status of the + // final result as a Result<(), TestError>. + // + // This value along with the test expectation + // from the test file will be used to decide if + // the test was a success or a fail. + let typechecked_code = typecheck(code, ctx, &mut t_stack)?; + let mut i_stack: IStack = TopIsFirst::from(vals).0; + interpret(&typechecked_code, ctx, &mut i_stack)?; + Ok((t_stack, i_stack)) +} + +#[allow(dead_code)] +pub fn run_tzt_test(test: TztTest) -> Result<(), TztTestError> { + // Here we compare the outcome of the interpreting with the + // expectation from the test, and declare the result of the test + // accordingly. + let mut ctx = construct_context(&test); + let execution_result = execute_tzt_test_code(test.code, &mut ctx, test.input); + check_expectation(&mut ctx, test.output, execution_result) +} diff --git a/contrib/mir/src/tzt/context.rs b/contrib/mir/src/tzt/context.rs new file mode 100644 index 000000000000..515925c7e78a --- /dev/null +++ b/contrib/mir/src/tzt/context.rs @@ -0,0 +1,16 @@ +/******************************************************************************/ +/* */ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) [2023] Serokell */ +/* */ +/******************************************************************************/ + +use crate::context::*; +use crate::tzt::*; + +pub fn construct_context(test: &TztTest) -> Ctx { + Ctx { + gas: crate::gas::Gas::default(), + amount: test.amount.unwrap_or_default(), + } +} diff --git a/contrib/mir/src/tzt/expectation.rs b/contrib/mir/src/tzt/expectation.rs new file mode 100644 index 000000000000..c3af4aabc6a6 --- /dev/null +++ b/contrib/mir/src/tzt/expectation.rs @@ -0,0 +1,105 @@ +/******************************************************************************/ +/* */ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) [2023] Serokell */ +/* */ +/******************************************************************************/ + +use super::*; + +fn check_error_expectation( + ctx: &mut Ctx, + err_exp: ErrorExpectation, + err: TestError, +) -> Result<(), TztTestError> { + use ErrorExpectation as Ex; + use TestError as Er; + use TztTestError::*; + match (err_exp, err) { + (Ex::TypecheckerError(tc_error), Er::TypecheckerError(res_tc_error)) + if tc_error == res_tc_error => + { + Ok(()) + } + (Ex::InterpreterError(i_error), Er::InterpreterError(res_i_error)) + if unify_interpreter_error(ctx, &i_error, &res_i_error) => + { + Ok(()) + } + (err_exp, err) => Err(ExpectedDifferentError(err_exp, err)), + } +} + +fn unify_interpreter_error( + ctx: &mut Ctx, + exp: &InterpreterErrorExpectation, + err: &InterpretError, +) -> bool { + use InterpreterErrorExpectation::*; + match (exp, err) { + (FailedWith(value), InterpretError::FailedWith(typ, failed_typed_value)) => { + // Here we typecheck the untyped value from the expectation using the + // typed of the failed value we get from the interpreter. + match typecheck_value(ctx, &typ, value.clone()) { + Ok(exp_typed_val) => { + // Then both `Typedvalue`s are untyped and compared to get the result. Here + // untyping is done before comparing so that we are not using the Eq trait of + // TypedValue. It is thought to be a bit unsafe to use it generally outside the + // context of the interpreter, though here we have full type information for + // both values being compared, so it is probably safe to compare typed + // representation as well. + Value::from(exp_typed_val) == Value::from(failed_typed_value.clone()) + } + Err(_) => false, + } + } + (MutezOverflow(_, _), InterpretError::MutezOverflow) => true, + (GeneralOverflow(_, _), _) => todo!("General overflow is unsupported on interpreter"), + (_, _) => false, //Some error that we didn't expect happened. + } +} + +pub fn check_expectation( + ctx: &mut Ctx, + expected: TestExpectation, + real: Result<(FailingTypeStack, IStack), TestError>, +) -> Result<(), TztTestError> { + use TestExpectation::*; + use TztTestError::*; + match (expected, real) { + (ExpectSuccess(stk_exp), Ok((res_type_stack, i_stack))) => { + let (exp_stk_types, exp_stk_values): (Vec, Vec) = + stk_exp.into_iter().unzip(); + + let expected_type_stack = FailingTypeStack::Ok(TopIsFirst::from_iter(exp_stk_types).0); + let expected_stack = TopIsFirst::from_iter(exp_stk_values).0; + let result_stack: Stack = TopIsFirst::from_iter( + i_stack + .iter() + .map(|x| typed_value_to_value_optimized(x.clone())), + ) + .0; + // If the run was success, and the expectation is also of success check the expected + // stack. Stack types and values should match. + if res_type_stack == expected_type_stack && result_stack == expected_stack { + Ok(()) + } else { + Err(StackMismatch( + (expected_type_stack, expected_stack), + (res_type_stack, result_stack), + )) + } + } + (ExpectSuccess(_), Err(e)) => { + // If the run was failed, but the expectation expected + // a success, fail the test with appropriate error.. + Err(UnexpectedError(e)) + } + (ExpectError(_), Ok((_, i_stack))) => { + // If the run was success, but the expectation expected + // a failure, fail the test. + Err(UnexpectedSuccess(i_stack)) + } + (ExpectError(err_exp), Err(t_error)) => check_error_expectation(ctx, err_exp, t_error), + } +} -- GitLab From 9f051688241f96c24b17b19f03f48a9458b3f4f9 Mon Sep 17 00:00:00 2001 From: "Sandeep.C.R" Date: Fri, 6 Oct 2023 17:39:38 +0530 Subject: [PATCH 6/7] MIR: Add a binary target to export Tzt runner as CLI app --- contrib/mir/Cargo.toml | 4 ++++ contrib/mir/tzt_runner/main.rs | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 contrib/mir/tzt_runner/main.rs diff --git a/contrib/mir/Cargo.toml b/contrib/mir/Cargo.toml index b7424a23a395..8e779c7d0404 100644 --- a/contrib/mir/Cargo.toml +++ b/contrib/mir/Cargo.toml @@ -11,3 +11,7 @@ lalrpop-util = "0.20.0" checked = "0.5" thiserror = "1.0" logos = "0.13" + +[[bin]] +name = "tzt_runner" +path = "tzt_runner/main.rs" diff --git a/contrib/mir/tzt_runner/main.rs b/contrib/mir/tzt_runner/main.rs new file mode 100644 index 000000000000..ddc7fe5576bd --- /dev/null +++ b/contrib/mir/tzt_runner/main.rs @@ -0,0 +1,40 @@ +/******************************************************************************/ +/* */ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) [2023] Serokell */ +/* */ +/******************************************************************************/ + +use std::env; +use std::fs::read_to_string; + +use mir::tzt::*; + +fn run_test(file: &str) -> Result<(), String> { + let contents = read_to_string(file).map_err(|e| e.to_string())?; + let tzt_test = parse_tzt_test(&contents).map_err(|e| e.to_string())?; + + run_tzt_test(tzt_test).map_err(|e| format!("{:?}", e)) +} + +fn main() { + // Read the cmd line arguments as a list of Strings. + // First one is the name of the file being executed + // and the rest are the actual arguments, so drop the first one. + let test_files = &env::args().collect::>()[1..]; + + // Walk through all the test paths and execute each of them. + // Print the result for each run. + let mut exit_code = 0; + for test in test_files { + print!("Running {} : ", test); + match run_test(test) { + Ok(_) => println!("Ok"), + Err(e) => { + exit_code = 1; + println!("{}", e); + } + } + } + std::process::exit(exit_code) +} -- GitLab From 226862029b22aad1294dd94aa849e60164c26fae Mon Sep 17 00:00:00 2001 From: "Sandeep.C.R" Date: Mon, 2 Oct 2023 19:38:17 +0530 Subject: [PATCH 7/7] MIR: Add tests for the runner --- contrib/mir/tzt_runner/main.rs | 123 +++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/contrib/mir/tzt_runner/main.rs b/contrib/mir/tzt_runner/main.rs index ddc7fe5576bd..9aef0c74ee25 100644 --- a/contrib/mir/tzt_runner/main.rs +++ b/contrib/mir/tzt_runner/main.rs @@ -38,3 +38,126 @@ fn main() { } std::process::exit(exit_code) } + +#[cfg(test)] +mod tztrunner_tests { + use mir::tzt::*; + use TztTestError::*; + + #[test] + fn test_runner_success() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_ADD).unwrap(); + assert!(run_tzt_test(tzt_test).is_ok()); + } + + #[test] + fn test_runner_mismatch_stack() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_ADD_MISMATCH_STACK).unwrap(); + assert!(matches!(run_tzt_test(tzt_test), Err(StackMismatch(_, _)))); + } + + #[test] + fn test_runner_mismatch_stack_2() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_ADD_MISMATCH_STACK_2).unwrap(); + assert!(matches!(run_tzt_test(tzt_test), Err(StackMismatch(_, _)))); + } + + #[test] + fn test_runner_push() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_PUSH).unwrap(); + assert!(matches!(run_tzt_test(tzt_test), Ok(()))); + } + + #[test] + fn test_runner_amount() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_AMOUNT).unwrap(); + assert!(matches!(run_tzt_test(tzt_test), Ok(()))); + } + + #[should_panic(expected = "Duplicate field 'input' in test")] + #[test] + fn test_duplicate_field() { + let _ = parse_tzt_test(TZT_SAMPLE_DUPLICATE_FIELD).unwrap(); + } + + #[should_panic(expected = "Duplicate field 'output' in test")] + #[test] + fn test_duplicate_field_output() { + let _ = parse_tzt_test(TZT_SAMPLE_DUPLICATE_FIELD_OUTPUT).unwrap(); + } + + #[test] + fn test_runner_interpreter_error() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_MUTEZ_OVERFLOW).unwrap(); + let result = run_tzt_test(tzt_test); + assert!(result.is_ok()); + } + + #[test] + fn test_runner_interpreter_unexpected_fail() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_EXP_SUCC_BUT_FAIL).unwrap(); + assert!(matches!(run_tzt_test(tzt_test), Err(UnexpectedError(_)))); + } + + #[test] + fn test_runner_interpreter_unexpected_success() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_EXP_FAIL_BUT_SUCCEED).unwrap(); + assert!(matches!(run_tzt_test(tzt_test), Err(UnexpectedSuccess(_)))); + } + + #[test] + fn test_runner_interpreter_unexpected_fail_val() { + let tzt_test = parse_tzt_test(TZT_SAMPLE_FAIL_WITH_UNEXPECTED).unwrap(); + assert!(matches!( + run_tzt_test(tzt_test), + Err(ExpectedDifferentError(_, _)) + )); + } + + const TZT_SAMPLE_ADD: &str = "code { ADD } ; + input { Stack_elt int 5 ; Stack_elt int 5 } ; + output { Stack_elt int 10 }"; + + const TZT_SAMPLE_ADD_MISMATCH_STACK: &str = "code { ADD } ; + input { Stack_elt int 5 ; Stack_elt int 5 } ; + output { Stack_elt int 11 }"; + + const TZT_SAMPLE_ADD_MISMATCH_STACK_2: &str = "code {} ; + input { Stack_elt (list int) {} } ; + output { Stack_elt (list nat) {} }"; + + const TZT_SAMPLE_PUSH: &str = "code { PUSH nat 5; PUSH int 10 } ; + input {} ; + output { Stack_elt int 10; Stack_elt nat 5 }"; + + const TZT_SAMPLE_AMOUNT: &str = "code { AMOUNT } ; + input {} ; + amount 10 ; + output { Stack_elt mutez 10;}"; + + const TZT_SAMPLE_DUPLICATE_FIELD: &str = "code { ADD } ; + input { Stack_elt int 5 ; Stack_elt int 5 } ; + input { Stack_elt int 5 ; Stack_elt int 5 } ; + output { Stack_elt int 10 }"; + + const TZT_SAMPLE_DUPLICATE_FIELD_OUTPUT: &str = "code { ADD } ; + input { Stack_elt int 5 ; Stack_elt int 5 } ; + output { Stack_elt int 10 } ; + output { Stack_elt int 10 }"; + + const TZT_SAMPLE_MUTEZ_OVERFLOW: &str = r#"code { ADD } ; + input { Stack_elt mutez 9223372036854775807 ; Stack_elt mutez 1 } ; + output (MutezOverflow 9223372036854775807 1)"#; + + const TZT_SAMPLE_EXP_SUCC_BUT_FAIL: &str = r#"code { ADD } ; + input { Stack_elt mutez 9223372036854775807 ; Stack_elt mutez 1 } ; + output { Stack_elt mutez 10 }"#; + + const TZT_SAMPLE_EXP_FAIL_BUT_SUCCEED: &str = r#"code { ADD } ; + input { Stack_elt mutez 10 ; Stack_elt mutez 1 } ; + output (MutezOverflow 9223372036854775807 1)"#; + + const TZT_SAMPLE_FAIL_WITH_UNEXPECTED: &str = r#"code { FAILWITH } ; + input { Stack_elt int 10 ; } ; + output (Failed 11)"#; +} -- GitLab