diff --git a/src/format_macro.rs b/src/format_macro.rs new file mode 100644 index 0000000000..865c5107f7 --- /dev/null +++ b/src/format_macro.rs @@ -0,0 +1,258 @@ +//! Implementation of the `format!` macro +use gluon_codegen::Trace; + +use crate::{ + base::{ + ast::{self, AstClone, SpannedExpr}, + pos, + symbol::{Symbol, Symbols}, + types::TypeCache, + }, + parser::parse_expr, + vm::macros::{self, Macro, MacroExpander, MacroFuture}, +}; + +/** + * Format macro with expressions embedded in the format string + * + * ```ignore + * let x = 1 + * format! "x is {x}, x + 1 is {x+1}" + * ``` + */ +#[derive(Trace)] +#[gluon(crate_name = "vm")] +pub struct Format; + +fn expr_from_path<'ast>( + arena: &mut ast::OwnedArena<'ast, Symbol>, + symbols: &mut Symbols, + span: pos::Span, + path_head: &str, + path_tail: &[&str], +) -> SpannedExpr<'ast, Symbol> { + let mut head = pos::spanned( + span, + ast::Expr::Ident(ast::TypedIdent::new(symbols.simple_symbol(path_head))), + ); + + for &item in path_tail { + head = pos::spanned( + span, + ast::Expr::Projection( + arena.alloc(head), + symbols.simple_symbol(item), + Default::default(), + ), + ); + } + + head +} + +async fn run_import<'a, 'ast>( + importer: &dyn Macro, + env: &mut MacroExpander<'a>, + symbols: &mut Symbols, + arena: &mut ast::OwnedArena<'ast, Symbol>, + span: pos::Span, + path: &[&str], +) -> Result, macros::Error> { + let (path_head, path_tail) = match path { + [head, tail @ ..] => (head, tail), + _ => return Err(macros::Error::message("run_import for empty path")), + }; + + let path = expr_from_path(arena, symbols, span, path_head, path_tail); + let args = arena.alloc_extend(vec![path]); + + match importer.expand(env, symbols, arena, args).await? { + macros::LazyMacroResult::Done(res) => Ok(res), + macros::LazyMacroResult::Lazy(f) => f().await, + } +} + +impl Macro for Format { + fn expand<'r, 'a: 'r, 'b: 'r, 'c: 'r, 'ast: 'r>( + &self, + env: &'b mut MacroExpander<'a>, + symbols: &'c mut Symbols, + arena: &'b mut ast::OwnedArena<'ast, Symbol>, + args: &'b mut [SpannedExpr<'ast, Symbol>], + ) -> MacroFuture<'r, 'ast> { + Box::pin(async move { + let arg = match args { + [arg] => (arg), + _ => { + return Err(macros::Error::message(format!( + "format! expects 1 argument" + ))) + } + }; + + let span = arg.span; + let sp = |e: ast::Expr<'ast, Symbol>| pos::spanned(span, e); + + let import = env + .vm + .get_macros() + .get("import") + .ok_or_else(|| macros::Error::message(format!("Cannot find import macro")))?; + + let show_module = + run_import(&*import, env, symbols, arena, span, &["std", "show"]).await?; + let show_module = arena.alloc(show_module); + let show_func = arena.alloc(sp(ast::Expr::Projection( + show_module, + symbols.simple_symbol("show"), + Default::default(), + ))); + + let semigroup_module = + run_import(&*import, env, symbols, arena, span, &["std", "semigroup"]).await?; + let semigroup_module = arena.alloc(semigroup_module); + let append_func = arena.alloc(sp(ast::Expr::Projection( + semigroup_module, + symbols.simple_symbol("append"), + Default::default(), + ))); + + let format_string = match &arg.value { + ast::Expr::Literal(ast::Literal::String(text)) => text, + _ => { + return Err(macros::Error::message(format!( + "format! expects a string argument" + ))) + } + }; + + let show_expr = |e: ast::SpannedExpr<'ast, Symbol>| { + let func = show_func.ast_clone(arena.borrow()); + + sp(ast::Expr::App { + func, + implicit_args: arena.alloc_extend(vec![]), + args: arena.alloc_extend(vec![e]), + }) + }; + + let app_exprs = |lhs, rhs| { + let func = append_func.ast_clone(arena.borrow()); + + sp(ast::Expr::App { + func, + implicit_args: arena.alloc_extend(vec![]), + args: arena.alloc_extend(vec![lhs, rhs]), + }) + }; + + let literal_expr = |val: String| sp(ast::Expr::Literal(ast::Literal::String(val))); + + let type_cache = TypeCache::new(); + + let mut remaining = format_string.as_str(); + + let mut result_expr = None; + + while let Some(find_result) = find_expr(remaining)? { + remaining = find_result.remaining; + + let sub_expr = parse_expr(arena.borrow(), symbols, &type_cache, find_result.expr) + .map_err(|err| { + macros::Error::message(format!( + "format! could not parse subexpression: {}", + err + )) + })?; + + let part_expr = app_exprs( + literal_expr(find_result.prefix.to_owned()), + show_expr(sub_expr), + ); + + result_expr = match result_expr.take() { + None => Some(part_expr), + Some(prev_expr) => Some(app_exprs(prev_expr, part_expr)), + }; + } + + let result_expr = match result_expr.take() { + None => literal_expr(remaining.to_owned()), + Some(result_expr) => { + if remaining.is_empty() { + result_expr + } else { + app_exprs(result_expr, literal_expr(remaining.to_owned())) + } + } + }; + + Ok(result_expr.into()) + }) + } +} + +const OPEN_BRACE: &'static str = "{"; +const CLOSE_BRACE: &'static str = "}"; + +struct FindExprResult<'a> { + prefix: &'a str, + expr: &'a str, + remaining: &'a str, +} + +fn find_expr<'a>(format_str: &'a str) -> Result>, macros::Error> { + let prefix_end_ix = match format_str.find(OPEN_BRACE) { + None => return Ok(None), + Some(ix) => ix, + }; + + let prefix = &format_str[..prefix_end_ix]; + let expr_start = OPEN_BRACE.len() + prefix_end_ix; + + let mut brace_depth = 1; + + let mut expr_end = expr_start; + let mut brace_end = expr_start; + + while brace_depth != 0 { + let next_open = format_str[brace_end..].find(OPEN_BRACE); + let next_close = format_str[brace_end..].find(CLOSE_BRACE); + + let (brace_ix, is_close) = match (next_open, next_close) { + (None, None) => break, + (None, Some(ix)) => (ix, true), + (Some(ix), None) => (ix, false), + (Some(open_ix), Some(close_ix)) => { + if open_ix < close_ix { + (open_ix, false) + } else { + (close_ix, true) + } + } + }; + + expr_end = brace_end + brace_ix; + brace_end = if is_close { + brace_depth -= 1; + expr_end + OPEN_BRACE.len() + } else { + brace_depth += 1; + expr_end + CLOSE_BRACE.len() + }; + } + + if brace_depth != 0 { + return Err(macros::Error::message(format!("mismatched braces"))); + } + + let expr = &format_str[expr_start..expr_end]; + + let remaining = &format_str[brace_end..]; + + Ok(Some(FindExprResult { + prefix, + expr, + remaining, + })) +} diff --git a/src/lib.rs b/src/lib.rs index a244db7726..48dd2710d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ pub mod compiler_pipeline; #[macro_use] pub mod import; pub mod lift_io; +pub mod format_macro; #[doc(hidden)] pub mod query; pub mod std_lib; @@ -958,6 +959,8 @@ impl VmBuilder { } macros.insert(String::from("lift_io"), lift_io::LiftIo); + + macros.insert(String::from("format"), format_macro::Format); } add_extern_module_with_deps(