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.
This commit is contained in:
Ivan Boldyrev 2024-05-24 16:27:51 +04:00 committed by GitHub
parent 79d1c11a0e
commit 04bacb7039
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 253 additions and 7 deletions

View File

@ -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<W: io::Write> {
output: W,
indent_step: usize,
try_hopon: bool,
}
impl<W: io::Write> Beautifier<W> {
/// 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<W: io::Write> Beautifier<W> {
}
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(())
}

View File

@ -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)
}

View File

@ -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"
),
);
}

View File

@ -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());
}

View File

@ -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<HopOn<'i>> {
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,
}
}

View File

@ -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`.

View File

@ -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<PathBuf>,
input: Option<PathBuf>,
@ -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(())
}

View File

@ -19,6 +19,13 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn beautify(air_script: String) -> Result<String, JsError> {
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<String, JsError> {
let mut output = vec![];
air_beautifier::beautify(&air_script, &mut output, false)?;
Ok(unsafe { String::from_utf8_unchecked(output) })
}