Basic implementation of if-else pattern

This commit is contained in:
Ivan Boldyrev 2024-05-20 23:47:58 +02:00 committed by Ivan Boldyrev
parent 04bacb7039
commit 21f5985aa0
8 changed files with 588 additions and 6 deletions

1
Cargo.lock generated
View File

@ -59,6 +59,7 @@ dependencies = [
name = "air-beautifier"
version = "0.4.3"
dependencies = [
"air-lambda-parser",
"aquavm-air-parser",
"itertools",
"thiserror",

View File

@ -14,6 +14,7 @@ name = "air_beautifier"
path = "src/lib.rs"
[dependencies]
air-lambda-parser = { version = "0.1.0", path = "../air-lib/lambda/parser" }
aquavm-air-parser = { version = "0.12.0", path = "../air-lib/air-parser" }
itertools = "0.10.5"
thiserror = "1.0.50"

View File

@ -14,7 +14,8 @@
* limitations under the License.
*/
use super::r#virtual::try_hopon;
use crate::r#virtual::try_hopon;
use crate::r#virtual::try_if_else;
use air_parser::ast;
@ -85,6 +86,7 @@ pub struct Beautifier<W: io::Write> {
output: W,
indent_step: usize,
try_hopon: bool,
try_if_else: bool,
}
impl<W: io::Write> Beautifier<W> {
@ -95,6 +97,7 @@ impl<W: io::Write> Beautifier<W> {
output,
indent_step: DEFAULT_INDENT_STEP,
try_hopon: false,
try_if_else: false,
}
}
@ -105,13 +108,14 @@ impl<W: io::Write> Beautifier<W> {
output,
indent_step,
try_hopon: false,
try_if_else: false,
}
}
#[inline]
/// Enable all patterns in the emited code.
pub fn enable_all_patterns(self) -> Self {
self.enable_try_hopon()
self.enable_try_hopon().enable_try_if_else()
}
#[inline]
@ -120,6 +124,12 @@ impl<W: io::Write> Beautifier<W> {
self
}
#[inline]
pub fn enable_try_if_else(mut self) -> Self {
self.try_if_else = true;
self
}
/// Unwrap the Beautifier into the underlying writer.
pub fn into_inner(self) -> W {
self.output
@ -277,6 +287,18 @@ impl<W: io::Write> Beautifier<W> {
return self.beautify_simple(&hop_on, indent);
}
}
if self.try_if_else {
if let Some(if_else) = try_if_else(new) {
multiline!(
self, indent;
"if {}:", if_else.condition;
&if_else.then_body;
"else:";
&if_else.else_body
);
return Ok(());
}
}
// else
compound!(self, indent, new);
Ok(())

View File

@ -24,6 +24,15 @@
unused_unsafe,
unreachable_patterns
)]
// needed for matching AST trees.
// https://github.com/rust-lang/rust/issues/29641
//
// n.b. box_patterns are intended to be eventually replaced by deref_patterns:
// https://github.com/rust-lang/rust/issues/29641
//
// one can get rid of it by splitting the match into parts;
// or use https://crates.io/crates/match_deref
#![feature(box_patterns)]
mod beautifier;
mod r#virtual;

View File

