diff --git a/docsite/site/content/reference/expressions.md b/docsite/site/content/reference/expressions.md index 1901266..43f8525 100644 --- a/docsite/site/content/reference/expressions.md +++ b/docsite/site/content/reference/expressions.md @@ -681,4 +681,29 @@ This will output a line to stderr something like the below: This is helpful when developing shared modules or ucg libraries. +Convert Expressions +------------------- + +UCG has convert expressions which will turn any UCG value into a string using the specified conversion format. +This expression is similar to the out expression except instead of writing to a file it writes to a string. + +It's useful for previewing the result of converting a ucg value in the repl or for composing multiple conversion +formats together into a single composite ucg value. + +You can experiment with conversion in the repl: + +``` +> convert json {foo="bar"}; +'{ + "foo": "bar" +}' +> +``` + +Or store a converted value into a UCG string: + +``` +let converted = convert json {foo="bar"}; +``` + Next: Statements \ No newline at end of file diff --git a/docsite/site/content/reference/grammar.md b/docsite/site/content/reference/grammar.md index 8bcaf92..69d70bd 100644 --- a/docsite/site/content/reference/grammar.md +++ b/docsite/site/content/reference/grammar.md @@ -55,6 +55,7 @@ filter_keyword: "filter" ; module_keyword: "module" ; mod_keyword: "mod" ; out_keyword: "out" ; +convert_keyword: "convert" ; assert_keyword: "assert" ; fail_keyword: "fail" ; trace_keyword: "TRACE" ; @@ -230,11 +231,13 @@ expr: binary_expr | non_operator_expr ; ``` let_statement: let_keyword, bareword, equal, expr ; out_statement: out_keyword, bareword, str ; +convert_statement: convert_keyword, bareword, str ; assert_statement: assert_keyword, pipe, { statement }, pipe ; simple_statement: expr ; statement: ( let_statement | out_statement + | convert_statement | assert_statement | simple_statement ), semicolon ; ``` diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0c7bfe1..bf9f413 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -775,6 +775,9 @@ pub enum Statement { // Identify an Expression for output. Output(Position, Token, Expression), + + // Print the expression to stdout. + Print(Position, Token, Expression), } impl Statement { @@ -784,6 +787,7 @@ impl Statement { Statement::Let(ref def) => &def.pos, Statement::Assert(ref pos, _) => pos, Statement::Output(ref pos, _, _) => pos, + Statement::Print(ref pos, _, _) => pos, } } } diff --git a/src/ast/printer/mod.rs b/src/ast/printer/mod.rs index 4c0d25b..6346ec7 100644 --- a/src/ast/printer/mod.rs +++ b/src/ast/printer/mod.rs @@ -552,6 +552,10 @@ where write!(&mut self.w, "out {} ", _tok.fragment)?; self.render_expr(&_expr)?; } + Statement::Print(_, _tok, _expr) => { + write!(&mut self.w, "print {} ", _tok.fragment)?; + self.render_expr(&_expr)?; + } }; write!(self.w, ";\n\n")?; self.last_line = line; diff --git a/src/ast/walk.rs b/src/ast/walk.rs index 349820e..d67422f 100644 --- a/src/ast/walk.rs +++ b/src/ast/walk.rs @@ -22,6 +22,9 @@ pub trait Walker { Statement::Output(_, _, ref mut expr) => { self.walk_expression(expr); } + Statement::Print(_, _, ref mut expr) => { + self.walk_expression(expr); + } } } diff --git a/src/build/compile_test.rs b/src/build/compile_test.rs index 8610282..962e3ef 100644 --- a/src/build/compile_test.rs +++ b/src/build/compile_test.rs @@ -19,11 +19,13 @@ use regex::Regex; use super::assets::MemoryCache; use super::FileBuilder; +use crate::convert::ConverterRegistry; fn assert_build(input: &str) { let i_paths = Vec::new(); let cache = MemoryCache::new(); - let mut b = FileBuilder::new("", &i_paths, Rc::new(RefCell::new(cache))); + let registry = ConverterRegistry::make_registry(); + let mut b = FileBuilder::new("", &i_paths, Rc::new(RefCell::new(cache)), ®istry); b.enable_validate_mode(); b.eval_string(input).unwrap(); if !b.assert_collector.success { @@ -34,7 +36,8 @@ fn assert_build(input: &str) { fn assert_build_failure(input: &str, expect: Vec) { let i_paths = Vec::new(); let cache = MemoryCache::new(); - let mut b = FileBuilder::new("", &i_paths, Rc::new(RefCell::new(cache))); + let registry = ConverterRegistry::make_registry(); + let mut b = FileBuilder::new("", &i_paths, Rc::new(RefCell::new(cache)), ®istry); b.enable_validate_mode(); let err = b.eval_string(input); match err { diff --git a/src/build/mod.rs b/src/build/mod.rs index b16977f..215bfbd 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -32,6 +32,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::ast::*; use crate::build::format::{ExpressionFormatter, FormatRenderer, SimpleFormatter}; use crate::build::scope::{find_in_fieldlist, Scope, ValueMap}; +use crate::convert::ConverterRegistry; use crate::convert::ImporterRegistry; use crate::error; use crate::iter::OffsetStrIter; @@ -112,6 +113,7 @@ where pub assert_collector: AssertCollector, scope: Scope, import_registry: ImporterRegistry, + converter_registry: &'a ConverterRegistry, // NOTE(jwall): We use interior mutability here because we need // our asset cache to be shared by multiple different sub-builders. // We use Rc to handle the reference counting for us and we use @@ -156,6 +158,7 @@ where working_dir: P, import_paths: &'a Vec, cache: Rc>, + converter_registry: &'a ConverterRegistry, ) -> Self { let env_vars: Vec<(String, String)> = env::vars().collect(); let scope = scope::Scope::new(Rc::new(Val::Env(env_vars))); @@ -173,6 +176,7 @@ where }, scope: scope, import_registry: ImporterRegistry::make_registry(), + converter_registry: converter_registry, assets: cache, out_lock: None, is_module: false, @@ -195,6 +199,7 @@ where assets: self.assets.clone(), // This is admittedly a little wasteful but we can live with it for now. import_registry: ImporterRegistry::make_registry(), + converter_registry: self.converter_registry, scope: self.scope.spawn_clean(), out_lock: None, is_module: false, @@ -365,6 +370,8 @@ where normalized.push(&path); // First see if the normalized file exists or not. if !normalized.exists() && use_import_path { + // TODO(jwall): Support importing from a zip file in this + // import_path? // If it does not then look for it in the list of import_paths for mut p in self.import_path.iter().cloned() { p.push(&path); @@ -425,7 +432,6 @@ where } let sep = format!("{}", std::path::MAIN_SEPARATOR); let raw_path = def.path.fragment.replace("/", &sep); - // Try a relative path first. let normalized = match self.find_file(&raw_path, true) { Ok(p) => p, Err(e) => { @@ -551,6 +557,31 @@ where .to_boxed()) } } + &Statement::Print(ref pos, ref typ, ref expr) => { + if let None = self.out_lock { + let val = self.eval_expr(expr, &child_scope)?; + match self.converter_registry.get_converter(&typ.fragment) { + Some(c) => { + let mut buf = Vec::new(); + c.convert(val.clone(), &mut buf)?; + Ok(Rc::new(Val::Str(String::from_utf8(buf)?))) + } + None => Err(error::BuildError::with_pos( + format!("Invalid Converter specified for print {}", typ.fragment), + error::ErrorType::Unsupported, + pos.clone(), + ) + .to_boxed()), + } + } else { + Err(error::BuildError::with_pos( + format!("You can only have one output per file."), + error::ErrorType::Unsupported, + pos.clone(), + ) + .to_boxed()) + } + } } } diff --git a/src/build/test.rs b/src/build/test.rs index 5c0c7c8..078711e 100644 --- a/src/build/test.rs +++ b/src/build/test.rs @@ -15,6 +15,7 @@ use super::assets; use super::assets::MemoryCache; use super::{FileBuilder, SelectDef, Val}; use crate::ast::*; +use crate::convert::ConverterRegistry; use std; use std::cell::RefCell; @@ -37,7 +38,8 @@ fn test_expr_to_val<'a, C: assets::Cache>( fn test_eval_div_expr_fail() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); test_expr_to_val( vec![( Expression::Binary(BinaryOpDef { @@ -63,7 +65,8 @@ fn test_eval_div_expr_fail() { fn test_eval_mul_expr_fail() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); test_expr_to_val( vec![( Expression::Binary(BinaryOpDef { @@ -89,7 +92,8 @@ fn test_eval_mul_expr_fail() { fn test_eval_subtract_expr_fail() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); test_expr_to_val( vec![( Expression::Binary(BinaryOpDef { @@ -114,7 +118,8 @@ fn test_eval_subtract_expr_fail() { fn test_eval_add_expr_fail() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); test_expr_to_val( vec![( Expression::Binary(BinaryOpDef { @@ -139,7 +144,8 @@ fn test_eval_add_expr_fail() { fn test_eval_simple_lookup_error() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); b.scope .build_output .entry(value_node!("var1".to_string(), Position::new(1, 0, 0))) @@ -157,7 +163,8 @@ fn test_eval_simple_lookup_error() { fn test_expr_copy_no_such_tuple() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); test_expr_to_val( vec![( Expression::Copy(CopyDef { @@ -179,7 +186,8 @@ fn test_expr_copy_no_such_tuple() { fn test_select_expr_not_a_string() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); b.scope .build_output .entry(value_node!("foo".to_string(), Position::new(1, 0, 0))) diff --git a/src/convert/exec.rs b/src/convert/exec.rs index 95dbe94..3b9bc81 100644 --- a/src/convert/exec.rs +++ b/src/convert/exec.rs @@ -197,6 +197,7 @@ mod exec_test { use crate::build::assets::MemoryCache; use crate::build::FileBuilder; use crate::convert::traits::Converter; + use crate::convert::ConverterRegistry; use std; use std::cell::RefCell; @@ -206,7 +207,8 @@ mod exec_test { fn convert_just_command_test() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); let conv = ExecConverter::new(); b.eval_string( "let script = { @@ -228,7 +230,8 @@ mod exec_test { fn convert_command_with_env_test() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); let conv = ExecConverter::new(); b.eval_string( "let script = { @@ -257,7 +260,8 @@ mod exec_test { fn convert_command_with_arg_test() { let i_paths = Vec::new(); let cache = Rc::new(RefCell::new(MemoryCache::new())); - let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache); + let registry = ConverterRegistry::make_registry(); + let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, cache, ®istry); let conv = ExecConverter::new(); b.eval_string( "let script = { diff --git a/src/main.rs b/src/main.rs index 0d33b8c..ce28f2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,12 +99,14 @@ fn build_file<'a, C: Cache>( strict: bool, import_paths: &'a Vec, cache: Rc>, + registry: &'a ConverterRegistry, ) -> Result, Box> { let mut file_path_buf = PathBuf::from(file); if file_path_buf.is_relative() { file_path_buf = std::env::current_dir()?.join(file_path_buf); } - let mut builder = build::FileBuilder::new(std::env::current_dir()?, import_paths, cache); + let mut builder = + build::FileBuilder::new(std::env::current_dir()?, import_paths, cache, registry); builder.set_strict(strict); if validate { builder.enable_validate_mode(); @@ -121,9 +123,10 @@ fn do_validate( strict: bool, import_paths: &Vec, cache: Rc>, + registry: &ConverterRegistry, ) -> bool { println!("Validating {}", file); - match build_file(file, true, strict, import_paths, cache) { + match build_file(file, true, strict, import_paths, cache, registry) { Ok(b) => { if b.assert_collector.success { println!("File {} Pass\n", file); @@ -148,7 +151,7 @@ fn do_compile( registry: &ConverterRegistry, ) -> bool { println!("Building {}", file); - let builder = match build_file(file, false, strict, import_paths, cache.clone()) { + let builder = match build_file(file, false, strict, import_paths, cache.clone(), registry) { Ok(builder) => builder, Err(err) => { eprintln!("{}", err); @@ -214,7 +217,13 @@ fn visit_ucg_files( } } else { if validate && path_as_string.ends_with("_test.ucg") { - if !do_validate(&path_as_string, strict, import_paths, cache.clone()) { + if !do_validate( + &path_as_string, + strict, + import_paths, + cache.clone(), + registry, + ) { result = false; summary.push_str(format!("{} - FAIL\n", path_as_string).as_str()) } else { @@ -234,7 +243,7 @@ fn visit_ucg_files( } } } else if validate && our_path.ends_with("_test.ucg") { - if !do_validate(&our_path, strict, import_paths, cache) { + if !do_validate(&our_path, strict, import_paths, cache, registry) { result = false; summary.push_str(format!("{} - FAIL\n", our_path).as_str()); } else { @@ -262,8 +271,12 @@ fn inspect_command( let file = matches.value_of("INPUT").unwrap_or("std/functional.ucg"); let sym = matches.value_of("expr"); let target = matches.value_of("target").unwrap_or("json"); - let mut builder = - build::FileBuilder::new(std::env::current_dir().unwrap(), import_paths, cache); + let mut builder = build::FileBuilder::new( + std::env::current_dir().unwrap(), + import_paths, + cache, + registry, + ); builder.set_strict(strict); match registry.get_converter(target) { Some(converter) => { @@ -512,6 +525,7 @@ fn print_repl_help() { fn do_repl( import_paths: &Vec, cache: Rc>, + registry: &ConverterRegistry, ) -> std::result::Result<(), Box> { let config = rustyline::Config::builder(); let mut editor = rustyline::Editor::<()>::with_config( @@ -544,7 +558,8 @@ fn do_repl( } } } - let mut builder = build::FileBuilder::new(std::env::current_dir()?, import_paths, cache); + let mut builder = + build::FileBuilder::new(std::env::current_dir()?, import_paths, cache, registry); // loop let mut lines = ucglib::io::StatementAccumulator::new(); println!("Welcome to the UCG repl. Ctrl-D to exit"); @@ -607,8 +622,12 @@ fn do_repl( } } -fn repl(import_paths: &Vec, cache: Rc>) { - if let Err(e) = do_repl(import_paths, cache) { +fn repl( + import_paths: &Vec, + cache: Rc>, + registry: &ConverterRegistry, +) { + if let Err(e) = do_repl(import_paths, cache, registry) { eprintln!("{}", e); process::exit(1); } @@ -652,7 +671,7 @@ fn main() { } else if let Some(_) = app_matches.subcommand_matches("env") { env_help() } else if let Some(_) = app_matches.subcommand_matches("repl") { - repl(&import_paths, cache) + repl(&import_paths, cache, ®istry) } else if let Some(matches) = app_matches.subcommand_matches("fmt") { if let Err(e) = fmt_command(matches) { eprintln!("{}", e); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index eb825d0..0df32e5 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -841,6 +841,18 @@ make_fn!( ) ); +make_fn!( + print_statement, Statement>, + do_each!( + pos => pos, + _ => word!("convert"), + typ => wrap_err!(must!(match_type!(BAREWORD)), "Expected converter name"), + expr => wrap_err!(must!(expression), "Expected Expression to print"), + _ => must!(punct!(";")), + (Statement::Print(pos, typ.clone(), expr.clone())) + ) +); + //trace_macros!(true); fn statement(i: SliceIter) -> Result, Statement> { return either!( @@ -848,6 +860,7 @@ fn statement(i: SliceIter) -> Result, Statement> { trace_parse!(assert_statement), trace_parse!(let_statement), trace_parse!(out_statement), + trace_parse!(print_statement), trace_parse!(expression_statement) ); } diff --git a/src/tokenizer/mod.rs b/src/tokenizer/mod.rs index 1a0b042..2c3b699 100644 --- a/src/tokenizer/mod.rs +++ b/src/tokenizer/mod.rs @@ -325,6 +325,10 @@ make_fn!(outtok, do_text_token_tok!(TokenType::BAREWORD, "out", WS) ); +make_fn!(converttok, + do_text_token_tok!(TokenType::BAREWORD, "convert", WS) +); + make_fn!(astok, do_text_token_tok!(TokenType::BAREWORD, "as", WS) ); @@ -442,6 +446,7 @@ fn token<'a>(input: OffsetStrIter<'a>) -> Result, Token> { nottok, lettok, outtok, + converttok, selecttok, asserttok, failtok,