33 files changed, 1397 insertions(+), 1568 deletions(-)
crates/ast/ast.rs+34-11| ... | ... | @@ -30,7 +30,7 @@ pub struct Param { |
| 30 | 30 | |
| 31 | 31 | #[derive(Debug, Clone)] |
| 32 | 32 | |
| 33 | pub struct Decl { | |
| 33 | pub struct VarDecl { | |
| 34 | 34 | pub name: String, |
| 35 | 35 | pub data_type: Type, |
| 36 | 36 | pub init: Expr, |
| ... | ... | @@ -40,6 +40,17 @@ pub struct Decl { |
| 40 | 40 | pub comment: Option<DocComment>, |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | #[derive(Debug, Clone)] | |
| 44 | pub struct FnDecl { | |
| 45 | pub name: String, | |
| 46 | pub return_type: Type, | |
| 47 | pub params: Vec<Param>, | |
| 48 | pub body: Expr, | |
| 49 | pub is_pub: bool, | |
| 50 | pub loc: Loc, | |
| 51 | pub comment: Option<DocComment>, | |
| 52 | } | |
| 53 | ||
| 43 | 54 | #[derive(Debug, Clone)] |
| 44 | 55 | pub struct Expr { |
| 45 | 56 | pub data: ExprData, |
| ... | ... | @@ -136,7 +147,7 @@ pub enum ExprData { |
| 136 | 147 | name: String, |
| 137 | 148 | params: Vec<Param>, |
| 138 | 149 | return_type: Type, |
| 139 | block: Block, | |
| 150 | body: Box<Expr>, | |
| 140 | 151 | }, |
| 141 | 152 | Call { |
| 142 | 153 | base: Box<Expr>, |
| ... | ... | @@ -159,14 +170,16 @@ pub enum ExprData { |
| 159 | 170 | #[derive(Debug, Clone)] |
| 160 | 171 | pub enum Statement { |
| 161 | 172 | Expr(Expr), |
| 162 | Decl(Decl), | |
| 173 | VarDecl(VarDecl), | |
| 174 | FnDecl(FnDecl), | |
| 163 | 175 | Comment(Comment), |
| 164 | 176 | } |
| 165 | 177 | |
| 166 | 178 | #[derive(Debug, Clone)] |
| 167 | 179 | pub enum TopLevelStatement { |
| 168 | 180 | Expr(Expr), |
| 169 | Decl(Decl), | |
| 181 | VarDecl(VarDecl), | |
| 182 | FnDecl(FnDecl), | |
| 170 | 183 | Comment(Comment), |
| 171 | 184 | } |
| 172 | 185 | |
| ... | ... | @@ -189,7 +202,8 @@ impl Statement { |
| 189 | 202 | pub fn to_top_level(self) -> TopLevelStatement { |
| 190 | 203 | match self { |
| 191 | 204 | Statement::Expr(expr) => TopLevelStatement::Expr(expr), |
| 192 | Statement::Decl(decl) => TopLevelStatement::Decl(decl), | |
| 205 | Statement::VarDecl(decl) => TopLevelStatement::VarDecl(decl), | |
| 206 | Statement::FnDecl(decl) => TopLevelStatement::FnDecl(decl), | |
| 193 | 207 | Statement::Comment(comment) => TopLevelStatement::Comment(comment), |
| 194 | 208 | } |
| 195 | 209 | } |
| ... | ... | @@ -198,7 +212,14 @@ impl Statement { |
| 198 | 212 | pub fn require_semicolon(&self, semicolon_hint: bool) -> bool { |
| 199 | 213 | match &self { |
| 200 | 214 | Statement::Expr(_) => !semicolon_hint, |
| 201 | Statement::Decl(_) => true, | |
| 215 | Statement::VarDecl(_) => true, | |
| 216 | _ => false, | |
| 217 | } | |
| 218 | } | |
| 219 | ||
| 220 | pub fn is_expression(&self) -> bool { | |
| 221 | match &self { | |
| 222 | Statement::Expr(_) => true, | |
| 202 | 223 | _ => false, |
| 203 | 224 | } |
| 204 | 225 | } |
| ... | ... | @@ -209,16 +230,18 @@ impl TopLevelStatement { |
| 209 | 230 | pub fn require_semicolon(&self, earlier_hint: bool) -> bool { |
| 210 | 231 | match &self { |
| 211 | 232 | TopLevelStatement::Expr(_) => !earlier_hint, |
| 212 | TopLevelStatement::Decl(_) => true, | |
| 233 | TopLevelStatement::VarDecl(_) => true, | |
| 234 | TopLevelStatement::FnDecl(_) => false, | |
| 213 | 235 | _ => false, |
| 214 | 236 | } |
| 215 | 237 | } |
| 216 | 238 | |
| 217 | pub fn to_statement(self) -> Statement { | |
| 239 | pub fn to_statement(self) -> Option<Statement> { | |
| 218 | 240 | match self { |
| 219 | TopLevelStatement::Expr(expr) => Statement::Expr(expr), | |
| 220 | TopLevelStatement::Decl(decl) => Statement::Decl(decl), | |
| 221 | TopLevelStatement::Comment(comment) => Statement::Comment(comment), | |
| 241 | TopLevelStatement::Expr(expr) => Some(Statement::Expr(expr)), | |
| 242 | TopLevelStatement::VarDecl(decl) => Some(Statement::VarDecl(decl)), | |
| 243 | TopLevelStatement::FnDecl(decl) => Some(Statement::FnDecl(decl)), | |
| 244 | TopLevelStatement::Comment(comment) => Some(Statement::Comment(comment)), | |
| 222 | 245 | } |
| 223 | 246 | } |
| 224 | 247 | } |
crates/js_ast/convert/block_convert_context.rs created+244| ... | ... | @@ -0,0 +1,244 @@ |
| 1 | use crate::js_ast::*; | |
| 2 | use ast::*; | |
| 3 | use std::collections::{HashMap, HashSet}; | |
| 4 | use std::vec; | |
| 5 | use tokenizer::Loc; | |
| 6 | ||
| 7 | use super::tree_shake_statement_list; | |
| 8 | ||
| 9 | /// A block convert context keeps track of state while converting non-valid JS expressions to JS | |
| 10 | pub struct BlockConvertContext { | |
| 11 | /// Statements in this block context | |
| 12 | /// This may be the merged contents of multiple blocks | |
| 13 | statements: Vec<JsStatement>, | |
| 14 | ids: HashSet<String>, | |
| 15 | next_id: u32, | |
| 16 | scopes: Vec<VirtualScope>, | |
| 17 | /// If the last item in `statements` is actually valid | |
| 18 | has_last_statement: bool, | |
| 19 | } | |
| 20 | ||
| 21 | struct VirtualScope { | |
| 22 | renames: HashMap<String, String>, | |
| 23 | } | |
| 24 | ||
| 25 | pub struct TempId { | |
| 26 | unmapped_name: String, | |
| 27 | } | |
| 28 | ||
| 29 | pub struct StatementHandle { | |
| 30 | index: Option<u32>, | |
| 31 | } | |
| 32 | ||
| 33 | impl StatementHandle { | |
| 34 | pub fn delete(self, ctx: &mut BlockConvertContext) { | |
| 35 | if let Some(index) = self.index { | |
| 36 | ctx.statements.remove(index as usize); | |
| 37 | } | |
| 38 | } | |
| 39 | } | |
| 40 | ||
| 41 | impl TempId { | |
| 42 | pub fn get(&self, hoist_context: &BlockConvertContext) -> String { | |
| 43 | hoist_context.remap_identifier(&self.unmapped_name) | |
| 44 | } | |
| 45 | } | |
| 46 | ||
| 47 | impl BlockConvertContext { | |
| 48 | pub fn new() -> Self { | |
| 49 | Self { | |
| 50 | statements: Vec::new(), | |
| 51 | ids: HashSet::new(), | |
| 52 | next_id: 0, | |
| 53 | scopes: vec![VirtualScope { | |
| 54 | renames: HashMap::new(), | |
| 55 | }], | |
| 56 | has_last_statement: false, | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | pub fn new_child(&self) -> Self { | |
| 61 | Self { | |
| 62 | statements: Vec::new(), | |
| 63 | ids: self.ids.clone(), | |
| 64 | next_id: 0, | |
| 65 | scopes: vec![VirtualScope { | |
| 66 | renames: HashMap::new(), | |
| 67 | }], | |
| 68 | has_last_statement: false, | |
| 69 | } | |
| 70 | } | |
| 71 | ||
| 72 | /// A virtual scope is a scope that exists in blight syntax | |
| 73 | /// but may or may not exist in blight. With these it is important to | |
| 74 | pub fn push_virtual_scope(&mut self) { | |
| 75 | self.scopes.push(VirtualScope { | |
| 76 | renames: HashMap::new(), | |
| 77 | }); | |
| 78 | } | |
| 79 | ||
| 80 | /// See `push_virtual_scope` for more information on what this is. | |
| 81 | pub fn pop_virtual_scope(&mut self) { | |
| 82 | self.scopes.pop(); | |
| 83 | debug_assert!(self.scopes.len() > 0); | |
| 84 | } | |
| 85 | ||
| 86 | pub fn generate_temp_id(&mut self, basename: &str) -> TempId { | |
| 87 | self.next_id += 1; | |
| 88 | loop { | |
| 89 | let name = format!("{}{}", basename, self.next_id); | |
| 90 | if !self.ids.contains(&name) { | |
| 91 | self.ids.insert(name.clone()); | |
| 92 | return TempId { | |
| 93 | unmapped_name: name, | |
| 94 | }; | |
| 95 | } | |
| 96 | self.next_id += 1; | |
| 97 | } | |
| 98 | } | |
| 99 | ||
| 100 | /// This does some extra optimizations: | |
| 101 | /// - `let a = b;` will not be emitted but instad write a variable alias | |
| 102 | pub fn add_statement(&mut self, statement: JsStatement) -> StatementHandle { | |
| 103 | match statement.data { | |
| 104 | JsStatementData::Declaration(JSDeclaration { | |
| 105 | name, | |
| 106 | init: | |
| 107 | Some(JsExpr { | |
| 108 | data: JsExprData::Identifier(init_name), | |
| 109 | .. | |
| 110 | }), | |
| 111 | is_constant: true, | |
| 112 | }) => { | |
| 113 | self.scopes | |
| 114 | .last_mut() | |
| 115 | .unwrap() | |
| 116 | .renames | |
| 117 | .insert(name, init_name); | |
| 118 | self.has_last_statement = false; | |
| 119 | return StatementHandle { index: None }; | |
| 120 | } | |
| 121 | _ => {} | |
| 122 | } | |
| 123 | self.statements.push(statement); | |
| 124 | self.has_last_statement = true; | |
| 125 | StatementHandle { | |
| 126 | index: Some(self.statements.len() as u32 - 1), | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | /// Get the last statement if it exists. | |
| 131 | /// If the statement was optimized away, this returns None | |
| 132 | pub fn last_statement(&self) -> Option<&JsStatement> { | |
| 133 | if self.has_last_statement { | |
| 134 | Some(self.statements.last().unwrap()) | |
| 135 | } else { | |
| 136 | None | |
| 137 | } | |
| 138 | } | |
| 139 | ||
| 140 | pub fn generate_id(&mut self, basename: &String) -> TempId { | |
| 141 | let mut id = 0u32; | |
| 142 | loop { | |
| 143 | let name = match id { | |
| 144 | 0 => basename.clone(), | |
| 145 | _ => format!("{}{}", basename, id), | |
| 146 | }; | |
| 147 | if !self.ids.contains(&name) { | |
| 148 | self.ids.insert(name.clone()); | |
| 149 | return TempId { | |
| 150 | unmapped_name: name, | |
| 151 | }; | |
| 152 | } | |
| 153 | id += 1; | |
| 154 | } | |
| 155 | } | |
| 156 | ||
| 157 | /// Given a blight identifier, remap it to a new identifier. | |
| 158 | pub fn remap_identifier(&self, name: &String) -> String { | |
| 159 | for scope in self.scopes.iter().rev() { | |
| 160 | if let Some(remapped) = scope.renames.get(name) { | |
| 161 | return remapped.clone(); | |
| 162 | } | |
| 163 | } | |
| 164 | name.clone() | |
| 165 | } | |
| 166 | ||
| 167 | /// Declare an identifier in the current scope. | |
| 168 | pub fn declare_identifier(&mut self, name: String) -> TempId { | |
| 169 | let remapped = self.generate_id(&name); | |
| 170 | let scope = self.scopes.last_mut().unwrap(); | |
| 171 | self.ids.insert(remapped.unmapped_name.clone()); | |
| 172 | scope.renames.insert(name, remapped.unmapped_name.clone()); | |
| 173 | remapped | |
| 174 | } | |
| 175 | ||
| 176 | pub fn take_last_statement(&mut self) -> Option<JsStatement> { | |
| 177 | if !self.has_last_statement { | |
| 178 | return None; | |
| 179 | } | |
| 180 | self.has_last_statement = false; | |
| 181 | Some(self.statements.pop().unwrap()) | |
| 182 | } | |
| 183 | ||
| 184 | pub fn take_last_statement_if_expr(&mut self) -> Option<JsExpr> { | |
| 185 | if !self.has_last_statement { | |
| 186 | return None; | |
| 187 | } | |
| 188 | match self.statements.last().unwrap().data { | |
| 189 | JsStatementData::Expression(_) => { | |
| 190 | self.has_last_statement = false; | |
| 191 | match self.statements.pop().unwrap().data { | |
| 192 | JsStatementData::Expression(expr) => Some(expr), | |
| 193 | _ => unreachable!(), | |
| 194 | } | |
| 195 | } | |
| 196 | _ => return None, | |
| 197 | } | |
| 198 | } | |
| 199 | } | |
| 200 | ||
| 201 | pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { | |
| 202 | JsStatement::new( | |
| 203 | JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), | |
| 204 | loc, | |
| 205 | None, | |
| 206 | ) | |
| 207 | } | |
| 208 | ||
| 209 | pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { | |
| 210 | JsExpr::new( | |
| 211 | JsExprData::Binary { | |
| 212 | op: BinaryOperator::Assign, | |
| 213 | left: Box::new(JsExpr::new( | |
| 214 | JsExprData::Identifier(target), | |
| 215 | loc.clone(), | |
| 216 | None, | |
| 217 | )), | |
| 218 | right: Box::new(expr), | |
| 219 | }, | |
| 220 | loc, | |
| 221 | None, | |
| 222 | ) | |
| 223 | } | |
| 224 | ||
| 225 | impl Into<JsBlock> for BlockConvertContext { | |
| 226 | fn into(self) -> JsBlock { | |
| 227 | JsBlock::new(self.into(), Loc::EMPTY) | |
| 228 | } | |
| 229 | } | |
| 230 | ||
| 231 | impl Into<JsModule> for BlockConvertContext { | |
| 232 | fn into(self) -> JsModule { | |
| 233 | JsModule { | |
| 234 | statements: self.into(), | |
| 235 | } | |
| 236 | } | |
| 237 | } | |
| 238 | ||
| 239 | impl Into<Vec<JsStatement>> for BlockConvertContext { | |
| 240 | fn into(mut self) -> Vec<JsStatement> { | |
| 241 | tree_shake_statement_list(&mut self.statements); | |
| 242 | self.statements | |
| 243 | } | |
| 244 | } |
crates/js_ast/convert/convert_valid_block.rs created+37| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | use super::convert_valid_statement_to_expr; | |
| 2 | use super::BlockConvertContext; | |
| 3 | use super::IsValidJSExpression; | |
| 4 | use crate::js_ast::*; | |
| 5 | use ast::*; | |
| 6 | use tokenizer::Loc; | |
| 7 | ||
| 8 | pub fn convert_valid_block(ctx: &mut BlockConvertContext, block: Block, loc: Loc) -> JsExpr { | |
| 9 | debug_assert!( | |
| 10 | block.is_valid_js_expression(), | |
| 11 | "must call convert_valid_block with valid js expression" | |
| 12 | ); | |
| 13 | ||
| 14 | let len = block.statements.len(); | |
| 15 | let loc = loc; | |
| 16 | let block_expr = match len { | |
| 17 | 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), | |
| 18 | // a block expression with a single expression is just that expression | |
| 19 | 1 => convert_valid_statement_to_expr(ctx, block.statements.into_iter().next().unwrap()), | |
| 20 | // Since blocks are not valid expressions in JS, we create a | |
| 21 | // comma expression from a block with more than one statement. | |
| 22 | 2.. => { | |
| 23 | let mut items = Vec::new(); | |
| 24 | for statement in block.statements.into_iter() { | |
| 25 | items.push(convert_valid_statement_to_expr(ctx, statement)); | |
| 26 | } | |
| 27 | JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) | |
| 28 | } | |
| 29 | }; | |
| 30 | ||
| 31 | // a block without implicit return is to be type void, so we wrap in a void expression | |
| 32 | if !block.has_implicit_return && len > 0 { | |
| 33 | JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) | |
| 34 | } else { | |
| 35 | block_expr | |
| 36 | } | |
| 37 | } |
crates/js_ast/convert/convert_valid_expr.rs created+115| ... | ... | @@ -0,0 +1,115 @@ |
| 1 | use super::convert_valid_block; | |
| 2 | use super::BlockConvertContext; | |
| 3 | use crate::convert::convert_valid_if; | |
| 4 | use crate::convert::process_function::process_function_to_arrow; | |
| 5 | use crate::convert::IsValidJSExpression; | |
| 6 | use crate::js_ast::*; | |
| 7 | use ast::*; | |
| 8 | ||
| 9 | /// Translate a blight ast expression that is a valid JS expression, to a JS expression | |
| 10 | /// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` | |
| 11 | pub fn convert_valid_expr(ctx: &mut BlockConvertContext, expr: Expr) -> JsExpr { | |
| 12 | // Assert may be skipped in release builds as all failures are | |
| 13 | // annotated with a release-checked panic | |
| 14 | debug_assert!( | |
| 15 | expr.is_valid_js_expression(), | |
| 16 | "must call convert_valid_expr with valid js expression (use hoist instead)" | |
| 17 | ); | |
| 18 | ||
| 19 | let loc = expr.loc; | |
| 20 | let comment = expr.comment; | |
| 21 | ||
| 22 | match expr.data { | |
| 23 | ExprData::Identifier(str) => JsExpr::new( | |
| 24 | JsExprData::Identifier(ctx.remap_identifier(&str)), | |
| 25 | loc, | |
| 26 | comment, | |
| 27 | ), | |
| 28 | ||
| 29 | ExprData::Literal(Literal::Bool(value)) => { | |
| 30 | JsExpr::new(JsExprData::Bool(value), loc, comment) | |
| 31 | } | |
| 32 | ExprData::Literal(Literal::Number(value)) => { | |
| 33 | JsExpr::new(JsExprData::Number(value), loc, comment) | |
| 34 | } | |
| 35 | ExprData::Literal(Literal::String(value)) => { | |
| 36 | JsExpr::new(JsExprData::String(value), loc, comment) | |
| 37 | } | |
| 38 | ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), | |
| 39 | ||
| 40 | ExprData::Block(block) => convert_valid_block(ctx, block, loc), | |
| 41 | ||
| 42 | ExprData::Binary { op, left, right } => { | |
| 43 | let left = convert_valid_expr(ctx, *left); | |
| 44 | let right = convert_valid_expr(ctx, *right); | |
| 45 | JsExpr::new( | |
| 46 | JsExprData::Binary { | |
| 47 | op, | |
| 48 | left: Box::new(left), | |
| 49 | right: Box::new(right), | |
| 50 | }, | |
| 51 | loc, | |
| 52 | comment, | |
| 53 | ) | |
| 54 | } | |
| 55 | ExprData::Unary { op, value } => { | |
| 56 | let value = convert_valid_expr(ctx, *value); | |
| 57 | JsExpr::new( | |
| 58 | JsExprData::Unary { | |
| 59 | op, | |
| 60 | value: Box::new(value), | |
| 61 | }, | |
| 62 | loc, | |
| 63 | comment, | |
| 64 | ) | |
| 65 | } | |
| 66 | ExprData::Fn { body, params, .. } => { | |
| 67 | process_function_to_arrow(ctx, *body, params, loc, comment) | |
| 68 | } | |
| 69 | ExprData::Builtin { id, arguments } => match id { | |
| 70 | BuiltinId::Raw => { | |
| 71 | let first_arg = arguments.iter().next().unwrap(); | |
| 72 | // TODO(@paperdave): this string clone is stupid | |
| 73 | let str = match first_arg.data.clone() { | |
| 74 | ExprData::Literal(Literal::String(s)) => s, | |
| 75 | _ => panic!("raw expects a string"), | |
| 76 | }; | |
| 77 | JsExpr::new(JsExprData::Raw(str), loc, comment) | |
| 78 | } | |
| 79 | }, | |
| 80 | ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), | |
| 81 | ||
| 82 | ExprData::Call { base, arguments } => { | |
| 83 | let base = convert_valid_expr(ctx, *base); | |
| 84 | let arguments = arguments | |
| 85 | .into_iter() | |
| 86 | .map(|x| convert_valid_expr(ctx, x)) | |
| 87 | .collect(); | |
| 88 | JsExpr::new( | |
| 89 | JsExprData::Call { | |
| 90 | base: Box::new(base), | |
| 91 | arguments, | |
| 92 | }, | |
| 93 | loc, | |
| 94 | comment, | |
| 95 | ) | |
| 96 | } | |
| 97 | ||
| 98 | ExprData::Field { base, field } => JsExpr::new( | |
| 99 | match field.data { | |
| 100 | ExprData::Literal(Literal::String(name)) => JsExprData::Dot { | |
| 101 | base: Box::new(convert_valid_expr(ctx, *base)), | |
| 102 | name, | |
| 103 | }, | |
| 104 | _ => JsExprData::Index { | |
| 105 | base: Box::new(convert_valid_expr(ctx, *base)), | |
| 106 | index: Box::new(convert_valid_expr(ctx, *field)), | |
| 107 | }, | |
| 108 | }, | |
| 109 | loc, | |
| 110 | comment, | |
| 111 | ), | |
| 112 | ||
| 113 | ExprData::Return(_) => unreachable!("convert_valid_expr called on return"), | |
| 114 | } | |
| 115 | } |
crates/js_ast/convert/convert_valid_if.rs created+38| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | use super::convert_valid_block; | |
| 2 | use super::convert_valid_expr; | |
| 3 | use super::BlockConvertContext; | |
| 4 | use crate::convert::IsValidJSExpression; | |
| 5 | use crate::js_ast::*; | |
| 6 | use ast::*; | |
| 7 | use tokenizer::Loc; | |
| 8 | ||
| 9 | /// Convert a valid blight if statement to a JS expression | |
| 10 | pub fn convert_valid_if( | |
| 11 | ctx: &mut BlockConvertContext, | |
| 12 | expr: If, | |
| 13 | loc: Loc, | |
| 14 | comment: Option<Comment>, | |
| 15 | ) -> JsExpr { | |
| 16 | debug_assert!( | |
| 17 | expr.is_valid_js_expression(), | |
| 18 | "convert_valid_if on an invalid expression (use hoist instead)" | |
| 19 | ); | |
| 20 | let condition = convert_valid_expr(ctx, *expr.condition); | |
| 21 | ||
| 22 | let yes = convert_valid_block(ctx, expr.yes.block, expr.yes.loc); | |
| 23 | let no = match expr.no { | |
| 24 | Some(BlockOrIf::Block(b)) => Some(convert_valid_block(ctx, b.block, b.loc)), | |
| 25 | Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), | |
| 26 | None => None, | |
| 27 | }; | |
| 28 | ||
| 29 | JsExpr::new( | |
| 30 | JsExprData::Ternary { | |
| 31 | condition: Box::new(condition), | |
| 32 | yes: Box::new(yes), | |
| 33 | no: no.map(Box::new), | |
| 34 | }, | |
| 35 | loc, | |
| 36 | comment, | |
| 37 | ) | |
| 38 | } |
crates/js_ast/convert/convert_valid_statement.rs created+32| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | use super::convert_valid_expr; | |
| 2 | use super::is_valid_js_expression::*; | |
| 3 | use super::BlockConvertContext; | |
| 4 | use crate::js_ast::*; | |
| 5 | use ast::*; | |
| 6 | ||
| 7 | /// Translate a blight ast statement that is a valid JS expression, to a JS expression | |
| 8 | /// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` | |
| 9 | pub fn convert_valid_statement_to_expr( | |
| 10 | ctx: &mut BlockConvertContext, | |
| 11 | statement: Statement, | |
| 12 | ) -> JsExpr { | |
| 13 | debug_assert!( | |
| 14 | statement.is_valid_js_expression(), | |
| 15 | "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" | |
| 16 | ); | |
| 17 | ||
| 18 | match statement { | |
| 19 | ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), | |
| 20 | _ => unreachable!("attempt to convert statement to js without hoisting"), | |
| 21 | } | |
| 22 | } | |
| 23 | ||
| 24 | /// Translate a blight ast statement that is a valid JS expression, to a JS statement | |
| 25 | /// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` | |
| 26 | /// | |
| 27 | /// TODO(@paperdave): Replace this with Into<JsStatement> for JsExpr | |
| 28 | pub fn convert_valid_statement(ctx: &mut BlockConvertContext, statement: Statement) -> JsStatement { | |
| 29 | let expr = convert_valid_statement_to_expr(ctx, statement); | |
| 30 | let loc = expr.loc.clone(); | |
| 31 | JsStatement::new(JsStatementData::Expression(expr), loc, None) | |
| 32 | } |
crates/js_ast/convert/hoist_block_to_expr.rs created+78| ... | ... | @@ -0,0 +1,78 @@ |
| 1 | use super::is_valid_js_expression::*; | |
| 2 | use super::process_statement; | |
| 3 | use super::BlockConvertContext; | |
| 4 | use crate::js_ast::*; | |
| 5 | use ast::*; | |
| 6 | use tokenizer::Loc; | |
| 7 | ||
| 8 | /// This block hoist is used when we have a block in an expression context. | |
| 9 | /// | |
| 10 | /// All the expressions in the block are lifted above the currently | |
| 11 | /// processed expression, and then a temporary variable is returned. | |
| 12 | /// (or the last expression might get inlined as the return value) | |
| 13 | /// | |
| 14 | /// A "virtual scope" is created for this block, which handles renaming | |
| 15 | /// situations like: | |
| 16 | /// | |
| 17 | /// let x = 1; | |
| 18 | /// { | |
| 19 | /// let x = 1; | |
| 20 | /// } | |
| 21 | /// call(x) | |
| 22 | /// | |
| 23 | /// The output JS of the above would be: | |
| 24 | /// | |
| 25 | /// const x = 1; | |
| 26 | /// const x2 = 1; | |
| 27 | /// call(x) | |
| 28 | /// | |
| 29 | /// For expressions that have branches, use `hoist_block_to_js_block` | |
| 30 | pub fn hoist_block_to_expr( | |
| 31 | ctx: &mut BlockConvertContext, | |
| 32 | block: Block, | |
| 33 | loc: Loc, | |
| 34 | comment: Option<Comment>, | |
| 35 | ) -> Option<JsExpr> { | |
| 36 | debug_assert!( | |
| 37 | !block.is_valid_js_expression(), | |
| 38 | "hoist_block on a valid expression, should call convert_valid_block instead" | |
| 39 | ); | |
| 40 | ||
| 41 | let temporary = ctx.generate_temp_id("_block"); | |
| 42 | ||
| 43 | ctx.push_virtual_scope(); | |
| 44 | ||
| 45 | for statement in block.statements { | |
| 46 | process_statement(ctx, statement); | |
| 47 | } | |
| 48 | ||
| 49 | ctx.pop_virtual_scope(); | |
| 50 | ||
| 51 | // If the last statement is an expression, we need | |
| 52 | // to assign to the temporary variable. | |
| 53 | let result = match ctx.last_statement().map(|x| &x.data) { | |
| 54 | Some(JsStatementData::Expression(_)) if block.has_implicit_return => { | |
| 55 | match ctx.take_last_statement().unwrap().data { | |
| 56 | JsStatementData::Expression(expr) => expr, | |
| 57 | _ => unreachable!(), | |
| 58 | } | |
| 59 | } | |
| 60 | _ => JsExpr::new(JsExprData::Null, loc.clone(), None), | |
| 61 | }; | |
| 62 | ||
| 63 | ctx.add_statement(JsStatement::new( | |
| 64 | JsStatementData::Declaration(JSDeclaration { | |
| 65 | name: temporary.get(&ctx), | |
| 66 | init: Some(result), | |
| 67 | is_constant: false, | |
| 68 | }), | |
| 69 | loc.clone(), | |
| 70 | comment, | |
| 71 | )); | |
| 72 | ||
| 73 | Some(JsExpr::new( | |
| 74 | JsExprData::Identifier(temporary.get(&ctx)), | |
| 75 | loc, | |
| 76 | None, | |
| 77 | )) | |
| 78 | } |
crates/js_ast/convert/hoist_block_to_js_block.rs created+68| ... | ... | @@ -0,0 +1,68 @@ |
| 1 | use crate::js_ast::*; | |
| 2 | use ast::*; | |
| 3 | use tokenizer::Loc; | |
| 4 | ||
| 5 | use super::*; | |
| 6 | ||
| 7 | /// This block hoist is used when we actually need to create a JsBlock, which | |
| 8 | /// is needed if there is an invalid expression within a branch of code. | |
| 9 | /// | |
| 10 | /// For example, `if` has two branches, the 'yes' and 'no' paths. | |
| 11 | /// When we cannot lower it as a ternary, we have to create two JsBlocks | |
| 12 | /// | |
| 13 | /// The `target` param is a shared temporary variable that will be assigned | |
| 14 | /// to the result of the block. | |
| 15 | /// | |
| 16 | /// Since blocks can be void or noreturn, this target is not always | |
| 17 | /// assigned to. The `did_assign` flag is used to indicate that. | |
| 18 | /// | |
| 19 | /// For expressions that do not have branches, use `hoist_block_to_expr` | |
| 20 | /// | |
| 21 | /// TODO(@paperdave): move `did_assign` into the return value | |
| 22 | pub fn hoist_block_to_js_block( | |
| 23 | ctx: &mut BlockConvertContext, | |
| 24 | block: Block, | |
| 25 | loc: Loc, | |
| 26 | target: &TempId, | |
| 27 | did_assign: &mut bool, | |
| 28 | ) -> JsBlock { | |
| 29 | // A non-block will become a block with a single expression: | |
| 30 | // _temp = expr; | |
| 31 | // | |
| 32 | // We will also apply this transformation to blocks that are valid | |
| 33 | // expressions, because that is more concise. | |
| 34 | if block.is_valid_js_expression() { | |
| 35 | return JsBlock::new( | |
| 36 | vec![new_js_assignment_statement( | |
| 37 | convert_valid_block(ctx, block, loc.clone()), | |
| 38 | target.get(ctx), | |
| 39 | loc.clone(), | |
| 40 | )], | |
| 41 | loc, | |
| 42 | ); | |
| 43 | } | |
| 44 | ||
| 45 | let mut child = ctx.new_child(); | |
| 46 | ||
| 47 | for statement in block.statements { | |
| 48 | if let None = process_statement(&mut child, statement) { | |
| 49 | return child.into(); | |
| 50 | }; | |
| 51 | } | |
| 52 | ||
| 53 | // If the last statement is an expression, we need | |
| 54 | // to assign to the temporary variable. | |
| 55 | match child.take_last_statement_if_expr() { | |
| 56 | Some(expr) => { | |
| 57 | *did_assign = true; | |
| 58 | child.add_statement(new_js_assignment_statement( | |
| 59 | expr, | |
| 60 | target.get(ctx), | |
| 61 | loc.clone(), | |
| 62 | )); | |
| 63 | } | |
| 64 | _ => {} | |
| 65 | } | |
| 66 | ||
| 67 | child.into() | |
| 68 | } |
crates/js_ast/convert/hoist_expr.rs created+132| ... | ... | @@ -0,0 +1,132 @@ |
| 1 | use super::convert_valid_expr; | |
| 2 | use super::hoist_block_to_expr; | |
| 3 | use super::hoist_if_expr; | |
| 4 | use super::is_valid_js_expression::*; | |
| 5 | use super::BlockConvertContext; | |
| 6 | // use super::HasSideEffects; | |
| 7 | use crate::js_ast::*; | |
| 8 | use crate::util::is_js_identifier; | |
| 9 | use ast::*; | |
| 10 | ||
| 11 | /// Pushes a decl with a generated identifier to the hoisted result | |
| 12 | /// and returns an identifier expression for it. | |
| 13 | pub fn hoist_expr_if_needed(ctx: &mut BlockConvertContext, expr: Expr) -> Option<JsExpr> { | |
| 14 | // if !expr.has_side_effects() { | |
| 15 | // return Some(ctx.hoist_expression_contents(expr)?); | |
| 16 | // } | |
| 17 | ||
| 18 | let init = hoist_expr_contents(ctx, expr)?; | |
| 19 | ||
| 20 | let id = ctx.generate_temp_id("_"); | |
| 21 | let loc = init.loc.clone(); | |
| 22 | ||
| 23 | ctx.add_statement(JsStatement::new( | |
| 24 | JsStatementData::Declaration(JSDeclaration { | |
| 25 | name: id.get(&ctx), | |
| 26 | init: Some(init), | |
| 27 | is_constant: true, | |
| 28 | }), | |
| 29 | loc.clone(), | |
| 30 | None, | |
| 31 | )); | |
| 32 | ||
| 33 | Some(JsExpr::new(JsExprData::Identifier(id.get(&ctx)), loc, None)) | |
| 34 | } | |
| 35 | ||
| 36 | /// Returning `None` from this means that the expression is noreturn, and that | |
| 37 | /// the rest of the statement never occurs and can be stripped. | |
| 38 | pub fn hoist_expr_contents(ctx: &mut BlockConvertContext, expr: Expr) -> Option<JsExpr> { | |
| 39 | if expr.is_valid_js_expression() { | |
| 40 | return Some(convert_valid_expr(ctx, expr)); | |
| 41 | } | |
| 42 | ||
| 43 | match expr.data { | |
| 44 | ExprData::Binary { op, left, right } => { | |
| 45 | let left = hoist_expr_if_needed(ctx, *left)?; | |
| 46 | let right = hoist_expr_if_needed(ctx, *right)?; | |
| 47 | ||
| 48 | Some(JsExpr::new( | |
| 49 | JsExprData::Binary { | |
| 50 | op, | |
| 51 | left: Box::new(left), | |
| 52 | right: Box::new(right), | |
| 53 | }, | |
| 54 | expr.loc, | |
| 55 | expr.comment, | |
| 56 | )) | |
| 57 | } | |
| 58 | ||
| 59 | ExprData::Unary { op, value } => { | |
| 60 | let value = hoist_expr_if_needed(ctx, *value)?; | |
| 61 | Some(JsExpr::new( | |
| 62 | JsExprData::Unary { | |
| 63 | op, | |
| 64 | value: Box::new(value), | |
| 65 | }, | |
| 66 | expr.loc, | |
| 67 | expr.comment, | |
| 68 | )) | |
| 69 | } | |
| 70 | ||
| 71 | ExprData::Field { base, field } => { | |
| 72 | let base = hoist_expr_if_needed(ctx, *base)?; | |
| 73 | let field = hoist_expr_if_needed(ctx, *field)?; | |
| 74 | ||
| 75 | Some(JsExpr::new( | |
| 76 | match field.data { | |
| 77 | JsExprData::String(name) if is_js_identifier(name.as_str()) => { | |
| 78 | JsExprData::Dot { | |
| 79 | base: Box::new(base), | |
| 80 | name, | |
| 81 | } | |
| 82 | } | |
| 83 | _ => JsExprData::Index { | |
| 84 | base: Box::new(base), | |
| 85 | index: Box::new(field), | |
| 86 | }, | |
| 87 | }, | |
| 88 | expr.loc, | |
| 89 | expr.comment, | |
| 90 | )) | |
| 91 | } | |
| 92 | ||
| 93 | ExprData::Block(block) => hoist_block_to_expr(ctx, block, expr.loc, expr.comment), | |
| 94 | ||
| 95 | ExprData::If(_) => hoist_if_expr(ctx, expr), | |
| 96 | ||
| 97 | ExprData::Return(expr) => { | |
| 98 | let loc = expr.loc.clone(); | |
| 99 | let return_value = hoist_expr_if_needed(ctx, *expr)?; | |
| 100 | ctx.add_statement(JsStatement::new( | |
| 101 | JsStatementData::Return(return_value), | |
| 102 | loc, | |
| 103 | None, | |
| 104 | )); | |
| 105 | None | |
| 106 | } | |
| 107 | ||
| 108 | ExprData::Call { base, arguments } => { | |
| 109 | let base = hoist_expr_if_needed(ctx, *base)?; | |
| 110 | let arguments = arguments | |
| 111 | .into_iter() | |
| 112 | .map(|x| hoist_expr_if_needed(ctx, x)) | |
| 113 | .collect::<Option<Vec<_>>>()?; | |
| 114 | ||
| 115 | Some(JsExpr::new( | |
| 116 | JsExprData::Call { | |
| 117 | base: Box::new(base), | |
| 118 | arguments, | |
| 119 | }, | |
| 120 | expr.loc, | |
| 121 | expr.comment, | |
| 122 | )) | |
| 123 | } | |
| 124 | ||
| 125 | ExprData::Fn { .. } | |
| 126 | | ExprData::Builtin { .. } | |
| 127 | | ExprData::Literal(_) | |
| 128 | | ExprData::Identifier(_) => { | |
| 129 | unreachable!("attempt to hoist an expression that does not require it") | |
| 130 | } | |
| 131 | } | |
| 132 | } |
crates/js_ast/convert/hoist_if.rs created+113| ... | ... | @@ -0,0 +1,113 @@ |
| 1 | use super::convert_valid_expr; | |
| 2 | use super::hoist_block_to_js_block; | |
| 3 | use super::hoist_expr_if_needed; | |
| 4 | use super::BlockConvertContext; | |
| 5 | use super::IsValidJSExpression; | |
| 6 | use super::TempId; | |
| 7 | use crate::js_ast::*; | |
| 8 | use ast::*; | |
| 9 | use tokenizer::Loc; | |
| 10 | ||
| 11 | pub fn hoist_if_expr(ctx: &mut BlockConvertContext, expr: Expr) -> Option<JsExpr> { | |
| 12 | let data = match expr.data { | |
| 13 | ExprData::If(data) => data, | |
| 14 | _ => unreachable!("hoist_if_expr called on non-if expression"), | |
| 15 | }; | |
| 16 | ||
| 17 | let temporary = ctx.generate_temp_id("_if"); | |
| 18 | ||
| 19 | let decl = ctx.add_statement(JsStatement::new( | |
| 20 | JsStatementData::Declaration(JSDeclaration { | |
| 21 | name: temporary.get(&ctx), | |
| 22 | init: None, | |
| 23 | is_constant: false, | |
| 24 | }), | |
| 25 | expr.loc.clone(), | |
| 26 | None, | |
| 27 | )); | |
| 28 | ||
| 29 | let condition = hoist_expr_if_needed(ctx, *data.condition)?; | |
| 30 | let mut did_assign = false; | |
| 31 | ||
| 32 | let js_if = hoist_if_branch( | |
| 33 | ctx, | |
| 34 | data.yes, | |
| 35 | data.no, | |
| 36 | condition, | |
| 37 | &mut did_assign, | |
| 38 | &temporary, | |
| 39 | ); | |
| 40 | ||
| 41 | ctx.add_statement(JsStatement::new( | |
| 42 | JsStatementData::If(js_if), | |
| 43 | expr.loc.clone(), | |
| 44 | None, | |
| 45 | )); | |
| 46 | ||
| 47 | if did_assign { | |
| 48 | Some(JsExpr::new( | |
| 49 | JsExprData::Identifier(temporary.get(&ctx)), | |
| 50 | expr.loc, | |
| 51 | expr.comment, | |
| 52 | )) | |
| 53 | } else { | |
| 54 | decl.delete(ctx); | |
| 55 | Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | pub fn hoist_if_branch( | |
| 60 | ctx: &mut BlockConvertContext, | |
| 61 | yes: BlockWithLoc, | |
| 62 | no: Option<BlockOrIf>, | |
| 63 | condition: JsExpr, | |
| 64 | did_assign: &mut bool, | |
| 65 | temporary: &TempId, | |
| 66 | ) -> JsIf { | |
| 67 | let yes = hoist_block_to_js_block(ctx, yes.block, yes.loc, &temporary, did_assign); | |
| 68 | ||
| 69 | let no = match no { | |
| 70 | Some(BlockOrIf::Block(no)) => Some(JsElse::Block(hoist_block_to_js_block( | |
| 71 | ctx, no.block, no.loc, &temporary, did_assign, | |
| 72 | ))), | |
| 73 | Some(BlockOrIf::If(else_if)) => { | |
| 74 | // If the condition of this block has a side effect, we need to | |
| 75 | // emit a block so that we can assign to the temporary variable. | |
| 76 | // | |
| 77 | // Otherwise, we can just emit the condition inline | |
| 78 | if else_if.condition.is_valid_js_expression() { | |
| 79 | let condition = convert_valid_expr(ctx, *else_if.condition.clone()); | |
| 80 | Some(JsElse::If(Box::new(hoist_if_branch( | |
| 81 | ctx, | |
| 82 | else_if.yes, | |
| 83 | else_if.no, | |
| 84 | condition, | |
| 85 | did_assign, | |
| 86 | temporary, | |
| 87 | )))) | |
| 88 | } else { | |
| 89 | let mut temp_block = ctx.new_child(); | |
| 90 | let condition = | |
| 91 | hoist_expr_if_needed(&mut temp_block, *else_if.condition.clone()).unwrap(); | |
| 92 | ||
| 93 | temp_block.add_statement(JsStatement::new( | |
| 94 | JsStatementData::If(hoist_if_branch( | |
| 95 | ctx, | |
| 96 | else_if.yes, | |
| 97 | else_if.no, | |
| 98 | condition, | |
| 99 | did_assign, | |
| 100 | temporary, | |
| 101 | )), | |
| 102 | Loc::EMPTY, | |
| 103 | None, | |
| 104 | )); | |
| 105 | ||
| 106 | Some(JsElse::Block(temp_block.into())) | |
| 107 | } | |
| 108 | } | |
| 109 | None => None, | |
| 110 | }; | |
| 111 | ||
| 112 | JsIf { condition, yes, no } | |
| 113 | } |
crates/js_ast/convert/is_valid_js_expression.rs created+91| ... | ... | @@ -0,0 +1,91 @@ |
| 1 | use ast::*; | |
| 2 | ||
| 3 | /// "Valid" in this context means that the expression does not contain JS statements | |
| 4 | /// Meaning conversion is a direct map from a blight as node into a JS one | |
| 5 | /// | |
| 6 | /// A valid expression should not go through any hoist logic, but if an expression | |
| 7 | /// isn't valid, then it means that it must be split into multiple JS statements | |
| 8 | /// inside of a block. | |
| 9 | pub trait IsValidJSExpression { | |
| 10 | /// Can this be lowered to a JS ast without hoisting items | |
| 11 | /// ex: a `return` expression is not a valid JS expression | |
| 12 | fn is_valid_js_expression(&self) -> bool; | |
| 13 | } | |
| 14 | ||
| 15 | impl IsValidJSExpression for ExprData { | |
| 16 | fn is_valid_js_expression(&self) -> bool { | |
| 17 | match self { | |
| 18 | // A block containing just expressions can be an expression | |
| 19 | ExprData::Block(block) => block.is_valid_js_expression(), | |
| 20 | ExprData::If(expr) => expr.is_valid_js_expression(), | |
| 21 | ExprData::Return(_) => false, | |
| 22 | ExprData::Builtin { id, .. } => match id { | |
| 23 | BuiltinId::Raw => true, | |
| 24 | }, | |
| 25 | ExprData::Binary { left, right, .. } => { | |
| 26 | left.is_valid_js_expression() && right.is_valid_js_expression() | |
| 27 | } | |
| 28 | ExprData::Unary { value, .. } => value.is_valid_js_expression(), | |
| 29 | ExprData::Fn { .. } => true, | |
| 30 | ExprData::Call { base, arguments } => { | |
| 31 | base.is_valid_js_expression() | |
| 32 | && arguments | |
| 33 | .iter() | |
| 34 | .all(|argument| argument.is_valid_js_expression()) | |
| 35 | } | |
| 36 | ExprData::Field { base, field } => { | |
| 37 | base.is_valid_js_expression() && field.is_valid_js_expression() | |
| 38 | } | |
| 39 | ExprData::Literal(_) | ExprData::Identifier(_) => true, | |
| 40 | } | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | impl IsValidJSExpression for Block { | |
| 45 | fn is_valid_js_expression(&self) -> bool { | |
| 46 | self.statements | |
| 47 | .iter() | |
| 48 | .all(|statement| statement.is_valid_js_expression()) | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | impl IsValidJSExpression for Expr { | |
| 53 | fn is_valid_js_expression(&self) -> bool { | |
| 54 | self.data.is_valid_js_expression() | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | impl IsValidJSExpression for If { | |
| 59 | fn is_valid_js_expression(&self) -> bool { | |
| 60 | self.condition.is_valid_js_expression() | |
| 61 | && self.yes.is_valid_js_expression() | |
| 62 | && match &self.no { | |
| 63 | Some(no) => no.is_valid_js_expression(), | |
| 64 | None => true, | |
| 65 | } | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | impl IsValidJSExpression for BlockOrIf { | |
| 70 | fn is_valid_js_expression(&self) -> bool { | |
| 71 | match self { | |
| 72 | BlockOrIf::Block(block) => block.is_valid_js_expression(), | |
| 73 | BlockOrIf::If(if_) => if_.is_valid_js_expression(), | |
| 74 | } | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | impl IsValidJSExpression for BlockWithLoc { | |
| 79 | fn is_valid_js_expression(&self) -> bool { | |
| 80 | self.block.is_valid_js_expression() | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | impl IsValidJSExpression for Statement { | |
| 85 | fn is_valid_js_expression(&self) -> bool { | |
| 86 | match self { | |
| 87 | Statement::Expr(expr) => expr.data.is_valid_js_expression(), | |
| 88 | _ => false, | |
| 89 | } | |
| 90 | } | |
| 91 | } |
crates/js_ast/convert/mod.rs created+42| ... | ... | @@ -0,0 +1,42 @@ |
| 1 | mod block_convert_context; | |
| 2 | mod convert_valid_block; | |
| 3 | mod convert_valid_expr; | |
| 4 | mod convert_valid_if; | |
| 5 | mod convert_valid_statement; | |
| 6 | mod hoist_block_to_expr; | |
| 7 | mod hoist_block_to_js_block; | |
| 8 | mod hoist_expr; | |
| 9 | mod hoist_if; | |
| 10 | mod is_valid_js_expression; | |
| 11 | mod process_function; | |
| 12 | mod process_statement; | |
| 13 | mod side_effects; | |
| 14 | mod tree_shake_statement_list; | |
| 15 | ||
| 16 | pub use block_convert_context::*; | |
| 17 | pub use convert_valid_block::*; | |
| 18 | pub use convert_valid_expr::*; | |
| 19 | pub use convert_valid_if::*; | |
| 20 | pub use convert_valid_statement::*; | |
| 21 | pub use hoist_block_to_expr::*; | |
| 22 | pub use hoist_block_to_js_block::*; | |
| 23 | pub use hoist_expr::*; | |
| 24 | pub use hoist_if::*; | |
| 25 | pub use is_valid_js_expression::*; | |
| 26 | pub use process_function::*; | |
| 27 | pub use process_statement::*; | |
| 28 | // pub use side_effects::*; | |
| 29 | pub use tree_shake_statement_list::*; | |
| 30 | ||
| 31 | use crate::js_ast::JsModule; | |
| 32 | use ast::Module; | |
| 33 | ||
| 34 | pub fn blight_to_js(blight: Module) -> JsModule { | |
| 35 | let mut root_context = BlockConvertContext::new(); | |
| 36 | ||
| 37 | for statement in blight.statements { | |
| 38 | process_top_level_statement(&mut root_context, statement).unwrap(); | |
| 39 | } | |
| 40 | ||
| 41 | root_context.into() | |
| 42 | } |
crates/js_ast/convert/process_function.rs created+63| ... | ... | @@ -0,0 +1,63 @@ |
| 1 | use super::convert_valid_expr; | |
| 2 | use super::hoist_expr_if_needed; | |
| 3 | use super::process_statement; | |
| 4 | use super::BlockConvertContext; | |
| 5 | use super::IsValidJSExpression; | |
| 6 | use crate::js_ast::*; | |
| 7 | use ast::*; | |
| 8 | use tokenizer::Loc; | |
| 9 | ||
| 10 | pub fn process_function_to_arrow( | |
| 11 | ctx: &mut BlockConvertContext, | |
| 12 | body: Expr, | |
| 13 | params: Vec<Param>, | |
| 14 | loc: Loc, | |
| 15 | comment: Option<Comment>, | |
| 16 | ) -> JsExpr { | |
| 17 | // A block that is a valid expression will just become | |
| 18 | // that expression. We can convert multiple expressions | |
| 19 | // into a comma expression. | |
| 20 | let body = if body.is_valid_js_expression() { | |
| 21 | JsArrowBody::Expr(convert_valid_expr(ctx, body)) | |
| 22 | } else { | |
| 23 | let mut child = ctx.new_child(); | |
| 24 | ||
| 25 | match body.data { | |
| 26 | ExprData::Block(block) => { | |
| 27 | for statement in block.statements { | |
| 28 | if let None = process_statement(&mut child, statement) { | |
| 29 | break; | |
| 30 | }; | |
| 31 | } | |
| 32 | ||
| 33 | if let Some(expr) = child.take_last_statement_if_expr() { | |
| 34 | child.add_statement(JsStatement::new( | |
| 35 | JsStatementData::Return(expr), | |
| 36 | loc.clone(), | |
| 37 | None, | |
| 38 | )); | |
| 39 | } | |
| 40 | } | |
| 41 | _ => { | |
| 42 | if let Some(expr) = hoist_expr_if_needed(&mut child, body) { | |
| 43 | child.add_statement(JsStatement::new( | |
| 44 | JsStatementData::Return(expr), | |
| 45 | loc.clone(), | |
| 46 | None, | |
| 47 | )); | |
| 48 | } | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | JsArrowBody::Block(child.into()) | |
| 53 | }; | |
| 54 | ||
| 55 | JsExpr::new( | |
| 56 | JsExprData::ArrowFunction { | |
| 57 | params: params.into_iter().map(|x| x.name).collect(), | |
| 58 | body: Box::new(body), | |
| 59 | }, | |
| 60 | loc.clone(), | |
| 61 | comment.map(|x| x.into()), | |
| 62 | ) | |
| 63 | } |
crates/js_ast/convert/process_statement.rs created+95| ... | ... | @@ -0,0 +1,95 @@ |
| 1 | use super::convert_valid_statement; | |
| 2 | use super::hoist_expr_contents; | |
| 3 | use super::process_function_to_arrow; | |
| 4 | use super::BlockConvertContext; | |
| 5 | use super::IsValidJSExpression; | |
| 6 | use crate::js_ast::*; | |
| 7 | use ast::*; | |
| 8 | ||
| 9 | pub fn process_statement(ctx: &mut BlockConvertContext, statement: Statement) -> Option<()> { | |
| 10 | if statement.is_valid_js_expression() { | |
| 11 | let result = convert_valid_statement(ctx, statement); | |
| 12 | ctx.add_statement(result); | |
| 13 | return Some(()); | |
| 14 | } | |
| 15 | ||
| 16 | match statement { | |
| 17 | Statement::VarDecl(decl) => { | |
| 18 | // visit init before name, this way allows for: | |
| 19 | // | |
| 20 | // let a = a; | |
| 21 | // | |
| 22 | // and `a` will be looked up before it is re-declared. | |
| 23 | // we will also return in the noreturn case | |
| 24 | let init = hoist_expr_contents(ctx, decl.init)?; | |
| 25 | ||
| 26 | let name = ctx.declare_identifier(decl.name); | |
| 27 | ||
| 28 | ctx.add_statement(JsStatement::new( | |
| 29 | JsStatementData::Declaration(JSDeclaration { | |
| 30 | name: name.get(&ctx), | |
| 31 | init: Some(init), | |
| 32 | is_constant: decl.is_constant, | |
| 33 | }), | |
| 34 | decl.loc, | |
| 35 | decl.comment.map(|x| x.into()), | |
| 36 | )); | |
| 37 | ||
| 38 | Some(()) | |
| 39 | } | |
| 40 | Statement::Expr(expr) => { | |
| 41 | if let Some(expr) = hoist_expr_contents(ctx, expr) { | |
| 42 | let loc = expr.loc.clone(); | |
| 43 | ctx.add_statement(JsStatement::new( | |
| 44 | JsStatementData::Expression(expr), | |
| 45 | loc, | |
| 46 | None, | |
| 47 | )); | |
| 48 | Some(()) | |
| 49 | } else { | |
| 50 | None | |
| 51 | } | |
| 52 | } | |
| 53 | Statement::FnDecl(FnDecl { | |
| 54 | name, | |
| 55 | params, | |
| 56 | body, | |
| 57 | loc, | |
| 58 | comment, | |
| 59 | .. | |
| 60 | }) => { | |
| 61 | let name = ctx.declare_identifier(name); | |
| 62 | ||
| 63 | let processed = Some(process_function_to_arrow( | |
| 64 | ctx, | |
| 65 | body, | |
| 66 | params, | |
| 67 | loc.clone(), | |
| 68 | None, | |
| 69 | )); | |
| 70 | ||
| 71 | ctx.add_statement(JsStatement::new( | |
| 72 | JsStatementData::Declaration(JSDeclaration { | |
| 73 | name: name.get(&ctx), | |
| 74 | init: processed, | |
| 75 | is_constant: true, | |
| 76 | }), | |
| 77 | loc, | |
| 78 | comment.map(|x| x.into()), | |
| 79 | )); | |
| 80 | ||
| 81 | Some(()) | |
| 82 | } | |
| 83 | Statement::Comment(_) => Some(()), | |
| 84 | } | |
| 85 | } | |
| 86 | ||
| 87 | pub fn process_top_level_statement( | |
| 88 | ctx: &mut BlockConvertContext, | |
| 89 | statement: TopLevelStatement, | |
| 90 | ) -> Option<()> { | |
| 91 | match statement.to_statement() { | |
| 92 | Some(stmt) => process_statement(ctx, stmt), | |
| 93 | None => todo!("process_top_level_statement"), | |
| 94 | } | |
| 95 | } |
crates/js_ast/convert/side_effects.rs created+91| ... | ... | @@ -0,0 +1,91 @@ |
| 1 | use ast::*; | |
| 2 | ||
| 3 | pub trait HasSideEffects { | |
| 4 | /// Can this item be removed from the AST without changing the program's behavior? | |
| 5 | fn has_side_effects(&self) -> bool; | |
| 6 | } | |
| 7 | ||
| 8 | impl HasSideEffects for ExprData { | |
| 9 | fn has_side_effects(&self) -> bool { | |
| 10 | match self { | |
| 11 | ExprData::Literal(_) => false, | |
| 12 | ExprData::Identifier(_) => false, | |
| 13 | ||
| 14 | ExprData::Block(block) => block.has_side_effects(), | |
| 15 | ExprData::Binary { left, right, .. } => { | |
| 16 | left.has_side_effects() || right.has_side_effects() | |
| 17 | } | |
| 18 | ExprData::Unary { value, .. } => value.has_side_effects(), | |
| 19 | ExprData::If(ifexpr) => ifexpr.has_side_effects(), | |
| 20 | ||
| 21 | // note: a function *declaration* has no side effects, but the call *can* | |
| 22 | ExprData::Fn { .. } => false, | |
| 23 | ||
| 24 | // TODO(@paperdave): implement detection of pure functions | |
| 25 | ExprData::Call { base, arguments } => { | |
| 26 | base.has_side_effects() || arguments.iter().any(|x| x.has_side_effects()) | |
| 27 | } | |
| 28 | ||
| 29 | // note: we assume field access has side effects, ie: a getter doesn't throw | |
| 30 | ExprData::Field { base, field } => base.has_side_effects() || field.has_side_effects(), | |
| 31 | ||
| 32 | // note: we assume the code in @raw always has side effects | |
| 33 | ExprData::Builtin { id, .. } => match id { | |
| 34 | BuiltinId::Raw => true, | |
| 35 | }, | |
| 36 | ||
| 37 | ExprData::Return(_) => true, | |
| 38 | } | |
| 39 | } | |
| 40 | } | |
| 41 | ||
| 42 | impl HasSideEffects for Statement { | |
| 43 | fn has_side_effects(&self) -> bool { | |
| 44 | match self { | |
| 45 | Statement::Expr(expr) => expr.has_side_effects(), | |
| 46 | Statement::VarDecl(decl) => decl.init.has_side_effects(), | |
| 47 | Statement::Comment(_) => false, | |
| 48 | Statement::FnDecl(_) => false, | |
| 49 | } | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | impl HasSideEffects for Block { | |
| 54 | fn has_side_effects(&self) -> bool { | |
| 55 | self.statements | |
| 56 | .iter() | |
| 57 | .any(|statement| statement.has_side_effects()) | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | impl HasSideEffects for Expr { | |
| 62 | fn has_side_effects(&self) -> bool { | |
| 63 | self.data.has_side_effects() | |
| 64 | } | |
| 65 | } | |
| 66 | ||
| 67 | impl HasSideEffects for If { | |
| 68 | fn has_side_effects(&self) -> bool { | |
| 69 | self.condition.has_side_effects() | |
| 70 | || self.yes.has_side_effects() | |
| 71 | || match &self.no { | |
| 72 | Some(no) => no.has_side_effects(), | |
| 73 | None => false, | |
| 74 | } | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | impl HasSideEffects for BlockOrIf { | |
| 79 | fn has_side_effects(&self) -> bool { | |
| 80 | match self { | |
| 81 | BlockOrIf::Block(block) => block.has_side_effects(), | |
| 82 | BlockOrIf::If(if_) => if_.has_side_effects(), | |
| 83 | } | |
| 84 | } | |
| 85 | } | |
| 86 | ||
| 87 | impl HasSideEffects for BlockWithLoc { | |
| 88 | fn has_side_effects(&self) -> bool { | |
| 89 | self.block.has_side_effects() | |
| 90 | } | |
| 91 | } |
crates/js_ast/convert/tree_shake_statement_list.rs created+31| ... | ... | @@ -0,0 +1,31 @@ |
| 1 | use crate::js_ast::*; | |
| 2 | ||
| 3 | /// Removes statements that are not needed from a JS block. Very limited. | |
| 4 | /// | |
| 5 | /// BlockConvertContext will automatically execute this when converting | |
| 6 | /// itself into a Statement list or other data structure. | |
| 7 | /// | |
| 8 | /// What this does: | |
| 9 | /// - Remove discarded literals `null` -> `` | |
| 10 | pub fn tree_shake_statement_list(list: &mut Vec<JsStatement>) { | |
| 11 | let mut set_i = 0; | |
| 12 | for i in 0..list.len() { | |
| 13 | let stmt = list.get(i).unwrap(); | |
| 14 | let should_keep = match &stmt.data { | |
| 15 | JsStatementData::Expression(expr) => !expr.is_literal(), | |
| 16 | JsStatementData::Return(_) => true, | |
| 17 | JsStatementData::Export(_) => true, | |
| 18 | JsStatementData::Declaration(_) => true, | |
| 19 | JsStatementData::Function(_) => true, | |
| 20 | JsStatementData::If(_) => true, | |
| 21 | }; | |
| 22 | ||
| 23 | if should_keep { | |
| 24 | if set_i != i { | |
| 25 | list.swap(i, set_i) | |
| 26 | } | |
| 27 | set_i += 1; | |
| 28 | } | |
| 29 | } | |
| 30 | list.truncate(set_i) | |
| 31 | } |
crates/js_ast/convert_valid_expr.rs deleted-204| ... | ... | @@ -1,204 +0,0 @@ |
| 1 | //! "Valid" in this context means that the expression does not contain JS statements | |
| 2 | //! Meaning conversion is a direct map from a blight as node into a JS one | |
| 3 | //! | |
| 4 | //! See `is_valid_js_expression` for more details on what qualifies | |
| 5 | //! | |
| 6 | //! For other nodes, it must hoist statements to the nearest JS block, | |
| 7 | //! is done in the `hoist_*` functions in `host.rs`. | |
| 8 | ||
| 9 | use ast::{BlockOrIf, Comment, ExprData, If}; | |
| 10 | use ast::{BlockWithLoc, BuiltinId, Expr, Literal, Statement}; | |
| 11 | use tokenizer::Loc; | |
| 12 | ||
| 13 | use crate::is_valid_js_expression::IsValidJSExpression; | |
| 14 | use crate::js_ast::*; | |
| 15 | use crate::HoistContext; | |
| 16 | ||
| 17 | /// Translate a blight ast expression that is a valid JS expression, to a JS expression | |
| 18 | /// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` | |
| 19 | pub fn convert_valid_expr(ctx: &mut HoistContext, expr: Expr) -> JsExpr { | |
| 20 | // Assert may be skipped in release builds as all failures are | |
| 21 | // annotated with a release-checked panic | |
| 22 | debug_assert!( | |
| 23 | expr.is_valid_js_expression(), | |
| 24 | "must call convert_valid_expr with valid js expression (use hoist instead)" | |
| 25 | ); | |
| 26 | ||
| 27 | let loc = expr.loc; | |
| 28 | let comment = expr.comment; | |
| 29 | ||
| 30 | match expr.data { | |
| 31 | ExprData::Identifier(str) => JsExpr::new( | |
| 32 | JsExprData::Identifier(ctx.remap_identifier(&str)), | |
| 33 | loc, | |
| 34 | comment, | |
| 35 | ), | |
| 36 | ||
| 37 | ExprData::Literal(Literal::Bool(value)) => { | |
| 38 | JsExpr::new(JsExprData::Bool(value), loc, comment) | |
| 39 | } | |
| 40 | ExprData::Literal(Literal::Number(value)) => { | |
| 41 | JsExpr::new(JsExprData::Number(value), loc, comment) | |
| 42 | } | |
| 43 | ExprData::Literal(Literal::String(value)) => { | |
| 44 | JsExpr::new(JsExprData::String(value), loc, comment) | |
| 45 | } | |
| 46 | ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), | |
| 47 | ||
| 48 | ExprData::Block(block) => convert_valid_block(ctx, BlockWithLoc { block, loc }), | |
| 49 | ||
| 50 | ExprData::Binary { op, left, right } => { | |
| 51 | let left = convert_valid_expr(ctx, *left); | |
| 52 | let right = convert_valid_expr(ctx, *right); | |
| 53 | JsExpr::new( | |
| 54 | JsExprData::Binary { | |
| 55 | op, | |
| 56 | left: Box::new(left), | |
| 57 | right: Box::new(right), | |
| 58 | }, | |
| 59 | loc, | |
| 60 | comment, | |
| 61 | ) | |
| 62 | } | |
| 63 | ExprData::Unary { op, value } => { | |
| 64 | let value = convert_valid_expr(ctx, *value); | |
| 65 | JsExpr::new( | |
| 66 | JsExprData::Unary { | |
| 67 | op, | |
| 68 | value: Box::new(value), | |
| 69 | }, | |
| 70 | loc, | |
| 71 | comment, | |
| 72 | ) | |
| 73 | } | |
| 74 | ExprData::Fn { block, params, .. } => JsExpr::new( | |
| 75 | JsExprData::ArrowFunction { | |
| 76 | params: params.into_iter().map(|x| x.name).collect(), | |
| 77 | body: Box::new(ctx.hoist_block_to_arrow_fn_body(BlockWithLoc { | |
| 78 | block, | |
| 79 | loc: loc.clone(), | |
| 80 | })), | |
| 81 | }, | |
| 82 | loc, | |
| 83 | comment, | |
| 84 | ), | |
| 85 | ExprData::Builtin { id, arguments } => match id { | |
| 86 | BuiltinId::Raw => { | |
| 87 | let first_arg = arguments.iter().next().unwrap(); | |
| 88 | // TODO(@paperdave): this string clone is stupid | |
| 89 | let str = match first_arg.data.clone() { | |
| 90 | ExprData::Literal(Literal::String(s)) => s, | |
| 91 | _ => panic!("raw expects a string"), | |
| 92 | }; | |
| 93 | JsExpr::new(JsExprData::Raw(str), loc, comment) | |
| 94 | } | |
| 95 | }, | |
| 96 | ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), | |
| 97 | ||
| 98 | ExprData::Call { base, arguments } => { | |
| 99 | let base = convert_valid_expr(ctx, *base); | |
| 100 | let arguments = arguments | |
| 101 | .into_iter() | |
| 102 | .map(|x| convert_valid_expr(ctx, x)) | |
| 103 | .collect(); | |
| 104 | JsExpr::new( | |
| 105 | JsExprData::Call { | |
| 106 | base: Box::new(base), | |
| 107 | arguments, | |
| 108 | }, | |
| 109 | loc, | |
| 110 | comment, | |
| 111 | ) | |
| 112 | } | |
| 113 | ||
| 114 | ExprData::Field { base, field } => JsExpr::new( | |
| 115 | match field.data { | |
| 116 | ExprData::Literal(Literal::String(name)) => JsExprData::Dot { | |
| 117 | base: Box::new(convert_valid_expr(ctx, *base)), | |
| 118 | name, | |
| 119 | }, | |
| 120 | _ => JsExprData::Index { | |
| 121 | base: Box::new(convert_valid_expr(ctx, *base)), | |
| 122 | index: Box::new(convert_valid_expr(ctx, *field)), | |
| 123 | }, | |
| 124 | }, | |
| 125 | loc, | |
| 126 | comment, | |
| 127 | ), | |
| 128 | ||
| 129 | ExprData::Return(_) => unreachable!("convert_valid_expr does not support return"), | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | pub fn convert_valid_block(ctx: &mut HoistContext, b: BlockWithLoc) -> JsExpr { | |
| 134 | debug_assert!( | |
| 135 | b.is_valid_js_expression(), | |
| 136 | "must call convert_valid_block with valid js expression" | |
| 137 | ); | |
| 138 | ||
| 139 | let len = b.block.statements.len(); | |
| 140 | let loc = b.loc; | |
| 141 | let block_expr = match len { | |
| 142 | 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), | |
| 143 | // a block expression with a single expression is just that expression | |
| 144 | 1 => convert_valid_statement_to_expr(ctx, b.block.statements.into_iter().next().unwrap()), | |
| 145 | // Since blocks are not valid expressions in JS, we create a | |
| 146 | // comma expression from a block with more than one statement. | |
| 147 | 2.. => { | |
| 148 | let mut items = Vec::new(); | |
| 149 | for statement in b.block.statements.into_iter() { | |
| 150 | items.push(convert_valid_statement_to_expr(ctx, statement)); | |
| 151 | } | |
| 152 | JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) | |
| 153 | } | |
| 154 | }; | |
| 155 | ||
| 156 | // a block without implicit return is to be type void, so we wrap in a void expression | |
| 157 | if !b.block.has_implicit_return && len > 0 { | |
| 158 | JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) | |
| 159 | } else { | |
| 160 | block_expr | |
| 161 | } | |
| 162 | } | |
| 163 | ||
| 164 | pub fn convert_valid_statement_to_expr(ctx: &mut HoistContext, statement: Statement) -> JsExpr { | |
| 165 | debug_assert!( | |
| 166 | statement.is_valid_js_expression(), | |
| 167 | "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" | |
| 168 | ); | |
| 169 | ||
| 170 | match statement { | |
| 171 | ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), | |
| 172 | _ => unreachable!("attempt to convert statement to js without hoisting"), | |
| 173 | } | |
| 174 | } | |
| 175 | ||
| 176 | pub fn convert_valid_statement(ctx: &mut HoistContext, statement: Statement) -> JsStatement { | |
| 177 | let expr = convert_valid_statement_to_expr(ctx, statement); | |
| 178 | let loc = expr.loc.clone(); | |
| 179 | JsStatement::new(JsStatementData::Expression(expr), loc, None) | |
| 180 | } | |
| 181 | ||
| 182 | pub fn convert_valid_if( | |
| 183 | ctx: &mut HoistContext, | |
| 184 | expr: If, | |
| 185 | loc: Loc, | |
| 186 | comment: Option<Comment>, | |
| 187 | ) -> JsExpr { | |
| 188 | let condition = convert_valid_expr(ctx, *expr.condition); | |
| 189 | let yes = convert_valid_block(ctx, expr.yes); | |
| 190 | let no = match expr.no { | |
| 191 | Some(BlockOrIf::Block(block)) => Some(convert_valid_block(ctx, block)), | |
| 192 | Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), | |
| 193 | None => None, | |
| 194 | }; | |
| 195 | JsExpr::new( | |
| 196 | JsExprData::Ternary { | |
| 197 | condition: Box::new(condition), | |
| 198 | yes: Box::new(yes), | |
| 199 | no: no.map(Box::new), | |
| 200 | }, | |
| 201 | loc, | |
| 202 | comment, | |
| 203 | ) | |
| 204 | } |
crates/js_ast/has_side_effects.rs deleted-90| ... | ... | @@ -1,90 +0,0 @@ |
| 1 | use ast::*; | |
| 2 | ||
| 3 | pub trait HasSideEffects { | |
| 4 | /// Can this item be removed from the AST without changing the program's behavior? | |
| 5 | fn has_side_effects(&self) -> bool; | |
| 6 | } | |
| 7 | ||
| 8 | impl HasSideEffects for ExprData { | |
| 9 | fn has_side_effects(&self) -> bool { | |
| 10 | match self { | |
| 11 | ExprData::Literal(_) => false, | |
| 12 | ExprData::Identifier(_) => false, | |
| 13 | ||
| 14 | ExprData::Block(block) => block.has_side_effects(), | |
| 15 | ExprData::Binary { left, right, .. } => { | |
| 16 | left.has_side_effects() || right.has_side_effects() | |
| 17 | } | |
| 18 | ExprData::Unary { value, .. } => value.has_side_effects(), | |
| 19 | ExprData::If(ifexpr) => ifexpr.has_side_effects(), | |
| 20 | ||
| 21 | // note: a function *declaration* has no side effects, but the call *can* | |
| 22 | ExprData::Fn { .. } => false, | |
| 23 | ||
| 24 | // TODO(@paperdave): implement detection of pure functions | |
| 25 | ExprData::Call { base, arguments } => { | |
| 26 | base.has_side_effects() || arguments.iter().any(|x| x.has_side_effects()) | |
| 27 | } | |
| 28 | ||
| 29 | // note: we assume field access has side effects, ie: a getter doesn't throw | |
| 30 | ExprData::Field { base, field } => base.has_side_effects() || field.has_side_effects(), | |
| 31 | ||
| 32 | // note: we assume the code in @raw always has side effects | |
| 33 | ExprData::Builtin { id, .. } => match id { | |
| 34 | BuiltinId::Raw => true, | |
| 35 | }, | |
| 36 | ||
| 37 | ExprData::Return(_) => true, | |
| 38 | } | |
| 39 | } | |
| 40 | } | |
| 41 | ||
| 42 | impl HasSideEffects for Statement { | |
| 43 | fn has_side_effects(&self) -> bool { | |
| 44 | match self { | |
| 45 | Statement::Expr(expr) => expr.has_side_effects(), | |
| 46 | Statement::Decl(decl) => decl.init.has_side_effects(), | |
| 47 | Statement::Comment(_) => false, | |
| 48 | } | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | impl HasSideEffects for Block { | |
| 53 | fn has_side_effects(&self) -> bool { | |
| 54 | self.statements | |
| 55 | .iter() | |
| 56 | .any(|statement| statement.has_side_effects()) | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | impl HasSideEffects for Expr { | |
| 61 | fn has_side_effects(&self) -> bool { | |
| 62 | self.data.has_side_effects() | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | impl HasSideEffects for If { | |
| 67 | fn has_side_effects(&self) -> bool { | |
| 68 | self.condition.has_side_effects() | |
| 69 | || self.yes.has_side_effects() | |
| 70 | || match &self.no { | |
| 71 | Some(no) => no.has_side_effects(), | |
| 72 | None => false, | |
| 73 | } | |
| 74 | } | |
| 75 | } | |
| 76 | ||
| 77 | impl HasSideEffects for BlockOrIf { | |
| 78 | fn has_side_effects(&self) -> bool { | |
| 79 | match self { | |
| 80 | BlockOrIf::Block(block) => block.has_side_effects(), | |
| 81 | BlockOrIf::If(if_) => if_.has_side_effects(), | |
| 82 | } | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | impl HasSideEffects for BlockWithLoc { | |
| 87 | fn has_side_effects(&self) -> bool { | |
| 88 | self.block.has_side_effects() | |
| 89 | } | |
| 90 | } |
crates/js_ast/hoist.rs deleted-638| ... | ... | @@ -1,638 +0,0 @@ |
| 1 | use std::collections::{HashMap, HashSet}; | |
| 2 | use std::vec; | |
| 3 | ||
| 4 | use ast::{BinaryOperator, BlockOrIf, BlockWithLoc, Expr, Statement}; | |
| 5 | use has_side_effects::HasSideEffects; | |
| 6 | use tokenizer::Loc; | |
| 7 | ||
| 8 | use crate::*; | |
| 9 | ||
| 10 | pub struct HoistContext { | |
| 11 | pub statements: Vec<JsStatement>, | |
| 12 | pub ids: HashSet<String>, | |
| 13 | next_id: u32, | |
| 14 | scopes: Vec<HoistScope>, | |
| 15 | has_last_statement: bool, | |
| 16 | } | |
| 17 | ||
| 18 | struct HoistScope { | |
| 19 | renames: HashMap<String, String>, | |
| 20 | } | |
| 21 | ||
| 22 | pub struct TempId { | |
| 23 | unmapped_name: String, | |
| 24 | } | |
| 25 | ||
| 26 | impl TempId { | |
| 27 | pub fn get(&self, hoist_context: &HoistContext) -> String { | |
| 28 | hoist_context.remap_identifier(&self.unmapped_name) | |
| 29 | } | |
| 30 | } | |
| 31 | ||
| 32 | impl HoistContext { | |
| 33 | pub fn new() -> Self { | |
| 34 | Self { | |
| 35 | statements: Vec::new(), | |
| 36 | ids: HashSet::new(), | |
| 37 | next_id: 0, | |
| 38 | scopes: vec![HoistScope { | |
| 39 | renames: HashMap::new(), | |
| 40 | }], | |
| 41 | has_last_statement: false, | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | pub fn new_child(&self) -> Self { | |
| 46 | Self { | |
| 47 | statements: Vec::new(), | |
| 48 | ids: self.ids.clone(), | |
| 49 | next_id: 0, | |
| 50 | scopes: vec![HoistScope { | |
| 51 | renames: HashMap::new(), | |
| 52 | }], | |
| 53 | has_last_statement: false, | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | /// A virtual scope is a scope that exists in blight syntax | |
| 58 | /// but may or may not exist in blight. With these it is important to | |
| 59 | pub fn push_virtual_scope(&mut self) { | |
| 60 | self.scopes.push(HoistScope { | |
| 61 | renames: HashMap::new(), | |
| 62 | }); | |
| 63 | } | |
| 64 | ||
| 65 | /// See `push_virtual_scope` for more information on what this is. | |
| 66 | pub fn pop_virtual_scope(&mut self) { | |
| 67 | self.scopes.pop(); | |
| 68 | } | |
| 69 | ||
| 70 | pub fn generate_temp_id(&mut self, basename: &str) -> TempId { | |
| 71 | self.next_id += 1; | |
| 72 | loop { | |
| 73 | let name = format!("{}{}", basename, self.next_id); | |
| 74 | if !self.ids.contains(&name) { | |
| 75 | self.ids.insert(name.clone()); | |
| 76 | return TempId { | |
| 77 | unmapped_name: name, | |
| 78 | }; | |
| 79 | } | |
| 80 | self.next_id += 1; | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | /// This does some extra optimizations: | |
| 85 | /// - `let a = b;` will not be emitted but instad write a variable alias | |
| 86 | pub fn add_statement(&mut self, statement: JsStatement) { | |
| 87 | match statement.data { | |
| 88 | JsStatementData::Declaration(JSDeclaration { | |
| 89 | name, | |
| 90 | init: | |
| 91 | Some(JsExpr { | |
| 92 | data: JsExprData::Identifier(init_name), | |
| 93 | .. | |
| 94 | }), | |
| 95 | is_constant: true, | |
| 96 | }) => { | |
| 97 | self.scopes | |
| 98 | .last_mut() | |
| 99 | .unwrap() | |
| 100 | .renames | |
| 101 | .insert(name, init_name); | |
| 102 | self.has_last_statement = false; | |
| 103 | return; | |
| 104 | } | |
| 105 | _ => {} | |
| 106 | } | |
| 107 | self.statements.push(statement); | |
| 108 | self.has_last_statement = true; | |
| 109 | } | |
| 110 | ||
| 111 | /// Get the last statement if it exists. | |
| 112 | /// If the statement was optimized away, this returns None | |
| 113 | pub fn last_statement(&self) -> Option<&JsStatement> { | |
| 114 | if self.has_last_statement { | |
| 115 | Some(self.statements.last().unwrap()) | |
| 116 | } else { | |
| 117 | None | |
| 118 | } | |
| 119 | } | |
| 120 | ||
| 121 | pub fn generate_id(&mut self, basename: &String) -> TempId { | |
| 122 | let mut id = 0u32; | |
| 123 | loop { | |
| 124 | let name = match id { | |
| 125 | 0 => basename.clone(), | |
| 126 | _ => format!("{}{}", basename, id), | |
| 127 | }; | |
| 128 | if !self.ids.contains(&name) { | |
| 129 | self.ids.insert(name.clone()); | |
| 130 | return TempId { | |
| 131 | unmapped_name: name, | |
| 132 | }; | |
| 133 | } | |
| 134 | id += 1; | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | /// Given a blight identifier, remap it to a new identifier. | |
| 139 | pub fn remap_identifier(&self, name: &String) -> String { | |
| 140 | for scope in self.scopes.iter().rev() { | |
| 141 | if let Some(remapped) = scope.renames.get(name) { | |
| 142 | return remapped.clone(); | |
| 143 | } | |
| 144 | } | |
| 145 | name.clone() | |
| 146 | } | |
| 147 | ||
| 148 | /// Declare an identifier in the current scope. | |
| 149 | pub fn declare_identifier(&mut self, name: String) -> TempId { | |
| 150 | let remapped = self.generate_id(&name); | |
| 151 | let scope = self.scopes.last_mut().unwrap(); | |
| 152 | self.ids.insert(remapped.unmapped_name.clone()); | |
| 153 | scope.renames.insert(name, remapped.unmapped_name.clone()); | |
| 154 | remapped | |
| 155 | } | |
| 156 | ||
| 157 | pub fn process_statement(&mut self, statement: Statement) -> Option<()> { | |
| 158 | if statement.is_valid_js_expression() { | |
| 159 | let result = convert_valid_statement(self, statement); | |
| 160 | self.add_statement(result); | |
| 161 | return Some(()); | |
| 162 | } | |
| 163 | ||
| 164 | match statement { | |
| 165 | Statement::Decl(decl) => { | |
| 166 | // visit init before name, this way allows for: | |
| 167 | // | |
| 168 | // let a = a; | |
| 169 | // | |
| 170 | // and `a` will be looked up before it is re-declared. | |
| 171 | if let Some(init) = self.hoist_expression_contents(decl.init) { | |
| 172 | let name = self.declare_identifier(decl.name); | |
| 173 | ||
| 174 | self.add_statement(JsStatement::new( | |
| 175 | JsStatementData::Declaration(crate::JSDeclaration { | |
| 176 | name: name.get(&self), | |
| 177 | init: Some(init), | |
| 178 | is_constant: decl.is_constant, | |
| 179 | }), | |
| 180 | decl.loc, | |
| 181 | decl.comment.map(|x| x.into()), | |
| 182 | )); | |
| 183 | Some(()) | |
| 184 | } else { | |
| 185 | None | |
| 186 | } | |
| 187 | } | |
| 188 | Statement::Expr(expr) => { | |
| 189 | if let Some(expr) = self.hoist_expression_contents(expr) { | |
| 190 | let loc = expr.loc.clone(); | |
| 191 | self.add_statement(JsStatement::new( | |
| 192 | JsStatementData::Expression(expr), | |
| 193 | loc, | |
| 194 | None, | |
| 195 | )); | |
| 196 | Some(()) | |
| 197 | } else { | |
| 198 | None | |
| 199 | } | |
| 200 | } | |
| 201 | Statement::Comment(_) => Some(()), | |
| 202 | } | |
| 203 | } | |
| 204 | ||
| 205 | /// Pushes a decl with a generated identifier to the hoisted result | |
| 206 | /// and returns an identifier expression for it. | |
| 207 | pub fn hoist_expression_to_temporary(&mut self, expr: Expr) -> Option<JsExpr> { | |
| 208 | // TODO | |
| 209 | if !expr.has_side_effects() { | |
| 210 | return Some(self.hoist_expression_contents(expr)?); | |
| 211 | } | |
| 212 | ||
| 213 | let init = self.hoist_expression_contents(expr)?; | |
| 214 | ||
| 215 | let id = self.generate_temp_id("_temp"); | |
| 216 | let loc = init.loc.clone(); | |
| 217 | ||
| 218 | self.add_statement(JsStatement::new( | |
| 219 | JsStatementData::Declaration(JSDeclaration { | |
| 220 | name: id.get(&self), | |
| 221 | init: Some(init), | |
| 222 | is_constant: true, | |
| 223 | }), | |
| 224 | loc.clone(), | |
| 225 | None, | |
| 226 | )); | |
| 227 | ||
| 228 | Some(JsExpr::new( | |
| 229 | JsExprData::Identifier(id.get(&self)), | |
| 230 | loc, | |
| 231 | None, | |
| 232 | )) | |
| 233 | } | |
| 234 | ||
| 235 | // Returning `None` from this means that the expression is noreturn, and that | |
| 236 | // the rest of the statement never occurs and can be stripped. | |
| 237 | pub fn hoist_expression_contents(&mut self, expr: Expr) -> Option<JsExpr> { | |
| 238 | if expr.is_valid_js_expression() { | |
| 239 | return Some(convert_valid_expr(self, expr)); | |
| 240 | } | |
| 241 | ||
| 242 | match expr.data { | |
| 243 | ast::ExprData::Binary { op, left, right } => { | |
| 244 | let left = self.hoist_expression_to_temporary(*left)?; | |
| 245 | let right = self.hoist_expression_to_temporary(*right)?; | |
| 246 | ||
| 247 | Some(JsExpr::new( | |
| 248 | JsExprData::Binary { | |
| 249 | op, | |
| 250 | left: Box::new(left), | |
| 251 | right: Box::new(right), | |
| 252 | }, | |
| 253 | expr.loc, | |
| 254 | expr.comment, | |
| 255 | )) | |
| 256 | } | |
| 257 | ||
| 258 | ast::ExprData::Unary { op, value } => { | |
| 259 | let value = self.hoist_expression_to_temporary(*value)?; | |
| 260 | Some(JsExpr::new( | |
| 261 | JsExprData::Unary { | |
| 262 | op, | |
| 263 | value: Box::new(value), | |
| 264 | }, | |
| 265 | expr.loc, | |
| 266 | expr.comment, | |
| 267 | )) | |
| 268 | } | |
| 269 | ||
| 270 | ast::ExprData::Field { base, field } => { | |
| 271 | let base = self.hoist_expression_to_temporary(*base)?; | |
| 272 | let field = self.hoist_expression_to_temporary(*field)?; | |
| 273 | ||
| 274 | Some(JsExpr::new( | |
| 275 | match field.data { | |
| 276 | JsExprData::String(name) if is_js_identifier(name.as_str()) => { | |
| 277 | JsExprData::Dot { | |
| 278 | base: Box::new(base), | |
| 279 | name, | |
| 280 | } | |
| 281 | } | |
| 282 | _ => JsExprData::Index { | |
| 283 | base: Box::new(base), | |
| 284 | index: Box::new(field), | |
| 285 | }, | |
| 286 | }, | |
| 287 | expr.loc, | |
| 288 | expr.comment, | |
| 289 | )) | |
| 290 | } | |
| 291 | ||
| 292 | ast::ExprData::Block(block) => { | |
| 293 | let temporary = self.generate_temp_id("_block"); | |
| 294 | let result = self.hoist_block_to_js_block_in_same_context(BlockWithLoc { | |
| 295 | block, | |
| 296 | loc: expr.loc.clone(), | |
| 297 | }); | |
| 298 | ||
| 299 | self.add_statement(JsStatement::new( | |
| 300 | JsStatementData::Declaration(crate::JSDeclaration { | |
| 301 | name: temporary.get(&self), | |
| 302 | init: Some(result), | |
| 303 | is_constant: false, | |
| 304 | }), | |
| 305 | expr.loc.clone(), | |
| 306 | None, | |
| 307 | )); | |
| 308 | ||
| 309 | Some(JsExpr::new( | |
| 310 | JsExprData::Identifier(temporary.get(&self)), | |
| 311 | expr.loc, | |
| 312 | expr.comment, | |
| 313 | )) | |
| 314 | } | |
| 315 | ||
| 316 | ast::ExprData::If(data) => { | |
| 317 | let temporary = self.generate_temp_id("_if"); | |
| 318 | ||
| 319 | let decl_index = self.statements.len(); | |
| 320 | self.add_statement(JsStatement::new( | |
| 321 | JsStatementData::Declaration(crate::JSDeclaration { | |
| 322 | name: temporary.get(&self), | |
| 323 | init: None, | |
| 324 | is_constant: false, | |
| 325 | }), | |
| 326 | expr.loc.clone(), | |
| 327 | None, | |
| 328 | )); | |
| 329 | ||
| 330 | let condition = self.hoist_expression_to_temporary(*data.condition)?; | |
| 331 | let mut did_assign = false; | |
| 332 | ||
| 333 | let js_if = | |
| 334 | self.hoist_js_if(data.yes, data.no, condition, &mut did_assign, &temporary); | |
| 335 | ||
| 336 | self.add_statement(JsStatement::new( | |
| 337 | JsStatementData::If(js_if), | |
| 338 | expr.loc.clone(), | |
| 339 | None, | |
| 340 | )); | |
| 341 | ||
| 342 | if did_assign { | |
| 343 | Some(JsExpr::new( | |
| 344 | JsExprData::Identifier(temporary.get(&self)), | |
| 345 | expr.loc, | |
| 346 | expr.comment, | |
| 347 | )) | |
| 348 | } else { | |
| 349 | self.statements.remove(decl_index); | |
| 350 | Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) | |
| 351 | } | |
| 352 | } | |
| 353 | ||
| 354 | ast::ExprData::Return(expr) => { | |
| 355 | let loc = expr.loc.clone(); | |
| 356 | let return_value = self.hoist_expression_to_temporary(*expr)?; | |
| 357 | self.add_statement(JsStatement::new( | |
| 358 | JsStatementData::Return(return_value), | |
| 359 | loc, | |
| 360 | None, | |
| 361 | )); | |
| 362 | None | |
| 363 | } | |
| 364 | ||
| 365 | ast::ExprData::Call { base, arguments } => { | |
| 366 | let base = self.hoist_expression_to_temporary(*base)?; | |
| 367 | let arguments = arguments | |
| 368 | .into_iter() | |
| 369 | .map(|x| self.hoist_expression_to_temporary(x)) | |
| 370 | .collect::<Option<Vec<_>>>()?; | |
| 371 | ||
| 372 | Some(JsExpr::new( | |
| 373 | JsExprData::Call { | |
| 374 | base: Box::new(base), | |
| 375 | arguments, | |
| 376 | }, | |
| 377 | expr.loc, | |
| 378 | expr.comment, | |
| 379 | )) | |
| 380 | } | |
| 381 | ||
| 382 | ast::ExprData::Fn { .. } | |
| 383 | | ast::ExprData::Builtin { .. } | |
| 384 | | ast::ExprData::Literal(_) | |
| 385 | | ast::ExprData::Identifier(_) => { | |
| 386 | unreachable!("attempt to hoist an expression that does not require it") | |
| 387 | } | |
| 388 | } | |
| 389 | } | |
| 390 | ||
| 391 | pub fn hoist_block_to_js_block( | |
| 392 | &mut self, | |
| 393 | b: BlockWithLoc, | |
| 394 | temporary: &String, | |
| 395 | did_assign: &mut bool, | |
| 396 | ) -> JsBlock { | |
| 397 | let loc = b.loc.clone(); | |
| 398 | ||
| 399 | // A non-block will become a block with a single expression: | |
| 400 | // _temp = expr; | |
| 401 | // | |
| 402 | // We will also apply this transformation to blocks that are valid | |
| 403 | // expressions, because that is more concise. | |
| 404 | if b.is_valid_js_expression() { | |
| 405 | return JsBlock::new( | |
| 406 | vec![new_js_assignment_statement( | |
| 407 | convert_valid_block(self, b), | |
| 408 | temporary.clone(), | |
| 409 | loc.clone(), | |
| 410 | )], | |
| 411 | loc, | |
| 412 | ); | |
| 413 | } | |
| 414 | ||
| 415 | let mut child = self.new_child(); | |
| 416 | ||
| 417 | for statement in b.block.statements { | |
| 418 | if let None = child.process_statement(statement) { | |
| 419 | tree_shake_statement_list(&mut child.statements); | |
| 420 | return JsBlock::new(child.statements, loc); | |
| 421 | }; | |
| 422 | } | |
| 423 | ||
| 424 | // If the last statement is an expression, we need | |
| 425 | // to assign to the temporary variable. | |
| 426 | match child.statements.last().map(|x| &x.data) { | |
| 427 | Some(JsStatementData::Expression(_)) => { | |
| 428 | *did_assign = true; | |
| 429 | ||
| 430 | let last = child.statements.pop().unwrap(); | |
| 431 | let value = match last { | |
| 432 | JsStatement { | |
| 433 | data: JsStatementData::Expression(expr), | |
| 434 | .. | |
| 435 | } => expr, | |
| 436 | _ => { | |
| 437 | self.statements.push(last); | |
| 438 | JsExpr::new(JsExprData::Null, loc.clone(), None) | |
| 439 | } | |
| 440 | }; | |
| 441 | child.statements.push(new_js_assignment_statement( | |
| 442 | value, | |
| 443 | temporary.clone(), | |
| 444 | loc.clone(), | |
| 445 | )); | |
| 446 | } | |
| 447 | _ => {} | |
| 448 | } | |
| 449 | ||
| 450 | tree_shake_statement_list(&mut child.statements); | |
| 451 | ||
| 452 | JsBlock::new(child.statements, loc) | |
| 453 | } | |
| 454 | ||
| 455 | pub fn hoist_block_to_arrow_fn_body(&mut self, b: BlockWithLoc) -> JsArrowBody { | |
| 456 | let loc = b.loc.clone(); | |
| 457 | ||
| 458 | // A block that is a valid expression will just become | |
| 459 | // that expression. We can convert multiple expressions | |
| 460 | // into a comma expression. | |
| 461 | if b.is_valid_js_expression() { | |
| 462 | return JsArrowBody::Expr(convert_valid_block(self, b)); | |
| 463 | } | |
| 464 | ||
| 465 | let mut child = self.new_child(); | |
| 466 | ||
| 467 | for statement in b.block.statements { | |
| 468 | if let None = child.process_statement(statement) { | |
| 469 | break; | |
| 470 | }; | |
| 471 | } | |
| 472 | ||
| 473 | // If the last statement is an expression, | |
| 474 | // we need to return that expression. | |
| 475 | match child.statements.last().map(|x| &x.data) { | |
| 476 | Some(JsStatementData::Expression(_)) => { | |
| 477 | let last = child.statements.pop().unwrap(); | |
| 478 | let value = match last { | |
| 479 | JsStatement { | |
| 480 | data: JsStatementData::Expression(expr), | |
| 481 | .. | |
| 482 | } => expr, | |
| 483 | _ => { | |
| 484 | self.statements.push(last); | |
| 485 | JsExpr::new(JsExprData::Null, loc.clone(), None) | |
| 486 | } | |
| 487 | }; | |
| 488 | child.statements.push(JsStatement::new( | |
| 489 | JsStatementData::Return(value), | |
| 490 | loc.clone(), | |
| 491 | None, | |
| 492 | )); | |
| 493 | } | |
| 494 | _ => {} | |
| 495 | } | |
| 496 | ||
| 497 | tree_shake_statement_list(&mut child.statements); | |
| 498 | ||
| 499 | JsArrowBody::Block(JsBlock::new(child.statements, loc)) | |
| 500 | } | |
| 501 | ||
| 502 | pub fn hoist_block_to_js_block_in_same_context(&mut self, b: BlockWithLoc) -> JsExpr { | |
| 503 | let loc = b.loc.clone(); | |
| 504 | self.push_virtual_scope(); | |
| 505 | ||
| 506 | debug_assert!(!b.block.is_valid_js_expression()); | |
| 507 | ||
| 508 | for statement in b.block.statements { | |
| 509 | self.process_statement(statement); | |
| 510 | } | |
| 511 | ||
| 512 | self.pop_virtual_scope(); | |
| 513 | ||
| 514 | // If the last statement is an expression, we need | |
| 515 | // to assign to the temporary variable. | |
| 516 | match self.last_statement().map(|x| &x.data) { | |
| 517 | Some(JsStatementData::Expression(_)) if b.block.has_implicit_return => { | |
| 518 | match self.statements.pop().unwrap().data { | |
| 519 | JsStatementData::Expression(expr) => expr, | |
| 520 | _ => unreachable!(), | |
| 521 | } | |
| 522 | } | |
| 523 | _ => JsExpr::new(JsExprData::Null, loc.clone(), None), | |
| 524 | } | |
| 525 | } | |
| 526 | ||
| 527 | pub fn hoist_js_if( | |
| 528 | &mut self, | |
| 529 | yes: BlockWithLoc, | |
| 530 | no: Option<BlockOrIf>, | |
| 531 | condition: JsExpr, | |
| 532 | did_assign: &mut bool, | |
| 533 | temporary: &TempId, | |
| 534 | ) -> JsIf { | |
| 535 | let yes = self.hoist_block_to_js_block(yes, &temporary.get(&self), did_assign); | |
| 536 | ||
| 537 | let no = match no { | |
| 538 | Some(BlockOrIf::Block(no)) => Some(JsElse::Block(self.hoist_block_to_js_block( | |
| 539 | no, | |
| 540 | &temporary.get(&self), | |
| 541 | did_assign, | |
| 542 | ))), | |
| 543 | Some(BlockOrIf::If(else_if)) => { | |
| 544 | // If the condition of this block has a side effect, we need to | |
| 545 | // emit a block so that we can assign to the temporary variable. | |
| 546 | // | |
| 547 | // Otherwise, we can just emit the condition inline | |
| 548 | if else_if.condition.is_valid_js_expression() { | |
| 549 | let condition = convert_valid_expr(self, *else_if.condition.clone()); | |
| 550 | Some(JsElse::If(Box::new(self.hoist_js_if( | |
| 551 | else_if.yes, | |
| 552 | else_if.no, | |
| 553 | condition, | |
| 554 | did_assign, | |
| 555 | temporary, | |
| 556 | )))) | |
| 557 | } else { | |
| 558 | let mut temp_block = self.new_child(); | |
| 559 | let condition = temp_block | |
| 560 | .hoist_expression_to_temporary(*else_if.condition.clone()) | |
| 561 | .unwrap(); | |
| 562 | ||
| 563 | temp_block.add_statement(JsStatement::new( | |
| 564 | JsStatementData::If(self.hoist_js_if( | |
| 565 | else_if.yes, | |
| 566 | else_if.no, | |
| 567 | condition, | |
| 568 | did_assign, | |
| 569 | temporary, | |
| 570 | )), | |
| 571 | Loc::EMPTY, | |
| 572 | None, | |
| 573 | )); | |
| 574 | ||
| 575 | tree_shake_statement_list(&mut temp_block.statements); | |
| 576 | ||
| 577 | Some(JsElse::Block(JsBlock::new( | |
| 578 | temp_block.statements, | |
| 579 | Loc::EMPTY, | |
| 580 | ))) | |
| 581 | } | |
| 582 | } | |
| 583 | None => None, | |
| 584 | }; | |
| 585 | ||
| 586 | JsIf { condition, yes, no } | |
| 587 | } | |
| 588 | } | |
| 589 | ||
| 590 | pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { | |
| 591 | JsStatement::new( | |
| 592 | JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), | |
| 593 | loc, | |
| 594 | None, | |
| 595 | ) | |
| 596 | } | |
| 597 | ||
| 598 | pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { | |
| 599 | JsExpr::new( | |
| 600 | JsExprData::Binary { | |
| 601 | op: BinaryOperator::Assign, | |
| 602 | left: Box::new(JsExpr::new( | |
| 603 | JsExprData::Identifier(target), | |
| 604 | loc.clone(), | |
| 605 | None, | |
| 606 | )), | |
| 607 | right: Box::new(expr), | |
| 608 | }, | |
| 609 | loc, | |
| 610 | None, | |
| 611 | ) | |
| 612 | } | |
| 613 | ||
| 614 | /// Remove | |
| 615 | /// - Discarded literals | |
| 616 | pub fn tree_shake_statement_list(list: &mut Vec<JsStatement>) { | |
| 617 | let mut set_i = 0; | |
| 618 | for i in 0..list.len() { | |
| 619 | let stmt = list.get(i).unwrap(); | |
| 620 | let should_keep = match &stmt.data { | |
| 621 | JsStatementData::Expression(expr) => !expr.is_literal(), | |
| 622 | JsStatementData::Return(_) => true, | |
| 623 | JsStatementData::Export(_) => true, | |
| 624 | JsStatementData::Declaration(_) => true, | |
| 625 | JsStatementData::Function(_) => true, | |
| 626 | JsStatementData::If(_) => true, | |
| 627 | // | |
| 628 | }; | |
| 629 | ||
| 630 | if should_keep { | |
| 631 | if set_i != i { | |
| 632 | list.swap(i, set_i) | |
| 633 | } | |
| 634 | set_i += 1; | |
| 635 | } | |
| 636 | } | |
| 637 | list.truncate(set_i) | |
| 638 | } |
crates/js_ast/is_valid_js_expression.rs deleted-85| ... | ... | @@ -1,85 +0,0 @@ |
| 1 | use ast::*; | |
| 2 | ||
| 3 | pub trait IsValidJSExpression { | |
| 4 | /// Can this be lowered to a JS ast without hoisting items | |
| 5 | /// ex: a `return` expression is not a valid JS expression | |
| 6 | fn is_valid_js_expression(&self) -> bool; | |
| 7 | } | |
| 8 | ||
| 9 | impl IsValidJSExpression for ExprData { | |
| 10 | fn is_valid_js_expression(&self) -> bool { | |
| 11 | match self { | |
| 12 | // A block containing just expressions can be an expression | |
| 13 | ExprData::Block(block) => block.is_valid_js_expression(), | |
| 14 | ExprData::If(expr) => expr.is_valid_js_expression(), | |
| 15 | ExprData::Return(_) => false, | |
| 16 | ExprData::Builtin { id, .. } => match id { | |
| 17 | BuiltinId::Raw => true, | |
| 18 | }, | |
| 19 | ExprData::Binary { left, right, .. } => { | |
| 20 | left.is_valid_js_expression() && right.is_valid_js_expression() | |
| 21 | } | |
| 22 | ExprData::Unary { value, .. } => value.is_valid_js_expression(), | |
| 23 | ExprData::Fn { .. } => true, | |
| 24 | ExprData::Call { base, arguments } => { | |
| 25 | base.is_valid_js_expression() | |
| 26 | && arguments | |
| 27 | .iter() | |
| 28 | .all(|argument| argument.is_valid_js_expression()) | |
| 29 | } | |
| 30 | ExprData::Field { base, field } => { | |
| 31 | base.is_valid_js_expression() && field.is_valid_js_expression() | |
| 32 | } | |
| 33 | ExprData::Literal(_) | ExprData::Identifier(_) => true, | |
| 34 | } | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | impl IsValidJSExpression for Block { | |
| 39 | fn is_valid_js_expression(&self) -> bool { | |
| 40 | self.statements | |
| 41 | .iter() | |
| 42 | .all(|statement| statement.is_valid_js_expression()) | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | impl IsValidJSExpression for Expr { | |
| 47 | fn is_valid_js_expression(&self) -> bool { | |
| 48 | self.data.is_valid_js_expression() | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | impl IsValidJSExpression for If { | |
| 53 | fn is_valid_js_expression(&self) -> bool { | |
| 54 | self.condition.is_valid_js_expression() | |
| 55 | && self.yes.is_valid_js_expression() | |
| 56 | && match &self.no { | |
| 57 | Some(no) => no.is_valid_js_expression(), | |
| 58 | None => true, | |
| 59 | } | |
| 60 | } | |
| 61 | } | |
| 62 | ||
| 63 | impl IsValidJSExpression for BlockOrIf { | |
| 64 | fn is_valid_js_expression(&self) -> bool { | |
| 65 | match self { | |
| 66 | BlockOrIf::Block(block) => block.is_valid_js_expression(), | |
| 67 | BlockOrIf::If(if_) => if_.is_valid_js_expression(), | |
| 68 | } | |
| 69 | } | |
| 70 | } | |
| 71 | ||
| 72 | impl IsValidJSExpression for BlockWithLoc { | |
| 73 | fn is_valid_js_expression(&self) -> bool { | |
| 74 | self.block.is_valid_js_expression() | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | impl IsValidJSExpression for Statement { | |
| 79 | fn is_valid_js_expression(&self) -> bool { | |
| 80 | match self { | |
| 81 | Statement::Expr(expr) => expr.data.is_valid_js_expression(), | |
| 82 | _ => false, | |
| 83 | } | |
| 84 | } | |
| 85 | } |
crates/js_ast/lib.rs+2-23| ... | ... | @@ -1,28 +1,7 @@ |
| 1 | mod convert_valid_expr; | |
| 2 | mod has_side_effects; | |
| 3 | mod hoist; | |
| 4 | mod is_valid_js_expression; | |
| 1 | mod convert; | |
| 5 | 2 | mod js_ast; |
| 6 | 3 | mod util; |
| 7 | 4 | |
| 8 | use ast::Module; | |
| 9 | ||
| 10 | pub use convert_valid_expr::*; | |
| 11 | pub use hoist::*; | |
| 12 | pub use is_valid_js_expression::*; | |
| 5 | pub use convert::*; | |
| 13 | 6 | pub use js_ast::*; |
| 14 | 7 | pub use util::*; |
| 15 | ||
| 16 | pub fn blight_to_js(blight: Module) -> JsModule { | |
| 17 | let mut root_context = HoistContext::new(); | |
| 18 | ||
| 19 | for statement in blight.statements { | |
| 20 | if let None = root_context.process_statement(statement.to_statement()) { | |
| 21 | unreachable!("top level return is disallowed by the parser") | |
| 22 | }; | |
| 23 | } | |
| 24 | ||
| 25 | JsModule { | |
| 26 | statements: root_context.statements, | |
| 27 | } | |
| 28 | } |
crates/js_printer/lib.rs+14| ... | ... | @@ -231,6 +231,10 @@ impl Print for JsExprData { |
| 231 | 231 | body.print(writer) |
| 232 | 232 | } |
| 233 | 233 | JsExprData::Ternary { condition, yes, no } => { |
| 234 | let wrap = false; | |
| 235 | if wrap { | |
| 236 | write!(writer, "(")?; | |
| 237 | } | |
| 234 | 238 | condition.print(writer)?; |
| 235 | 239 | write!(writer, "?")?; |
| 236 | 240 | yes.print(writer)?; |
| ... | ... | @@ -240,15 +244,25 @@ impl Print for JsExprData { |
| 240 | 244 | } else { |
| 241 | 245 | write!(writer, "null")?; |
| 242 | 246 | } |
| 247 | if wrap { | |
| 248 | write!(writer, ")")?; | |
| 249 | } | |
| 243 | 250 | Ok(()) |
| 244 | 251 | } |
| 245 | 252 | JsExprData::Comma { items } => { |
| 253 | let wrap = true; | |
| 254 | if wrap { | |
| 255 | write!(writer, "(")?; | |
| 256 | } | |
| 246 | 257 | for (i, item) in items.iter().enumerate() { |
| 247 | 258 | if i > 0 { |
| 248 | 259 | write!(writer, ",")?; |
| 249 | 260 | } |
| 250 | 261 | item.print(writer)?; |
| 251 | 262 | } |
| 263 | if wrap { | |
| 264 | write!(writer, ")")?; | |
| 265 | } | |
| 252 | 266 | Ok(()) |
| 253 | 267 | } |
| 254 | 268 | JsExprData::Void(expr) => { |
crates/js_printer/src/lib.rs deleted-16| ... | ... | @@ -1,16 +0,0 @@ |
| 1 | mod print_expression; | |
| 2 | mod print_statement; | |
| 3 | ||
| 4 | use print_statement::print_statement; | |
| 5 | ||
| 6 | use js_ast::JsStatement; | |
| 7 | ||
| 8 | pub fn print(forest: Vec<JsStatement>) -> String { | |
| 9 | let mut result = String::new(); | |
| 10 | ||
| 11 | for tree in forest { | |
| 12 | result.push_str(&print_statement(&tree)); | |
| 13 | } | |
| 14 | ||
| 15 | result | |
| 16 | } |
crates/js_printer/src/print_expression.rs deleted-276| ... | ... | @@ -1,276 +0,0 @@ |
| 1 | use js_ast::{JsExpr, JsExprData}; | |
| 2 | ||
| 3 | use crate::print_statement::print_statement; | |
| 4 | ||
| 5 | pub fn print_expression(expr: &JsExpr) -> String { | |
| 6 | match &expr.data { | |
| 7 | JsExprData::Raw(value) => format!("{}", value), | |
| 8 | JsExprData::Number(value) => format!("{}", value), | |
| 9 | JsExprData::BigInt(value) => format!("{}n", value), | |
| 10 | JsExprData::String(value) => { | |
| 11 | let mut result = String::new(); | |
| 12 | // TODO: This is an INCREDIBLY naive implementation of escaping. | |
| 13 | // Also it's probably not even correct. | |
| 14 | for char in value.chars() { | |
| 15 | match char { | |
| 16 | '\n' => result.push_str("\\n"), | |
| 17 | '\r' => result.push_str("\\r"), | |
| 18 | '\t' => result.push_str("\\t"), | |
| 19 | '"' => result.push_str("\\\""), | |
| 20 | '\\' => result.push_str("\\\\"), | |
| 21 | _ => result.push(char), | |
| 22 | } | |
| 23 | } | |
| 24 | format!("\"{}\"", result) | |
| 25 | } | |
| 26 | JsExprData::Bool(value) => format!("{}", value), | |
| 27 | JsExprData::Identifier(value) => format!("{}", value), | |
| 28 | JsExprData::Array(value) => format!( | |
| 29 | "[{}]", | |
| 30 | value | |
| 31 | .iter() | |
| 32 | .map(|x| print_expression(x)) | |
| 33 | .collect::<Vec<String>>() | |
| 34 | .join(",") | |
| 35 | ), | |
| 36 | JsExprData::Object(value) => format!( | |
| 37 | "{{{}}}", | |
| 38 | value | |
| 39 | .iter() | |
| 40 | .map(|entry| format!( | |
| 41 | "{}:{}", | |
| 42 | print_expression(&entry.key), | |
| 43 | print_expression(&entry.value) | |
| 44 | )) | |
| 45 | .collect::<Vec<String>>() | |
| 46 | .join(",") | |
| 47 | ), | |
| 48 | JsExprData::Binary { | |
| 49 | op: operator, | |
| 50 | left, | |
| 51 | right, | |
| 52 | } => format!( | |
| 53 | "{}{}{}", | |
| 54 | print_expression(&left), | |
| 55 | operator.clone().to_string(), | |
| 56 | print_expression(&right) | |
| 57 | ), | |
| 58 | // JsExprData::Function { | |
| 59 | // arguments, | |
| 60 | // value, | |
| 61 | // name: _, | |
| 62 | // params: _, | |
| 63 | // block, | |
| 64 | // } => format!( | |
| 65 | // "({})=>{{{}}}", | |
| 66 | // arguments.join(","), | |
| 67 | // value | |
| 68 | // .iter() | |
| 69 | // .map(|x| format!("{}", print_statement(x))) | |
| 70 | // .collect::<Vec<String>>() | |
| 71 | // .join("") | |
| 72 | // ), | |
| 73 | JsExprData::Call { base, arguments } => format!( | |
| 74 | "{}({})", | |
| 75 | print_expression(&base), | |
| 76 | arguments | |
| 77 | .iter() | |
| 78 | .map(|x| print_expression(x)) | |
| 79 | .collect::<Vec<String>>() | |
| 80 | .join(",") | |
| 81 | ), | |
| 82 | JsExprData::Null => todo!(), | |
| 83 | JsExprData::Undefined => todo!(), | |
| 84 | JsExprData::Unary { | |
| 85 | op: operator, | |
| 86 | value, | |
| 87 | } => todo!(), | |
| 88 | JsExprData::Dot { base, name } => todo!(), | |
| 89 | JsExprData::Index { base, index } => todo!(), | |
| 90 | JsExprData::Function { | |
| 91 | name, | |
| 92 | params, | |
| 93 | block, | |
| 94 | } => todo!(), | |
| 95 | JsExprData::Ternary { condition, yes, no } => todo!(), | |
| 96 | JsExprData::ArrowFunction { params, block } => todo!(), | |
| 97 | JsExprData::Comma { items } => todo!(), | |
| 98 | JsExprData::Void(_) => todo!(), | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | #[cfg(test)] | |
| 103 | mod tests { | |
| 104 | use crate::print_expression::print_expression; | |
| 105 | use js_ast::JsExprData; | |
| 106 | use js_ast::JsStatement; | |
| 107 | use num::{bigint::Sign, BigInt}; | |
| 108 | use std::collections::HashMap; | |
| 109 | ||
| 110 | #[test] | |
| 111 | fn number() { | |
| 112 | assert_eq!(print_expression(&JsExprData::Number(1.0)), "1"); | |
| 113 | assert_eq!(print_expression(&JsExprData::Number(1.5)), "1.5"); | |
| 114 | assert_eq!(print_expression(&JsExprData::Number(1.5e10)), "15000000000"); | |
| 115 | assert_eq!( | |
| 116 | print_expression(&JsExprData::Number(1.5e-10)), | |
| 117 | "0.00000000015" | |
| 118 | ); | |
| 119 | } | |
| 120 | ||
| 121 | #[test] | |
| 122 | fn bigint() { | |
| 123 | assert_eq!( | |
| 124 | print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![1]))), | |
| 125 | "1n" | |
| 126 | ); | |
| 127 | assert_eq!( | |
| 128 | print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![10000000]))), | |
| 129 | "10000000n" | |
| 130 | ); | |
| 131 | assert_eq!( | |
| 132 | print_expression(&JsExprData::BigInt(BigInt::new( | |
| 133 | Sign::Minus, | |
| 134 | vec![10000000] | |
| 135 | ))), | |
| 136 | "-10000000n" | |
| 137 | ); | |
| 138 | } | |
| 139 | ||
| 140 | #[test] | |
| 141 | fn string() { | |
| 142 | assert_eq!( | |
| 143 | print_expression(&JsExprData::String("".to_string())), | |
| 144 | "\"\"" | |
| 145 | ); | |
| 146 | assert_eq!( | |
| 147 | print_expression(&JsExprData::String("hello".to_string())), | |
| 148 | "\"hello\"" | |
| 149 | ); | |
| 150 | assert_eq!( | |
| 151 | print_expression(&JsExprData::String("hello\nworld".to_string())), | |
| 152 | "\"hello\\nworld\"" | |
| 153 | ); | |
| 154 | } | |
| 155 | ||
| 156 | #[test] | |
| 157 | fn bool() { | |
| 158 | assert_eq!(print_expression(&JsExprData::Bool(true)), "true"); | |
| 159 | assert_eq!(print_expression(&JsExprData::Bool(false)), "false"); | |
| 160 | } | |
| 161 | ||
| 162 | #[test] | |
| 163 | fn identifier() { | |
| 164 | assert_eq!( | |
| 165 | print_expression(&JsExprData::Identifier("hello".to_string())), | |
| 166 | "hello" | |
| 167 | ); | |
| 168 | } | |
| 169 | ||
| 170 | #[test] | |
| 171 | fn array() { | |
| 172 | assert_eq!(print_expression(&JsExprData::Array(vec![])), "[]"); | |
| 173 | assert_eq!( | |
| 174 | print_expression(&JsExprData::Array(vec![JsExprData::Number(1.0)])), | |
| 175 | "[1]" | |
| 176 | ); | |
| 177 | assert_eq!( | |
| 178 | print_expression(&JsExprData::Array(vec![ | |
| 179 | JsExprData::Number(1.0), | |
| 180 | JsExprData::Number(2.0) | |
| 181 | ])), | |
| 182 | "[1,2]" | |
| 183 | ); | |
| 184 | } | |
| 185 | ||
| 186 | #[test] | |
| 187 | fn object() { | |
| 188 | assert_eq!(print_expression(&JsExprData::Object(HashMap::new())), "{}"); | |
| 189 | let mut map = HashMap::new(); | |
| 190 | map.insert("hello".to_string(), JsExprData::Number(1.0)); | |
| 191 | assert_eq!(print_expression(&JsExprData::Object(map)), "{\"hello\":1}"); | |
| 192 | } | |
| 193 | ||
| 194 | #[test] | |
| 195 | fn operation() { | |
| 196 | assert_eq!( | |
| 197 | print_expression(&JsExprData::Binary { | |
| 198 | op: AnyOperator::Add, | |
| 199 | left: Box::new(JsExprData::Number(1.0)), | |
| 200 | right: Box::new(JsExprData::Number(2.0)), | |
| 201 | }), | |
| 202 | "1+2" | |
| 203 | ); | |
| 204 | assert_eq!( | |
| 205 | print_expression(&JsExprData::Binary { | |
| 206 | op: AnyOperator::Sub, | |
| 207 | left: Box::new(JsExprData::Number(1.0)), | |
| 208 | right: Box::new(JsExprData::Number(2.0)), | |
| 209 | }), | |
| 210 | "1-2" | |
| 211 | ); | |
| 212 | } | |
| 213 | ||
| 214 | #[test] | |
| 215 | fn function() { | |
| 216 | use crate::print_expression::print_expression; | |
| 217 | use js_ast::JsExprData; | |
| 218 | ||
| 219 | assert_eq!( | |
| 220 | print_expression(&JsExprData::Function { | |
| 221 | arguments: vec![], | |
| 222 | value: vec![], | |
| 223 | }), | |
| 224 | "()=>{}" | |
| 225 | ); | |
| 226 | assert_eq!( | |
| 227 | print_expression(&JsExprData::Function { | |
| 228 | arguments: vec!["a".to_string()], | |
| 229 | value: vec![], | |
| 230 | }), | |
| 231 | "(a)=>{}" | |
| 232 | ); | |
| 233 | assert_eq!( | |
| 234 | print_expression(&JsExprData::Function { | |
| 235 | arguments: vec!["a".to_string(), "b".to_string()], | |
| 236 | value: vec![], | |
| 237 | }), | |
| 238 | "(a,b)=>{}" | |
| 239 | ); | |
| 240 | assert_eq!( | |
| 241 | print_expression(&JsExprData::Function { | |
| 242 | arguments: vec!["a".to_string(), "b".to_string()], | |
| 243 | value: vec![JsStatement::Declaration { | |
| 244 | name: "a".to_string(), | |
| 245 | value: JsExprData::Number(1.0) | |
| 246 | }], | |
| 247 | }), | |
| 248 | "(a,b)=>{let a=1;}" | |
| 249 | ); | |
| 250 | } | |
| 251 | ||
| 252 | #[test] | |
| 253 | fn function_call() { | |
| 254 | assert_eq!( | |
| 255 | print_expression(&JsExprData::Call { | |
| 256 | name: Box::new(JsExprData::Identifier("hello".to_string())), | |
| 257 | arguments: vec![], | |
| 258 | }), | |
| 259 | "hello()" | |
| 260 | ); | |
| 261 | assert_eq!( | |
| 262 | print_expression(&JsExprData::Call { | |
| 263 | name: Box::new(JsExprData::Identifier("hello".to_string())), | |
| 264 | arguments: vec![JsExprData::Number(1.0)], | |
| 265 | }), | |
| 266 | "hello(1)" | |
| 267 | ); | |
| 268 | assert_eq!( | |
| 269 | print_expression(&JsExprData::Call { | |
| 270 | name: Box::new(JsExprData::Identifier("hello".to_string())), | |
| 271 | arguments: vec![JsExprData::Number(1.0), JsExprData::Number(2.0)], | |
| 272 | }), | |
| 273 | "hello(1,2)" | |
| 274 | ); | |
| 275 | } | |
| 276 | } |
crates/js_printer/src/print_statement.rs deleted-153| ... | ... | @@ -1,153 +0,0 @@ |
| 1 | use js_ast::{If, JSDeclaration, JSFunctionDeclaration, JsStatement, JsStatementData}; | |
| 2 | ||
| 3 | use crate::print_expression::print_expression; | |
| 4 | ||
| 5 | pub fn print_statement(statement: &JsStatement) -> String { | |
| 6 | match &statement.data { | |
| 7 | JsStatementData::Expression(value) => format!("{};", print_expression(&value)), | |
| 8 | // JsStatementData::Export(value) => format!("export {}", print_statement(value)), | |
| 9 | JsStatementData::Export(value) => todo!(), | |
| 10 | JsStatementData::Return(value) => format!("return {};", print_expression(&value)), | |
| 11 | JsStatementData::Declaration(Declaration { | |
| 12 | name, | |
| 13 | init, | |
| 14 | is_constant, | |
| 15 | }) => { | |
| 16 | format!( | |
| 17 | "{} {}={};", | |
| 18 | match is_constant { | |
| 19 | true => "const", | |
| 20 | false => "let", | |
| 21 | }, | |
| 22 | name, | |
| 23 | print_expression(&init) | |
| 24 | ) | |
| 25 | } | |
| 26 | JsStatementData::Function(JSFunctionDeclaration { | |
| 27 | name, | |
| 28 | block, | |
| 29 | params, | |
| 30 | }) => format!( | |
| 31 | "function {}({}){{{}}}", | |
| 32 | name, | |
| 33 | params.join(","), | |
| 34 | block | |
| 35 | .statements | |
| 36 | .iter() | |
| 37 | .map(|x| format!("{}", print_statement(x))) | |
| 38 | .collect::<Vec<String>>() | |
| 39 | .join("") | |
| 40 | ), | |
| 41 | JsStatementData::If(JsIf { condition, yes, no }) => { | |
| 42 | let else_value = match no { | |
| 43 | Some(value) => format!( | |
| 44 | "else{{{}}}", | |
| 45 | value | |
| 46 | .statements | |
| 47 | .iter() | |
| 48 | .map(|x| format!("{}", print_statement(x))) | |
| 49 | .collect::<Vec<String>>() | |
| 50 | .join("") | |
| 51 | ), | |
| 52 | None => "".to_string(), | |
| 53 | }; | |
| 54 | format!( | |
| 55 | "if({}){{{}}}{}", | |
| 56 | print_expression(&condition), | |
| 57 | yes.statements | |
| 58 | .iter() | |
| 59 | .map(|x| format!("{}", print_statement(x))) | |
| 60 | .collect::<Vec<String>>() | |
| 61 | .join(""), | |
| 62 | else_value | |
| 63 | ) | |
| 64 | } | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | #[cfg(test)] | |
| 69 | mod tests { | |
| 70 | use crate::print_statement::print_statement; | |
| 71 | use js_ast::JsExpression; | |
| 72 | use js_ast::JsStatement; | |
| 73 | ||
| 74 | #[test] | |
| 75 | fn variable_declaration() { | |
| 76 | assert_eq!( | |
| 77 | print_statement(&JsStatement::Declaration { | |
| 78 | name: "x".to_string(), | |
| 79 | value: JsExpression::Number(1.0), | |
| 80 | }), | |
| 81 | "let x=1;" | |
| 82 | ); | |
| 83 | } | |
| 84 | ||
| 85 | #[test] | |
| 86 | fn constant_declaration() { | |
| 87 | assert_eq!( | |
| 88 | print_statement(&JsStatement::ConstantDeclaration { | |
| 89 | name: "x".to_string(), | |
| 90 | value: JsExpression::Number(1.0), | |
| 91 | }), | |
| 92 | "const x=1;" | |
| 93 | ); | |
| 94 | } | |
| 95 | ||
| 96 | #[test] | |
| 97 | fn function_declaration() { | |
| 98 | assert_eq!( | |
| 99 | print_statement(&JsStatement::Function { | |
| 100 | name: "x".to_string(), | |
| 101 | arguments: vec!["y".to_string()], | |
| 102 | value: vec![JsStatement::Expression(JsExpression::Number(1.0))], | |
| 103 | }), | |
| 104 | "function x(y){1;}" | |
| 105 | ); | |
| 106 | } | |
| 107 | ||
| 108 | #[test] | |
| 109 | fn expression() { | |
| 110 | assert_eq!( | |
| 111 | print_statement(&JsStatement::Expression(JsExpression::Number(1.0))), | |
| 112 | "1;" | |
| 113 | ); | |
| 114 | } | |
| 115 | ||
| 116 | #[test] | |
| 117 | fn export() { | |
| 118 | assert_eq!( | |
| 119 | print_statement(&JsStatement::Export(Box::new(JsStatement::Expression( | |
| 120 | JsExpression::Number(1.0) | |
| 121 | )))), | |
| 122 | "export 1;" | |
| 123 | ); | |
| 124 | } | |
| 125 | ||
| 126 | #[test] | |
| 127 | fn return_statement() { | |
| 128 | assert_eq!( | |
| 129 | print_statement(&JsStatement::Return(JsExpression::Number(1.0))), | |
| 130 | "return 1;" | |
| 131 | ); | |
| 132 | } | |
| 133 | ||
| 134 | #[test] | |
| 135 | fn if_statement() { | |
| 136 | assert_eq!( | |
| 137 | print_statement(&JsStatement::If { | |
| 138 | condition: JsExpression::Number(1.0), | |
| 139 | value: vec![JsStatement::Expression(JsExpression::Number(2.0))], | |
| 140 | else_value: None, | |
| 141 | }), | |
| 142 | "if(1){2;}" | |
| 143 | ); | |
| 144 | assert_eq!( | |
| 145 | print_statement(&JsStatement::If { | |
| 146 | condition: JsExpression::Number(1.0), | |
| 147 | value: vec![JsStatement::Expression(JsExpression::Number(2.0))], | |
| 148 | else_value: Some(vec![JsStatement::Expression(JsExpression::Number(3.0))]), | |
| 149 | }), | |
| 150 | "if(1){2;}else{3;}" | |
| 151 | ); | |
| 152 | } | |
| 153 | } |
crates/parser/parse_block.rs+1-9| ... | ... | @@ -12,9 +12,6 @@ pub fn parse_block(p: &mut Parser) -> Result<Block> { |
| 12 | 12 | |
| 13 | 13 | let mut statements = Vec::new(); |
| 14 | 14 | |
| 15 | let was_top_level = p.is_top_level; | |
| 16 | p.is_top_level = false; | |
| 17 | ||
| 18 | 15 | 'outer: loop { |
| 19 | 16 | let is_statement_like_expression = p |
| 20 | 17 | .peek() |
| ... | ... | @@ -23,7 +20,6 @@ pub fn parse_block(p: &mut Parser) -> Result<Block> { |
| 23 | 20 | let statement = match parse_statement(p) { |
| 24 | 21 | Ok(statement) => statement, |
| 25 | 22 | Err(err) => { |
| 26 | p.is_top_level = was_top_level; | |
| 27 | 23 | return Err(err); |
| 28 | 24 | } |
| 29 | 25 | }; |
| ... | ... | @@ -35,7 +31,6 @@ pub fn parse_block(p: &mut Parser) -> Result<Block> { |
| 35 | 31 | match p.peek() { |
| 36 | 32 | Some(Token::ClosedBrace) => { |
| 37 | 33 | p.next(); |
| 38 | p.is_top_level = was_top_level; | |
| 39 | 34 | return Ok(Block { |
| 40 | 35 | statements, |
| 41 | 36 | has_implicit_return: false, |
| ... | ... | @@ -46,14 +41,12 @@ pub fn parse_block(p: &mut Parser) -> Result<Block> { |
| 46 | 41 | }, |
| 47 | 42 | Some(Token::ClosedBrace) => { |
| 48 | 43 | p.next(); |
| 49 | if statement.require_semicolon(is_statement_like_expression) { | |
| 50 | p.is_top_level = was_top_level; | |
| 44 | if !statement.is_expression() { | |
| 51 | 45 | return Err(p |
| 52 | 46 | .error("Expected semicolon or '}' after statement".to_string()) |
| 53 | 47 | .into()); |
| 54 | 48 | } |
| 55 | 49 | |
| 56 | p.is_top_level = was_top_level; | |
| 57 | 50 | return Ok(Block { |
| 58 | 51 | statements, |
| 59 | 52 | has_implicit_return: true, |
| ... | ... | @@ -61,7 +54,6 @@ pub fn parse_block(p: &mut Parser) -> Result<Block> { |
| 61 | 54 | } |
| 62 | 55 | _ => { |
| 63 | 56 | if statement.require_semicolon(is_statement_like_expression) { |
| 64 | p.is_top_level = was_top_level; | |
| 65 | 57 | return Err(p |
| 66 | 58 | .error("Expected semicolon or '}' after statement".to_string()) |
| 67 | 59 | .into()); |
crates/parser/parse_declaration.rs+3-3| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | use ast::{Decl, DocComment, Type}; | |
| 1 | use ast::{DocComment, Type, VarDecl}; | |
| 2 | 2 | use miette::Result; |
| 3 | 3 | use tokenizer::Token; |
| 4 | 4 | |
| ... | ... | @@ -11,7 +11,7 @@ pub fn parse_declaration( |
| 11 | 11 | p: &mut Parser, |
| 12 | 12 | is_pub: bool, |
| 13 | 13 | comment: Option<DocComment>, |
| 14 | ) -> Result<Decl> { | |
| 14 | ) -> Result<VarDecl> { | |
| 15 | 15 | let loc = p.loc(); |
| 16 | 16 | let is_constant = match p.next() { |
| 17 | 17 | Some(Token::Var) => false, |
| ... | ... | @@ -38,7 +38,7 @@ pub fn parse_declaration( |
| 38 | 38 | .into()) |
| 39 | 39 | } |
| 40 | 40 | }; |
| 41 | Ok(Decl { | |
| 41 | Ok(VarDecl { | |
| 42 | 42 | name: name.clone(), |
| 43 | 43 | data_type: return_type, |
| 44 | 44 | init, |
crates/parser/parse_expr_basic.rs+1-1| ... | ... | @@ -63,7 +63,7 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result<Expr> { |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | 65 | Some(Token::Return) => { |
| 66 | if p.is_top_level { | |
| 66 | if p.allow_return { | |
| 67 | 67 | return Err(p |
| 68 | 68 | .error("'return' cannot be used at top level".to_string()) |
| 69 | 69 | .into()); |
crates/parser/parse_fn_declaration.rs+44-44| ... | ... | @@ -1,8 +1,9 @@ |
| 1 | use ast::{Decl, DocComment, Expr, ExprData, Type}; | |
| 1 | use ast::{DocComment, Expr, ExprData, FnDecl, Type}; | |
| 2 | 2 | use miette::Result; |
| 3 | 3 | use tokenizer::Token; |
| 4 | 4 | |
| 5 | 5 | use crate::parse_block::parse_block; |
| 6 | use crate::parse_expr::parse_expr; | |
| 6 | 7 | use crate::parse_param::parse_param; |
| 7 | 8 | use crate::parse_type::parse_type; |
| 8 | 9 | use crate::Parser; |
| ... | ... | @@ -12,7 +13,7 @@ pub fn parse_fn_declaration( |
| 12 | 13 | p: &mut Parser, |
| 13 | 14 | is_pub: bool, |
| 14 | 15 | comment: Option<DocComment>, |
| 15 | ) -> Result<Decl> { | |
| 16 | ) -> Result<FnDecl> { | |
| 16 | 17 | let loc = p.loc(); |
| 17 | 18 | match p.next() { |
| 18 | 19 | Some(Token::Fn) => (), |
| ... | ... | @@ -57,9 +58,13 @@ pub fn parse_fn_declaration( |
| 57 | 58 | break; |
| 58 | 59 | } |
| 59 | 60 | _ => { |
| 61 | let actual = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); | |
| 60 | 62 | return Err(p |
| 61 | .error("Expected ',' or ')' after identifier".to_string()) | |
| 62 | .into()) | |
| 63 | .error(format!( | |
| 64 | "Expected ',' or ')' after identifier, got {}", | |
| 65 | actual | |
| 66 | )) | |
| 67 | .into()); | |
| 63 | 68 | } |
| 64 | 69 | }; |
| 65 | 70 | } |
| ... | ... | @@ -69,19 +74,6 @@ pub fn parse_fn_declaration( |
| 69 | 74 | .into()) |
| 70 | 75 | } |
| 71 | 76 | } |
| 72 | ||
| 73 | match p.peek() { | |
| 74 | Some(Token::Comma) => p.next(), | |
| 75 | Some(Token::ClosedParen) => { | |
| 76 | _ = p.next(); | |
| 77 | break; | |
| 78 | } | |
| 79 | _ => { | |
| 80 | return Err(p | |
| 81 | .error("Expected ',' or ')' after identifier".to_string()) | |
| 82 | .into()) | |
| 83 | } | |
| 84 | }; | |
| 85 | 77 | } |
| 86 | 78 | |
| 87 | 79 | let return_type = match p.peek() { |
| ... | ... | @@ -92,31 +84,39 @@ pub fn parse_fn_declaration( |
| 92 | 84 | _ => Type::Infer, |
| 93 | 85 | }; |
| 94 | 86 | |
| 95 | if let Some(Token::OpenBrace) = p.peek() { | |
| 96 | let block_loc = p.loc(); | |
| 97 | let block = parse_block(p)?; | |
| 98 | let function_body = ExprData::Fn { | |
| 99 | name: name.clone(), | |
| 100 | params, | |
| 101 | return_type, | |
| 102 | block, | |
| 103 | }; | |
| 104 | Ok(Decl { | |
| 105 | name, | |
| 106 | data_type: Type::Any, | |
| 107 | init: Expr::new(function_body, block_loc, None), | |
| 108 | is_constant: true, | |
| 109 | is_pub, | |
| 110 | comment, | |
| 111 | loc: loc, | |
| 112 | }) | |
| 113 | } else { | |
| 114 | let actually = p.peek().map_or("EOF".to_string(), |x| x.to_string()); | |
| 115 | return Err(p | |
| 116 | .error(format!( | |
| 117 | "Expected a block to follow function declaration, but got {}", | |
| 118 | actually, | |
| 119 | )) | |
| 120 | .into()); | |
| 121 | } | |
| 87 | let was_allow_return = p.allow_return; | |
| 88 | p.allow_return = true; | |
| 89 | ||
| 90 | let body = match p.peek() { | |
| 91 | Some(Token::OpenBrace) => { | |
| 92 | let block_loc = p.loc(); | |
| 93 | let block = parse_block(p)?; | |
| 94 | Expr::new(ExprData::Block(block), block_loc, None) | |
| 95 | } | |
| 96 | Some(Token::BigRightArrow) => { | |
| 97 | _ = p.next(); | |
| 98 | parse_expr(p)? | |
| 99 | } | |
| 100 | _ => { | |
| 101 | let actually = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); | |
| 102 | return Err(p | |
| 103 | .error(format!( | |
| 104 | "Expected a block or '=>' to follow function declaration, but got {}", | |
| 105 | actually, | |
| 106 | )) | |
| 107 | .into()); | |
| 108 | } | |
| 109 | }; | |
| 110 | ||
| 111 | p.allow_return = was_allow_return; | |
| 112 | ||
| 113 | Ok(FnDecl { | |
| 114 | name, | |
| 115 | return_type, | |
| 116 | params, | |
| 117 | body, | |
| 118 | is_pub, | |
| 119 | comment, | |
| 120 | loc, | |
| 121 | }) | |
| 122 | 122 | } |
crates/parser/parse_statement.rs+12-8| ... | ... | @@ -28,10 +28,12 @@ pub fn parse_statement(p: &mut Parser) -> Result<Statement> { |
| 28 | 28 | Some(Token::Pub) => { |
| 29 | 29 | _ = p.next(); |
| 30 | 30 | match p.peek() { |
| 31 | Some(Token::Var) | Some(Token::Let) => { | |
| 32 | Ok(Statement::Decl(parse_declaration(p, true, m_doc_comment)?)) | |
| 33 | } | |
| 34 | Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration( | |
| 31 | Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( | |
| 32 | p, | |
| 33 | true, | |
| 34 | m_doc_comment, | |
| 35 | )?)), | |
| 36 | Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( | |
| 35 | 37 | p, |
| 36 | 38 | true, |
| 37 | 39 | m_doc_comment, |
| ... | ... | @@ -40,10 +42,12 @@ pub fn parse_statement(p: &mut Parser) -> Result<Statement> { |
| 40 | 42 | None => Err(p.error("Unexpected end of file".to_string()).into()), |
| 41 | 43 | } |
| 42 | 44 | } |
| 43 | Some(Token::Var) | Some(Token::Let) => { | |
| 44 | Ok(Statement::Decl(parse_declaration(p, false, m_doc_comment)?)) | |
| 45 | } | |
| 46 | Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration( | |
| 45 | Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( | |
| 46 | p, | |
| 47 | false, | |
| 48 | m_doc_comment, | |
| 49 | )?)), | |
| 50 | Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( | |
| 47 | 51 | p, |
| 48 | 52 | false, |
| 49 | 53 | m_doc_comment, |
crates/parser/parser.rs+2-2| ... | ... | @@ -53,7 +53,7 @@ pub struct Parser { |
| 53 | 53 | cursor: usize, |
| 54 | 54 | |
| 55 | 55 | /// Whether or not the parser is at the top level |
| 56 | pub is_top_level: bool, | |
| 56 | pub allow_return: bool, | |
| 57 | 57 | |
| 58 | 58 | comments: Vec<String>, |
| 59 | 59 | } |
| ... | ... | @@ -63,7 +63,7 @@ impl Parser { |
| 63 | 63 | Parser { |
| 64 | 64 | tokens, |
| 65 | 65 | cursor: 0, |
| 66 | is_top_level: true, | |
| 66 | allow_return: false, | |
| 67 | 67 | comments: vec![], |
| 68 | 68 | } |
| 69 | 69 | } |
crates/tokenizer/tokenizer.rs+12-5| ... | ... | @@ -5,6 +5,7 @@ use thiserror::Error; |
| 5 | 5 | pub struct Loc {} |
| 6 | 6 | |
| 7 | 7 | impl Loc { |
| 8 | /// TODO(@paperdave): Remove this. every usage of this is incorrect. | |
| 8 | 9 | pub const EMPTY: Loc = Loc {}; |
| 9 | 10 | |
| 10 | 11 | pub fn new() -> Self { |
| ... | ... | @@ -44,12 +45,14 @@ pub enum Token { |
| 44 | 45 | AndAnd, // && |
| 45 | 46 | Bang, // ! |
| 46 | 47 | BangEqual, // != |
| 48 | BigRightArrow, // => | |
| 47 | 49 | Caret, // ^ |
| 48 | 50 | ClosedBrace, // } |
| 49 | 51 | ClosedBracket, // ] |
| 50 | 52 | ClosedParen, // ) |
| 51 | 53 | Colon, // : |
| 52 | 54 | Comma, // , |
| 55 | Dot, // . | |
| 53 | 56 | Equal, // = |
| 54 | 57 | EqualEqual, // == |
| 55 | 58 | Gt, // > |
| ... | ... | @@ -73,14 +76,13 @@ pub enum Token { |
| 73 | 76 | Star, // * |
| 74 | 77 | StarStar, // ** |
| 75 | 78 | Tilde, // ~ |
| 76 | Dot, // . | |
| 77 | 79 | } |
| 78 | 80 | |
| 79 | 81 | impl Token { |
| 80 | 82 | pub fn is_start_of_statement_like_expression(&self) -> bool { |
| 81 | 83 | match &self { |
| 82 | Token::If | Token::OpenBrace => false, | |
| 83 | _ => true, | |
| 84 | Token::If | Token::OpenBrace => true, | |
| 85 | _ => false, | |
| 84 | 86 | } |
| 85 | 87 | } |
| 86 | 88 | } |
| ... | ... | @@ -110,17 +112,20 @@ impl std::fmt::Display for Token { |
| 110 | 112 | Token::AndAnd => "&&".to_string(), |
| 111 | 113 | Token::Bang => "!".to_string(), |
| 112 | 114 | Token::BangEqual => "!=".to_string(), |
| 115 | Token::BigRightArrow => "=>".to_string(), | |
| 113 | 116 | Token::Caret => "^".to_string(), |
| 114 | 117 | Token::ClosedBrace => "}".to_string(), |
| 115 | 118 | Token::ClosedBracket => "]".to_string(), |
| 116 | 119 | Token::ClosedParen => ")".to_string(), |
| 117 | 120 | Token::Colon => ":".to_string(), |
| 118 | 121 | Token::Comma => ",".to_string(), |
| 122 | Token::Dot => ".".to_string(), | |
| 119 | 123 | Token::Equal => "=".to_string(), |
| 120 | 124 | Token::EqualEqual => "==".to_string(), |
| 121 | 125 | Token::Gt => ">".to_string(), |
| 122 | 126 | Token::GtEqual => ">=".to_string(), |
| 123 | 127 | Token::GtGt => ">>".to_string(), |
| 128 | Token::GtGtGt => ">>>".to_string(), | |
| 124 | 129 | Token::Lt => "<".to_string(), |
| 125 | 130 | Token::LtEqual => "<=".to_string(), |
| 126 | 131 | Token::LtLt => "<<".to_string(), |
| ... | ... | @@ -138,8 +143,6 @@ impl std::fmt::Display for Token { |
| 138 | 143 | Token::Star => "*".to_string(), |
| 139 | 144 | Token::StarStar => "**".to_string(), |
| 140 | 145 | Token::Tilde => "~".to_string(), |
| 141 | Token::Dot => ".".to_string(), | |
| 142 | Token::GtGtGt => ">>>".to_string(), | |
| 143 | 146 | }; |
| 144 | 147 | |
| 145 | 148 | write!(f, "{}", token) |
| ... | ... | @@ -234,6 +237,10 @@ pub fn tokenize(content: String) -> Result<Vec<Token>> { |
| 234 | 237 | chars.next(); |
| 235 | 238 | tokens.push(Token::EqualEqual); |
| 236 | 239 | } |
| 240 | Some((_, '>')) => { | |
| 241 | chars.next(); | |
| 242 | tokens.push(Token::BigRightArrow); | |
| 243 | } | |
| 237 | 244 | _ => tokens.push(Token::Equal), |
| 238 | 245 | }, |
| 239 | 246 | ';' => tokens.push(Token::Semicolon), |
examples/basics/stupid.blight created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | ||
| 2 | fn add(x: int, y: int) => x + y |