From 1c0ff2c97917115b773d41d418c99600a3f06905 Mon Sep 17 00:00:00 2001 From: Ivan Boldyrev Date: Fri, 20 May 2022 17:07:22 +0300 Subject: [PATCH] `air-beautifier` lib and `air-beautify` binary (#266) The `air-beautify` util takes an AIR script input with Lisp-like syntax and transforms it into experimental indentation-based syntax. Closing #184. --- Cargo.lock | 107 +++++- Cargo.toml | 2 + crates/beautifier/Cargo.toml | 19 + crates/beautifier/src/beautifier.rs | 220 +++++++++++ crates/beautifier/src/lib.rs | 52 +++ crates/beautifier/src/tests/beautifier.rs | 351 ++++++++++++++++++ crates/beautifier/src/tests/deeply_nested.air | 26 ++ .../src/tests/deeply_nested_expected.txt | 24 ++ crates/beautifier/src/tests/mod.rs | 60 +++ tools/cli/air-beautify/Cargo.toml | 14 + tools/cli/air-beautify/src/main.rs | 80 ++++ 11 files changed, 951 insertions(+), 4 deletions(-) create mode 100644 crates/beautifier/Cargo.toml create mode 100644 crates/beautifier/src/beautifier.rs create mode 100644 crates/beautifier/src/lib.rs create mode 100644 crates/beautifier/src/tests/beautifier.rs create mode 100644 crates/beautifier/src/tests/deeply_nested.air create mode 100644 crates/beautifier/src/tests/deeply_nested_expected.txt create mode 100644 crates/beautifier/src/tests/mod.rs create mode 100644 tools/cli/air-beautify/Cargo.toml create mode 100644 tools/cli/air-beautify/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5628fc69..12c1384d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "air-beautifier" +version = "0.1.0" +dependencies = [ + "air-parser", + "itertools 0.10.3", + "thiserror", +] + +[[package]] +name = "air-beautify" +version = "0.1.0" +dependencies = [ + "air-beautifier", + "anyhow", + "clap 3.1.18", +] + [[package]] name = "air-execution-info-collector" version = "0.1.0" @@ -376,10 +394,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", - "textwrap", + "textwrap 0.11.0", "unicode-width", ] +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap 0.15.0", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -498,7 +555,7 @@ checksum = "ab327ed7354547cc2ef43cbe20ef68b988e70b4b593cbd66a2a61733123a3d23" dependencies = [ "atty", "cast", - "clap", + "clap 2.34.0", "criterion-plot", "csv", "itertools 0.10.3", @@ -959,6 +1016,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1518,6 +1581,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "os_str_bytes" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" + [[package]] name = "output_vt100" version = "0.1.3" @@ -1710,6 +1779,30 @@ dependencies = [ "output_vt100", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -2061,7 +2154,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro2", "quote", "syn", @@ -2119,6 +2212,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.30" @@ -2294,7 +2393,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c2bb690b44cb1b0fdcc54d4998d21f8bdaf706b93775425e440b174f39ad16" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro2", "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index 75f5f8d5..d5fa96eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ members = [ "crates/air-lib/polyplets", "crates/air-lib/test-utils", "crates/air-lib/trace-handler", + "crates/beautifier", "crates/data-store", + "tools/cli/air-beautify", ] exclude = [ diff --git a/crates/beautifier/Cargo.toml b/crates/beautifier/Cargo.toml new file mode 100644 index 00000000..0a581827 --- /dev/null +++ b/crates/beautifier/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "air-beautifier" +version = "0.1.0" +description = "AIR human-readable format transformer library" +authors = ["Fluence Labs"] +edition = "2018" +license = "Apache-2.0" +repository = "https://github.com/fluencelabs/aquavm/tree/master/crates/beautifier" +publish = false +keywords = ["fluence", "air", "beautifier"] + +[lib] +name = "air_beautifier" +path = "src/lib.rs" + +[dependencies] +air-parser = { path = "../air-lib/air-parser" } +itertools = "0.10.3" +thiserror = "1.0.30" diff --git a/crates/beautifier/src/beautifier.rs b/crates/beautifier/src/beautifier.rs new file mode 100644 index 00000000..c1accae6 --- /dev/null +++ b/crates/beautifier/src/beautifier.rs @@ -0,0 +1,220 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * 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. + */ + +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] + +use air_parser::ast; + +use std::fmt::Display; +use std::io; + +pub const DEFAULT_INDENT_STEP: usize = 4; + +macro_rules! multiline { + ($beautifier:expr, $indent:expr $(; $fmt1:literal $(, $arg1:expr)*; $nest:expr)+) => ({ + let indent_step = $beautifier.indent_step; + $({ + let out = &mut $beautifier.output; + $crate::beautifier::fmt_indent(out, $indent)?; + writeln!(out, $fmt1 $(, $arg1)*)?; + } + $crate::beautifier::Beautifier::beautify_walker($beautifier, $nest, $indent + indent_step)?; + )+ + Ok(()) + }); +} + +macro_rules! compound { + ($beautifier:expr, $indent:expr, $instruction:expr) => ({ + multiline!( + $beautifier, $indent; + "{}:", $instruction; + &$instruction.instruction + ) + }); +} + +fn fmt_indent(output: &mut impl io::Write, indent: usize) -> io::Result<()> { + write!(output, "{:indent$}", "", indent = indent) +} + +struct CallArgs<'ctx, 'i>(&'ctx [ast::Value<'i>]); + +impl<'ctx, 'i> Display for CallArgs<'ctx, 'i> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use itertools::Itertools as _; + + f.write_fmt(format_args!("{}", self.0.iter().format(", "))) + } +} + +struct CallTriplet<'ctx, 'i>(&'ctx ast::Triplet<'i>); + +impl<'ctx, 'i> Display for CallTriplet<'ctx, 'i> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{} ({}, {})", + self.0.peer_pk, self.0.service_id, self.0.function_name + )) + } +} + +/// Error produced by the Beautifier. +#[derive(Debug, thiserror::Error)] +pub enum BeautifyError { + #[error("{0}")] + Parse(String), + #[error(transparent)] + Io(#[from] io::Error), +} + +/// AIR beautifier. +pub struct Beautifier { + output: W, + indent_step: usize, +} + +impl Beautifier { + /// Beautifier for the output with default indentation step. + pub fn new(output: W) -> Self { + Self { + output, + indent_step: DEFAULT_INDENT_STEP, + } + } + + /// Beautifier for the output with custom indentation step. + pub fn new_with_indent(output: W, indent_step: usize) -> Self { + Self { + output, + indent_step, + } + } + + /// Unwrap the Beautifier into the underlying writer. + pub fn into_inner(self) -> W { + self.output + } + + /// Emit beautified code for the `air_script`. + pub fn beautify(&mut self, air_script: &str) -> Result<(), BeautifyError> { + let tree = air_parser::parse(air_script).map_err(BeautifyError::Parse)?; + self.beautify_ast(tree) + } + + /// Emit beautified code for the `ast`. + pub fn beautify_ast<'i>( + &mut self, + ast: impl AsRef>, + ) -> Result<(), BeautifyError> { + Ok(self.beautify_walker(ast.as_ref(), 0)?) + } + + fn beautify_walker(&mut self, node: &ast::Instruction, indent: usize) -> io::Result<()> { + match node { + ast::Instruction::Call(call) => self.beautify_call(call, indent), + ast::Instruction::Ap(ap) => self.beautify_simple(ap, indent), + ast::Instruction::Seq(seq) => self.beautify_seq(seq, indent), + ast::Instruction::Par(par) => self.beautify_par(par, indent), + ast::Instruction::Xor(xor) => self.beautify_xor(xor, indent), + ast::Instruction::Match(match_) => self.beautify_match(match_, indent), + ast::Instruction::MisMatch(mismatch) => self.beautify_mismatch(mismatch, indent), + ast::Instruction::Fail(fail) => self.beautify_simple(fail, indent), + ast::Instruction::FoldScalar(fold_scalar) => { + self.beautify_fold_scalar(fold_scalar, indent) + } + ast::Instruction::FoldStream(fold_stream) => { + self.beautify_fold_stream(fold_stream, indent) + } + ast::Instruction::New(new) => self.beautify_new(new, indent), + ast::Instruction::Next(next) => self.beautify_simple(next, indent), + ast::Instruction::Null(null) => self.beautify_simple(null, indent), + ast::Instruction::Error => self.beautify_simple("error", indent), + } + } + + fn beautify_call(&mut self, call: &ast::Call, indent: usize) -> io::Result<()> { + fmt_indent(&mut self.output, indent)?; + match &call.output { + ast::CallOutputValue::Variable(v) => write!(&mut self.output, "{} <- ", v)?, + ast::CallOutputValue::None => {} + } + writeln!( + &mut self.output, + "call {} [{}]", + CallTriplet(&call.triplet), + CallArgs(call.args.as_slice()) + ) + } + + fn beautify_simple(&mut self, instruction: impl Display, indent: usize) -> io::Result<()> { + fmt_indent(&mut self.output, indent)?; + writeln!(&mut self.output, "{}", instruction) + } + + fn beautify_seq(&mut self, seq: &ast::Seq, indent: usize) -> io::Result<()> { + self.beautify_walker(&seq.0, indent)?; + self.beautify_walker(&seq.1, indent) + } + + fn beautify_par(&mut self, par: &ast::Par, indent: usize) -> io::Result<()> { + multiline!( + self, indent; + "par:"; + &par.0; + "|"; // TODO: SHOULD BE UNINDENTED AS PER SPEC; OR WE MAY CHANGE THE SPEC + &par.1 + ) + } + + fn beautify_xor(&mut self, xor: &ast::Xor, indent: usize) -> io::Result<()> { + multiline!( + self, indent; + "try:"; + &xor.0; + "catch:"; + &xor.1 + ) + } + + fn beautify_match(&mut self, match_: &ast::Match, indent: usize) -> io::Result<()> { + compound!(self, indent, match_) + } + + fn beautify_mismatch(&mut self, mismatch: &ast::MisMatch, indent: usize) -> io::Result<()> { + compound!(self, indent, mismatch) + } + + fn beautify_fold_scalar(&mut self, fold: &ast::FoldScalar, indent: usize) -> io::Result<()> { + compound!(self, indent, fold) + } + + fn beautify_fold_stream(&mut self, fold: &ast::FoldStream, indent: usize) -> io::Result<()> { + compound!(self, indent, fold) + } + + fn beautify_new(&mut self, new: &ast::New, indent: usize) -> io::Result<()> { + compound!(self, indent, new) + } +} diff --git a/crates/beautifier/src/lib.rs b/crates/beautifier/src/lib.rs new file mode 100644 index 00000000..e9af1dfe --- /dev/null +++ b/crates/beautifier/src/lib.rs @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * 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. + */ + +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] + +mod beautifier; + +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> { + let mut beautifier = Beautifier::new(output); + beautifier.beautify(air_script) +} + +/// Beautify the `air_script` to a string with default settings. +/// Return error on parsing error. +pub fn beautify_to_string(air_script: &str) -> Result { + let ast = air_parser::parse(air_script)?; + let mut buffer = vec![]; + let mut beautifier = Beautifier::new(&mut buffer); + + beautifier.beautify_ast(&ast).unwrap(); + // Safety: safe because Beautifier produces valid utf8 strings + Ok(unsafe { String::from_utf8_unchecked(buffer) }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/beautifier/src/tests/beautifier.rs b/crates/beautifier/src/tests/beautifier.rs new file mode 100644 index 00000000..fab882dd --- /dev/null +++ b/crates/beautifier/src/tests/beautifier.rs @@ -0,0 +1,351 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * 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. + */ + +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] + +use crate::{beautify_to_string, Beautifier}; + +#[test] +fn ap_with_literal() { + let script = r#"(ap "some_string" $stream)"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"ap "some_string" $stream +"# + ); +} + +#[test] +fn ap_with_number() { + let script = "(ap -100 $stream)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"ap -100 $stream +"# + ); +} + +#[test] +fn ap_with_bool() { + let script = "(ap true $stream)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"ap true $stream +"# + ); +} + +#[test] +fn ap_with_last_error() { + let script = "(ap %last_error%.$.message! $stream)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "ap %last_error%.$.message $stream\n"); +} + +#[test] +fn ap_with_empty_array() { + let script = "(ap [] $stream)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "ap [] $stream\n"); +} + +#[test] +fn ap_with_init_peer_id() { + let script = "(ap %init_peer_id% $stream)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "ap %init_peer_id% $stream\n"); +} + +#[test] +fn ap_with_timestamp() { + let script = "(ap %timestamp% $stream)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "ap %timestamp% $stream\n"); +} + +#[test] +fn ap_with_ttl() { + let script = r#" + (ap %ttl% $stream) + "#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "ap %ttl% $stream\n"); +} + +#[test] +fn seq() { + let script = "(seq (null) (null))"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + "null +null +" + ) +} + +#[test] +fn seq_nested_pre() { + let script = "(seq (seq (null) (null)) (null))"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + "null +null +null +" + ); +} + +#[test] +fn seq_nested_post() { + let script = "(seq (null) (seq (null) (null)))"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + "null +null +null +" + ); +} + +#[test] +fn par() { + let script = "(par (null) (null))"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + "par: + null +| + null +" + ); +} + +#[test] +fn match_() { + let script = r#"(seq + (seq + (call "a" ("" "") [] a) + (call "b" ("" "") [] b)) + (match a b (null)))"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"a <- call "a" ("", "") [] +b <- call "b" ("", "") [] +match a b: + null +"#, + ); +} + +#[test] +fn mismatch() { + let script = r#"(seq + (seq + (call "a" ("" "") [] a) + (call "b" ("" "") [] b)) + (mismatch a b (null)))"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"a <- call "a" ("", "") [] +b <- call "b" ("", "") [] +mismatch a b: + null +"#, + ); +} + +#[test] +fn fail_last_error() { + let script = "(fail %last_error%)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "fail %last_error%\n"); +} + +#[test] +fn fail_expr() { + let script = "(fail var)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "fail var\n"); +} + +#[test] +fn fail_common() { + let script = r#"(fail 123 "Message")"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"fail 123 "Message" +"# + ); +} + +#[test] +fn fold_scalar() { + let script = r#"(seq (call "it" ("" "") [] var) (fold var i (null)))"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"var <- call "it" ("", "") [] +fold var i: + null +"# + ); +} + +#[test] +fn fold_stream() { + let script = r#"(seq (call "it" ("" "") [] $var) (fold $var i (null)))"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"$var <- call "it" ("", "") [] +fold $var i: + null +"# + ); +} + +#[test] +fn call_var() { + let script = "(call \"{0}\" (\"a\" \"b\") [\"stream_1\" \"stream_2\"] streamvar)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + "streamvar <- call \"{0}\" (\"a\", \"b\") [\"stream_1\", \"stream_2\"]\n" + ); +} + +#[test] +fn call_novar() { + let script = r#"(call "{0}" ("a" "b") ["stream_1" "stream_2"])"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"call "{0}" ("a", "b") ["stream_1", "stream_2"] +"# + ); +} + +#[test] +fn call_noargs() { + let script = r#"(call "{0}" ("a" "b") [])"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"call "{0}" ("a", "b") [] +"# + ); +} + +#[test] +fn next() { + let script = r#"(seq (call "{0}" ("a" "b") ["stream_1"] j) (fold j i (next i)))"#; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + r#"j <- call "{0}" ("a", "b") ["stream_1"] +fold j i: + next i +"# + ); +} + +#[test] +fn new() { + let script = "(new var (seq (null) (null)))"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!( + output, + "new var: + null + null +" + ); +} + +#[test] +fn null() { + let script = "(null)"; + let output = beautify_to_string(script).unwrap(); + + assert_eq!(output, "null\n"); +} + +#[test] +fn custom_indent_step() { + let mut output = vec![]; + let mut beautifier = Beautifier::new_with_indent(&mut output, 2); + let script = "(new var1 (new var (seq (null) (null))))"; + beautifier.beautify(script).unwrap(); + + assert_eq!( + String::from_utf8(output).unwrap(), + "new var1: + new var: + null + null +" + ); +} + +#[test] +fn deeply_nested() { + let script = include_str!("deeply_nested.air"); + let output = beautify_to_string(script).unwrap(); + let expected = include_str!("deeply_nested_expected.txt"); + assert_eq!(output, expected); +} diff --git a/crates/beautifier/src/tests/deeply_nested.air b/crates/beautifier/src/tests/deeply_nested.air new file mode 100644 index 00000000..0f909c3c --- /dev/null +++ b/crates/beautifier/src/tests/deeply_nested.air @@ -0,0 +1,26 @@ +(xor + (seq + (seq + (call "me" ("" "") [] var) + (ap var.$.value data)) + (par + (par + (call "other1" ("set" "") [data]) + (call "other2" ("insert" "") [data])) + (seq + (ap "key" key) + (seq + (call "other3" ("" "") [key data] $values) + (fold $values i + (seq + (xor + (seq + (call "other2" ("insert" "") [i] j) + (call "other3" ("report" "") [j])) + (fail 3 "fail to fold")) + (next i))))))) + (par + (seq + (call "other1" ("report" "error") [%last_error%] rep) + (call "other2" ("report" "error") [%last_error% rep])) + (call "other3" ("report" "error") [%last_error%]))) diff --git a/crates/beautifier/src/tests/deeply_nested_expected.txt b/crates/beautifier/src/tests/deeply_nested_expected.txt new file mode 100644 index 00000000..c7ccb746 --- /dev/null +++ b/crates/beautifier/src/tests/deeply_nested_expected.txt @@ -0,0 +1,24 @@ +try: + var <- call "me" ("", "") [] + ap var.$.value data + par: + par: + call "other1" ("set", "") [data] + | + call "other2" ("insert", "") [data] + | + ap "key" key + $values <- call "other3" ("", "") [key, data] + fold $values i: + try: + j <- call "other2" ("insert", "") [i] + call "other3" ("report", "") [j] + catch: + fail 3 "fail to fold" + next i +catch: + par: + rep <- call "other1" ("report", "error") [%last_error%] + call "other2" ("report", "error") [%last_error%, rep] + | + call "other3" ("report", "error") [%last_error%] diff --git a/crates/beautifier/src/tests/mod.rs b/crates/beautifier/src/tests/mod.rs new file mode 100644 index 00000000..ddf145c2 --- /dev/null +++ b/crates/beautifier/src/tests/mod.rs @@ -0,0 +1,60 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * 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. + */ + +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] + +mod beautifier; + +use crate::{beautify, beautify_to_string, BeautifyError}; + +#[test] +fn beautify_valid() { + let air_script = "(seq (null) (null))"; + let mut buffer = vec![]; + let res = beautify(air_script, &mut buffer); + assert!(matches!(res, Ok(()))); + assert_eq!(std::str::from_utf8(&buffer).unwrap(), "null\nnull\n"); +} + +#[test] +fn beautify_invalid() { + let air_script = "(seq (null))"; + let mut buffer = vec![]; + let res = beautify(air_script, &mut buffer); + assert!(matches!(res, Err(BeautifyError::Parse(_)))); +} + +#[test] +fn beautify_to_string_valid() { + let air_script = "(seq (null) (null))"; + let res = beautify_to_string(air_script).unwrap(); + assert_eq!(res, "null\nnull\n"); +} + +#[test] +fn beautify_to_string_invalid() { + let air_script = "(seq (null))"; + let res = beautify_to_string(air_script); + assert!(matches!(res, Err(_))); +} diff --git a/tools/cli/air-beautify/Cargo.toml b/tools/cli/air-beautify/Cargo.toml new file mode 100644 index 00000000..ac90253c --- /dev/null +++ b/tools/cli/air-beautify/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "air-beautify" +version = "0.1.0" +edition = "2021" +description = "AIR human-readable format transformer binary" +authors = ["Fluence Labs"] +license = "Apache-2.0" +publish = false +keywords = ["fluence", "air", "beautifier"] + +[dependencies] +air-beautifier = { path = "../../../crates/beautifier" } +clap = { version = "3.1.18", features = ["derive"] } +anyhow = "1.0.56" diff --git a/tools/cli/air-beautify/src/main.rs b/tools/cli/air-beautify/src/main.rs new file mode 100644 index 00000000..82c0e582 --- /dev/null +++ b/tools/cli/air-beautify/src/main.rs @@ -0,0 +1,80 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * 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. + */ + +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] + +use air_beautifier::Beautifier; +use anyhow::{Context, Result}; +use clap::Parser; + +use std::{io, path::PathBuf}; + +#[derive(Parser)] +struct Args { + #[clap(short, long, default_value_t = air_beautifier::DEFAULT_INDENT_STEP)] + indent_step: usize, + #[clap(short, long)] + output: Option, + input: Option, +} + +fn read_script(args: &Args) -> Result { + use std::io::Read; + + let air_script = match &args.input { + Some(in_path) => std::fs::read_to_string(in_path)?, + None => { + let mut buffer = String::new(); + let mut stdin = io::stdin().lock(); + + stdin.read_to_string(&mut buffer)?; + buffer + } + }; + + Ok(air_script) +} + +fn build_output(args: &Args) -> Result> { + let output: Box = match &args.output { + Some(out_path) => { + let file = std::fs::File::create(out_path)?; + Box::new(file) + } + None => { + let stdout = io::stdout().lock(); + Box::new(stdout) + } + }; + Ok(output) +} + +fn main() -> Result<()> { + let args = Args::parse(); + 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)?; + Ok(()) +}