From 04bacb70390e05250e8c4931c8a5381a55580159 Mon Sep 17 00:00:00 2001 From: Ivan Boldyrev Date: Fri, 24 May 2024 16:27:51 +0400 Subject: [PATCH] feat(cli)!: hopon virtual instruction in the beautifier (#840) AIR beautifier may output virtual `hopon` instruction based on specific pattern generated by the Aqua compiler. The `air_beatifier::beatify` function has now an extra argument that determines if to perform the virtual instruction detection, giving more readable output. The `air-beautify-wasm` crate has behavior of the `beautify` function changed: the functions now extracts virtual instructions, so it is a drop-in replacement for previous version with new functionality. New exported function `beautify_raw` is added, that doesn't look for virtual instructions' patterns, formatting the code as is. --- crates/beautifier/src/beautifier.rs | 25 ++++++ crates/beautifier/src/lib.rs | 10 ++- crates/beautifier/src/tests/beautifier.rs | 103 ++++++++++++++++++++++ crates/beautifier/src/tests/mod.rs | 17 +++- crates/beautifier/src/virtual.rs | 84 ++++++++++++++++++ tools/cli/air/README.md | 2 + tools/cli/air/src/beautify/mod.rs | 10 ++- tools/wasm/air-beautify-wasm/src/lib.rs | 9 +- 8 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 crates/beautifier/src/virtual.rs diff --git a/crates/beautifier/src/beautifier.rs b/crates/beautifier/src/beautifier.rs index c12b608f..d48e5e6a 100644 --- a/crates/beautifier/src/beautifier.rs +++ b/crates/beautifier/src/beautifier.rs @@ -14,6 +14,8 @@ * limitations under the License. */ +use super::r#virtual::try_hopon; + use air_parser::ast; use std::fmt::Display; @@ -82,25 +84,42 @@ pub enum BeautifyError { pub struct Beautifier { output: W, indent_step: usize, + try_hopon: bool, } impl Beautifier { /// Beautifier for the output with default indentation step. + #[inline] pub fn new(output: W) -> Self { Self { output, indent_step: DEFAULT_INDENT_STEP, + try_hopon: false, } } /// Beautifier for the output with custom indentation step. + #[inline] pub fn new_with_indent(output: W, indent_step: usize) -> Self { Self { output, indent_step, + try_hopon: false, } } + #[inline] + /// Enable all patterns in the emited code. + pub fn enable_all_patterns(self) -> Self { + self.enable_try_hopon() + } + + #[inline] + pub fn enable_try_hopon(mut self) -> Self { + self.try_hopon = true; + self + } + /// Unwrap the Beautifier into the underlying writer. pub fn into_inner(self) -> W { self.output @@ -253,6 +272,12 @@ impl Beautifier { } fn beautify_new(&mut self, new: &ast::New<'_>, indent: usize) -> io::Result<()> { + if self.try_hopon { + if let Some(hop_on) = try_hopon(new) { + return self.beautify_simple(&hop_on, indent); + } + } + // else compound!(self, indent, new); Ok(()) } diff --git a/crates/beautifier/src/lib.rs b/crates/beautifier/src/lib.rs index 7bd04b91..2db6b605 100644 --- a/crates/beautifier/src/lib.rs +++ b/crates/beautifier/src/lib.rs @@ -26,14 +26,22 @@ )] mod beautifier; +mod r#virtual; pub use crate::beautifier::{Beautifier, BeautifyError, DEFAULT_INDENT_STEP}; use std::io; /// Beautify the `air_script` with default settings to the `output`. -pub fn beautify(air_script: &str, output: &mut impl io::Write) -> Result<(), BeautifyError> { +pub fn beautify( + air_script: &str, + output: &mut impl io::Write, + enable_patterns: bool, +) -> Result<(), BeautifyError> { let mut beautifier = Beautifier::new(output); + if enable_patterns { + beautifier = beautifier.enable_all_patterns(); + } beautifier.beautify(air_script) } diff --git a/crates/beautifier/src/tests/beautifier.rs b/crates/beautifier/src/tests/beautifier.rs index 920595ca..2b66229f 100644 --- a/crates/beautifier/src/tests/beautifier.rs +++ b/crates/beautifier/src/tests/beautifier.rs @@ -388,3 +388,106 @@ fn fail_error() { let output = beautify_to_string(script).unwrap(); assert_eq!(output, "fail :error:\n"); } + +#[test] +fn hopon_on() { + let script = r#"(new $ephemeral (new #ephemeral (canon "relay" $ephemeral #ephemeral)))"#; + + 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(), "hopon \"relay\"\n"); +} + +#[test] +fn hopon_off() { + let script = r#"(new $ephemeral (new #ephemeral (canon "relay" $ephemeral #ephemeral)))"#; + + let mut output = vec![]; + let mut beautifier = Beautifier::new(&mut output); + beautifier.beautify(script).unwrap(); + + assert_eq!( + String::from_utf8(output).unwrap(), + concat!( + "new $ephemeral:\n", + " new #ephemeral:\n", + " canon \"relay\" $ephemeral #ephemeral\n" + ), + ); +} + +#[test] +fn hopon_canon_mismatch() { + let script = r#"(new $ephemeral (new #can (canon "relay" $ephemeral #ephemeral)))"#; + + 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 $ephemeral:\n", + " new #can:\n", + " canon \"relay\" $ephemeral #ephemeral\n" + ), + ); +} + +#[test] +fn hopon_stream_mismatch() { + let script = r#"(new $stream (new #ephemeral (canon "relay" $ephemeral #ephemeral)))"#; + + 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 $stream:\n", + " new #ephemeral:\n", + " canon \"relay\" $ephemeral #ephemeral\n" + ), + ); +} + +#[test] +fn hopon_nested() { + let script = + r#"(new $other (new $ephemeral (new #ephemeral (canon "relay" $ephemeral #ephemeral))) )"#; + + 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(), + "new $other:\n hopon \"relay\"\n", + ); +} + +// this is bug that should be eventually fixed: it uses top-level #can +// instead of the nested one which disappeared +// +// the compiler doesn't generate such code, but it can be crafted manually +#[test] +fn hopon_shadowing() { + let script = r#"(new #can (new $ephemeral (new #can (canon #can.$.[0] $ephemeral #can))) )"#; + + 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 #can:\n", + " new $ephemeral:\n", + " new #can:\n", + " canon #can.$.[0] $ephemeral #can\n" + ), + ); +} diff --git a/crates/beautifier/src/tests/mod.rs b/crates/beautifier/src/tests/mod.rs index ddf145c2..4b9c1e14 100644 --- a/crates/beautifier/src/tests/mod.rs +++ b/crates/beautifier/src/tests/mod.rs @@ -32,8 +32,17 @@ use crate::{beautify, beautify_to_string, BeautifyError}; fn beautify_valid() { let air_script = "(seq (null) (null))"; let mut buffer = vec![]; - let res = beautify(air_script, &mut buffer); - assert!(matches!(res, Ok(()))); + let res = beautify(air_script, &mut buffer, false); + assert!(res.is_ok()); + assert_eq!(std::str::from_utf8(&buffer).unwrap(), "null\nnull\n"); +} + +#[test] +fn beautify_valid_with_patterns() { + let air_script = "(seq (null) (null))"; + let mut buffer = vec![]; + let res = beautify(air_script, &mut buffer, true); + assert!(res.is_ok()); assert_eq!(std::str::from_utf8(&buffer).unwrap(), "null\nnull\n"); } @@ -41,7 +50,7 @@ fn beautify_valid() { fn beautify_invalid() { let air_script = "(seq (null))"; let mut buffer = vec![]; - let res = beautify(air_script, &mut buffer); + let res = beautify(air_script, &mut buffer, false); assert!(matches!(res, Err(BeautifyError::Parse(_)))); } @@ -56,5 +65,5 @@ fn beautify_to_string_valid() { fn beautify_to_string_invalid() { let air_script = "(seq (null))"; let res = beautify_to_string(air_script); - assert!(matches!(res, Err(_))); + assert!(res.is_err()); } diff --git a/crates/beautifier/src/virtual.rs b/crates/beautifier/src/virtual.rs new file mode 100644 index 00000000..5e32086a --- /dev/null +++ b/crates/beautifier/src/virtual.rs @@ -0,0 +1,84 @@ +/* + * 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 air_parser::ast; + +use core::fmt; +use std::fmt::Display; + +/// A virtual `hopon` instruction. +pub(crate) struct HopOn<'i> { + pub peer_id: ast::ResolvableToPeerIdVariable<'i>, +} + +impl Display for HopOn<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "hopon {}", self.peer_id) + } +} + +/// Try to parse the `new` instruction and its nested elements as a virtual `hopon` instruction. +/// +/// For example: +/// ```clojure +/// (new #uniq1_name +/// (new $uniq2_name +/// (canon peer_id $uniq2_name #uniq1_name))) +/// ``` +/// is parsed as a virtual instruction +/// ```clojure +/// (hopon peer_id) +/// ``` +pub(crate) fn try_hopon<'i>(root_new: &ast::New<'i>) -> Option> { + let expected_stream_name = &root_new.argument; + + if let (ast::Instruction::New(nested_new), ast::NewArgument::Stream(stream_name)) = + (&root_new.instruction, expected_stream_name) + { + let expected_nested_canon_name = &nested_new.argument; + + if let (ast::Instruction::Canon(canon), ast::NewArgument::CanonStream(nested_canon_name)) = + (&nested_new.instruction, expected_nested_canon_name) + { + if canon.canon_stream.name == nested_canon_name.name + && canon.stream.name == stream_name.name + // this condition handles case that is never generated by an Aqua compiler, but + // can be crafted manually + // + // see `hopon_shadowing` test for an example + && !canon_shadows_peer_id(nested_canon_name.name, &canon.peer_id) + { + return Some(HopOn { + peer_id: canon.peer_id.clone(), + }); + } + } + } + + None +} + +fn canon_shadows_peer_id(canon_name: &str, peer_id: &ast::ResolvableToPeerIdVariable<'_>) -> bool { + use ast::ResolvableToPeerIdVariable::*; + match peer_id { + InitPeerId => false, + Literal(_) => false, + Scalar(_) => false, + ScalarWithLambda(_) => false, + CanonStreamMapWithLambda(_) => false, + CanonStreamWithLambda(canon_with_lambda) => canon_with_lambda.name == canon_name, + } +} diff --git a/tools/cli/air/README.md b/tools/cli/air/README.md index 6fd53a63..187bf84a 100644 --- a/tools/cli/air/README.md +++ b/tools/cli/air/README.md @@ -10,6 +10,8 @@ This subcommand reads an AIR script from standard input and prints it in human-r It outputs to standard output or a file. +With `--patterns` options, it tries to recognize certain patterns that Aqua compiler emits and outputs it as more human readable Aqua-like syntax. Currently only `hopon` syntax is recognized. + ## `air run` Alias: `air r`. diff --git a/tools/cli/air/src/beautify/mod.rs b/tools/cli/air/src/beautify/mod.rs index d4f94b90..9ce32b05 100644 --- a/tools/cli/air/src/beautify/mod.rs +++ b/tools/cli/air/src/beautify/mod.rs @@ -25,6 +25,8 @@ use std::{io, path::PathBuf}; pub(crate) struct Args { #[clap(short, long, default_value_t = air_beautifier::DEFAULT_INDENT_STEP)] indent_step: usize, + #[clap(short, long, help = "Recognize virtual instruction patterns")] + patterns: bool, #[clap(short, long)] output: Option, input: Option, @@ -65,6 +67,12 @@ pub(crate) fn beautify(args: Args) -> Result<()> { let air_script = read_script(&args).context("failed to read the input")?; let output = build_output(&args).context("failed to open the output")?; - Beautifier::new_with_indent(output, args.indent_step).beautify(&air_script)?; + let mut beautifier = Beautifier::new_with_indent(output, args.indent_step); + + if args.patterns { + beautifier = beautifier.enable_all_patterns(); + } + + beautifier.beautify(&air_script)?; Ok(()) } diff --git a/tools/wasm/air-beautify-wasm/src/lib.rs b/tools/wasm/air-beautify-wasm/src/lib.rs index 5819be88..50fce466 100644 --- a/tools/wasm/air-beautify-wasm/src/lib.rs +++ b/tools/wasm/air-beautify-wasm/src/lib.rs @@ -19,6 +19,13 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn beautify(air_script: String) -> Result { let mut output = vec![]; - air_beautifier::beautify(&air_script, &mut output)?; + air_beautifier::beautify(&air_script, &mut output, true)?; + Ok(unsafe { String::from_utf8_unchecked(output) }) +} + +#[wasm_bindgen] +pub fn beautify_raw(air_script: String) -> Result { + let mut output = vec![]; + air_beautifier::beautify(&air_script, &mut output, false)?; Ok(unsafe { String::from_utf8_unchecked(output) }) }