@ -0,0 +1,308 @@
/*
* Copyright 2024 Fluence DAO
*
* 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.
*/
use crate::Beautifier;
#[test]
fn if_else_nested() {
let script = r#"
(new my-var (new -if-else-error-
(new -else-error-
(new -if-error-
(xor
(match "a" "test"
(ap 0 $result)
)
(seq
(ap :error: -if-error-)
(xor
(match :error:.$.error_code 10001
(ap 1 $result)
)
(seq
(seq
(ap :error: -else-error-)
(xor
(match :error:.$.error_code 10001
(ap -if-error- -if-else-error-)
)
(ap -else-error- -if-else-error-)
)
)
(fail -if-else-error-)
)
)
)
)
)
)
) )
"#;
let mut output = vec![];
let mut beautifier = Beautifier::new(&mut output).enable_all_patterns();
beautifier.beautify(script).unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
concat!(
"new my-var:\n",
r#" if "a" == "test":"#,
"\n",
" ap 0 $result\n",
" else:\n",
" ap 1 $result\n",
),
);
}
#[test]
fn if_else_match_on() {
let script = r#"
(new -if-else-error-
(new -else-error-
(new -if-error-
(xor
(match "a" "test"
(ap 0 $result)
)
(seq
(ap :error: -if-error-)
(xor
(match :error:.$.error_code 10001
(ap 1 $result)
)
(seq
(seq
(ap :error: -else-error-)
(xor
(match :error:.$.error_code 10001
(ap -if-error- -if-else-error-)
)
(ap -else-error- -if-else-error-)
)
)
(fail -if-else-error-)
)
)
)
)
)
)
)
"#;
let mut output = vec![];
let mut beautifier = Beautifier::new(&mut output).enable_all_patterns();
beautifier.beautify(script).unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
concat!(
r#"if "a" == "test":"#,
"\n",
" ap 0 $result\n",
"else:\n",
" ap 1 $result\n",
),
);
}
#[test]
fn if_else_match_off() {
let script = r#"
(new -if-else-error-
(new -else-error-
(new -if-error-
(xor
(match "a" "test"
(ap 0 $result)
)
(seq
(ap :error: -if-error-)
(xor
(match :error:.$.error_code 10001
(ap 1 $result)
)
(seq
(seq
(ap :error: -else-error-)
(xor
(match :error:.$.error_code 10001
(ap -if-error- -if-else-error-)
)
(ap -else-error- -if-else-error-)
)
)
(fail -if-else-error-)
)
)
)
)
)
)
)
"#;
let mut output = vec![];
let mut beautifier = Beautifier::new(&mut output);
beautifier.beautify(script).unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
concat!(
"new -if-else-error-:\n",
" new -else-error-:\n",
" new -if-error-:\n",
" try:\n",
r#" match "a" "test":"#,
"\n",
" ap 0 $result\n",
" catch:\n",
" ap :error: -if-error-\n",
" try:\n",
" match :error:.$.error_code 10001:\n",
" ap 1 $result\n",
" catch:\n",
" ap :error: -else-error-\n",
" try:\n",
" match :error:.$.error_code 10001:\n",
" ap -if-error- -if-else-error-\n",
" catch:\n",
" ap -else-error- -if-else-error-\n",
" fail -if-else-error-\n",
),
);
}
#[test]
fn if_else_mismatch_on() {
let script = r#"
(new -if-else-error-
(new -else-error-
(new -if-error-
(xor
(mismatch "a" "test"
(ap 0 $result)
)
(seq
(ap :error: -if-error-)
(xor
(match :error:.$.error_code 10001
(ap 1 $result)
)
(seq
(seq
(ap :error: -else-error-)
(xor
(match :error:.$.error_code 10001
(ap -if-error- -if-else-error-)
)
(ap -else-error- -if-else-error-)
)
)
(fail -if-else-error-)
)
)
)
)
)
)
)
"#;
let mut output = vec![];
let mut beautifier = Beautifier::new(&mut output).enable_all_patterns();
beautifier.beautify(script).unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
concat!(
r#"if "a" != "test":"#,
"\n",
" ap 0 $result\n",
"else:\n",
" ap 1 $result\n",
),
);
}
#[test]
fn if_else_mismatch_off() {
let script = r#"
(new -if-else-error-
(new -else-error-
(new -if-error-
(xor
(mismatch "a" "test"
(ap 0 $result)
)
(seq
(ap :error: -if-error-)
(xor
(match :error:.$.error_code 10001
(ap 1 $result)
)
(seq
(seq
(ap :error: -else-error-)
(xor
(match :error:.$.error_code 10001
(ap -if-error- -if-else-error-)
)
(ap -else-error- -if-else-error-)
)
)
(fail -if-else-error-)
)
)
)
)
)
)
)
"#;
let mut output = vec![];
let mut beautifier = Beautifier::new(&mut output);
beautifier.beautify(script).unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
concat!(
"new -if-else-error-:\n",
" new -else-error-:\n",
" new -if-error-:\n",
" try:\n",
r#" mismatch "a" "test":"#,
"\n",
" ap 0 $result\n",
" catch:\n",
" ap :error: -if-error-\n",
" try:\n",
" match :error:.$.error_code 10001:\n",
" ap 1 $result\n",
" catch:\n",
" ap :error: -else-error-\n",
" try:\n",
" match :error:.$.error_code 10001:\n",
" ap -if-error- -if-else-error-\n",
" catch:\n",
" ap -else-error- -if-else-error-\n",
" fail -if-else-error-\n",
),
);
}

