diff --git a/Cargo.lock b/Cargo.lock index aa72fe2b..dbbcfab6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1047,6 +1047,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot 0.5.0", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + [[package]] name = "criterion" version = "0.8.2" @@ -1058,7 +1084,7 @@ dependencies = [ "cast", "ciborium", "clap", - "criterion-plot", + "criterion-plot 0.8.2", "itertools 0.13.0", "num-traits", "oorandom", @@ -1072,6 +1098,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "criterion-plot" version = "0.8.2" @@ -2033,12 +2069,32 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -3840,6 +3896,7 @@ dependencies = [ name = "techscript_formatter" version = "2.0.0" dependencies = [ + "criterion 0.5.1", "techscript_ast", "techscript_common", "techscript_errors", @@ -3928,7 +3985,7 @@ dependencies = [ name = "techscript_lsp" version = "2.0.0" dependencies = [ - "criterion", + "criterion 0.8.2", "serde", "serde_json", "techscript_ast", diff --git a/tools/formatter/Cargo.toml b/tools/formatter/Cargo.toml index d866500d..97f469d2 100644 --- a/tools/formatter/Cargo.toml +++ b/tools/formatter/Cargo.toml @@ -11,3 +11,10 @@ techscript_ast = { path = "../../compiler/ast" } techscript_errors = { path = "../../compiler/errors" } techscript_lexer = { path = "../../compiler/lexer" } techscript_parser = { path = "../../compiler/parser" } + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "formatter_bench" +harness = false diff --git a/tools/formatter/benches/formatter_bench.rs b/tools/formatter/benches/formatter_bench.rs new file mode 100644 index 00000000..1ff92aed --- /dev/null +++ b/tools/formatter/benches/formatter_bench.rs @@ -0,0 +1,22 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use techscript_ast::{Expression, LiteralVal, Pattern, Program, Statement}; +use techscript_formatter::{DocumentFormatter, Formatter}; + +fn bench_format(c: &mut Criterion) { + let source = " + make var_0 = 0 + say \"Hello 0\" + make var_1 = 1 + say \"Hello 1\" + " + .repeat(2500); // 10000 statements + + let formatter = DocumentFormatter::new(4); + + c.bench_function("format 10000 statements", |b| { + b.iter(|| formatter.format_source(black_box(&source))) + }); +} + +criterion_group!(benches, bench_format); +criterion_main!(benches); diff --git a/tools/formatter/src/lib.rs b/tools/formatter/src/lib.rs index 4f084e4a..d19cfd24 100644 --- a/tools/formatter/src/lib.rs +++ b/tools/formatter/src/lib.rs @@ -1,3 +1,4 @@ +use std::fmt::Write; use techscript_ast::{DSLBlock, DSLChild, Program, Statement}; pub trait Formatter { @@ -20,28 +21,27 @@ impl DocumentFormatter { self.format(&program) } - fn format_indent(&self, indent: usize) -> String { - " ".repeat(indent * self.indent_size) + fn write_indent(&self, indent: usize, output: &mut String) { + for _ in 0..(indent * self.indent_size) { + output.push(' '); + } } - fn format_dsl_block(&self, block: &DSLBlock, indent: usize) -> String { - let mut output = String::new(); - let base = self.format_indent(indent); - - output.push_str(&base); + fn format_dsl_block_into(&self, block: &DSLBlock, indent: usize, output: &mut String) { + self.write_indent(indent, output); output.push_str(&block.kind); for arg in &block.args { output.push(' '); - output.push_str(&self.format_expr(arg)); + self.format_expr_into(arg, output); } output.push('\n'); for prop in &block.properties { - output.push_str(&self.format_indent(indent + 1)); + self.write_indent(indent + 1, output); output.push_str(&prop.name); if let Some(ref val) = prop.value { output.push(' '); - output.push_str(&self.format_expr(val)); + self.format_expr_into(val, output); } output.push('\n'); } @@ -49,107 +49,126 @@ impl DocumentFormatter { for child in &block.children { match child { DSLChild::Block(sub_block) => { - output.push_str(&self.format_dsl_block(sub_block, indent + 1)); + self.format_dsl_block_into(sub_block, indent + 1, output); } DSLChild::Code(code_block) => { - output.push_str(&self.format_indent(indent + 1)); + self.write_indent(indent + 1, output); output.push_str("code\n"); for stmt in &code_block.statements { - output.push_str(&self.format_stmt(stmt, indent + 2)); + self.format_stmt_into(stmt, indent + 2, output); } } DSLChild::Property(prop) => { - output.push_str(&self.format_indent(indent + 1)); + self.write_indent(indent + 1, output); output.push_str(&prop.name); if let Some(ref val) = prop.value { output.push(' '); - output.push_str(&self.format_expr(val)); + self.format_expr_into(val, output); } output.push('\n'); } } } - output.push_str(&base); + self.write_indent(indent, output); output.push_str("end\n"); - output } - fn format_expr(&self, expr: &techscript_ast::Expression) -> String { + fn format_expr_into(&self, expr: &techscript_ast::Expression, output: &mut String) { match expr { - techscript_ast::Expression::Literal(lit) => self.format_lit(&lit.value), - techscript_ast::Expression::Identifier(ident) => ident.name.clone(), + techscript_ast::Expression::Literal(lit) => self.format_lit_into(&lit.value, output), + techscript_ast::Expression::Identifier(ident) => output.push_str(&ident.name), techscript_ast::Expression::FString(fs) => { - let mut s = "f\"".to_string(); + output.push_str("f\""); for part in &fs.parts { match part { - techscript_ast::FStringPart::Literal(l) => s.push_str(l), - techscript_ast::FStringPart::Expr(_) => s.push_str("{}"), + techscript_ast::FStringPart::Literal(l) => output.push_str(l), + techscript_ast::FStringPart::Expr(_) => output.push_str("{}"), } } - s.push('"'); - s + output.push('"'); + } + _ => { + let _ = write!(output, "{:?}", expr); } - _ => format!("{:?}", expr), } } - fn format_lit(&self, lit: &techscript_ast::LiteralVal) -> String { + fn format_lit_into(&self, lit: &techscript_ast::LiteralVal, output: &mut String) { match lit { - techscript_ast::LiteralVal::Str(s) => format!("\"{}\"", s), - techscript_ast::LiteralVal::Int(i) => i.to_string(), - techscript_ast::LiteralVal::Float(f) => f.to_string(), - techscript_ast::LiteralVal::Bool(b) => b.to_string(), - techscript_ast::LiteralVal::None => "none".to_string(), + techscript_ast::LiteralVal::Str(s) => { + let _ = write!(output, "\"{}\"", s); + } + techscript_ast::LiteralVal::Int(i) => { + let _ = write!(output, "{}", i); + } + techscript_ast::LiteralVal::Float(f) => { + let _ = write!(output, "{}", f); + } + techscript_ast::LiteralVal::Bool(b) => { + let _ = write!(output, "{}", b); + } + techscript_ast::LiteralVal::None => output.push_str("none"), } } - fn format_stmt(&self, stmt: &Statement, indent: usize) -> String { - let base = self.format_indent(indent); + fn format_stmt_into(&self, stmt: &Statement, indent: usize, output: &mut String) { match stmt { - Statement::DSL(block) => self.format_dsl_block(block, indent), + Statement::DSL(block) => self.format_dsl_block_into(block, indent, output), Statement::VarDecl(decl) => { + self.write_indent(indent, output); + output.push_str("make "); let name = match &decl.pattern { - techscript_ast::Pattern::Single(ident) => ident.name.clone(), - _ => "".to_string(), + techscript_ast::Pattern::Single(ident) => ident.name.as_str(), + _ => "", }; - format!( - "{}make {} = {}\n", - base, - name, - self.format_expr(&decl.initializer) - ) + output.push_str(name); + output.push_str(" = "); + self.format_expr_into(&decl.initializer, output); + output.push('\n'); } Statement::ConstDecl(decl) => { + self.write_indent(indent, output); + output.push_str("const "); let name = match &decl.pattern { - techscript_ast::Pattern::Single(ident) => ident.name.clone(), - _ => "".to_string(), + techscript_ast::Pattern::Single(ident) => ident.name.as_str(), + _ => "", }; - format!( - "{}const {} = {}\n", - base, - name, - self.format_expr(&decl.initializer) - ) + output.push_str(name); + output.push_str(" = "); + self.format_expr_into(&decl.initializer, output); + output.push('\n'); + } + Statement::Say(s) => { + self.write_indent(indent, output); + output.push_str("say "); + self.format_expr_into(&s.value, output); + output.push('\n'); } - Statement::Say(s) => format!("{}say {}\n", base, self.format_expr(&s.value)), Statement::Return(ret) => { + self.write_indent(indent, output); if let Some(ref val) = ret.value { - format!("{}return {}\n", base, self.format_expr(val)) + output.push_str("return "); + self.format_expr_into(val, output); + output.push('\n'); } else { - format!("{}return\n", base) + output.push_str("return\n"); } } - _ => format!("{}\n", base), + _ => { + self.write_indent(indent, output); + output.push_str("\n"); + } } } } impl Formatter for DocumentFormatter { fn format(&self, program: &Program) -> String { - let mut output = String::new(); + // Pre-allocate capacity, assuming roughly ~32 bytes per statement as a heuristic + let mut output = String::with_capacity(program.statements.len() * 32); for stmt in &program.statements { - output.push_str(&self.format_stmt(stmt, 0)); + self.format_stmt_into(stmt, 0, &mut output); } output } diff --git a/tools/formatter/tests/formatter_tests.rs b/tools/formatter/tests/formatter_tests.rs index 0cdd0055..9be3181b 100644 --- a/tools/formatter/tests/formatter_tests.rs +++ b/tools/formatter/tests/formatter_tests.rs @@ -6,3 +6,18 @@ fn test_formatter_instantiation() { let output = formatter.format_source(""); assert_eq!(output, ""); } + +#[test] +fn test_formatter_complex() { + let source = " + make var_0 = 0 + say \"Hello 0\" + make var_1 = 1 + say \"Hello 1\" + "; + + let formatter = DocumentFormatter::new(4); + let formatted = formatter.format_source(source); + assert!(formatted.contains("make var_0 = 0")); + assert!(formatted.contains("say \"Hello 0\"")); +}