From cd0400a1a24ca1bd96fb883589958dabece01154 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 22 Jan 2024 04:42:33 -0800 Subject: [PATCH] organize the converter --- crates/ast/ast.rs | 45 +- .../js_ast/convert/block_convert_context.rs | 244 +++++++ crates/js_ast/convert/convert_valid_block.rs | 37 + crates/js_ast/convert/convert_valid_expr.rs | 115 ++++ crates/js_ast/convert/convert_valid_if.rs | 38 ++ .../js_ast/convert/convert_valid_statement.rs | 32 + crates/js_ast/convert/hoist_block_to_expr.rs | 78 +++ .../js_ast/convert/hoist_block_to_js_block.rs | 68 ++ crates/js_ast/convert/hoist_expr.rs | 132 ++++ crates/js_ast/convert/hoist_if.rs | 113 ++++ .../{ => convert}/is_valid_js_expression.rs | 6 + crates/js_ast/convert/mod.rs | 42 ++ crates/js_ast/convert/process_function.rs | 63 ++ crates/js_ast/convert/process_statement.rs | 95 +++ .../side_effects.rs} | 3 +- .../convert/tree_shake_statement_list.rs | 31 + crates/js_ast/convert_valid_expr.rs | 204 ------ crates/js_ast/hoist.rs | 638 ------------------ crates/js_ast/lib.rs | 25 +- crates/js_printer/lib.rs | 14 + crates/js_printer/src/lib.rs | 16 - crates/js_printer/src/print_expression.rs | 276 -------- crates/js_printer/src/print_statement.rs | 153 ----- crates/parser/parse_block.rs | 10 +- crates/parser/parse_declaration.rs | 6 +- crates/parser/parse_expr_basic.rs | 2 +- crates/parser/parse_fn_declaration.rs | 88 +-- crates/parser/parse_statement.rs | 20 +- crates/parser/parser.rs | 4 +- crates/tokenizer/tokenizer.rs | 17 +- examples/basics/stupid.blight | 2 + 31 files changed, 1223 insertions(+), 1394 deletions(-) create mode 100644 crates/js_ast/convert/block_convert_context.rs create mode 100644 crates/js_ast/convert/convert_valid_block.rs create mode 100644 crates/js_ast/convert/convert_valid_expr.rs create mode 100644 crates/js_ast/convert/convert_valid_if.rs create mode 100644 crates/js_ast/convert/convert_valid_statement.rs create mode 100644 crates/js_ast/convert/hoist_block_to_expr.rs create mode 100644 crates/js_ast/convert/hoist_block_to_js_block.rs create mode 100644 crates/js_ast/convert/hoist_expr.rs create mode 100644 crates/js_ast/convert/hoist_if.rs rename crates/js_ast/{ => convert}/is_valid_js_expression.rs (88%) create mode 100644 crates/js_ast/convert/mod.rs create mode 100644 crates/js_ast/convert/process_function.rs create mode 100644 crates/js_ast/convert/process_statement.rs rename crates/js_ast/{has_side_effects.rs => convert/side_effects.rs} (95%) create mode 100644 crates/js_ast/convert/tree_shake_statement_list.rs delete mode 100644 crates/js_ast/convert_valid_expr.rs delete mode 100644 crates/js_ast/hoist.rs delete mode 100644 crates/js_printer/src/lib.rs delete mode 100644 crates/js_printer/src/print_expression.rs delete mode 100644 crates/js_printer/src/print_statement.rs create mode 100644 examples/basics/stupid.blight diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs index bdda2b34cd68a35b8c640bd1b18f1c6988ed6c5b..680c579aa79c8a3a0dd1cc19cc010ea8cd46cf7e 100644 --- a/crates/ast/ast.rs +++ b/crates/ast/ast.rs @@ -30,7 +30,7 @@ pub struct Param { #[derive(Debug, Clone)] -pub struct Decl { +pub struct VarDecl { pub name: String, pub data_type: Type, pub init: Expr, @@ -40,6 +40,17 @@ pub struct Decl { pub comment: Option, } +#[derive(Debug, Clone)] +pub struct FnDecl { + pub name: String, + pub return_type: Type, + pub params: Vec, + pub body: Expr, + pub is_pub: bool, + pub loc: Loc, + pub comment: Option, +} + #[derive(Debug, Clone)] pub struct Expr { pub data: ExprData, @@ -136,7 +147,7 @@ pub enum ExprData { name: String, params: Vec, return_type: Type, - block: Block, + body: Box, }, Call { base: Box, @@ -159,14 +170,16 @@ pub enum ExprData { #[derive(Debug, Clone)] pub enum Statement { Expr(Expr), - Decl(Decl), + VarDecl(VarDecl), + FnDecl(FnDecl), Comment(Comment), } #[derive(Debug, Clone)] pub enum TopLevelStatement { Expr(Expr), - Decl(Decl), + VarDecl(VarDecl), + FnDecl(FnDecl), Comment(Comment), } @@ -189,7 +202,8 @@ impl Statement { pub fn to_top_level(self) -> TopLevelStatement { match self { Statement::Expr(expr) => TopLevelStatement::Expr(expr), - Statement::Decl(decl) => TopLevelStatement::Decl(decl), + Statement::VarDecl(decl) => TopLevelStatement::VarDecl(decl), + Statement::FnDecl(decl) => TopLevelStatement::FnDecl(decl), Statement::Comment(comment) => TopLevelStatement::Comment(comment), } } @@ -198,7 +212,14 @@ impl Statement { pub fn require_semicolon(&self, semicolon_hint: bool) -> bool { match &self { Statement::Expr(_) => !semicolon_hint, - Statement::Decl(_) => true, + Statement::VarDecl(_) => true, + _ => false, + } + } + + pub fn is_expression(&self) -> bool { + match &self { + Statement::Expr(_) => true, _ => false, } } @@ -209,16 +230,18 @@ impl TopLevelStatement { pub fn require_semicolon(&self, earlier_hint: bool) -> bool { match &self { TopLevelStatement::Expr(_) => !earlier_hint, - TopLevelStatement::Decl(_) => true, + TopLevelStatement::VarDecl(_) => true, + TopLevelStatement::FnDecl(_) => false, _ => false, } } - pub fn to_statement(self) -> Statement { + pub fn to_statement(self) -> Option { match self { - TopLevelStatement::Expr(expr) => Statement::Expr(expr), - TopLevelStatement::Decl(decl) => Statement::Decl(decl), - TopLevelStatement::Comment(comment) => Statement::Comment(comment), + TopLevelStatement::Expr(expr) => Some(Statement::Expr(expr)), + TopLevelStatement::VarDecl(decl) => Some(Statement::VarDecl(decl)), + TopLevelStatement::FnDecl(decl) => Some(Statement::FnDecl(decl)), + TopLevelStatement::Comment(comment) => Some(Statement::Comment(comment)), } } } diff --git a/crates/js_ast/convert/block_convert_context.rs b/crates/js_ast/convert/block_convert_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..de41e77cae5f13c2d98b6215e925fd637a9bebb6 --- /dev/null +++ b/crates/js_ast/convert/block_convert_context.rs @@ -0,0 +1,244 @@ +use crate::js_ast::*; +use ast::*; +use std::collections::{HashMap, HashSet}; +use std::vec; +use tokenizer::Loc; + +use super::tree_shake_statement_list; + +/// A block convert context keeps track of state while converting non-valid JS expressions to JS +pub struct BlockConvertContext { + /// Statements in this block context + /// This may be the merged contents of multiple blocks + statements: Vec, + ids: HashSet, + next_id: u32, + scopes: Vec, + /// If the last item in `statements` is actually valid + has_last_statement: bool, +} + +struct VirtualScope { + renames: HashMap, +} + +pub struct TempId { + unmapped_name: String, +} + +pub struct StatementHandle { + index: Option, +} + +impl StatementHandle { + pub fn delete(self, ctx: &mut BlockConvertContext) { + if let Some(index) = self.index { + ctx.statements.remove(index as usize); + } + } +} + +impl TempId { + pub fn get(&self, hoist_context: &BlockConvertContext) -> String { + hoist_context.remap_identifier(&self.unmapped_name) + } +} + +impl BlockConvertContext { + pub fn new() -> Self { + Self { + statements: Vec::new(), + ids: HashSet::new(), + next_id: 0, + scopes: vec![VirtualScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + pub fn new_child(&self) -> Self { + Self { + statements: Vec::new(), + ids: self.ids.clone(), + next_id: 0, + scopes: vec![VirtualScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + /// A virtual scope is a scope that exists in blight syntax + /// but may or may not exist in blight. With these it is important to + pub fn push_virtual_scope(&mut self) { + self.scopes.push(VirtualScope { + renames: HashMap::new(), + }); + } + + /// See `push_virtual_scope` for more information on what this is. + pub fn pop_virtual_scope(&mut self) { + self.scopes.pop(); + debug_assert!(self.scopes.len() > 0); + } + + pub fn generate_temp_id(&mut self, basename: &str) -> TempId { + self.next_id += 1; + loop { + let name = format!("{}{}", basename, self.next_id); + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + self.next_id += 1; + } + } + + /// This does some extra optimizations: + /// - `let a = b;` will not be emitted but instad write a variable alias + pub fn add_statement(&mut self, statement: JsStatement) -> StatementHandle { + match statement.data { + JsStatementData::Declaration(JSDeclaration { + name, + init: + Some(JsExpr { + data: JsExprData::Identifier(init_name), + .. + }), + is_constant: true, + }) => { + self.scopes + .last_mut() + .unwrap() + .renames + .insert(name, init_name); + self.has_last_statement = false; + return StatementHandle { index: None }; + } + _ => {} + } + self.statements.push(statement); + self.has_last_statement = true; + StatementHandle { + index: Some(self.statements.len() as u32 - 1), + } + } + + /// Get the last statement if it exists. + /// If the statement was optimized away, this returns None + pub fn last_statement(&self) -> Option<&JsStatement> { + if self.has_last_statement { + Some(self.statements.last().unwrap()) + } else { + None + } + } + + pub fn generate_id(&mut self, basename: &String) -> TempId { + let mut id = 0u32; + loop { + let name = match id { + 0 => basename.clone(), + _ => format!("{}{}", basename, id), + }; + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + id += 1; + } + } + + /// Given a blight identifier, remap it to a new identifier. + pub fn remap_identifier(&self, name: &String) -> String { + for scope in self.scopes.iter().rev() { + if let Some(remapped) = scope.renames.get(name) { + return remapped.clone(); + } + } + name.clone() + } + + /// Declare an identifier in the current scope. + pub fn declare_identifier(&mut self, name: String) -> TempId { + let remapped = self.generate_id(&name); + let scope = self.scopes.last_mut().unwrap(); + self.ids.insert(remapped.unmapped_name.clone()); + scope.renames.insert(name, remapped.unmapped_name.clone()); + remapped + } + + pub fn take_last_statement(&mut self) -> Option { + if !self.has_last_statement { + return None; + } + self.has_last_statement = false; + Some(self.statements.pop().unwrap()) + } + + pub fn take_last_statement_if_expr(&mut self) -> Option { + if !self.has_last_statement { + return None; + } + match self.statements.last().unwrap().data { + JsStatementData::Expression(_) => { + self.has_last_statement = false; + match self.statements.pop().unwrap().data { + JsStatementData::Expression(expr) => Some(expr), + _ => unreachable!(), + } + } + _ => return None, + } + } +} + +pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { + JsStatement::new( + JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), + loc, + None, + ) +} + +pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { + JsExpr::new( + JsExprData::Binary { + op: BinaryOperator::Assign, + left: Box::new(JsExpr::new( + JsExprData::Identifier(target), + loc.clone(), + None, + )), + right: Box::new(expr), + }, + loc, + None, + ) +} + +impl Into for BlockConvertContext { + fn into(self) -> JsBlock { + JsBlock::new(self.into(), Loc::EMPTY) + } +} + +impl Into for BlockConvertContext { + fn into(self) -> JsModule { + JsModule { + statements: self.into(), + } + } +} + +impl Into> for BlockConvertContext { + fn into(mut self) -> Vec { + tree_shake_statement_list(&mut self.statements); + self.statements + } +} diff --git a/crates/js_ast/convert/convert_valid_block.rs b/crates/js_ast/convert/convert_valid_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb40111073d4e3ea44604c0117ed7419b879935f --- /dev/null +++ b/crates/js_ast/convert/convert_valid_block.rs @@ -0,0 +1,37 @@ +use super::convert_valid_statement_to_expr; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn convert_valid_block(ctx: &mut BlockConvertContext, block: Block, loc: Loc) -> JsExpr { + debug_assert!( + block.is_valid_js_expression(), + "must call convert_valid_block with valid js expression" + ); + + let len = block.statements.len(); + let loc = loc; + let block_expr = match len { + 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), + // a block expression with a single expression is just that expression + 1 => convert_valid_statement_to_expr(ctx, block.statements.into_iter().next().unwrap()), + // Since blocks are not valid expressions in JS, we create a + // comma expression from a block with more than one statement. + 2.. => { + let mut items = Vec::new(); + for statement in block.statements.into_iter() { + items.push(convert_valid_statement_to_expr(ctx, statement)); + } + JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) + } + }; + + // a block without implicit return is to be type void, so we wrap in a void expression + if !block.has_implicit_return && len > 0 { + JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) + } else { + block_expr + } +} diff --git a/crates/js_ast/convert/convert_valid_expr.rs b/crates/js_ast/convert/convert_valid_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ba30c694c43c60d8c543c4b64be40ac6c7a32c6 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_expr.rs @@ -0,0 +1,115 @@ +use super::convert_valid_block; +use super::BlockConvertContext; +use crate::convert::convert_valid_if; +use crate::convert::process_function::process_function_to_arrow; +use crate::convert::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; + +/// Translate a blight ast expression that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_expr(ctx: &mut BlockConvertContext, expr: Expr) -> JsExpr { + // Assert may be skipped in release builds as all failures are + // annotated with a release-checked panic + debug_assert!( + expr.is_valid_js_expression(), + "must call convert_valid_expr with valid js expression (use hoist instead)" + ); + + let loc = expr.loc; + let comment = expr.comment; + + match expr.data { + ExprData::Identifier(str) => JsExpr::new( + JsExprData::Identifier(ctx.remap_identifier(&str)), + loc, + comment, + ), + + ExprData::Literal(Literal::Bool(value)) => { + JsExpr::new(JsExprData::Bool(value), loc, comment) + } + ExprData::Literal(Literal::Number(value)) => { + JsExpr::new(JsExprData::Number(value), loc, comment) + } + ExprData::Literal(Literal::String(value)) => { + JsExpr::new(JsExprData::String(value), loc, comment) + } + ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), + + ExprData::Block(block) => convert_valid_block(ctx, block, loc), + + ExprData::Binary { op, left, right } => { + let left = convert_valid_expr(ctx, *left); + let right = convert_valid_expr(ctx, *right); + JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + loc, + comment, + ) + } + ExprData::Unary { op, value } => { + let value = convert_valid_expr(ctx, *value); + JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + loc, + comment, + ) + } + ExprData::Fn { body, params, .. } => { + process_function_to_arrow(ctx, *body, params, loc, comment) + } + ExprData::Builtin { id, arguments } => match id { + BuiltinId::Raw => { + let first_arg = arguments.iter().next().unwrap(); + // TODO(@paperdave): this string clone is stupid + let str = match first_arg.data.clone() { + ExprData::Literal(Literal::String(s)) => s, + _ => panic!("raw expects a string"), + }; + JsExpr::new(JsExprData::Raw(str), loc, comment) + } + }, + ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), + + ExprData::Call { base, arguments } => { + let base = convert_valid_expr(ctx, *base); + let arguments = arguments + .into_iter() + .map(|x| convert_valid_expr(ctx, x)) + .collect(); + JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + loc, + comment, + ) + } + + ExprData::Field { base, field } => JsExpr::new( + match field.data { + ExprData::Literal(Literal::String(name)) => JsExprData::Dot { + base: Box::new(convert_valid_expr(ctx, *base)), + name, + }, + _ => JsExprData::Index { + base: Box::new(convert_valid_expr(ctx, *base)), + index: Box::new(convert_valid_expr(ctx, *field)), + }, + }, + loc, + comment, + ), + + ExprData::Return(_) => unreachable!("convert_valid_expr called on return"), + } +} diff --git a/crates/js_ast/convert/convert_valid_if.rs b/crates/js_ast/convert/convert_valid_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..1e71398123c950b76f1f1de7e6e3088f3995fb79 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_if.rs @@ -0,0 +1,38 @@ +use super::convert_valid_block; +use super::convert_valid_expr; +use super::BlockConvertContext; +use crate::convert::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +/// Convert a valid blight if statement to a JS expression +pub fn convert_valid_if( + ctx: &mut BlockConvertContext, + expr: If, + loc: Loc, + comment: Option, +) -> JsExpr { + debug_assert!( + expr.is_valid_js_expression(), + "convert_valid_if on an invalid expression (use hoist instead)" + ); + let condition = convert_valid_expr(ctx, *expr.condition); + + let yes = convert_valid_block(ctx, expr.yes.block, expr.yes.loc); + let no = match expr.no { + Some(BlockOrIf::Block(b)) => Some(convert_valid_block(ctx, b.block, b.loc)), + Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), + None => None, + }; + + JsExpr::new( + JsExprData::Ternary { + condition: Box::new(condition), + yes: Box::new(yes), + no: no.map(Box::new), + }, + loc, + comment, + ) +} diff --git a/crates/js_ast/convert/convert_valid_statement.rs b/crates/js_ast/convert/convert_valid_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..f02fb4e407e920b50233e188a1f14fb6fbc72b24 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_statement.rs @@ -0,0 +1,32 @@ +use super::convert_valid_expr; +use super::is_valid_js_expression::*; +use super::BlockConvertContext; +use crate::js_ast::*; +use ast::*; + +/// Translate a blight ast statement that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_statement_to_expr( + ctx: &mut BlockConvertContext, + statement: Statement, +) -> JsExpr { + debug_assert!( + statement.is_valid_js_expression(), + "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" + ); + + match statement { + ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), + _ => unreachable!("attempt to convert statement to js without hoisting"), + } +} + +/// Translate a blight ast statement that is a valid JS expression, to a JS statement +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +/// +/// TODO(@paperdave): Replace this with Into for JsExpr +pub fn convert_valid_statement(ctx: &mut BlockConvertContext, statement: Statement) -> JsStatement { + let expr = convert_valid_statement_to_expr(ctx, statement); + let loc = expr.loc.clone(); + JsStatement::new(JsStatementData::Expression(expr), loc, None) +} diff --git a/crates/js_ast/convert/hoist_block_to_expr.rs b/crates/js_ast/convert/hoist_block_to_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa419cedde552f566140b6474ddf08bee0f84bbd --- /dev/null +++ b/crates/js_ast/convert/hoist_block_to_expr.rs @@ -0,0 +1,78 @@ +use super::is_valid_js_expression::*; +use super::process_statement; +use super::BlockConvertContext; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +/// This block hoist is used when we have a block in an expression context. +/// +/// All the expressions in the block are lifted above the currently +/// processed expression, and then a temporary variable is returned. +/// (or the last expression might get inlined as the return value) +/// +/// A "virtual scope" is created for this block, which handles renaming +/// situations like: +/// +/// let x = 1; +/// { +/// let x = 1; +/// } +/// call(x) +/// +/// The output JS of the above would be: +/// +/// const x = 1; +/// const x2 = 1; +/// call(x) +/// +/// For expressions that have branches, use `hoist_block_to_js_block` +pub fn hoist_block_to_expr( + ctx: &mut BlockConvertContext, + block: Block, + loc: Loc, + comment: Option, +) -> Option { + debug_assert!( + !block.is_valid_js_expression(), + "hoist_block on a valid expression, should call convert_valid_block instead" + ); + + let temporary = ctx.generate_temp_id("_block"); + + ctx.push_virtual_scope(); + + for statement in block.statements { + process_statement(ctx, statement); + } + + ctx.pop_virtual_scope(); + + // If the last statement is an expression, we need + // to assign to the temporary variable. + let result = match ctx.last_statement().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) if block.has_implicit_return => { + match ctx.take_last_statement().unwrap().data { + JsStatementData::Expression(expr) => expr, + _ => unreachable!(), + } + } + _ => JsExpr::new(JsExprData::Null, loc.clone(), None), + }; + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: temporary.get(&ctx), + init: Some(result), + is_constant: false, + }), + loc.clone(), + comment, + )); + + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&ctx)), + loc, + None, + )) +} diff --git a/crates/js_ast/convert/hoist_block_to_js_block.rs b/crates/js_ast/convert/hoist_block_to_js_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..2dd10483b209d1d999b593d2763cb4b3b9109c2c --- /dev/null +++ b/crates/js_ast/convert/hoist_block_to_js_block.rs @@ -0,0 +1,68 @@ +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +use super::*; + +/// This block hoist is used when we actually need to create a JsBlock, which +/// is needed if there is an invalid expression within a branch of code. +/// +/// For example, `if` has two branches, the 'yes' and 'no' paths. +/// When we cannot lower it as a ternary, we have to create two JsBlocks +/// +/// The `target` param is a shared temporary variable that will be assigned +/// to the result of the block. +/// +/// Since blocks can be void or noreturn, this target is not always +/// assigned to. The `did_assign` flag is used to indicate that. +/// +/// For expressions that do not have branches, use `hoist_block_to_expr` +/// +/// TODO(@paperdave): move `did_assign` into the return value +pub fn hoist_block_to_js_block( + ctx: &mut BlockConvertContext, + block: Block, + loc: Loc, + target: &TempId, + did_assign: &mut bool, +) -> JsBlock { + // A non-block will become a block with a single expression: + // _temp = expr; + // + // We will also apply this transformation to blocks that are valid + // expressions, because that is more concise. + if block.is_valid_js_expression() { + return JsBlock::new( + vec![new_js_assignment_statement( + convert_valid_block(ctx, block, loc.clone()), + target.get(ctx), + loc.clone(), + )], + loc, + ); + } + + let mut child = ctx.new_child(); + + for statement in block.statements { + if let None = process_statement(&mut child, statement) { + return child.into(); + }; + } + + // If the last statement is an expression, we need + // to assign to the temporary variable. + match child.take_last_statement_if_expr() { + Some(expr) => { + *did_assign = true; + child.add_statement(new_js_assignment_statement( + expr, + target.get(ctx), + loc.clone(), + )); + } + _ => {} + } + + child.into() +} diff --git a/crates/js_ast/convert/hoist_expr.rs b/crates/js_ast/convert/hoist_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..a79cb5a9928c6a95e0e49e9e9a18ff44ffd77646 --- /dev/null +++ b/crates/js_ast/convert/hoist_expr.rs @@ -0,0 +1,132 @@ +use super::convert_valid_expr; +use super::hoist_block_to_expr; +use super::hoist_if_expr; +use super::is_valid_js_expression::*; +use super::BlockConvertContext; +// use super::HasSideEffects; +use crate::js_ast::*; +use crate::util::is_js_identifier; +use ast::*; + +/// Pushes a decl with a generated identifier to the hoisted result +/// and returns an identifier expression for it. +pub fn hoist_expr_if_needed(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + // if !expr.has_side_effects() { + // return Some(ctx.hoist_expression_contents(expr)?); + // } + + let init = hoist_expr_contents(ctx, expr)?; + + let id = ctx.generate_temp_id("_"); + let loc = init.loc.clone(); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: id.get(&ctx), + init: Some(init), + is_constant: true, + }), + loc.clone(), + None, + )); + + Some(JsExpr::new(JsExprData::Identifier(id.get(&ctx)), loc, None)) +} + +/// Returning `None` from this means that the expression is noreturn, and that +/// the rest of the statement never occurs and can be stripped. +pub fn hoist_expr_contents(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + if expr.is_valid_js_expression() { + return Some(convert_valid_expr(ctx, expr)); + } + + match expr.data { + ExprData::Binary { op, left, right } => { + let left = hoist_expr_if_needed(ctx, *left)?; + let right = hoist_expr_if_needed(ctx, *right)?; + + Some(JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Unary { op, value } => { + let value = hoist_expr_if_needed(ctx, *value)?; + Some(JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Field { base, field } => { + let base = hoist_expr_if_needed(ctx, *base)?; + let field = hoist_expr_if_needed(ctx, *field)?; + + Some(JsExpr::new( + match field.data { + JsExprData::String(name) if is_js_identifier(name.as_str()) => { + JsExprData::Dot { + base: Box::new(base), + name, + } + } + _ => JsExprData::Index { + base: Box::new(base), + index: Box::new(field), + }, + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Block(block) => hoist_block_to_expr(ctx, block, expr.loc, expr.comment), + + ExprData::If(_) => hoist_if_expr(ctx, expr), + + ExprData::Return(expr) => { + let loc = expr.loc.clone(); + let return_value = hoist_expr_if_needed(ctx, *expr)?; + ctx.add_statement(JsStatement::new( + JsStatementData::Return(return_value), + loc, + None, + )); + None + } + + ExprData::Call { base, arguments } => { + let base = hoist_expr_if_needed(ctx, *base)?; + let arguments = arguments + .into_iter() + .map(|x| hoist_expr_if_needed(ctx, x)) + .collect::>>()?; + + Some(JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Fn { .. } + | ExprData::Builtin { .. } + | ExprData::Literal(_) + | ExprData::Identifier(_) => { + unreachable!("attempt to hoist an expression that does not require it") + } + } +} diff --git a/crates/js_ast/convert/hoist_if.rs b/crates/js_ast/convert/hoist_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..cbe2fffe3866392543f3d7c67f1fddf3b9fa44f0 --- /dev/null +++ b/crates/js_ast/convert/hoist_if.rs @@ -0,0 +1,113 @@ +use super::convert_valid_expr; +use super::hoist_block_to_js_block; +use super::hoist_expr_if_needed; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use super::TempId; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn hoist_if_expr(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + let data = match expr.data { + ExprData::If(data) => data, + _ => unreachable!("hoist_if_expr called on non-if expression"), + }; + + let temporary = ctx.generate_temp_id("_if"); + + let decl = ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: temporary.get(&ctx), + init: None, + is_constant: false, + }), + expr.loc.clone(), + None, + )); + + let condition = hoist_expr_if_needed(ctx, *data.condition)?; + let mut did_assign = false; + + let js_if = hoist_if_branch( + ctx, + data.yes, + data.no, + condition, + &mut did_assign, + &temporary, + ); + + ctx.add_statement(JsStatement::new( + JsStatementData::If(js_if), + expr.loc.clone(), + None, + )); + + if did_assign { + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&ctx)), + expr.loc, + expr.comment, + )) + } else { + decl.delete(ctx); + Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) + } +} + +pub fn hoist_if_branch( + ctx: &mut BlockConvertContext, + yes: BlockWithLoc, + no: Option, + condition: JsExpr, + did_assign: &mut bool, + temporary: &TempId, +) -> JsIf { + let yes = hoist_block_to_js_block(ctx, yes.block, yes.loc, &temporary, did_assign); + + let no = match no { + Some(BlockOrIf::Block(no)) => Some(JsElse::Block(hoist_block_to_js_block( + ctx, no.block, no.loc, &temporary, did_assign, + ))), + Some(BlockOrIf::If(else_if)) => { + // If the condition of this block has a side effect, we need to + // emit a block so that we can assign to the temporary variable. + // + // Otherwise, we can just emit the condition inline + if else_if.condition.is_valid_js_expression() { + let condition = convert_valid_expr(ctx, *else_if.condition.clone()); + Some(JsElse::If(Box::new(hoist_if_branch( + ctx, + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )))) + } else { + let mut temp_block = ctx.new_child(); + let condition = + hoist_expr_if_needed(&mut temp_block, *else_if.condition.clone()).unwrap(); + + temp_block.add_statement(JsStatement::new( + JsStatementData::If(hoist_if_branch( + ctx, + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )), + Loc::EMPTY, + None, + )); + + Some(JsElse::Block(temp_block.into())) + } + } + None => None, + }; + + JsIf { condition, yes, no } +} diff --git a/crates/js_ast/is_valid_js_expression.rs b/crates/js_ast/convert/is_valid_js_expression.rs similarity index 88% rename from crates/js_ast/is_valid_js_expression.rs rename to crates/js_ast/convert/is_valid_js_expression.rs index 382b8712e3f33dc10e713d382e8859352e74f8f6..feb2f206c2daf10b6be3f68358ad4097788adc08 100644 --- a/crates/js_ast/is_valid_js_expression.rs +++ b/crates/js_ast/convert/is_valid_js_expression.rs @@ -1,5 +1,11 @@ use ast::*; +/// "Valid" in this context means that the expression does not contain JS statements +/// Meaning conversion is a direct map from a blight as node into a JS one +/// +/// A valid expression should not go through any hoist logic, but if an expression +/// isn't valid, then it means that it must be split into multiple JS statements +/// inside of a block. pub trait IsValidJSExpression { /// Can this be lowered to a JS ast without hoisting items /// ex: a `return` expression is not a valid JS expression diff --git a/crates/js_ast/convert/mod.rs b/crates/js_ast/convert/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..870912a6c10fe558c69cde35f714fd9961f9497a --- /dev/null +++ b/crates/js_ast/convert/mod.rs @@ -0,0 +1,42 @@ +mod block_convert_context; +mod convert_valid_block; +mod convert_valid_expr; +mod convert_valid_if; +mod convert_valid_statement; +mod hoist_block_to_expr; +mod hoist_block_to_js_block; +mod hoist_expr; +mod hoist_if; +mod is_valid_js_expression; +mod process_function; +mod process_statement; +mod side_effects; +mod tree_shake_statement_list; + +pub use block_convert_context::*; +pub use convert_valid_block::*; +pub use convert_valid_expr::*; +pub use convert_valid_if::*; +pub use convert_valid_statement::*; +pub use hoist_block_to_expr::*; +pub use hoist_block_to_js_block::*; +pub use hoist_expr::*; +pub use hoist_if::*; +pub use is_valid_js_expression::*; +pub use process_function::*; +pub use process_statement::*; +// pub use side_effects::*; +pub use tree_shake_statement_list::*; + +use crate::js_ast::JsModule; +use ast::Module; + +pub fn blight_to_js(blight: Module) -> JsModule { + let mut root_context = BlockConvertContext::new(); + + for statement in blight.statements { + process_top_level_statement(&mut root_context, statement).unwrap(); + } + + root_context.into() +} diff --git a/crates/js_ast/convert/process_function.rs b/crates/js_ast/convert/process_function.rs new file mode 100644 index 0000000000000000000000000000000000000000..d45090b7889d25c279447ce1dfc3bd7f3297b19e --- /dev/null +++ b/crates/js_ast/convert/process_function.rs @@ -0,0 +1,63 @@ +use super::convert_valid_expr; +use super::hoist_expr_if_needed; +use super::process_statement; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn process_function_to_arrow( + ctx: &mut BlockConvertContext, + body: Expr, + params: Vec, + loc: Loc, + comment: Option, +) -> JsExpr { + // A block that is a valid expression will just become + // that expression. We can convert multiple expressions + // into a comma expression. + let body = if body.is_valid_js_expression() { + JsArrowBody::Expr(convert_valid_expr(ctx, body)) + } else { + let mut child = ctx.new_child(); + + match body.data { + ExprData::Block(block) => { + for statement in block.statements { + if let None = process_statement(&mut child, statement) { + break; + }; + } + + if let Some(expr) = child.take_last_statement_if_expr() { + child.add_statement(JsStatement::new( + JsStatementData::Return(expr), + loc.clone(), + None, + )); + } + } + _ => { + if let Some(expr) = hoist_expr_if_needed(&mut child, body) { + child.add_statement(JsStatement::new( + JsStatementData::Return(expr), + loc.clone(), + None, + )); + } + } + } + + JsArrowBody::Block(child.into()) + }; + + JsExpr::new( + JsExprData::ArrowFunction { + params: params.into_iter().map(|x| x.name).collect(), + body: Box::new(body), + }, + loc.clone(), + comment.map(|x| x.into()), + ) +} diff --git a/crates/js_ast/convert/process_statement.rs b/crates/js_ast/convert/process_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..f2cdaabb1636d75a56592e38f641a90d48b3aa58 --- /dev/null +++ b/crates/js_ast/convert/process_statement.rs @@ -0,0 +1,95 @@ +use super::convert_valid_statement; +use super::hoist_expr_contents; +use super::process_function_to_arrow; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; + +pub fn process_statement(ctx: &mut BlockConvertContext, statement: Statement) -> Option<()> { + if statement.is_valid_js_expression() { + let result = convert_valid_statement(ctx, statement); + ctx.add_statement(result); + return Some(()); + } + + match statement { + Statement::VarDecl(decl) => { + // visit init before name, this way allows for: + // + // let a = a; + // + // and `a` will be looked up before it is re-declared. + // we will also return in the noreturn case + let init = hoist_expr_contents(ctx, decl.init)?; + + let name = ctx.declare_identifier(decl.name); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: name.get(&ctx), + init: Some(init), + is_constant: decl.is_constant, + }), + decl.loc, + decl.comment.map(|x| x.into()), + )); + + Some(()) + } + Statement::Expr(expr) => { + if let Some(expr) = hoist_expr_contents(ctx, expr) { + let loc = expr.loc.clone(); + ctx.add_statement(JsStatement::new( + JsStatementData::Expression(expr), + loc, + None, + )); + Some(()) + } else { + None + } + } + Statement::FnDecl(FnDecl { + name, + params, + body, + loc, + comment, + .. + }) => { + let name = ctx.declare_identifier(name); + + let processed = Some(process_function_to_arrow( + ctx, + body, + params, + loc.clone(), + None, + )); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: name.get(&ctx), + init: processed, + is_constant: true, + }), + loc, + comment.map(|x| x.into()), + )); + + Some(()) + } + Statement::Comment(_) => Some(()), + } +} + +pub fn process_top_level_statement( + ctx: &mut BlockConvertContext, + statement: TopLevelStatement, +) -> Option<()> { + match statement.to_statement() { + Some(stmt) => process_statement(ctx, stmt), + None => todo!("process_top_level_statement"), + } +} diff --git a/crates/js_ast/has_side_effects.rs b/crates/js_ast/convert/side_effects.rs similarity index 95% rename from crates/js_ast/has_side_effects.rs rename to crates/js_ast/convert/side_effects.rs index 81c16730f2cd157a8f1b9236588bea0a05854a96..5eeaea8a53069fe637943bc72add4cb5c202fcc5 100644 --- a/crates/js_ast/has_side_effects.rs +++ b/crates/js_ast/convert/side_effects.rs @@ -43,8 +43,9 @@ impl HasSideEffects for Statement { fn has_side_effects(&self) -> bool { match self { Statement::Expr(expr) => expr.has_side_effects(), - Statement::Decl(decl) => decl.init.has_side_effects(), + Statement::VarDecl(decl) => decl.init.has_side_effects(), Statement::Comment(_) => false, + Statement::FnDecl(_) => false, } } } diff --git a/crates/js_ast/convert/tree_shake_statement_list.rs b/crates/js_ast/convert/tree_shake_statement_list.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7a631e29c443f71ddfe2409eb400df6ebe534cc --- /dev/null +++ b/crates/js_ast/convert/tree_shake_statement_list.rs @@ -0,0 +1,31 @@ +use crate::js_ast::*; + +/// Removes statements that are not needed from a JS block. Very limited. +/// +/// BlockConvertContext will automatically execute this when converting +/// itself into a Statement list or other data structure. +/// +/// What this does: +/// - Remove discarded literals `null` -> `` +pub fn tree_shake_statement_list(list: &mut Vec) { + let mut set_i = 0; + for i in 0..list.len() { + let stmt = list.get(i).unwrap(); + let should_keep = match &stmt.data { + JsStatementData::Expression(expr) => !expr.is_literal(), + JsStatementData::Return(_) => true, + JsStatementData::Export(_) => true, + JsStatementData::Declaration(_) => true, + JsStatementData::Function(_) => true, + JsStatementData::If(_) => true, + }; + + if should_keep { + if set_i != i { + list.swap(i, set_i) + } + set_i += 1; + } + } + list.truncate(set_i) +} diff --git a/crates/js_ast/convert_valid_expr.rs b/crates/js_ast/convert_valid_expr.rs deleted file mode 100644 index 90e13ddc003e4066d45034cba29c8b3f4d438432..0000000000000000000000000000000000000000 --- a/crates/js_ast/convert_valid_expr.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! "Valid" in this context means that the expression does not contain JS statements -//! Meaning conversion is a direct map from a blight as node into a JS one -//! -//! See `is_valid_js_expression` for more details on what qualifies -//! -//! For other nodes, it must hoist statements to the nearest JS block, -//! is done in the `hoist_*` functions in `host.rs`. - -use ast::{BlockOrIf, Comment, ExprData, If}; -use ast::{BlockWithLoc, BuiltinId, Expr, Literal, Statement}; -use tokenizer::Loc; - -use crate::is_valid_js_expression::IsValidJSExpression; -use crate::js_ast::*; -use crate::HoistContext; - -/// Translate a blight ast expression that is a valid JS expression, to a JS expression -/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` -pub fn convert_valid_expr(ctx: &mut HoistContext, expr: Expr) -> JsExpr { - // Assert may be skipped in release builds as all failures are - // annotated with a release-checked panic - debug_assert!( - expr.is_valid_js_expression(), - "must call convert_valid_expr with valid js expression (use hoist instead)" - ); - - let loc = expr.loc; - let comment = expr.comment; - - match expr.data { - ExprData::Identifier(str) => JsExpr::new( - JsExprData::Identifier(ctx.remap_identifier(&str)), - loc, - comment, - ), - - ExprData::Literal(Literal::Bool(value)) => { - JsExpr::new(JsExprData::Bool(value), loc, comment) - } - ExprData::Literal(Literal::Number(value)) => { - JsExpr::new(JsExprData::Number(value), loc, comment) - } - ExprData::Literal(Literal::String(value)) => { - JsExpr::new(JsExprData::String(value), loc, comment) - } - ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), - - ExprData::Block(block) => convert_valid_block(ctx, BlockWithLoc { block, loc }), - - ExprData::Binary { op, left, right } => { - let left = convert_valid_expr(ctx, *left); - let right = convert_valid_expr(ctx, *right); - JsExpr::new( - JsExprData::Binary { - op, - left: Box::new(left), - right: Box::new(right), - }, - loc, - comment, - ) - } - ExprData::Unary { op, value } => { - let value = convert_valid_expr(ctx, *value); - JsExpr::new( - JsExprData::Unary { - op, - value: Box::new(value), - }, - loc, - comment, - ) - } - ExprData::Fn { block, params, .. } => JsExpr::new( - JsExprData::ArrowFunction { - params: params.into_iter().map(|x| x.name).collect(), - body: Box::new(ctx.hoist_block_to_arrow_fn_body(BlockWithLoc { - block, - loc: loc.clone(), - })), - }, - loc, - comment, - ), - ExprData::Builtin { id, arguments } => match id { - BuiltinId::Raw => { - let first_arg = arguments.iter().next().unwrap(); - // TODO(@paperdave): this string clone is stupid - let str = match first_arg.data.clone() { - ExprData::Literal(Literal::String(s)) => s, - _ => panic!("raw expects a string"), - }; - JsExpr::new(JsExprData::Raw(str), loc, comment) - } - }, - ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), - - ExprData::Call { base, arguments } => { - let base = convert_valid_expr(ctx, *base); - let arguments = arguments - .into_iter() - .map(|x| convert_valid_expr(ctx, x)) - .collect(); - JsExpr::new( - JsExprData::Call { - base: Box::new(base), - arguments, - }, - loc, - comment, - ) - } - - ExprData::Field { base, field } => JsExpr::new( - match field.data { - ExprData::Literal(Literal::String(name)) => JsExprData::Dot { - base: Box::new(convert_valid_expr(ctx, *base)), - name, - }, - _ => JsExprData::Index { - base: Box::new(convert_valid_expr(ctx, *base)), - index: Box::new(convert_valid_expr(ctx, *field)), - }, - }, - loc, - comment, - ), - - ExprData::Return(_) => unreachable!("convert_valid_expr does not support return"), - } -} - -pub fn convert_valid_block(ctx: &mut HoistContext, b: BlockWithLoc) -> JsExpr { - debug_assert!( - b.is_valid_js_expression(), - "must call convert_valid_block with valid js expression" - ); - - let len = b.block.statements.len(); - let loc = b.loc; - let block_expr = match len { - 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), - // a block expression with a single expression is just that expression - 1 => convert_valid_statement_to_expr(ctx, b.block.statements.into_iter().next().unwrap()), - // Since blocks are not valid expressions in JS, we create a - // comma expression from a block with more than one statement. - 2.. => { - let mut items = Vec::new(); - for statement in b.block.statements.into_iter() { - items.push(convert_valid_statement_to_expr(ctx, statement)); - } - JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) - } - }; - - // a block without implicit return is to be type void, so we wrap in a void expression - if !b.block.has_implicit_return && len > 0 { - JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) - } else { - block_expr - } -} - -pub fn convert_valid_statement_to_expr(ctx: &mut HoistContext, statement: Statement) -> JsExpr { - debug_assert!( - statement.is_valid_js_expression(), - "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" - ); - - match statement { - ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), - _ => unreachable!("attempt to convert statement to js without hoisting"), - } -} - -pub fn convert_valid_statement(ctx: &mut HoistContext, statement: Statement) -> JsStatement { - let expr = convert_valid_statement_to_expr(ctx, statement); - let loc = expr.loc.clone(); - JsStatement::new(JsStatementData::Expression(expr), loc, None) -} - -pub fn convert_valid_if( - ctx: &mut HoistContext, - expr: If, - loc: Loc, - comment: Option, -) -> JsExpr { - let condition = convert_valid_expr(ctx, *expr.condition); - let yes = convert_valid_block(ctx, expr.yes); - let no = match expr.no { - Some(BlockOrIf::Block(block)) => Some(convert_valid_block(ctx, block)), - Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), - None => None, - }; - JsExpr::new( - JsExprData::Ternary { - condition: Box::new(condition), - yes: Box::new(yes), - no: no.map(Box::new), - }, - loc, - comment, - ) -} diff --git a/crates/js_ast/hoist.rs b/crates/js_ast/hoist.rs deleted file mode 100644 index dfe289b9e0560009e5c5c475f686ecec097ddd0f..0000000000000000000000000000000000000000 --- a/crates/js_ast/hoist.rs +++ /dev/null @@ -1,638 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::vec; - -use ast::{BinaryOperator, BlockOrIf, BlockWithLoc, Expr, Statement}; -use has_side_effects::HasSideEffects; -use tokenizer::Loc; - -use crate::*; - -pub struct HoistContext { - pub statements: Vec, - pub ids: HashSet, - next_id: u32, - scopes: Vec, - has_last_statement: bool, -} - -struct HoistScope { - renames: HashMap, -} - -pub struct TempId { - unmapped_name: String, -} - -impl TempId { - pub fn get(&self, hoist_context: &HoistContext) -> String { - hoist_context.remap_identifier(&self.unmapped_name) - } -} - -impl HoistContext { - pub fn new() -> Self { - Self { - statements: Vec::new(), - ids: HashSet::new(), - next_id: 0, - scopes: vec![HoistScope { - renames: HashMap::new(), - }], - has_last_statement: false, - } - } - - pub fn new_child(&self) -> Self { - Self { - statements: Vec::new(), - ids: self.ids.clone(), - next_id: 0, - scopes: vec![HoistScope { - renames: HashMap::new(), - }], - has_last_statement: false, - } - } - - /// A virtual scope is a scope that exists in blight syntax - /// but may or may not exist in blight. With these it is important to - pub fn push_virtual_scope(&mut self) { - self.scopes.push(HoistScope { - renames: HashMap::new(), - }); - } - - /// See `push_virtual_scope` for more information on what this is. - pub fn pop_virtual_scope(&mut self) { - self.scopes.pop(); - } - - pub fn generate_temp_id(&mut self, basename: &str) -> TempId { - self.next_id += 1; - loop { - let name = format!("{}{}", basename, self.next_id); - if !self.ids.contains(&name) { - self.ids.insert(name.clone()); - return TempId { - unmapped_name: name, - }; - } - self.next_id += 1; - } - } - - /// This does some extra optimizations: - /// - `let a = b;` will not be emitted but instad write a variable alias - pub fn add_statement(&mut self, statement: JsStatement) { - match statement.data { - JsStatementData::Declaration(JSDeclaration { - name, - init: - Some(JsExpr { - data: JsExprData::Identifier(init_name), - .. - }), - is_constant: true, - }) => { - self.scopes - .last_mut() - .unwrap() - .renames - .insert(name, init_name); - self.has_last_statement = false; - return; - } - _ => {} - } - self.statements.push(statement); - self.has_last_statement = true; - } - - /// Get the last statement if it exists. - /// If the statement was optimized away, this returns None - pub fn last_statement(&self) -> Option<&JsStatement> { - if self.has_last_statement { - Some(self.statements.last().unwrap()) - } else { - None - } - } - - pub fn generate_id(&mut self, basename: &String) -> TempId { - let mut id = 0u32; - loop { - let name = match id { - 0 => basename.clone(), - _ => format!("{}{}", basename, id), - }; - if !self.ids.contains(&name) { - self.ids.insert(name.clone()); - return TempId { - unmapped_name: name, - }; - } - id += 1; - } - } - - /// Given a blight identifier, remap it to a new identifier. - pub fn remap_identifier(&self, name: &String) -> String { - for scope in self.scopes.iter().rev() { - if let Some(remapped) = scope.renames.get(name) { - return remapped.clone(); - } - } - name.clone() - } - - /// Declare an identifier in the current scope. - pub fn declare_identifier(&mut self, name: String) -> TempId { - let remapped = self.generate_id(&name); - let scope = self.scopes.last_mut().unwrap(); - self.ids.insert(remapped.unmapped_name.clone()); - scope.renames.insert(name, remapped.unmapped_name.clone()); - remapped - } - - pub fn process_statement(&mut self, statement: Statement) -> Option<()> { - if statement.is_valid_js_expression() { - let result = convert_valid_statement(self, statement); - self.add_statement(result); - return Some(()); - } - - match statement { - Statement::Decl(decl) => { - // visit init before name, this way allows for: - // - // let a = a; - // - // and `a` will be looked up before it is re-declared. - if let Some(init) = self.hoist_expression_contents(decl.init) { - let name = self.declare_identifier(decl.name); - - self.add_statement(JsStatement::new( - JsStatementData::Declaration(crate::JSDeclaration { - name: name.get(&self), - init: Some(init), - is_constant: decl.is_constant, - }), - decl.loc, - decl.comment.map(|x| x.into()), - )); - Some(()) - } else { - None - } - } - Statement::Expr(expr) => { - if let Some(expr) = self.hoist_expression_contents(expr) { - let loc = expr.loc.clone(); - self.add_statement(JsStatement::new( - JsStatementData::Expression(expr), - loc, - None, - )); - Some(()) - } else { - None - } - } - Statement::Comment(_) => Some(()), - } - } - - /// Pushes a decl with a generated identifier to the hoisted result - /// and returns an identifier expression for it. - pub fn hoist_expression_to_temporary(&mut self, expr: Expr) -> Option { - // TODO - if !expr.has_side_effects() { - return Some(self.hoist_expression_contents(expr)?); - } - - let init = self.hoist_expression_contents(expr)?; - - let id = self.generate_temp_id("_temp"); - let loc = init.loc.clone(); - - self.add_statement(JsStatement::new( - JsStatementData::Declaration(JSDeclaration { - name: id.get(&self), - init: Some(init), - is_constant: true, - }), - loc.clone(), - None, - )); - - Some(JsExpr::new( - JsExprData::Identifier(id.get(&self)), - loc, - None, - )) - } - - // Returning `None` from this means that the expression is noreturn, and that - // the rest of the statement never occurs and can be stripped. - pub fn hoist_expression_contents(&mut self, expr: Expr) -> Option { - if expr.is_valid_js_expression() { - return Some(convert_valid_expr(self, expr)); - } - - match expr.data { - ast::ExprData::Binary { op, left, right } => { - let left = self.hoist_expression_to_temporary(*left)?; - let right = self.hoist_expression_to_temporary(*right)?; - - Some(JsExpr::new( - JsExprData::Binary { - op, - left: Box::new(left), - right: Box::new(right), - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Unary { op, value } => { - let value = self.hoist_expression_to_temporary(*value)?; - Some(JsExpr::new( - JsExprData::Unary { - op, - value: Box::new(value), - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Field { base, field } => { - let base = self.hoist_expression_to_temporary(*base)?; - let field = self.hoist_expression_to_temporary(*field)?; - - Some(JsExpr::new( - match field.data { - JsExprData::String(name) if is_js_identifier(name.as_str()) => { - JsExprData::Dot { - base: Box::new(base), - name, - } - } - _ => JsExprData::Index { - base: Box::new(base), - index: Box::new(field), - }, - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Block(block) => { - let temporary = self.generate_temp_id("_block"); - let result = self.hoist_block_to_js_block_in_same_context(BlockWithLoc { - block, - loc: expr.loc.clone(), - }); - - self.add_statement(JsStatement::new( - JsStatementData::Declaration(crate::JSDeclaration { - name: temporary.get(&self), - init: Some(result), - is_constant: false, - }), - expr.loc.clone(), - None, - )); - - Some(JsExpr::new( - JsExprData::Identifier(temporary.get(&self)), - expr.loc, - expr.comment, - )) - } - - ast::ExprData::If(data) => { - let temporary = self.generate_temp_id("_if"); - - let decl_index = self.statements.len(); - self.add_statement(JsStatement::new( - JsStatementData::Declaration(crate::JSDeclaration { - name: temporary.get(&self), - init: None, - is_constant: false, - }), - expr.loc.clone(), - None, - )); - - let condition = self.hoist_expression_to_temporary(*data.condition)?; - let mut did_assign = false; - - let js_if = - self.hoist_js_if(data.yes, data.no, condition, &mut did_assign, &temporary); - - self.add_statement(JsStatement::new( - JsStatementData::If(js_if), - expr.loc.clone(), - None, - )); - - if did_assign { - Some(JsExpr::new( - JsExprData::Identifier(temporary.get(&self)), - expr.loc, - expr.comment, - )) - } else { - self.statements.remove(decl_index); - Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) - } - } - - ast::ExprData::Return(expr) => { - let loc = expr.loc.clone(); - let return_value = self.hoist_expression_to_temporary(*expr)?; - self.add_statement(JsStatement::new( - JsStatementData::Return(return_value), - loc, - None, - )); - None - } - - ast::ExprData::Call { base, arguments } => { - let base = self.hoist_expression_to_temporary(*base)?; - let arguments = arguments - .into_iter() - .map(|x| self.hoist_expression_to_temporary(x)) - .collect::>>()?; - - Some(JsExpr::new( - JsExprData::Call { - base: Box::new(base), - arguments, - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Fn { .. } - | ast::ExprData::Builtin { .. } - | ast::ExprData::Literal(_) - | ast::ExprData::Identifier(_) => { - unreachable!("attempt to hoist an expression that does not require it") - } - } - } - - pub fn hoist_block_to_js_block( - &mut self, - b: BlockWithLoc, - temporary: &String, - did_assign: &mut bool, - ) -> JsBlock { - let loc = b.loc.clone(); - - // A non-block will become a block with a single expression: - // _temp = expr; - // - // We will also apply this transformation to blocks that are valid - // expressions, because that is more concise. - if b.is_valid_js_expression() { - return JsBlock::new( - vec![new_js_assignment_statement( - convert_valid_block(self, b), - temporary.clone(), - loc.clone(), - )], - loc, - ); - } - - let mut child = self.new_child(); - - for statement in b.block.statements { - if let None = child.process_statement(statement) { - tree_shake_statement_list(&mut child.statements); - return JsBlock::new(child.statements, loc); - }; - } - - // If the last statement is an expression, we need - // to assign to the temporary variable. - match child.statements.last().map(|x| &x.data) { - Some(JsStatementData::Expression(_)) => { - *did_assign = true; - - let last = child.statements.pop().unwrap(); - let value = match last { - JsStatement { - data: JsStatementData::Expression(expr), - .. - } => expr, - _ => { - self.statements.push(last); - JsExpr::new(JsExprData::Null, loc.clone(), None) - } - }; - child.statements.push(new_js_assignment_statement( - value, - temporary.clone(), - loc.clone(), - )); - } - _ => {} - } - - tree_shake_statement_list(&mut child.statements); - - JsBlock::new(child.statements, loc) - } - - pub fn hoist_block_to_arrow_fn_body(&mut self, b: BlockWithLoc) -> JsArrowBody { - let loc = b.loc.clone(); - - // A block that is a valid expression will just become - // that expression. We can convert multiple expressions - // into a comma expression. - if b.is_valid_js_expression() { - return JsArrowBody::Expr(convert_valid_block(self, b)); - } - - let mut child = self.new_child(); - - for statement in b.block.statements { - if let None = child.process_statement(statement) { - break; - }; - } - - // If the last statement is an expression, - // we need to return that expression. - match child.statements.last().map(|x| &x.data) { - Some(JsStatementData::Expression(_)) => { - let last = child.statements.pop().unwrap(); - let value = match last { - JsStatement { - data: JsStatementData::Expression(expr), - .. - } => expr, - _ => { - self.statements.push(last); - JsExpr::new(JsExprData::Null, loc.clone(), None) - } - }; - child.statements.push(JsStatement::new( - JsStatementData::Return(value), - loc.clone(), - None, - )); - } - _ => {} - } - - tree_shake_statement_list(&mut child.statements); - - JsArrowBody::Block(JsBlock::new(child.statements, loc)) - } - - pub fn hoist_block_to_js_block_in_same_context(&mut self, b: BlockWithLoc) -> JsExpr { - let loc = b.loc.clone(); - self.push_virtual_scope(); - - debug_assert!(!b.block.is_valid_js_expression()); - - for statement in b.block.statements { - self.process_statement(statement); - } - - self.pop_virtual_scope(); - - // If the last statement is an expression, we need - // to assign to the temporary variable. - match self.last_statement().map(|x| &x.data) { - Some(JsStatementData::Expression(_)) if b.block.has_implicit_return => { - match self.statements.pop().unwrap().data { - JsStatementData::Expression(expr) => expr, - _ => unreachable!(), - } - } - _ => JsExpr::new(JsExprData::Null, loc.clone(), None), - } - } - - pub fn hoist_js_if( - &mut self, - yes: BlockWithLoc, - no: Option, - condition: JsExpr, - did_assign: &mut bool, - temporary: &TempId, - ) -> JsIf { - let yes = self.hoist_block_to_js_block(yes, &temporary.get(&self), did_assign); - - let no = match no { - Some(BlockOrIf::Block(no)) => Some(JsElse::Block(self.hoist_block_to_js_block( - no, - &temporary.get(&self), - did_assign, - ))), - Some(BlockOrIf::If(else_if)) => { - // If the condition of this block has a side effect, we need to - // emit a block so that we can assign to the temporary variable. - // - // Otherwise, we can just emit the condition inline - if else_if.condition.is_valid_js_expression() { - let condition = convert_valid_expr(self, *else_if.condition.clone()); - Some(JsElse::If(Box::new(self.hoist_js_if( - else_if.yes, - else_if.no, - condition, - did_assign, - temporary, - )))) - } else { - let mut temp_block = self.new_child(); - let condition = temp_block - .hoist_expression_to_temporary(*else_if.condition.clone()) - .unwrap(); - - temp_block.add_statement(JsStatement::new( - JsStatementData::If(self.hoist_js_if( - else_if.yes, - else_if.no, - condition, - did_assign, - temporary, - )), - Loc::EMPTY, - None, - )); - - tree_shake_statement_list(&mut temp_block.statements); - - Some(JsElse::Block(JsBlock::new( - temp_block.statements, - Loc::EMPTY, - ))) - } - } - None => None, - }; - - JsIf { condition, yes, no } - } -} - -pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { - JsStatement::new( - JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), - loc, - None, - ) -} - -pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { - JsExpr::new( - JsExprData::Binary { - op: BinaryOperator::Assign, - left: Box::new(JsExpr::new( - JsExprData::Identifier(target), - loc.clone(), - None, - )), - right: Box::new(expr), - }, - loc, - None, - ) -} - -/// Remove -/// - Discarded literals -pub fn tree_shake_statement_list(list: &mut Vec) { - let mut set_i = 0; - for i in 0..list.len() { - let stmt = list.get(i).unwrap(); - let should_keep = match &stmt.data { - JsStatementData::Expression(expr) => !expr.is_literal(), - JsStatementData::Return(_) => true, - JsStatementData::Export(_) => true, - JsStatementData::Declaration(_) => true, - JsStatementData::Function(_) => true, - JsStatementData::If(_) => true, - // - }; - - if should_keep { - if set_i != i { - list.swap(i, set_i) - } - set_i += 1; - } - } - list.truncate(set_i) -} diff --git a/crates/js_ast/lib.rs b/crates/js_ast/lib.rs index c915dc7b4c9d8e4c28d637b834f070ec2164e7b7..9d29be2c29a04ae7f9bc7701413e57ec70196e62 100644 --- a/crates/js_ast/lib.rs +++ b/crates/js_ast/lib.rs @@ -1,28 +1,7 @@ -mod convert_valid_expr; -mod has_side_effects; -mod hoist; -mod is_valid_js_expression; +mod convert; mod js_ast; mod util; -use ast::Module; - -pub use convert_valid_expr::*; -pub use hoist::*; -pub use is_valid_js_expression::*; +pub use convert::*; pub use js_ast::*; pub use util::*; - -pub fn blight_to_js(blight: Module) -> JsModule { - let mut root_context = HoistContext::new(); - - for statement in blight.statements { - if let None = root_context.process_statement(statement.to_statement()) { - unreachable!("top level return is disallowed by the parser") - }; - } - - JsModule { - statements: root_context.statements, - } -} diff --git a/crates/js_printer/lib.rs b/crates/js_printer/lib.rs index 4ea0d8c03ae395ff34e563a248dbeceaefce9b55..b90a4a2850196ea232831af66d36017ead017334 100644 --- a/crates/js_printer/lib.rs +++ b/crates/js_printer/lib.rs @@ -231,6 +231,10 @@ impl Print for JsExprData { body.print(writer) } JsExprData::Ternary { condition, yes, no } => { + let wrap = false; + if wrap { + write!(writer, "(")?; + } condition.print(writer)?; write!(writer, "?")?; yes.print(writer)?; @@ -240,15 +244,25 @@ impl Print for JsExprData { } else { write!(writer, "null")?; } + if wrap { + write!(writer, ")")?; + } Ok(()) } JsExprData::Comma { items } => { + let wrap = true; + if wrap { + write!(writer, "(")?; + } for (i, item) in items.iter().enumerate() { if i > 0 { write!(writer, ",")?; } item.print(writer)?; } + if wrap { + write!(writer, ")")?; + } Ok(()) } JsExprData::Void(expr) => { diff --git a/crates/js_printer/src/lib.rs b/crates/js_printer/src/lib.rs deleted file mode 100644 index 49f3aa65ab7579c2a6e9a4b0470a4bc256e51557..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod print_expression; -mod print_statement; - -use print_statement::print_statement; - -use js_ast::JsStatement; - -pub fn print(forest: Vec) -> String { - let mut result = String::new(); - - for tree in forest { - result.push_str(&print_statement(&tree)); - } - - result -} diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs deleted file mode 100644 index a27468c1c49ea1ee5ce13fd99450b645fe3fdd16..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/print_expression.rs +++ /dev/null @@ -1,276 +0,0 @@ -use js_ast::{JsExpr, JsExprData}; - -use crate::print_statement::print_statement; - -pub fn print_expression(expr: &JsExpr) -> String { - match &expr.data { - JsExprData::Raw(value) => format!("{}", value), - JsExprData::Number(value) => format!("{}", value), - JsExprData::BigInt(value) => format!("{}n", value), - JsExprData::String(value) => { - let mut result = String::new(); - // TODO: This is an INCREDIBLY naive implementation of escaping. - // Also it's probably not even correct. - for char in value.chars() { - match char { - '\n' => result.push_str("\\n"), - '\r' => result.push_str("\\r"), - '\t' => result.push_str("\\t"), - '"' => result.push_str("\\\""), - '\\' => result.push_str("\\\\"), - _ => result.push(char), - } - } - format!("\"{}\"", result) - } - JsExprData::Bool(value) => format!("{}", value), - JsExprData::Identifier(value) => format!("{}", value), - JsExprData::Array(value) => format!( - "[{}]", - value - .iter() - .map(|x| print_expression(x)) - .collect::>() - .join(",") - ), - JsExprData::Object(value) => format!( - "{{{}}}", - value - .iter() - .map(|entry| format!( - "{}:{}", - print_expression(&entry.key), - print_expression(&entry.value) - )) - .collect::>() - .join(",") - ), - JsExprData::Binary { - op: operator, - left, - right, - } => format!( - "{}{}{}", - print_expression(&left), - operator.clone().to_string(), - print_expression(&right) - ), - // JsExprData::Function { - // arguments, - // value, - // name: _, - // params: _, - // block, - // } => format!( - // "({})=>{{{}}}", - // arguments.join(","), - // value - // .iter() - // .map(|x| format!("{}", print_statement(x))) - // .collect::>() - // .join("") - // ), - JsExprData::Call { base, arguments } => format!( - "{}({})", - print_expression(&base), - arguments - .iter() - .map(|x| print_expression(x)) - .collect::>() - .join(",") - ), - JsExprData::Null => todo!(), - JsExprData::Undefined => todo!(), - JsExprData::Unary { - op: operator, - value, - } => todo!(), - JsExprData::Dot { base, name } => todo!(), - JsExprData::Index { base, index } => todo!(), - JsExprData::Function { - name, - params, - block, - } => todo!(), - JsExprData::Ternary { condition, yes, no } => todo!(), - JsExprData::ArrowFunction { params, block } => todo!(), - JsExprData::Comma { items } => todo!(), - JsExprData::Void(_) => todo!(), - } -} - -#[cfg(test)] -mod tests { - use crate::print_expression::print_expression; - use js_ast::JsExprData; - use js_ast::JsStatement; - use num::{bigint::Sign, BigInt}; - use std::collections::HashMap; - - #[test] - fn number() { - assert_eq!(print_expression(&JsExprData::Number(1.0)), "1"); - assert_eq!(print_expression(&JsExprData::Number(1.5)), "1.5"); - assert_eq!(print_expression(&JsExprData::Number(1.5e10)), "15000000000"); - assert_eq!( - print_expression(&JsExprData::Number(1.5e-10)), - "0.00000000015" - ); - } - - #[test] - fn bigint() { - assert_eq!( - print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![1]))), - "1n" - ); - assert_eq!( - print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![10000000]))), - "10000000n" - ); - assert_eq!( - print_expression(&JsExprData::BigInt(BigInt::new( - Sign::Minus, - vec![10000000] - ))), - "-10000000n" - ); - } - - #[test] - fn string() { - assert_eq!( - print_expression(&JsExprData::String("".to_string())), - "\"\"" - ); - assert_eq!( - print_expression(&JsExprData::String("hello".to_string())), - "\"hello\"" - ); - assert_eq!( - print_expression(&JsExprData::String("hello\nworld".to_string())), - "\"hello\\nworld\"" - ); - } - - #[test] - fn bool() { - assert_eq!(print_expression(&JsExprData::Bool(true)), "true"); - assert_eq!(print_expression(&JsExprData::Bool(false)), "false"); - } - - #[test] - fn identifier() { - assert_eq!( - print_expression(&JsExprData::Identifier("hello".to_string())), - "hello" - ); - } - - #[test] - fn array() { - assert_eq!(print_expression(&JsExprData::Array(vec![])), "[]"); - assert_eq!( - print_expression(&JsExprData::Array(vec![JsExprData::Number(1.0)])), - "[1]" - ); - assert_eq!( - print_expression(&JsExprData::Array(vec![ - JsExprData::Number(1.0), - JsExprData::Number(2.0) - ])), - "[1,2]" - ); - } - - #[test] - fn object() { - assert_eq!(print_expression(&JsExprData::Object(HashMap::new())), "{}"); - let mut map = HashMap::new(); - map.insert("hello".to_string(), JsExprData::Number(1.0)); - assert_eq!(print_expression(&JsExprData::Object(map)), "{\"hello\":1}"); - } - - #[test] - fn operation() { - assert_eq!( - print_expression(&JsExprData::Binary { - op: AnyOperator::Add, - left: Box::new(JsExprData::Number(1.0)), - right: Box::new(JsExprData::Number(2.0)), - }), - "1+2" - ); - assert_eq!( - print_expression(&JsExprData::Binary { - op: AnyOperator::Sub, - left: Box::new(JsExprData::Number(1.0)), - right: Box::new(JsExprData::Number(2.0)), - }), - "1-2" - ); - } - - #[test] - fn function() { - use crate::print_expression::print_expression; - use js_ast::JsExprData; - - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec![], - value: vec![], - }), - "()=>{}" - ); - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec!["a".to_string()], - value: vec![], - }), - "(a)=>{}" - ); - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec!["a".to_string(), "b".to_string()], - value: vec![], - }), - "(a,b)=>{}" - ); - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec!["a".to_string(), "b".to_string()], - value: vec![JsStatement::Declaration { - name: "a".to_string(), - value: JsExprData::Number(1.0) - }], - }), - "(a,b)=>{let a=1;}" - ); - } - - #[test] - fn function_call() { - assert_eq!( - print_expression(&JsExprData::Call { - name: Box::new(JsExprData::Identifier("hello".to_string())), - arguments: vec![], - }), - "hello()" - ); - assert_eq!( - print_expression(&JsExprData::Call { - name: Box::new(JsExprData::Identifier("hello".to_string())), - arguments: vec![JsExprData::Number(1.0)], - }), - "hello(1)" - ); - assert_eq!( - print_expression(&JsExprData::Call { - name: Box::new(JsExprData::Identifier("hello".to_string())), - arguments: vec![JsExprData::Number(1.0), JsExprData::Number(2.0)], - }), - "hello(1,2)" - ); - } -} diff --git a/crates/js_printer/src/print_statement.rs b/crates/js_printer/src/print_statement.rs deleted file mode 100644 index c7d78693136d06b23070c35e9b55ab1dfa70e90f..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/print_statement.rs +++ /dev/null @@ -1,153 +0,0 @@ -use js_ast::{If, JSDeclaration, JSFunctionDeclaration, JsStatement, JsStatementData}; - -use crate::print_expression::print_expression; - -pub fn print_statement(statement: &JsStatement) -> String { - match &statement.data { - JsStatementData::Expression(value) => format!("{};", print_expression(&value)), - // JsStatementData::Export(value) => format!("export {}", print_statement(value)), - JsStatementData::Export(value) => todo!(), - JsStatementData::Return(value) => format!("return {};", print_expression(&value)), - JsStatementData::Declaration(Declaration { - name, - init, - is_constant, - }) => { - format!( - "{} {}={};", - match is_constant { - true => "const", - false => "let", - }, - name, - print_expression(&init) - ) - } - JsStatementData::Function(JSFunctionDeclaration { - name, - block, - params, - }) => format!( - "function {}({}){{{}}}", - name, - params.join(","), - block - .statements - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - JsStatementData::If(JsIf { condition, yes, no }) => { - let else_value = match no { - Some(value) => format!( - "else{{{}}}", - value - .statements - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - None => "".to_string(), - }; - format!( - "if({}){{{}}}{}", - print_expression(&condition), - yes.statements - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join(""), - else_value - ) - } - } -} - -#[cfg(test)] -mod tests { - use crate::print_statement::print_statement; - use js_ast::JsExpression; - use js_ast::JsStatement; - - #[test] - fn variable_declaration() { - assert_eq!( - print_statement(&JsStatement::Declaration { - name: "x".to_string(), - value: JsExpression::Number(1.0), - }), - "let x=1;" - ); - } - - #[test] - fn constant_declaration() { - assert_eq!( - print_statement(&JsStatement::ConstantDeclaration { - name: "x".to_string(), - value: JsExpression::Number(1.0), - }), - "const x=1;" - ); - } - - #[test] - fn function_declaration() { - assert_eq!( - print_statement(&JsStatement::Function { - name: "x".to_string(), - arguments: vec!["y".to_string()], - value: vec![JsStatement::Expression(JsExpression::Number(1.0))], - }), - "function x(y){1;}" - ); - } - - #[test] - fn expression() { - assert_eq!( - print_statement(&JsStatement::Expression(JsExpression::Number(1.0))), - "1;" - ); - } - - #[test] - fn export() { - assert_eq!( - print_statement(&JsStatement::Export(Box::new(JsStatement::Expression( - JsExpression::Number(1.0) - )))), - "export 1;" - ); - } - - #[test] - fn return_statement() { - assert_eq!( - print_statement(&JsStatement::Return(JsExpression::Number(1.0))), - "return 1;" - ); - } - - #[test] - fn if_statement() { - assert_eq!( - print_statement(&JsStatement::If { - condition: JsExpression::Number(1.0), - value: vec![JsStatement::Expression(JsExpression::Number(2.0))], - else_value: None, - }), - "if(1){2;}" - ); - assert_eq!( - print_statement(&JsStatement::If { - condition: JsExpression::Number(1.0), - value: vec![JsStatement::Expression(JsExpression::Number(2.0))], - else_value: Some(vec![JsStatement::Expression(JsExpression::Number(3.0))]), - }), - "if(1){2;}else{3;}" - ); - } -} diff --git a/crates/parser/parse_block.rs b/crates/parser/parse_block.rs index a9ad16ed402ac0509be65f1a95ce8a4bd76ec980..4ae805a759d0dd7b58c6642c94dc83e5c821c9e4 100644 --- a/crates/parser/parse_block.rs +++ b/crates/parser/parse_block.rs @@ -12,9 +12,6 @@ pub fn parse_block(p: &mut Parser) -> Result { let mut statements = Vec::new(); - let was_top_level = p.is_top_level; - p.is_top_level = false; - 'outer: loop { let is_statement_like_expression = p .peek() @@ -23,7 +20,6 @@ pub fn parse_block(p: &mut Parser) -> Result { let statement = match parse_statement(p) { Ok(statement) => statement, Err(err) => { - p.is_top_level = was_top_level; return Err(err); } }; @@ -35,7 +31,6 @@ pub fn parse_block(p: &mut Parser) -> Result { match p.peek() { Some(Token::ClosedBrace) => { p.next(); - p.is_top_level = was_top_level; return Ok(Block { statements, has_implicit_return: false, @@ -46,14 +41,12 @@ pub fn parse_block(p: &mut Parser) -> Result { }, Some(Token::ClosedBrace) => { p.next(); - if statement.require_semicolon(is_statement_like_expression) { - p.is_top_level = was_top_level; + if !statement.is_expression() { return Err(p .error("Expected semicolon or '}' after statement".to_string()) .into()); } - p.is_top_level = was_top_level; return Ok(Block { statements, has_implicit_return: true, @@ -61,7 +54,6 @@ pub fn parse_block(p: &mut Parser) -> Result { } _ => { if statement.require_semicolon(is_statement_like_expression) { - p.is_top_level = was_top_level; return Err(p .error("Expected semicolon or '}' after statement".to_string()) .into()); diff --git a/crates/parser/parse_declaration.rs b/crates/parser/parse_declaration.rs index 9e5176ed38fa2d414c15e048774d8012b62a0f49..80f140076bb711e3fdf03716bbccd416096a0c35 100644 --- a/crates/parser/parse_declaration.rs +++ b/crates/parser/parse_declaration.rs @@ -1,4 +1,4 @@ -use ast::{Decl, DocComment, Type}; +use ast::{DocComment, Type, VarDecl}; use miette::Result; use tokenizer::Token; @@ -11,7 +11,7 @@ pub fn parse_declaration( p: &mut Parser, is_pub: bool, comment: Option, -) -> Result { +) -> Result { let loc = p.loc(); let is_constant = match p.next() { Some(Token::Var) => false, @@ -38,7 +38,7 @@ pub fn parse_declaration( .into()) } }; - Ok(Decl { + Ok(VarDecl { name: name.clone(), data_type: return_type, init, diff --git a/crates/parser/parse_expr_basic.rs b/crates/parser/parse_expr_basic.rs index d1949c82fc65ad390e482f92707eb95b24415346..efcf48fb65ecc9b0f8c8512e682ddfe7aab3d96b 100644 --- a/crates/parser/parse_expr_basic.rs +++ b/crates/parser/parse_expr_basic.rs @@ -63,7 +63,7 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result { } Some(Token::Return) => { - if p.is_top_level { + if p.allow_return { return Err(p .error("'return' cannot be used at top level".to_string()) .into()); diff --git a/crates/parser/parse_fn_declaration.rs b/crates/parser/parse_fn_declaration.rs index 32b4e3cff41688b8ee04d9c6e2cba54d01c64291..c1b576cec8709710e413f4f1ae9726d6ba80635f 100644 --- a/crates/parser/parse_fn_declaration.rs +++ b/crates/parser/parse_fn_declaration.rs @@ -1,8 +1,9 @@ -use ast::{Decl, DocComment, Expr, ExprData, Type}; +use ast::{DocComment, Expr, ExprData, FnDecl, Type}; use miette::Result; use tokenizer::Token; use crate::parse_block::parse_block; +use crate::parse_expr::parse_expr; use crate::parse_param::parse_param; use crate::parse_type::parse_type; use crate::Parser; @@ -12,7 +13,7 @@ pub fn parse_fn_declaration( p: &mut Parser, is_pub: bool, comment: Option, -) -> Result { +) -> Result { let loc = p.loc(); match p.next() { Some(Token::Fn) => (), @@ -57,9 +58,13 @@ pub fn parse_fn_declaration( break; } _ => { + let actual = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); return Err(p - .error("Expected ',' or ')' after identifier".to_string()) - .into()) + .error(format!( + "Expected ',' or ')' after identifier, got {}", + actual + )) + .into()); } }; } @@ -69,19 +74,6 @@ pub fn parse_fn_declaration( .into()) } } - - match p.peek() { - Some(Token::Comma) => p.next(), - Some(Token::ClosedParen) => { - _ = p.next(); - break; - } - _ => { - return Err(p - .error("Expected ',' or ')' after identifier".to_string()) - .into()) - } - }; } let return_type = match p.peek() { @@ -92,31 +84,39 @@ pub fn parse_fn_declaration( _ => Type::Infer, }; - if let Some(Token::OpenBrace) = p.peek() { - let block_loc = p.loc(); - let block = parse_block(p)?; - let function_body = ExprData::Fn { - name: name.clone(), - params, - return_type, - block, - }; - Ok(Decl { - name, - data_type: Type::Any, - init: Expr::new(function_body, block_loc, None), - is_constant: true, - is_pub, - comment, - loc: loc, - }) - } else { - let actually = p.peek().map_or("EOF".to_string(), |x| x.to_string()); - return Err(p - .error(format!( - "Expected a block to follow function declaration, but got {}", - actually, - )) - .into()); - } + let was_allow_return = p.allow_return; + p.allow_return = true; + + let body = match p.peek() { + Some(Token::OpenBrace) => { + let block_loc = p.loc(); + let block = parse_block(p)?; + Expr::new(ExprData::Block(block), block_loc, None) + } + Some(Token::BigRightArrow) => { + _ = p.next(); + parse_expr(p)? + } + _ => { + let actually = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); + return Err(p + .error(format!( + "Expected a block or '=>' to follow function declaration, but got {}", + actually, + )) + .into()); + } + }; + + p.allow_return = was_allow_return; + + Ok(FnDecl { + name, + return_type, + params, + body, + is_pub, + comment, + loc, + }) } diff --git a/crates/parser/parse_statement.rs b/crates/parser/parse_statement.rs index a135c1d4e6de8d3d17c0f0b3d7b03fa3f62c4859..537f9512571cf182854ce26e549ded5c65d729be 100644 --- a/crates/parser/parse_statement.rs +++ b/crates/parser/parse_statement.rs @@ -28,10 +28,12 @@ pub fn parse_statement(p: &mut Parser) -> Result { Some(Token::Pub) => { _ = p.next(); match p.peek() { - Some(Token::Var) | Some(Token::Let) => { - Ok(Statement::Decl(parse_declaration(p, true, m_doc_comment)?)) - } - Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration( + Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( + p, + true, + m_doc_comment, + )?)), + Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( p, true, m_doc_comment, @@ -40,10 +42,12 @@ pub fn parse_statement(p: &mut Parser) -> Result { None => Err(p.error("Unexpected end of file".to_string()).into()), } } - Some(Token::Var) | Some(Token::Let) => { - Ok(Statement::Decl(parse_declaration(p, false, m_doc_comment)?)) - } - Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration( + Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( + p, + false, + m_doc_comment, + )?)), + Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( p, false, m_doc_comment, diff --git a/crates/parser/parser.rs b/crates/parser/parser.rs index 44f7cdd6f8750dee21ec88a0c746afc2b60f03ae..465ce180e9bdd2c388361825407ee0d14bedbf05 100644 --- a/crates/parser/parser.rs +++ b/crates/parser/parser.rs @@ -53,7 +53,7 @@ pub struct Parser { cursor: usize, /// Whether or not the parser is at the top level - pub is_top_level: bool, + pub allow_return: bool, comments: Vec, } @@ -63,7 +63,7 @@ impl Parser { Parser { tokens, cursor: 0, - is_top_level: true, + allow_return: false, comments: vec![], } } diff --git a/crates/tokenizer/tokenizer.rs b/crates/tokenizer/tokenizer.rs index aee421347f0e4a1a7a044043c4748acaadfe5360..06f2495729efddae8d1a4a3945f2805341d42201 100644 --- a/crates/tokenizer/tokenizer.rs +++ b/crates/tokenizer/tokenizer.rs @@ -5,6 +5,7 @@ use thiserror::Error; pub struct Loc {} impl Loc { + /// TODO(@paperdave): Remove this. every usage of this is incorrect. pub const EMPTY: Loc = Loc {}; pub fn new() -> Self { @@ -44,12 +45,14 @@ pub enum Token { AndAnd, // && Bang, // ! BangEqual, // != + BigRightArrow, // => Caret, // ^ ClosedBrace, // } ClosedBracket, // ] ClosedParen, // ) Colon, // : Comma, // , + Dot, // . Equal, // = EqualEqual, // == Gt, // > @@ -73,14 +76,13 @@ pub enum Token { Star, // * StarStar, // ** Tilde, // ~ - Dot, // . } impl Token { pub fn is_start_of_statement_like_expression(&self) -> bool { match &self { - Token::If | Token::OpenBrace => false, - _ => true, + Token::If | Token::OpenBrace => true, + _ => false, } } } @@ -110,17 +112,20 @@ impl std::fmt::Display for Token { Token::AndAnd => "&&".to_string(), Token::Bang => "!".to_string(), Token::BangEqual => "!=".to_string(), + Token::BigRightArrow => "=>".to_string(), Token::Caret => "^".to_string(), Token::ClosedBrace => "}".to_string(), Token::ClosedBracket => "]".to_string(), Token::ClosedParen => ")".to_string(), Token::Colon => ":".to_string(), Token::Comma => ",".to_string(), + Token::Dot => ".".to_string(), Token::Equal => "=".to_string(), Token::EqualEqual => "==".to_string(), Token::Gt => ">".to_string(), Token::GtEqual => ">=".to_string(), Token::GtGt => ">>".to_string(), + Token::GtGtGt => ">>>".to_string(), Token::Lt => "<".to_string(), Token::LtEqual => "<=".to_string(), Token::LtLt => "<<".to_string(), @@ -138,8 +143,6 @@ impl std::fmt::Display for Token { Token::Star => "*".to_string(), Token::StarStar => "**".to_string(), Token::Tilde => "~".to_string(), - Token::Dot => ".".to_string(), - Token::GtGtGt => ">>>".to_string(), }; write!(f, "{}", token) @@ -234,6 +237,10 @@ pub fn tokenize(content: String) -> Result> { chars.next(); tokens.push(Token::EqualEqual); } + Some((_, '>')) => { + chars.next(); + tokens.push(Token::BigRightArrow); + } _ => tokens.push(Token::Equal), }, ';' => tokens.push(Token::Semicolon), diff --git a/examples/basics/stupid.blight b/examples/basics/stupid.blight new file mode 100644 index 0000000000000000000000000000000000000000..4144819ae054ff63f524006f9d3c00e2e4793ba8 --- /dev/null +++ b/examples/basics/stupid.blight @@ -0,0 +1,2 @@ + +fn add(x: int, y: int) => x + y -- 2.54.0