View File

@ -25,6 +25,7 @@
)]
mod beautifier;
mod if_else;
use crate::{beautify, beautify_to_string, BeautifyError};

View File

@ -14,17 +14,18 @@
* limitations under the License.
*/
use air_parser::ast;
mod if_else;
use core::fmt;
use std::fmt::Display;
use air_parser::ast;
pub(crate) use if_else::try_if_else;
use std::fmt;
/// A virtual `hopon` instruction.
pub(crate) struct HopOn<'i> {
pub peer_id: ast::ResolvableToPeerIdVariable<'i>,
}
impl Display for HopOn<'_> {
impl fmt::Display for HopOn<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "hopon {}", self.peer_id)
}

View File

@ -0,0 +1,239 @@
/*
* Copyright 2024 Fluence DAO
*
* 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.
*/
use std::fmt;
use air_parser::ast;
/// A virtual 'if-else' instruction.
pub(crate) struct IfElse<'i> {
pub(crate) condition: Condition<'i>,
pub(crate) then_body: &'i ast::Instruction<'i>,
pub(crate) else_body: &'i ast::Instruction<'i>,
}
#[derive(Debug)]
pub(crate) enum Condition<'i> {
Match {
left_value: &'i ast::ImmutableValue<'i>,
right_value: &'i ast::ImmutableValue<'i>,
},
Mismatch {
left_value: &'i ast::ImmutableValue<'i>,
right_value: &'i ast::ImmutableValue<'i>,
},
}
impl fmt::Display for Condition<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Condition::Match {
left_value,
right_value,
} => write!(f, "{left_value} == {right_value}"),
Condition::Mismatch {
left_value,
right_value,
} => write!(f, "{left_value} != {right_value}"),
}
}
}
/// Try to parse an instruction and its nested elements as a virtual `if else` instruction.
///
/// For example:
/// ```clojure
/// (todo)
/// ```
/// is parsed as a virtual instruction
/// ```python
/// if expr1 == expr2:
/// st1
/// else:
/// st2
/// ```
pub(crate) fn try_if_else<'i>(root_new: &'i ast::New<'i>) -> Option<IfElse<'i>> {
use ast::Instruction::New;
let root_scalar = if let ast::NewArgument::Scalar(root_scalar) = &root_new.argument {
root_scalar
} else {
return None;
};
let nested1_new = if let New(netsted1_new) = &root_new.instruction {
netsted1_new
} else {
return None;
};
let nested1_scalar = if let ast::NewArgument::Scalar(nested1_scalar) = &nested1_new.argument {
nested1_scalar
} else {
return None;
};
let nested2_new = if let New(netsted2_new) = &nested1_new.instruction {
netsted2_new
} else {
return None;
};
let nested2_scalar = if let ast::NewArgument::Scalar(nested2_scalar) = &nested2_new.argument {
nested2_scalar
} else {
return None;
};
if let ast::Instruction::Xor(box ast::Xor(
expected_match_or_mismatch,
ast::Instruction::Seq(error_handling),
)) = &nested2_new.instruction
{
let (condition, then_body) = match expected_match_or_mismatch {
ast::Instruction::Match(match_) => {
let condition = Condition::Match {
left_value: &match_.left_value,
right_value: &match_.right_value,
};
let then_branch = &match_.instruction;
(condition, then_branch)
}
ast::Instruction::MisMatch(mismatch) => {
let condition = Condition::Mismatch {
left_value: &mismatch.left_value,
right_value: &mismatch.right_value,
};
let then_branch = &mismatch.instruction;
(condition, then_branch)
}
_ => return None,
};
if let Some(else_body) =
validate_error_handling(error_handling, root_scalar, nested1_scalar, nested2_scalar)
{
// todo!("check free variables");
Some(IfElse {
condition,
then_body,
else_body,
})
} else {
None
}
} else {
None
}
}
fn validate_error_handling<'i>(
root: &'i ast::Seq<'i>,
if_else_error: &ast::Scalar<'i>,
else_error: &ast::Scalar<'i>,
if_error: &ast::Scalar<'i>,
) -> Option<&'i ast::Instruction<'i>> {
if validate_error_handling_ap(&root.0, if_error) {
validate_error_handling_xor(&root.1, if_else_error, else_error, if_error)
} else {
None
}
}
fn validate_error_handling_xor<'i>(
instruction: &'i ast::Instruction<'i>,
if_else_error: &ast::Scalar<'i>,
else_error: &ast::Scalar<'i>,
if_error: &ast::Scalar<'i>,
) -> Option<&'i ast::Instruction<'i>> {
if let ast::Instruction::Xor(xor) = instruction {
if !validate_error_handling_xor_second(&xor.1, if_else_error, else_error, if_error) {
return None;
}
if let ast::Instruction::Match(match_) = &xor.0 {
// check arguments: match :error:.$.error_code 10001
let lambda =
air_lambda_parser::parse(".$.error_code").expect("invalid internal lambda");
let expected_left =
ast::ImmutableValue::Error(ast::InstructionErrorAST::new(Some(lambda)));
let expected_right = ast::ImmutableValue::Number(ast::Number::Int(10001));
if match_.left_value == expected_left && match_.right_value == expected_right {
return Some(&match_.instruction);
}
}
}
None
}
fn validate_error_handling_ap(
instruction: &ast::Instruction<'_>,
if_error: &ast::Scalar<'_>,
) -> bool {
match instruction {
ast::Instruction::Ap(ap) => {
let expected_argument = ast::ApArgument::Error(ast::InstructionErrorAST { lens: None });
let result_matches = match &ap.result {
ast::ApResult::Scalar(scalar) => scalar.name == if_error.name,
ast::ApResult::Stream(_) => false,
};
result_matches && ap.argument == expected_argument
}
_ => false,
}
}
fn validate_error_handling_xor_second(
else_instruction: &ast::Instruction<'_>,
if_else_error: &ast::Scalar<'_>,
else_error: &ast::Scalar<'_>,
if_error: &ast::Scalar<'_>,
) -> bool {
let air_script = format!(
// the new instruction are needed to pass a parser validator; they are removed
r#"(new {if_else_error}
(new {else_error}
(new {if_error}
; real code
(seq
(seq
(ap :error: {else_error})
(xor
(match :error:.$.error_code 10001
(ap {if_error} {if_else_error})
)
(ap {else_error} {if_else_error})
)
)
(fail {if_else_error})
))))"#
);
let expected_tree = air_parser::parse(&air_script).expect("invalid internal AIR");
let expected_tree = pop_new_from_tree(expected_tree, 3);
else_instruction.to_string() == expected_tree.to_string()
}
fn pop_new_from_tree(mut expected_tree: ast::Instruction<'_>, arg: usize) -> ast::Instruction<'_> {
for _ in 0..arg {
match expected_tree {
ast::Instruction::New(new) => {
expected_tree = new.instruction;
}
_ => panic!("expected new, got: {expected_tree}"),
}
}
expected_tree
}