From 12864f7c49c005271c6c5610dfaa2cb71ff56ae7 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Mon, 22 Jan 2024 00:32:12 -0800 Subject: [PATCH] finish rewriting the project in rust --- Cargo.lock | 1 + Cargo.toml | 1 - crates/ast/ast.rs | 104 ++- crates/ast/operators.rs | 156 ++++- crates/cli/Cargo.lock | 532 --------------- crates/cli/Cargo.toml | 2 +- crates/cli/{src => }/main.rs | 31 +- crates/js_ast/Cargo.toml | 1 + crates/js_ast/convert_valid_expr.rs | 204 ++++++ crates/js_ast/has_side_effects.rs | 90 +++ crates/js_ast/hoist.rs | 614 ++++++++++++++++++ crates/js_ast/is_valid_js_expression.rs | 85 +++ crates/js_ast/js_ast.rs | 276 +++----- crates/js_ast/lib.rs | 421 +----------- crates/js_ast/util.rs | 17 + crates/js_printer/Cargo.toml | 1 + crates/js_printer/lib.rs | 358 ++++++++++ crates/js_printer/printer_tests.rs | 53 ++ crates/js_printer/src/print_expression.rs | 14 +- crates/js_printer/src/print_statement.rs | 12 +- crates/parser/parse_block.rs | 32 +- crates/parser/parse_call_arguments.rs | 5 + crates/parser/parse_expr.rs | 61 +- crates/parser/parse_expr_if.rs | 22 +- crates/parser/parse_fn_declaration.rs | 11 +- crates/parser/parse_module.rs | 6 +- crates/sema/Cargo.toml | 1 + crates/sema/{src => }/ast_type_coersion.rs | 0 crates/sema/{src/lib.rs => sema.rs} | 10 +- crates/tokenizer/Cargo.toml | 1 + crates/tokenizer/{src/lib.rs => tokenizer.rs} | 9 + 31 files changed, 1861 insertions(+), 1270 deletions(-) delete mode 100644 crates/cli/Cargo.lock rename crates/cli/{src => }/main.rs (85%) create mode 100644 crates/js_ast/convert_valid_expr.rs create mode 100644 crates/js_ast/has_side_effects.rs create mode 100644 crates/js_ast/hoist.rs create mode 100644 crates/js_ast/is_valid_js_expression.rs create mode 100644 crates/js_ast/util.rs create mode 100644 crates/js_printer/lib.rs create mode 100644 crates/js_printer/printer_tests.rs rename crates/sema/{src => }/ast_type_coersion.rs (100%) rename crates/sema/{src/lib.rs => sema.rs} (95%) rename crates/tokenizer/{src/lib.rs => tokenizer.rs} (98%) diff --git a/Cargo.lock b/Cargo.lock index 03588a003888ec3e8fa82a3bacbbed69357e14c5..e11d7608f4d0b1da5e46afd069efbe460f5c5b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ dependencies = [ "ast", "miette", "num", + "parser", "thiserror", "tokenizer", ] diff --git a/Cargo.toml b/Cargo.toml index fdcf94c7291631b43fbac96e4a24885312c0d65e..a1fb3c69c966fc3a4eeefa9afb84d0c02ba31b08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace.package] version = "0.1.0" -name = "blight" edition = "2023" license = "MIT" authors = ["Lino Le Van", "Dave Caruso"] diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs index 123a54fe5567f5ce9fab3273f1c98753a84ab2ac..46b9e6c60da841ff84f4af137d19f12dd3a34252 100644 --- a/crates/ast/ast.rs +++ b/crates/ast/ast.rs @@ -74,6 +74,19 @@ impl Comment { } result } + + pub fn to_doc_comment(self, loc: Loc) -> DocComment { + DocComment { + data: self.data, + loc, + } + } +} + +impl Into for DocComment { + fn into(self) -> Comment { + Comment { data: self.data } + } } impl DocComment { @@ -98,7 +111,13 @@ pub struct BlockWithLoc { pub struct If { pub condition: Box, pub yes: BlockWithLoc, - pub no: Option>, + pub no: Option, +} + +#[derive(Debug, Clone)] +pub enum BlockOrIf { + Block(BlockWithLoc), + If(Box), } #[derive(Debug, Clone)] @@ -166,67 +185,10 @@ impl BuiltinId { } } -impl ExprData { - pub fn is_valid_js_expression(&self) -> bool { - match self { - // A block containing just expressions can be an expression - ExprData::Block(block) => block.is_valid_js_expression(), - ExprData::If(expr) => expr.is_valid_js_expression(), - ExprData::Return(_) => false, - ExprData::Builtin { id, .. } => match id { - BuiltinId::Raw => true, - }, - _ => true, - } - } -} - -impl Block { - pub fn is_valid_js_expression(&self) -> bool { - self.statements - .iter() - .all(|statement| statement.is_valid_js_expression()) - } -} - -impl Expr { - pub fn is_valid_js_expression(&self) -> bool { - self.data.is_valid_js_expression() - } -} - -impl If { - pub fn is_valid_js_expression(&self) -> bool { - self.condition.is_valid_js_expression() - && self.yes.is_valid_js_expression() - && match &self.no { - Some(no) => no.is_valid_js_expression(), - None => true, - } - } -} - -impl BlockWithLoc { - pub fn is_valid_js_expression(&self) -> bool { - self.block.is_valid_js_expression() - } -} - impl Statement { - pub fn requires_semicolon(&self) -> bool { + pub fn is_expression(&self) -> bool { match &self { - Statement::Expr(expr) => match expr.data { - ExprData::Block(_) => false, - ExprData::If(_) => false, - _ => true, - }, - _ => false, - } - } - - pub fn is_valid_js_expression(&self) -> bool { - match self { - Statement::Expr(expr) => expr.data.is_valid_js_expression(), + Statement::Expr(_) => true, _ => false, } } @@ -238,16 +200,19 @@ impl Statement { Statement::Comment(comment) => TopLevelStatement::Comment(comment), } } + + pub fn is_comment(&self) -> bool { + match self { + Statement::Comment(_) => true, + _ => false, + } + } } impl TopLevelStatement { - pub fn requires_semicolon(&self) -> bool { + pub fn is_expression(&self) -> bool { match &self { - TopLevelStatement::Expr(expr) => match expr.data { - ExprData::Block(_) => false, - ExprData::If(_) => false, - _ => true, - }, + TopLevelStatement::Expr(_) => true, _ => false, } } @@ -259,4 +224,11 @@ impl TopLevelStatement { TopLevelStatement::Comment(comment) => Statement::Comment(comment), } } + + pub fn is_comment(&self) -> bool { + match self { + TopLevelStatement::Comment(_) => true, + _ => false, + } + } } diff --git a/crates/ast/operators.rs b/crates/ast/operators.rs index 18dd5ae68a5961b785e815adca22dd9c71418907..622be4fcc24c93e55588e9e341bccc1176e1cc54 100644 --- a/crates/ast/operators.rs +++ b/crates/ast/operators.rs @@ -24,6 +24,22 @@ pub enum BinaryOperator { NotEq, And, Or, + + Assign, + AssignAdd, + AssignSub, + AssignMul, + AssignDiv, + AssignPow, + AssignLshift, + AssignRshift, + AssignRshiftUnsigned, + AssignBitAnd, + AssignBitOr, + AssignBitXor, + AssignMod, + AssignAnd, + AssignOr, } #[derive(Debug, Clone, PartialEq)] @@ -34,8 +50,8 @@ pub enum UnaryOperator { } impl BinaryOperator { - pub fn to_string(&self) -> String { - String::from(match self { + pub fn to_str(&self) -> &'static str { + match self { Self::Add => "+", Self::Sub => "-", Self::Div => "/", @@ -56,7 +72,26 @@ impl BinaryOperator { Self::NotEq => "!=", Self::And => "&&", Self::Or => "||", - }) + Self::AssignAdd => "+=", + Self::AssignSub => "-=", + Self::AssignDiv => "/=", + Self::AssignMul => "*=", + Self::AssignPow => "**=", + Self::Assign => "=", + Self::AssignLshift => "<<=", + Self::AssignRshift => ">>=", + Self::AssignRshiftUnsigned => ">>>=", + Self::AssignBitAnd => "&=", + Self::AssignBitOr => "|=", + Self::AssignBitXor => "^=", + Self::AssignMod => "%=", + Self::AssignAnd => "&&=", + Self::AssignOr => "||=", + } + } + + pub fn to_string(&self) -> String { + String::from(self.to_str()) } pub fn from_token(token: &Token) -> Option { @@ -85,6 +120,10 @@ impl BinaryOperator { } } + pub fn level(&self) -> OperatorLevel { + OperatorLevel::from(self) + } + /// Error case of this means 'types do not allow this operation' pub fn perform(&self, left: Literal, right: Literal) -> Result { match self { @@ -139,17 +178,24 @@ impl BinaryOperator { }), _ => Err(()), }, + + // Impossible condition doing assign-add to a literal + _ => Err(()), } } } impl UnaryOperator { - pub fn to_string(self) -> String { - String::from(match self { + pub fn to_str(&self) -> &'static str { + match self { Self::Neg => "-", Self::Not => "!", Self::BitNot => "~", - }) + } + } + + pub fn to_string(&self) -> String { + String::from(self.to_str()) } pub fn perform(self, value: Literal) -> Result { @@ -171,8 +217,17 @@ impl UnaryOperator { } /// Taken from from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table -#[derive(PartialEq, PartialOrd)] -pub enum BinaryOperatorLevel { +/// +/// Blight doesn't have all of these operators, but the +/// printer uses it to determine where to put parens. +#[derive(PartialEq, PartialOrd, Debug)] +pub enum OperatorLevel { + Accessor, // e. e[] e() + New, // new + Postfix, // expr++ expr-- + Prefix, // ++e, --e, !e, ~e, +e, -e, typeof, void, delete + + // these are the binary operators Exponentiation, // ** Multiplicative, // *, /, % Additive, // +, - @@ -184,22 +239,77 @@ pub enum BinaryOperatorLevel { BitwiseOr, // | LogicalAnd, // && LogicalOr, // ||, ?? + + AssignmentAndOther, // =, +=, -=, *=, /=, %=, / <<=, >>=, >>>=, &=, ^=, |= + Comma, // , +} + +pub enum Associativity { + LeftToRight, + RightToLeft, +} + +impl OperatorLevel { + pub fn associativity(&self) -> Associativity { + use Associativity::*; + use OperatorLevel::*; + + match self { + Accessor => LeftToRight, + Exponentiation => RightToLeft, + Multiplicative => LeftToRight, + Additive => LeftToRight, + BitwiseShift => LeftToRight, + Relational => LeftToRight, + Equality => LeftToRight, + BitwiseAnd => LeftToRight, + BitwiseXor => LeftToRight, + BitwiseOr => LeftToRight, + LogicalAnd => LeftToRight, + LogicalOr => LeftToRight, + AssignmentAndOther => RightToLeft, + Comma => LeftToRight, + + // These do not make sense to have an associativity + // You will likely not run into this if you do + // BinaryOperator::level(o).associativity() + New | Postfix | Prefix => unreachable!("{:?} does not have an associativity", self), + } + } } -pub fn get_binary_op_level(token: &Token) -> Option { - use BinaryOperatorLevel::*; - match token { - Token::StarStar => Some(Exponentiation), - Token::Star | Token::Slash | Token::Percent => Some(Multiplicative), - Token::Plus | Token::Minus => Some(Additive), - Token::LtLt | Token::GtGt | Token::GtGtGt => Some(BitwiseShift), - Token::Gt | Token::GtEqual | Token::Lt | Token::LtEqual => Some(Relational), - Token::EqualEqual | Token::BangEqual => Some(Equality), - Token::And => Some(BitwiseAnd), - Token::Caret => Some(BitwiseXor), - Token::Or => Some(BitwiseOr), - Token::AndAnd => Some(LogicalAnd), - Token::OrOr => Some(LogicalOr), - _ => None, +impl From<&BinaryOperator> for OperatorLevel { + fn from(operator: &BinaryOperator) -> Self { + use BinaryOperator::*; + use OperatorLevel::*; + + match operator { + Add | Sub => Additive, + Mul | Div | Mod => Multiplicative, + Pow => Exponentiation, + Gt | Lt | Gte | Lte => Relational, + Lshift | Rshift | RshiftUnsigned => BitwiseShift, + BitAnd => BitwiseAnd, + BitOr => BitwiseOr, + BitXor => BitwiseXor, + Eq | NotEq => Equality, + And => LogicalAnd, + Or => LogicalOr, + + Assign | AssignAdd | AssignSub | AssignMul | AssignDiv | AssignPow | AssignLshift + | AssignRshift | AssignRshiftUnsigned | AssignBitAnd | AssignBitOr | AssignBitXor + | AssignMod | AssignAnd | AssignOr => AssignmentAndOther, + } + } +} + +impl From for OperatorLevel { + fn from(operator: UnaryOperator) -> Self { + use OperatorLevel::*; + use UnaryOperator::*; + + match operator { + Neg | Not | BitNot => Prefix, + } } } diff --git a/crates/cli/Cargo.lock b/crates/cli/Cargo.lock deleted file mode 100644 index bea6bbd39b14927d56ffb803015103f3cd74364f..0000000000000000000000000000000000000000 --- a/crates/cli/Cargo.lock +++ /dev/null @@ -1,532 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "anstream" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anstyle-parse" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "blight" -version = "0.1.0" -dependencies = [ - "clap", - "miette", - "thiserror", -] - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "4.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "is-terminal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - -[[package]] -name = "is_ci" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" - -[[package]] -name = "libc" -version = "0.2.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" - -[[package]] -name = "linux-raw-sys" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" - -[[package]] -name = "memchr" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" - -[[package]] -name = "miette" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" -dependencies = [ - "backtrace", - "backtrace-ext", - "is-terminal", - "miette-derive", - "once_cell", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "proc-macro2" -version = "1.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustix" -version = "0.38.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "supports-color" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" -dependencies = [ - "is-terminal", - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "supports-unicode" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "syn" -version = "2.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d27c2c202598d05175a6dd3af46824b7f747f8d8e9b14c623f19fa5069735d" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 8c257d214dad91041d7de4f1e29bc1ee6641750c..00920ac78a6265cf80755f257b26c88e56c2d8dd 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [[bin]] name = "blight" -path = "./src/main.rs" +path = "./main.rs" [dependencies] clap.workspace = true diff --git a/crates/cli/src/main.rs b/crates/cli/main.rs similarity index 85% rename from crates/cli/src/main.rs rename to crates/cli/main.rs index 00945e42be7c877440cc6f4466dfba7fad622f9c..dfcb80febf985e7247541887ad7a59bdd65d364f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/main.rs @@ -1,7 +1,7 @@ use std::{io::Write, path::PathBuf}; use clap::{Parser, Subcommand}; -use miette::Result; +use miette::{diagnostic, Result}; use js_printer; use tokenizer; @@ -50,21 +50,28 @@ fn get_runtime(runtime: &String) -> String { } } -pub fn transpile(content: String) -> Result { +pub fn transpile(content: String, writer: &mut dyn Write) -> Result<()> { // 1. Tokenize the input let tokens = tokenizer::tokenize(content)?; // 2. Parse the tokens into an AST - let blight_ast = parser::parse(tokens)?; - // 3. Type check the AST - // sema::check_module(&blight_ast)?; + let mut blight_ast = parser::parse(tokens)?; + // 3. Semantic analysis. + sema::analyze_module(&mut blight_ast)?; // 4. Optimize the AST - // optimize(tree.clone())?; + // optimize::optimize_module(&mut blight_ast); // 5. Translate the AST into javascript - let js_ast = js_ast::translate(&blight_ast)?; + let js_ast = js_ast::blight_to_js(blight_ast); // 6. Print the JS AST - let javascript = js_printer::print(js_ast); - // 7. Return the javascript - return Ok(javascript); + // TODO(@paperdave): use std::io::Write instead of a string + js_printer::Print::print(&js_ast, writer) + .map_err(|e| diagnostic!("Error while printing: {}", e))?; + Ok(()) +} + +pub fn transpile_to_string(content: String) -> Result { + let mut buffer = Vec::new(); + transpile(content, &mut buffer)?; + Ok(String::from_utf8(buffer).unwrap()) } pub fn astdump(content: String) -> Result { @@ -93,7 +100,7 @@ fn main() -> Result<()> { } }; - let mut result = transpile(contents.clone())?; + let mut result = transpile_to_string(contents.clone())?; if format { let mut formatter = std::process::Command::new("deno") @@ -124,7 +131,7 @@ fn main() -> Result<()> { } Commands::Run { file, target } => { let contents = std::fs::read_to_string(file.clone()).unwrap(); - let result = transpile(contents.clone())?; + let result = transpile_to_string(contents.clone())?; let mut temp_file = std::env::temp_dir(); temp_file.push("temp.js"); std::fs::write(temp_file.clone(), result).unwrap(); diff --git a/crates/js_ast/Cargo.toml b/crates/js_ast/Cargo.toml index f7b5438968013029fa069bd2d9040847c5454392..68087cf32a5792a7ae7f6ecac80399eb5883fdba 100644 --- a/crates/js_ast/Cargo.toml +++ b/crates/js_ast/Cargo.toml @@ -13,3 +13,4 @@ num.workspace = true ast = { path = "../ast" } tokenizer = { path = "../tokenizer" } # TODO: move Loc out of the tokenizer +parser = { path = "../parser" } # TODO: move Loc out of the tokenizer diff --git a/crates/js_ast/convert_valid_expr.rs b/crates/js_ast/convert_valid_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..90e13ddc003e4066d45034cba29c8b3f4d438432 --- /dev/null +++ b/crates/js_ast/convert_valid_expr.rs @@ -0,0 +1,204 @@ +//! "Valid" in this context means that the expression does not contain JS statements +//! Meaning conversion is a direct map from a blight as node into a JS one +//! +//! See `is_valid_js_expression` for more details on what qualifies +//! +//! For other nodes, it must hoist statements to the nearest JS block, +//! is done in the `hoist_*` functions in `host.rs`. + +use ast::{BlockOrIf, Comment, ExprData, If}; +use ast::{BlockWithLoc, BuiltinId, Expr, Literal, Statement}; +use tokenizer::Loc; + +use crate::is_valid_js_expression::IsValidJSExpression; +use crate::js_ast::*; +use crate::HoistContext; + +/// Translate a blight ast expression that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_expr(ctx: &mut HoistContext, expr: Expr) -> JsExpr { + // Assert may be skipped in release builds as all failures are + // annotated with a release-checked panic + debug_assert!( + expr.is_valid_js_expression(), + "must call convert_valid_expr with valid js expression (use hoist instead)" + ); + + let loc = expr.loc; + let comment = expr.comment; + + match expr.data { + ExprData::Identifier(str) => JsExpr::new( + JsExprData::Identifier(ctx.remap_identifier(&str)), + loc, + comment, + ), + + ExprData::Literal(Literal::Bool(value)) => { + JsExpr::new(JsExprData::Bool(value), loc, comment) + } + ExprData::Literal(Literal::Number(value)) => { + JsExpr::new(JsExprData::Number(value), loc, comment) + } + ExprData::Literal(Literal::String(value)) => { + JsExpr::new(JsExprData::String(value), loc, comment) + } + ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), + + ExprData::Block(block) => convert_valid_block(ctx, BlockWithLoc { block, loc }), + + ExprData::Binary { op, left, right } => { + let left = convert_valid_expr(ctx, *left); + let right = convert_valid_expr(ctx, *right); + JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + loc, + comment, + ) + } + ExprData::Unary { op, value } => { + let value = convert_valid_expr(ctx, *value); + JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + loc, + comment, + ) + } + ExprData::Fn { block, params, .. } => JsExpr::new( + JsExprData::ArrowFunction { + params: params.into_iter().map(|x| x.name).collect(), + body: Box::new(ctx.hoist_block_to_arrow_fn_body(BlockWithLoc { + block, + loc: loc.clone(), + })), + }, + loc, + comment, + ), + ExprData::Builtin { id, arguments } => match id { + BuiltinId::Raw => { + let first_arg = arguments.iter().next().unwrap(); + // TODO(@paperdave): this string clone is stupid + let str = match first_arg.data.clone() { + ExprData::Literal(Literal::String(s)) => s, + _ => panic!("raw expects a string"), + }; + JsExpr::new(JsExprData::Raw(str), loc, comment) + } + }, + ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), + + ExprData::Call { base, arguments } => { + let base = convert_valid_expr(ctx, *base); + let arguments = arguments + .into_iter() + .map(|x| convert_valid_expr(ctx, x)) + .collect(); + JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + loc, + comment, + ) + } + + ExprData::Field { base, field } => JsExpr::new( + match field.data { + ExprData::Literal(Literal::String(name)) => JsExprData::Dot { + base: Box::new(convert_valid_expr(ctx, *base)), + name, + }, + _ => JsExprData::Index { + base: Box::new(convert_valid_expr(ctx, *base)), + index: Box::new(convert_valid_expr(ctx, *field)), + }, + }, + loc, + comment, + ), + + ExprData::Return(_) => unreachable!("convert_valid_expr does not support return"), + } +} + +pub fn convert_valid_block(ctx: &mut HoistContext, b: BlockWithLoc) -> JsExpr { + debug_assert!( + b.is_valid_js_expression(), + "must call convert_valid_block with valid js expression" + ); + + let len = b.block.statements.len(); + let loc = b.loc; + let block_expr = match len { + 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), + // a block expression with a single expression is just that expression + 1 => convert_valid_statement_to_expr(ctx, b.block.statements.into_iter().next().unwrap()), + // Since blocks are not valid expressions in JS, we create a + // comma expression from a block with more than one statement. + 2.. => { + let mut items = Vec::new(); + for statement in b.block.statements.into_iter() { + items.push(convert_valid_statement_to_expr(ctx, statement)); + } + JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) + } + }; + + // a block without implicit return is to be type void, so we wrap in a void expression + if !b.block.has_implicit_return && len > 0 { + JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) + } else { + block_expr + } +} + +pub fn convert_valid_statement_to_expr(ctx: &mut HoistContext, statement: Statement) -> JsExpr { + debug_assert!( + statement.is_valid_js_expression(), + "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" + ); + + match statement { + ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), + _ => unreachable!("attempt to convert statement to js without hoisting"), + } +} + +pub fn convert_valid_statement(ctx: &mut HoistContext, statement: Statement) -> JsStatement { + let expr = convert_valid_statement_to_expr(ctx, statement); + let loc = expr.loc.clone(); + JsStatement::new(JsStatementData::Expression(expr), loc, None) +} + +pub fn convert_valid_if( + ctx: &mut HoistContext, + expr: If, + loc: Loc, + comment: Option, +) -> JsExpr { + let condition = convert_valid_expr(ctx, *expr.condition); + let yes = convert_valid_block(ctx, expr.yes); + let no = match expr.no { + Some(BlockOrIf::Block(block)) => Some(convert_valid_block(ctx, block)), + Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), + None => None, + }; + JsExpr::new( + JsExprData::Ternary { + condition: Box::new(condition), + yes: Box::new(yes), + no: no.map(Box::new), + }, + loc, + comment, + ) +} diff --git a/crates/js_ast/has_side_effects.rs b/crates/js_ast/has_side_effects.rs new file mode 100644 index 0000000000000000000000000000000000000000..81c16730f2cd157a8f1b9236588bea0a05854a96 --- /dev/null +++ b/crates/js_ast/has_side_effects.rs @@ -0,0 +1,90 @@ +use ast::*; + +pub trait HasSideEffects { + /// Can this item be removed from the AST without changing the program's behavior? + fn has_side_effects(&self) -> bool; +} + +impl HasSideEffects for ExprData { + fn has_side_effects(&self) -> bool { + match self { + ExprData::Literal(_) => false, + ExprData::Identifier(_) => false, + + ExprData::Block(block) => block.has_side_effects(), + ExprData::Binary { left, right, .. } => { + left.has_side_effects() || right.has_side_effects() + } + ExprData::Unary { value, .. } => value.has_side_effects(), + ExprData::If(ifexpr) => ifexpr.has_side_effects(), + + // note: a function *declaration* has no side effects, but the call *can* + ExprData::Fn { .. } => false, + + // TODO(@paperdave): implement detection of pure functions + ExprData::Call { base, arguments } => { + base.has_side_effects() || arguments.iter().any(|x| x.has_side_effects()) + } + + // note: we assume field access has side effects, ie: a getter doesn't throw + ExprData::Field { base, field } => base.has_side_effects() || field.has_side_effects(), + + // note: we assume the code in @raw always has side effects + ExprData::Builtin { id, .. } => match id { + BuiltinId::Raw => true, + }, + + ExprData::Return(_) => true, + } + } +} + +impl HasSideEffects for Statement { + fn has_side_effects(&self) -> bool { + match self { + Statement::Expr(expr) => expr.has_side_effects(), + Statement::Decl(decl) => decl.init.has_side_effects(), + Statement::Comment(_) => false, + } + } +} + +impl HasSideEffects for Block { + fn has_side_effects(&self) -> bool { + self.statements + .iter() + .any(|statement| statement.has_side_effects()) + } +} + +impl HasSideEffects for Expr { + fn has_side_effects(&self) -> bool { + self.data.has_side_effects() + } +} + +impl HasSideEffects for If { + fn has_side_effects(&self) -> bool { + self.condition.has_side_effects() + || self.yes.has_side_effects() + || match &self.no { + Some(no) => no.has_side_effects(), + None => false, + } + } +} + +impl HasSideEffects for BlockOrIf { + fn has_side_effects(&self) -> bool { + match self { + BlockOrIf::Block(block) => block.has_side_effects(), + BlockOrIf::If(if_) => if_.has_side_effects(), + } + } +} + +impl HasSideEffects for BlockWithLoc { + fn has_side_effects(&self) -> bool { + self.block.has_side_effects() + } +} diff --git a/crates/js_ast/hoist.rs b/crates/js_ast/hoist.rs new file mode 100644 index 0000000000000000000000000000000000000000..5351588aec2aae5653dcdebb524e2503df15aa9e --- /dev/null +++ b/crates/js_ast/hoist.rs @@ -0,0 +1,614 @@ +use std::collections::{HashMap, HashSet}; +use std::vec; + +use ast::{BinaryOperator, BlockOrIf, BlockWithLoc, Expr, Statement}; +use tokenizer::Loc; + +use crate::*; + +pub struct HoistContext { + pub statements: Vec, + pub ids: HashSet, + next_id: u32, + scopes: Vec, + has_last_statement: bool, +} + +struct HoistScope { + renames: HashMap, +} + +pub struct TempId { + unmapped_name: String, +} + +impl TempId { + pub fn get(&self, hoist_context: &HoistContext) -> String { + hoist_context.remap_identifier(&self.unmapped_name) + } +} + +impl HoistContext { + pub fn new() -> Self { + Self { + statements: Vec::new(), + ids: HashSet::new(), + next_id: 0, + scopes: vec![HoistScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + pub fn new_child(&self) -> Self { + Self { + statements: Vec::new(), + ids: self.ids.clone(), + next_id: 0, + scopes: vec![HoistScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + /// A virtual scope is a scope that exists in blight syntax + /// but may or may not exist in blight. With these it is important to + pub fn push_virtual_scope(&mut self) { + self.scopes.push(HoistScope { + renames: HashMap::new(), + }); + } + + /// See `push_virtual_scope` for more information on what this is. + pub fn pop_virtual_scope(&mut self) { + self.scopes.pop(); + } + + pub fn generate_temp_id(&mut self, basename: &str) -> TempId { + self.next_id += 1; + loop { + let name = format!("{}{}", basename, self.next_id); + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + self.next_id += 1; + } + } + + /// This does some extra optimizations: + /// - `let a = b;` will not be emitted but instad write a variable alias + pub fn add_statement(&mut self, statement: JsStatement) { + match statement.data { + JsStatementData::Declaration(JSDeclaration { + name, + init: + Some(JsExpr { + data: JsExprData::Identifier(init_name), + .. + }), + is_constant: true, + }) => { + self.scopes + .last_mut() + .unwrap() + .renames + .insert(name, init_name); + self.has_last_statement = false; + return; + } + _ => {} + } + self.statements.push(statement); + self.has_last_statement = true; + } + + /// Get the last statement if it exists. + /// If the statement was optimized away, this returns None + pub fn last_statement(&self) -> Option<&JsStatement> { + if self.has_last_statement { + Some(self.statements.last().unwrap()) + } else { + None + } + } + + pub fn generate_id(&mut self, basename: &String) -> TempId { + let mut id = 0u32; + loop { + let name = match id { + 0 => basename.clone(), + _ => format!("{}{}", basename, id), + }; + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + id += 1; + } + } + + /// Given a blight identifier, remap it to a new identifier. + pub fn remap_identifier(&self, name: &String) -> String { + for scope in self.scopes.iter().rev() { + if let Some(remapped) = scope.renames.get(name) { + return remapped.clone(); + } + } + name.clone() + } + + /// Declare an identifier in the current scope. + pub fn declare_identifier(&mut self, name: String) -> TempId { + let remapped = self.generate_id(&name); + let scope = self.scopes.last_mut().unwrap(); + self.ids.insert(remapped.unmapped_name.clone()); + scope.renames.insert(name, remapped.unmapped_name.clone()); + remapped + } + + pub fn process_statement(&mut self, statement: Statement) -> Option<()> { + if statement.is_valid_js_expression() { + let result = convert_valid_statement(self, statement); + if !result.is_literal() { + self.add_statement(result); + } + return Some(()); + } + + match statement { + Statement::Decl(decl) => { + // visit init before name, this way allows for: + // + // let a = a; + // + // and `a` will be looked up before it is re-declared. + if let Some(init) = self.hoist_expression_contents(decl.init) { + let name = self.declare_identifier(decl.name); + + self.add_statement(JsStatement::new( + JsStatementData::Declaration(crate::JSDeclaration { + name: name.get(&self), + init: Some(init), + is_constant: decl.is_constant, + }), + decl.loc, + decl.comment.map(|x| x.into()), + )); + Some(()) + } else { + None + } + } + Statement::Expr(expr) => { + if let Some(expr) = self.hoist_expression_contents(expr) { + let loc = expr.loc.clone(); + if !expr.is_literal() { + self.add_statement(JsStatement::new( + JsStatementData::Expression(expr), + loc, + None, + )) + } + Some(()) + } else { + None + } + } + Statement::Comment(_) => Some(()), + } + } + + /// Pushes a decl with a generated identifier to the hoisted result + /// and returns an identifier expression for it. + pub fn hoist_expression_to_temporary(&mut self, expr: Expr) -> Option { + // TODO + // if !expr.has_side_effects() { + // return Some(self.hoist_expression_contents(expr)?); + // } + + let init = self.hoist_expression_contents(expr)?; + + let id = self.generate_temp_id("_temp"); + let loc = init.loc.clone(); + + self.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: id.get(&self), + init: Some(init), + is_constant: true, + }), + loc.clone(), + None, + )); + + Some(JsExpr::new( + JsExprData::Identifier(id.get(&self)), + loc, + None, + )) + } + + // Returning `None` from this means that the expression is noreturn, and that + // the rest of the statement never occurs and can be stripped. + pub fn hoist_expression_contents(&mut self, expr: Expr) -> Option { + if expr.is_valid_js_expression() { + return Some(convert_valid_expr(self, expr)); + } + + match expr.data { + ast::ExprData::Binary { op, left, right } => { + let left = self.hoist_expression_to_temporary(*left)?; + let right = self.hoist_expression_to_temporary(*right)?; + + Some(JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Unary { op, value } => { + let value = self.hoist_expression_to_temporary(*value)?; + Some(JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Field { base, field } => { + let base = self.hoist_expression_to_temporary(*base)?; + let field = self.hoist_expression_to_temporary(*field)?; + + Some(JsExpr::new( + match field.data { + JsExprData::String(name) if is_js_identifier(name.as_str()) => { + JsExprData::Dot { + base: Box::new(base), + name, + } + } + _ => JsExprData::Index { + base: Box::new(base), + index: Box::new(field), + }, + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Block(block) => { + let temporary = self.generate_temp_id("_block"); + let result = self.hoist_block_to_js_block_in_same_context(BlockWithLoc { + block, + loc: expr.loc.clone(), + }); + + self.add_statement(JsStatement::new( + JsStatementData::Declaration(crate::JSDeclaration { + name: temporary.get(&self), + init: Some(result), + is_constant: false, + }), + expr.loc.clone(), + None, + )); + + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&self)), + expr.loc, + expr.comment, + )) + } + + ast::ExprData::If(data) => { + let temporary = self.generate_temp_id("_if"); + + let decl_index = self.statements.len(); + self.add_statement(JsStatement::new( + JsStatementData::Declaration(crate::JSDeclaration { + name: temporary.get(&self), + init: None, + is_constant: false, + }), + expr.loc.clone(), + None, + )); + + let condition = self.hoist_expression_to_temporary(*data.condition)?; + let mut did_assign = false; + + let js_if = + self.hoist_js_if(data.yes, data.no, condition, &mut did_assign, &temporary)?; + + self.add_statement(JsStatement::new( + JsStatementData::If(js_if), + expr.loc.clone(), + None, + )); + + if did_assign { + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&self)), + expr.loc, + expr.comment, + )) + } else { + self.statements.remove(decl_index); + Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) + } + } + + ast::ExprData::Return(expr) => { + let loc = expr.loc.clone(); + let return_value = self.hoist_expression_to_temporary(*expr)?; + self.add_statement(JsStatement::new( + JsStatementData::Return(return_value), + loc, + None, + )); + None + } + + ast::ExprData::Call { base, arguments } => { + let base = self.hoist_expression_to_temporary(*base)?; + let arguments = arguments + .into_iter() + .map(|x| self.hoist_expression_to_temporary(x)) + .collect::>>()?; + + Some(JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Fn { .. } + | ast::ExprData::Builtin { .. } + | ast::ExprData::Literal(_) + | ast::ExprData::Identifier(_) => { + unreachable!("attempt to hoist an expression that does not require it") + } + } + } + + pub fn hoist_block_to_js_block( + &mut self, + b: BlockWithLoc, + temporary: &String, + did_assign: &mut bool, + ) -> Option { + let loc = b.loc.clone(); + + // A non-block will become a block with a single expression: + // _temp = expr; + // + // We will also apply this transformation to blocks that are valid + // expressions, because that is more concise. + if b.is_valid_js_expression() { + return Some(JsBlock::new( + vec![new_js_assignment_statement( + convert_valid_block(self, b), + temporary.clone(), + loc.clone(), + )], + loc, + )); + } + + let mut child = self.new_child(); + + for statement in b.block.statements { + if let None = child.process_statement(statement) { + return None; + }; + } + + // If the last statement is an expression, we need + // to assign to the temporary variable. + match child.statements.last().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) => { + *did_assign = true; + + let last = child.statements.pop().unwrap(); + let value = match last { + JsStatement { + data: JsStatementData::Expression(expr), + .. + } => expr, + _ => { + self.statements.push(last); + JsExpr::new(JsExprData::Null, loc.clone(), None) + } + }; + child.statements.push(new_js_assignment_statement( + value, + temporary.clone(), + loc.clone(), + )); + } + _ => {} + } + + Some(JsBlock::new(child.statements, loc)) + } + + pub fn hoist_block_to_arrow_fn_body(&mut self, b: BlockWithLoc) -> JsArrowBody { + let loc = b.loc.clone(); + + // A block that is a valid expression will just become + // that expression. We can convert multiple expressions + // into a comma expression. + if b.is_valid_js_expression() { + return JsArrowBody::Expr(convert_valid_block(self, b)); + } + + let mut child = self.new_child(); + + for statement in b.block.statements { + if let None = child.process_statement(statement) { + break; + }; + } + + // If the last statement is an expression, + // we need to return that expression. + match child.statements.last().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) => { + let last = child.statements.pop().unwrap(); + let value = match last { + JsStatement { + data: JsStatementData::Expression(expr), + .. + } => expr, + _ => { + self.statements.push(last); + JsExpr::new(JsExprData::Null, loc.clone(), None) + } + }; + child.statements.push(JsStatement::new( + JsStatementData::Return(value), + loc.clone(), + None, + )); + } + _ => {} + } + + JsArrowBody::Block(JsBlock::new(child.statements, loc)) + } + + pub fn hoist_block_to_js_block_in_same_context(&mut self, b: BlockWithLoc) -> JsExpr { + let loc = b.loc.clone(); + self.push_virtual_scope(); + + debug_assert!(!b.block.is_valid_js_expression()); + + for statement in b.block.statements { + self.process_statement(statement); + } + + self.pop_virtual_scope(); + + // If the last statement is an expression, we need + // to assign to the temporary variable. + match self.last_statement().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) if b.block.has_implicit_return => { + let last = self.statements.pop().unwrap(); + match last { + JsStatement { + data: JsStatementData::Expression(expr), + .. + } => expr, + _ => { + self.statements.push(last); + JsExpr::new(JsExprData::Null, loc.clone(), None) + } + } + } + _ => JsExpr::new(JsExprData::Null, loc.clone(), None), + } + } + + pub fn hoist_js_if( + &mut self, + yes: BlockWithLoc, + no: Option, + condition: JsExpr, + did_assign: &mut bool, + temporary: &TempId, + ) -> Option { + let yes = self.hoist_block_to_js_block(yes, &temporary.get(&self), did_assign)?; + + let no = match no { + Some(BlockOrIf::Block(no)) => Some(JsElse::Block(self.hoist_block_to_js_block( + no, + &temporary.get(&self), + did_assign, + )?)), + Some(BlockOrIf::If(else_if)) => { + // If the condition of this block has a side effect, we need to + // emit a block so that we can assign to the temporary variable. + // + // Otherwise, we can just emit the condition inline + if else_if.condition.is_valid_js_expression() { + let condition = convert_valid_expr(self, *else_if.condition.clone()); + Some(JsElse::If(Box::new(self.hoist_js_if( + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )?))) + } else { + let mut temp_block = self.new_child(); + let condition = + temp_block.hoist_expression_to_temporary(*else_if.condition.clone())?; + + temp_block.add_statement(JsStatement::new( + JsStatementData::If(self.hoist_js_if( + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )?), + Loc::EMPTY, + None, + )); + + Some(JsElse::Block(JsBlock::new( + temp_block.statements, + Loc::EMPTY, + ))) + } + } + None => None, + }; + + Some(JsIf { condition, yes, no }) + } +} + +pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { + JsStatement::new( + JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), + loc, + None, + ) +} + +pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { + JsExpr::new( + JsExprData::Binary { + op: BinaryOperator::Assign, + left: Box::new(JsExpr::new( + JsExprData::Identifier(target), + loc.clone(), + None, + )), + right: Box::new(expr), + }, + loc, + None, + ) +} diff --git a/crates/js_ast/is_valid_js_expression.rs b/crates/js_ast/is_valid_js_expression.rs new file mode 100644 index 0000000000000000000000000000000000000000..382b8712e3f33dc10e713d382e8859352e74f8f6 --- /dev/null +++ b/crates/js_ast/is_valid_js_expression.rs @@ -0,0 +1,85 @@ +use ast::*; + +pub trait IsValidJSExpression { + /// Can this be lowered to a JS ast without hoisting items + /// ex: a `return` expression is not a valid JS expression + fn is_valid_js_expression(&self) -> bool; +} + +impl IsValidJSExpression for ExprData { + fn is_valid_js_expression(&self) -> bool { + match self { + // A block containing just expressions can be an expression + ExprData::Block(block) => block.is_valid_js_expression(), + ExprData::If(expr) => expr.is_valid_js_expression(), + ExprData::Return(_) => false, + ExprData::Builtin { id, .. } => match id { + BuiltinId::Raw => true, + }, + ExprData::Binary { left, right, .. } => { + left.is_valid_js_expression() && right.is_valid_js_expression() + } + ExprData::Unary { value, .. } => value.is_valid_js_expression(), + ExprData::Fn { .. } => true, + ExprData::Call { base, arguments } => { + base.is_valid_js_expression() + && arguments + .iter() + .all(|argument| argument.is_valid_js_expression()) + } + ExprData::Field { base, field } => { + base.is_valid_js_expression() && field.is_valid_js_expression() + } + ExprData::Literal(_) | ExprData::Identifier(_) => true, + } + } +} + +impl IsValidJSExpression for Block { + fn is_valid_js_expression(&self) -> bool { + self.statements + .iter() + .all(|statement| statement.is_valid_js_expression()) + } +} + +impl IsValidJSExpression for Expr { + fn is_valid_js_expression(&self) -> bool { + self.data.is_valid_js_expression() + } +} + +impl IsValidJSExpression for If { + fn is_valid_js_expression(&self) -> bool { + self.condition.is_valid_js_expression() + && self.yes.is_valid_js_expression() + && match &self.no { + Some(no) => no.is_valid_js_expression(), + None => true, + } + } +} + +impl IsValidJSExpression for BlockOrIf { + fn is_valid_js_expression(&self) -> bool { + match self { + BlockOrIf::Block(block) => block.is_valid_js_expression(), + BlockOrIf::If(if_) => if_.is_valid_js_expression(), + } + } +} + +impl IsValidJSExpression for BlockWithLoc { + fn is_valid_js_expression(&self) -> bool { + self.block.is_valid_js_expression() + } +} + +impl IsValidJSExpression for Statement { + fn is_valid_js_expression(&self) -> bool { + match self { + Statement::Expr(expr) => expr.data.is_valid_js_expression(), + _ => false, + } + } +} diff --git a/crates/js_ast/js_ast.rs b/crates/js_ast/js_ast.rs index bc68f6e919939807b35f6a473819937156393548..6b6c2520d16f139d927522898af6f4bdee367b30 100644 --- a/crates/js_ast/js_ast.rs +++ b/crates/js_ast/js_ast.rs @@ -1,4 +1,4 @@ -use ast::{BinaryOperator, BlockWithLoc, BuiltinId, Comment, Expr, UnaryOperator}; +use ast::{BinaryOperator, Comment, UnaryOperator}; use num::bigint::BigInt; use tokenizer::Loc; @@ -31,11 +31,11 @@ pub enum JsExprData { Identifier(String), Unary { - operator: UnaryOperator, + op: UnaryOperator, value: Box, }, Binary { - operator: BinaryOperator, + op: BinaryOperator, left: Box, right: Box, }, @@ -52,22 +52,23 @@ pub enum JsExprData { arguments: Vec, }, Function { - name: String, + name: Option, params: Vec, - block: Vec, + body: Box, }, - If { + ArrowFunction { + params: Vec, + body: Box, + }, + Ternary { condition: Box, - yes: Vec, + yes: Box, no: Option>, }, - ArrowFunction { - params: Vec, - block: Vec, - }, Comma { items: Vec, }, + Void(Box), } #[derive(Debug, PartialEq, Clone)] @@ -77,33 +78,56 @@ pub struct JsStatement { pub comment: Option, } +#[derive(Debug, PartialEq, Clone)] +pub struct JSFunctionDeclaration { + pub name: String, + pub params: Vec, + pub block: JsBlock, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JSDeclaration { + pub name: String, + pub init: Option, + pub is_constant: bool, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsIf { + pub condition: JsExpr, + pub yes: JsBlock, + pub no: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsElse { + Expr(JsExpr), + Block(JsBlock), + If(Box), +} + #[derive(Debug, PartialEq, Clone)] pub enum JsStatementData { Expression(JsExpr), Return(JsExpr), Export(JsExport), - Declaration { - name: String, - init: JsExpr, - is_constant: bool, - }, - Function { - name: String, - params: Vec, - block: JsBlock, - }, - If { - condition: JsExpr, - yes: JsBlock, - no: Option, - }, + Declaration(JSDeclaration), + Function(JSFunctionDeclaration), + If(JsIf), } #[derive(Debug, PartialEq, Clone)] pub enum JsExport { - Named(Vec), + Named(Vec), + Function(Box), + Declaration(Box), Default(JsExpr), - Equals(JsExpr), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JSNamedExport { + pub identifier: String, + pub export_name: String, } #[derive(Debug, PartialEq, Clone)] @@ -113,161 +137,61 @@ pub struct JsBlock { } impl JsExpr { - pub fn from_blight(expr: &Expr) -> Option { - use ast::ExprData::*; - - let loc = &expr.loc; - let comment = &expr.comment; - - match &expr.data { - Block(block) => Self::comma_expr_from_blight_block(&BlockWithLoc { - block: block.clone(), - loc: loc.clone(), - }), - Binary { op, left, right } => { - let left = Self::from_blight(&*left)?; - let right = Self::from_blight(&*right)?; - Some(JsExpr::new( - JsExprData::Binary { - operator: op.clone(), - left: Box::new(left), - right: Box::new(right), - }, - loc.clone(), - comment.clone(), - )) - } - Unary { op, value } => { - let value = Self::from_blight(&*value)?; - Some(JsExpr::new( - JsExprData::Unary { - operator: op.clone(), - value: Box::new(value), - }, - loc.clone(), - comment.clone(), - )) - } - Fn { - name, - params, - return_type, - block, - } => { - let block = Self::comma_expr_from_blight_block(&BlockWithLoc { - block: block.clone(), - loc: loc.clone(), - })?; - Some(JsExpr::new( - JsExprData::Function { - name: name.clone(), - params: vec![], - block: vec![JsStatement::new( - JsStatementData::Expression(block), - loc.clone(), - comment.clone(), - )], - }, - loc.clone(), - comment.clone(), - )) - } - Builtin { id, arguments } => match id { - BuiltinId::Raw => { - let mut args = arguments.iter(); - let arg = args.next().unwrap(); - let arg = match &arg.data { - Literal(ast::Literal::String(s)) => s, - _ => panic!("raw expects a string"), - }; - Some(JsExpr::new( - JsExprData::Raw(arg.clone()), - loc.clone(), - comment.clone(), - )) - } - }, - If(expr) => { - let condition = Self::from_blight(&expr.condition)?; - let yes = Self::comma_expr_from_blight_block(&expr.yes)?; - let no = match &expr.no { - Some(no) => Some(Self::from_blight(&no)?), - None => None, - }; - Some(JsExpr::new( - JsExprData::If { - condition: Box::new(condition), - yes: vec![yes], - no: no.map(Box::new), - }, - loc.clone(), - comment.clone(), - )) - } - - Call { base, arguments } => todo!(), - Field { base, field } => todo!(), - - Return(_) => None, - - Literal(_) => todo!(), - Identifier(_) => todo!(), - // Literal(ast::Literal::Number(n)) => { - // Some(JsExpr::new(JsExprData::Number(n), loc.clone(), comment)) - // } - // Literal(ast::Literal::Bool(b)) => Some(JsExpr::new(JsExprData::Bool(b), loc, comment)), - // Literal(ast::Literal::String(s)) => { - // Some(JsExpr::new(JsExprData::String(s), loc, comment)) - // } - // Literal(ast::Literal::Void) => Some(JsExpr::new(JsExprData::Null, loc, comment)), - // Identifier(id) => Some(JsExpr::new(JsExprData::Identifier(id), loc, comment)), - } - } - - /// Since blocks are not valid expressions in JS, this creates a - /// comma expression from a block with more than one statement. - /// - /// Not possible to create an ast => None - pub fn comma_expr_from_blight_block(b: &ast::BlockWithLoc) -> Option { - if !b.block.has_implicit_return { - return Some(JsExpr::new(JsExprData::Null, b.loc.clone(), None)); - } - // a block expression with a single statement is just that statement - if b.block.statements.len() == 1 && b.block.has_implicit_return { - match b.block.statements.first().unwrap() { - ast::Statement::Expr(expr) => Self::from_blight(expr), - _ => return None, - }; - } - let mut statements = Vec::new(); - for statement in &b.block.statements { - match statement { - ast::Statement::Expr(expr) => { - if let Some(expr) = Self::from_blight(&expr) { - statements.push(expr); - } - } - _ => return None, - } - } - // a block without implicit return is to be type void, so we append null - if !b.block.has_implicit_return { - statements.push(JsExpr::new(JsExprData::Null, b.loc.clone(), None)); - } - Some(JsExpr::new( - JsExprData::Comma { items: statements }, - b.loc.clone(), - None, - )) - } - pub fn new(data: JsExprData, loc: Loc, comment: Option) -> Self { Self { data, loc, comment } } + + pub fn is_literal(&self) -> bool { + match &self.data { + JsExprData::Number(_) => true, + JsExprData::BigInt(_) => true, + JsExprData::String(_) => true, + JsExprData::Bool(_) => true, + JsExprData::Null => true, + JsExprData::Undefined => true, + _ => false, + } + } +} + +impl JsExprData { + /// Helper for testing + pub fn str(value: &str) -> Self { + Self::String(value.to_string()) + } + + /// Helper for testing + pub fn str_id(value: &str) -> Self { + Self::Identifier(value.to_string()) + } +} + +impl JsBlock { + pub fn new(statements: Vec, loc: Loc) -> Self { + Self { statements, loc } + } } impl JsStatement { pub fn new(data: JsStatementData, loc: Loc, comment: Option) -> Self { Self { data, loc, comment } } + + pub(crate) fn is_literal(&self) -> bool { + match &self.data { + JsStatementData::Expression(expr) => expr.is_literal(), + _ => false, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsArrowBody { + Block(JsBlock), + Expr(JsExpr), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsModule { + pub statements: Vec, } diff --git a/crates/js_ast/lib.rs b/crates/js_ast/lib.rs index 0c27a7e5204b91d8e1acb6f61f4ec69f044d4cbd..c915dc7b4c9d8e4c28d637b834f070ec2164e7b7 100644 --- a/crates/js_ast/lib.rs +++ b/crates/js_ast/lib.rs @@ -1,411 +1,28 @@ -use std::collections::HashMap; - -use ast::{BlockWithLoc, Expr, ExprData, Literal, Module, Statement}; -pub use js_ast::*; -use miette::Result; -use tokenizer::Loc; - +mod convert_valid_expr; +mod has_side_effects; +mod hoist; +mod is_valid_js_expression; mod js_ast; +mod util; -struct StackContext { - statements: Vec, - /// Variables that are declared in this context. - /// The number is the number of times the variable has been declared. - /// This is used to allow for shadowing - variable_map: HashMap, - block: u32, -} - -impl StackContext { - fn new(block: u32) -> Self { - StackContext { - statements: Vec::new(), - variable_map: HashMap::new(), - block, - } - } -} - -struct TranslateContext { - context: Vec, - internal_counter: u32, - block_counter: u32, -} - -impl TranslateContext { - fn new() -> Self { - TranslateContext { - context: vec![StackContext::new(0)], - internal_counter: 0, - block_counter: 0, - } - } - - fn push_context(self: &mut Self) { - self.block_counter += 1; - self.context.push(StackContext::new(self.block_counter)); - } - - fn pop_context(self: &mut Self) -> Vec { - self.context.pop().unwrap().statements - } - - fn push_internal_variable(self: &mut Self, value: JsExpr) -> JsExpr { - let name = format!("${}", self.internal_counter); - let loc = value.loc.clone(); - self.context - .last_mut() - .unwrap() - .statements - .push(JsStatement::new( - JsStatementData::Declaration { - name: name.clone(), - init: value, - is_constant: false, - }, - loc, - None, - )); - self.internal_counter += 1; - JsExpr::new(JsExprData::Identifier(name), Loc::EMPTY, None) - } +use ast::Module; - fn push(self: &mut Self, statement: JsStatement) { - self.context.last_mut().unwrap().statements.push(statement); - } +pub use convert_valid_expr::*; +pub use hoist::*; +pub use is_valid_js_expression::*; +pub use js_ast::*; +pub use util::*; - // fn peek(self: &Self) -> &JsStatement { - // self.context.last().unwrap().statements.last().unwrap() - // } +pub fn blight_to_js(blight: Module) -> JsModule { + let mut root_context = HoistContext::new(); - fn declare_variable(self: &mut Self, name: String) -> String { - let context = self.context.last_mut().unwrap(); - if let Some(value) = context.variable_map.get_mut(name.as_str()) { - *value += 1; - format!("{}${}${}", name, value, context.block) - } else { - context.variable_map.insert(name.to_string(), 0); - format!("{}$0${}", name, context.block) - } + for statement in blight.statements { + if let None = root_context.process_statement(statement.to_statement()) { + unreachable!("top level return is disallowed by the parser") + }; } - fn get_identifier(self: &Self, name: String) -> String { - for context in self.context.iter().rev() { - if let Some(value) = context.variable_map.get(name.as_str()) { - return format!("{}${}${}", name, value, context.block); - } - } - panic!("Variable {} not found", name); + JsModule { + statements: root_context.statements, } } - -fn get_block_handle(b: &ast::BlockWithLoc, ctx: &mut TranslateContext) -> JsExpr { - // :) - if b.is_valid_js_expression() { - return JsExpr::comma_expr_from_blight_block(b).unwrap(); - } - - ctx.push_context(); - for (i, statement) in b.block.statements.iter().enumerate() { - if i == b.block.statements.len() - 1 { - break; - } else { - translate_statement(statement, ctx); - } - } - let result = match b.block.statements.last().unwrap() { - ast::Statement::Expr(expr) => get_expression_handle(expr, ctx), - stmt => { - translate_statement(stmt, ctx); - JsExpr::new(JsExprData::Null, Loc::EMPTY, None) - } - }; - let value = ctx.pop_context(); - value.iter().for_each(|x| ctx.push(x.clone())); - result -} - -// basically, expressions are really hard to translate -// in blight, everything is an expression -// in js, very few things are expressions -// here, we recursively expand out expressions into statements -// we then store the result of all the expressions in a variable -// and then return the name of that variable -fn get_expression_handle(ast: &Expr, ctx: &mut TranslateContext) -> JsExpr { - match ast.data.clone() { - ExprData::Literal(Literal::String(value)) => JsExpr::new( - JsExprData::String(value), - ast.loc.clone(), - ast.comment.clone(), - ), - ExprData::Literal(Literal::Number(value)) => JsExpr::new( - JsExprData::Number(value), - ast.loc.clone(), - ast.comment.clone(), - ), - ExprData::Literal(Literal::Void) => { - JsExpr::new(JsExprData::Null, ast.loc.clone(), ast.comment.clone()) - } - ExprData::Literal(Literal::Bool(b)) => { - JsExpr::new(JsExprData::Bool(b), ast.loc.clone(), ast.comment.clone()) - } - - ExprData::Identifier(value) => JsExpr::new( - JsExprData::Identifier(ctx.get_identifier(value.clone())), - ast.loc.clone(), - ast.comment.clone(), - ), - ExprData::Call { base, arguments } => { - let mut handles = Vec::new(); - for argument in arguments { - handles.push(get_expression_handle(&argument, ctx)); - } - let base = get_expression_handle(&base, ctx); - ctx.push_internal_variable(JsExpr::new( - JsExprData::Call { - base: Box::new(base), - arguments: handles, - }, - ast.loc.clone(), - ast.comment.clone(), - )) - } - ExprData::Builtin { id, arguments } => match id { - ast::BuiltinId::Raw => { - if arguments.len() != 1 { - panic!("@raw expects exactly one argument"); - } - match arguments.get(0).unwrap() { - Expr { - data: ExprData::Literal(Literal::String(value)), - .. - } => JsExpr::new( - JsExprData::Raw(value.clone()), - ast.loc.clone(), - ast.comment.clone(), - ), - _ => panic!("@raw expects a string literal"), - } - } - }, - ExprData::Binary { op, left, right } => { - let left_handle = get_expression_handle(&left, ctx); - let right_handle = get_expression_handle(&right, ctx); - JsExpr::new( - JsExprData::Binary { - operator: op.clone(), - left: Box::new(left_handle), - right: Box::new(right_handle), - }, - ast.loc.clone(), - ast.comment.clone(), - ) - } - - ExprData::Unary { op, value } => { - let value_handle = get_expression_handle(&value, ctx); - JsExpr::new( - JsExprData::Unary { - operator: op.clone(), - value: Box::new(value_handle), - }, - ast.loc.clone(), - ast.comment.clone(), - ) - } - - ExprData::Field { base, field } => { - let base = Box::new(get_expression_handle(&base, ctx)); - - match field.data { - ExprData::Literal(Literal::String(name)) => JsExpr::new( - JsExprData::Dot { base, name }, - ast.loc.clone(), - ast.comment.clone(), - ), - _ => JsExpr::new( - JsExprData::Index { - base, - index: Box::new(get_expression_handle(&field, ctx)), - }, - ast.loc.clone(), - ast.comment.clone(), - ), - } - } - - ExprData::Block(block) => get_block_handle( - &BlockWithLoc { - block, - loc: ast.loc.clone(), - }, - ctx, - ), - - ExprData::If(expr) => { - let handle = get_expression_handle(&expr.condition, ctx); - - let yes = get_block_handle(&expr.yes, ctx); - let no = match expr.no { - Some(block) => Some(get_expression_handle(&block, ctx)), - None => None, - }; - - // ctx.push(JsStatement::new( - // JsStatementData::If { - // condition: handle, - // yes, - // no, - // }, - // ast.loc, - // ast.comment, - // )); - - // panic!() - handle - } - ExprData::Fn { - name, - params, - return_type, - block, - } => todo!(), - ExprData::Return(_) => todo!(), - } -} - -fn translate_statement(ast: &Statement, ctx: &mut TranslateContext) { - match ast { - Statement::Decl(decl) => { - let handle = get_expression_handle(&decl.init, ctx); - let rename = ctx.declare_variable(decl.name.clone()); - ctx.push(JsStatement::new( - JsStatementData::Declaration { - name: rename, - init: handle, - is_constant: decl.is_constant.clone(), - }, - decl.loc.clone(), - None, - )); - } - Statement::Expr(expr) => { - let h = get_expression_handle(expr, ctx); - ctx.push(JsStatement::new( - JsStatementData::Expression(h), - expr.loc.clone(), - None, - )) - } - Statement::Comment(_) => {} // Ast::Function { - // name, - // arguments, - // return_type: _, - // block, - // } => { - // let rename = ctx.declare_variable(name.clone()); - // let arguments = arguments - // .iter() - // .map(|x| ctx.declare_variable(x.name.clone())) - // .collect(); - - // ctx.push_context(); - // for (i, statement) in block.value.iter().enumerate() { - // if block.implicit_return && i == block.value.len() - 1 { - // let handle = get_expression_handle(statement, ctx); - // ctx.push(JsStatement::Return(handle)); - // } else { - // translate_statement(statement, ctx); - // } - // } - // let value = ctx.pop_context(); - - // ctx.push(JsStatement::Function { - // name: rename, - // arguments, - // value, - // }); - // } - // Ast::Public(value) => { - // translate_statement(value, ctx); - // // I'm sorry, I just don't know how to get the original name properly... my bad - // match ctx.peek() { - // JsStatement::Function { name, .. } | JsStatement::Declaration { name, .. } => { - // let original_name = name.split("$").next().unwrap().to_string(); - // ctx.push(JsStatement::Export(Box::new(JsStatement::Declaration { - // name: original_name, - // value: JsExpr::Identifier(name.clone()), - // is_constant: true, - // }))); - // } - // _ => { - // panic!("Cannot make non-function public"); - // } - // } - // } - // Ast::Return(value) => { - // let handle = get_expression_handle(value, ctx); - // ctx.push(JsStatement::Return(handle)); - // } - // Ast::Block(block) => { - // ctx.push_context(); - // for (i, statement) in block.value.iter().enumerate() { - // if block.implicit_return && i == block.value.len() - 1 { - // let handle = get_expression_handle(statement, ctx); - // ctx.push(JsStatement::Return(handle)); - // } else { - // translate_statement(statement, ctx); - // } - // } - // let value = ctx.pop_context(); - // value.iter().for_each(|x| ctx.push(x.clone())); - // } - - // Ast::FunctionCall { name, arguments } => { - // let mut handles = Vec::new(); - // for argument in arguments { - // handles.push(get_expression_handle(argument, ctx)); - // } - // match *name.clone() { - // Ast::Identifier(name) => { - // ctx.push(JsStatement::Expression(JsExpr::Call { - // name: Box::new(JsExpr::Identifier(ctx.get_identifier(name.clone()))), - // arguments: handles, - // })); - // } - // Ast::BuiltIn(name) => match name.as_str() { - // "@raw" => { - // if handles.len() != 1 { - // panic!("@raw expects exactly one argument"); - // } - // match handles.get(0).unwrap() { - // JsExpr::String(value) => { - // ctx.push(JsStatement::Expression(JsExpr::Call { - // name: Box::new(JsExpr::Raw(value.clone())), - // arguments: handles, - // })); - // } - // _ => panic!("@raw expects a string argument"), - // } - // } - // _ => panic!("Unknown built-in function {}", name), - // }, - // _ => panic!("Function name must be an identifier"), - // } - // } - // Ast::Comment(_) | Ast::DocComment(_) => {} - // _ => { - // panic!("unimplemented: {:?}", ast); - // } - } -} - -pub fn translate(module: &Module) -> Result> { - let mut ctx = TranslateContext::new(); - - for ast in module.statements.iter() { - translate_statement(&ast.clone().to_statement(), &mut ctx); - } - - Ok(ctx.pop_context()) -} diff --git a/crates/js_ast/util.rs b/crates/js_ast/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..234ee189e91921b0e25bcc7a1e717ca485459d4c --- /dev/null +++ b/crates/js_ast/util.rs @@ -0,0 +1,17 @@ +/// TODO(@paperdave): handle the rest of the identifiers +pub fn is_js_identifier(s: &str) -> bool { + if s.is_empty() { + return false; + } + let mut chars = s.chars(); + let first = chars.next().unwrap(); + if !first.is_ascii_alphabetic() && first != '_' { + return false; + } + for c in chars { + if !c.is_ascii_alphanumeric() && c != '_' { + return false; + } + } + true +} diff --git a/crates/js_printer/Cargo.toml b/crates/js_printer/Cargo.toml index ec58b9490b3338b6f12f18906971afbdadb9f4ca..785143495163bac04e1b4dee49fe808c70aa7d15 100644 --- a/crates/js_printer/Cargo.toml +++ b/crates/js_printer/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [lib] +path = "./lib.rs" [dependencies] miette.workspace = true diff --git a/crates/js_printer/lib.rs b/crates/js_printer/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..4ea0d8c03ae395ff34e563a248dbeceaefce9b55 --- /dev/null +++ b/crates/js_printer/lib.rs @@ -0,0 +1,358 @@ +mod printer_tests; + +use js_ast::*; +use std::io::{Result, Write}; + +pub trait Print { + fn print(&self, writer: &mut dyn Write) -> Result<()>; + + fn print_to_string(&self) -> String { + let mut buffer = Vec::new(); + self.print(&mut buffer).unwrap(); + String::from_utf8(buffer).unwrap() + } +} + +// the printer: + +fn print_module_clause_identifier(writer: &mut dyn Write, export_name: &str) -> Result<()> { + // TODO(@paperdave): Escape invalid identifiers + if js_ast::is_js_identifier(export_name) { + write!(writer, "{}", export_name) + } else { + todo!("call JSON.stringify on identifier") + } +} + +fn print_js_string(writer: &mut dyn Write, string: &str) -> Result<()> { + write!(writer, "\"")?; + // TODO(@paperdave): Instead of writing each character, this should write + // chunks between the escapes instead of calling write! on each character individually. + for c in string.chars() { + match c { + '\n' => write!(writer, "\\n")?, + '\r' => write!(writer, "\\r")?, + '\t' => write!(writer, "\\t")?, + '"' => write!(writer, "\\\"")?, + '\\' => write!(writer, "\\\\")?, + '\u{0008}' => write!(writer, "\\b")?, + '\u{000C}' => write!(writer, "\\f")?, + '\u{0000}'..='\u{001F}' => write!(writer, "\\u{:04x}", c as u32)?, + // TODO(@paperdave): I think there are some larger code points that need to be escaped? + _ => write!(writer, "{}", c)?, + } + } + write!(writer, "\"") +} + +impl Print for JsStatement { + fn print(&self, w: &mut dyn Write) -> Result<()> { + match &self.data { + js_ast::JsStatementData::Expression(expr) => { + expr.print(w)?; + write!(w, ";") + } + js_ast::JsStatementData::Return(expr) => { + write!(w, "return ")?; + expr.print(w) + } + js_ast::JsStatementData::Export(export) => export.print(w), + js_ast::JsStatementData::Declaration(decl) => decl.print(w), + js_ast::JsStatementData::Function(function) => function.print(w), + js_ast::JsStatementData::If(js_if) => js_if.print(w), + } + } +} + +impl Print for JsExport { + fn print(&self, w: &mut dyn Write) -> Result<()> { + match self { + JsExport::Named(exports) => { + write!(w, "export{{")?; + for (i, export) in exports.iter().enumerate() { + if i > 0 { + write!(w, ",")?; + } + export.print(w)?; + } + Ok(()) + } + JsExport::Function(function) => { + write!(w, "export ")?; + function.print(w) + } + JsExport::Declaration(declaration) => { + write!(w, "export ")?; + declaration.print(w) + } + JsExport::Default(expr) => { + write!(w, "export default ")?; + expr.print(w) + } + } + } +} + +impl Print for JSNamedExport { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + if self.export_name == self.identifier { + write!(writer, "{}", self.identifier) + } else { + write!(writer, "{} as ", self.identifier)?; + print_module_clause_identifier(writer, &self.export_name) + } + } +} + +impl Print for JSDeclaration { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!( + writer, + "{} {}", + if self.is_constant { "const" } else { "let" }, + self.name, + )?; + if let Some(init) = &self.init { + write!(writer, " = ")?; + init.print(writer)?; + } + write!(writer, ";")?; + Ok(()) + } +} + +impl Print for JsExpr { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + self.data.print(writer) + } +} + +impl Print for JsExprData { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match &self { + JsExprData::Raw(data) => write!(writer, "{}", data), + JsExprData::Number(number) => write!(writer, "{}", number), + JsExprData::BigInt(bigint) => write!(writer, "{}", bigint), + JsExprData::String(string) => print_js_string(writer, string.as_str()), + JsExprData::Bool(bool) => write!(writer, "{}", bool), + JsExprData::Null => write!(writer, "null"), + JsExprData::Undefined => write!(writer, "undefined"), + JsExprData::Identifier(str) => write!(writer, "{}", str), + JsExprData::Array(items) => { + write!(writer, "[")?; + for (i, item) in items.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + item.print(writer)?; + } + write!(writer, "]") + } + JsExprData::Object(items) => { + write!(writer, "{{")?; + for (i, kv) in items.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + + kv.print(writer)?; + } + write!(writer, "}}") + } + JsExprData::Unary { op, value } => { + write!(writer, "{}", op.to_str())?; + value.print(writer) + } + JsExprData::Binary { op, left, right } => { + // TODO: figure out when we dont need to wrap + let wrap = true; + if wrap { + write!(writer, "(")?; + } + left.print(writer)?; + write!(writer, "{}", op.to_str())?; + right.print(writer)?; + if wrap { + write!(writer, ")")?; + } + Ok(()) + } + JsExprData::Dot { base, name } => { + base.print(writer)?; + write!(writer, ".{}", name) + } + JsExprData::Index { base, index } => { + base.print(writer)?; + write!(writer, "[")?; + index.print(writer)?; + write!(writer, "]") + } + JsExprData::Call { base, arguments } => { + base.print(writer)?; + write!(writer, "(")?; + for (i, argument) in arguments.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + argument.print(writer)?; + } + write!(writer, ")") + } + JsExprData::Function { body, name, params } => { + write!(writer, "function ")?; + if let Some(name) = name { + write!(writer, "{}", name)?; + } + write!(writer, "(")?; + for (i, param) in params.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + write!(writer, "{}", param)?; + } + write!(writer, ")")?; + body.print(writer) + } + JsExprData::ArrowFunction { body, params } => { + let print_parens = params.len() != 1; + if print_parens { + write!(writer, "(")?; + } + for (i, param) in params.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + write!(writer, "{}", param)?; + } + if print_parens { + write!(writer, ")")?; + } + write!(writer, "=>")?; + body.print(writer) + } + JsExprData::Ternary { condition, yes, no } => { + condition.print(writer)?; + write!(writer, "?")?; + yes.print(writer)?; + write!(writer, ":")?; + if let Some(no) = no { + no.print(writer)?; + } else { + write!(writer, "null")?; + } + Ok(()) + } + JsExprData::Comma { items } => { + for (i, item) in items.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + item.print(writer)?; + } + Ok(()) + } + JsExprData::Void(expr) => { + write!(writer, "void ")?; + expr.print(writer) + } + } + } +} + +impl Print for JSFunctionDeclaration { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!(writer, "function {}", self.name)?; + write!(writer, "(")?; + for (i, param) in self.params.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + write!(writer, "{}", param)?; + } + write!(writer, ")")?; + self.block.print(writer) + } +} + +impl Print for JsBlock { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!(writer, "{{")?; + for statement in self.statements.iter() { + statement.print(writer)?; + } + write!(writer, "}}") + } +} + +impl Print for JsIf { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!(writer, "if(")?; + self.condition.print(writer)?; + write!(writer, ")")?; + self.yes.print(writer)?; + if let Some(no) = &self.no { + no.print(writer)?; + } + Ok(()) + } +} + +impl Print for JsElse { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match self { + JsElse::If(js_if) => { + write!(writer, "else ")?; + js_if.print(writer) + } + JsElse::Block(block) => { + write!(writer, "else")?; + block.print(writer) + } + JsElse::Expr(expr) => { + write!(writer, "else ")?; + expr.print(writer) + } + } + } +} + +impl Print for JsKV { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match &self.key.data { + JsExprData::String(string) => { + if is_js_identifier(string) { + write!(writer, "{}", string)?; + } else { + print_js_string(writer, string.as_str())?; + } + } + _ => { + write!(writer, "[")?; + self.key.print(writer)?; + write!(writer, "]")?; + } + } + write!(writer, ":")?; + self.key.print(writer)?; + write!(writer, ":")?; + self.value.print(writer) + } +} + +impl Print for JsModule { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + for statement in self.statements.iter() { + statement.print(writer)?; + } + Ok(()) + } +} + +impl Print for JsArrowBody { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match self { + JsArrowBody::Block(block) => block.print(writer), + JsArrowBody::Expr(expr) => expr.print(writer), + } + } +} diff --git a/crates/js_printer/printer_tests.rs b/crates/js_printer/printer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..d69ab247dffe40609ed2892eca79fedb3c4f1a3a --- /dev/null +++ b/crates/js_printer/printer_tests.rs @@ -0,0 +1,53 @@ +#![cfg(test)] +use crate::Print; + +use js_ast::JsExprData as E; +use num::{bigint::Sign, BigInt}; + +macro_rules! assert_printed { + ($value:expr, $expected:expr) => { + assert_eq!($value.print_to_string(), $expected); + }; +} + +#[test] +fn number() { + assert_printed!(E::Number(1.0), "1"); + assert_printed!(E::Number(1.5), "1.5"); + assert_printed!(E::Number(1.5e10), "15000000000"); + assert_printed!(E::Number(1.5e-10), "0.00000000015"); + assert_printed!(E::Number(-1.0), "-1"); + assert_printed!(E::Number(-1.5), "-1.5"); + assert_printed!(E::Number(0f64), "0"); +} + +#[test] +fn bigint() { + assert_printed!(E::BigInt(BigInt::new(Sign::Plus, vec![1])), "1n"); + assert_printed!( + E::BigInt(BigInt::new(Sign::Plus, vec![10000000])), + "10000000n" + ); + assert_printed!( + E::BigInt(BigInt::new(Sign::Minus, vec![10000000])), + "-10000000n" + ); +} + +#[test] +fn boolean() { + assert_printed!(E::Bool(true), "true"); + assert_printed!(E::Bool(false), "false"); +} + +#[test] +fn string() { + assert_printed!(E::str(""), "\"\""); + assert_printed!(E::str("hello"), "\"hello\""); + assert_printed!(E::str("hello\nworld"), "\"hello\\nworld\""); +} + +#[test] +fn identifier() { + assert_printed!(E::str_id("hello"), "hello"); +} diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs index 26122cadfe2a55e259a05322fb103b3a5f64e9c9..a27468c1c49ea1ee5ce13fd99450b645fe3fdd16 100644 --- a/crates/js_printer/src/print_expression.rs +++ b/crates/js_printer/src/print_expression.rs @@ -46,7 +46,7 @@ pub fn print_expression(expr: &JsExpr) -> String { .join(",") ), JsExprData::Binary { - operator, + op: operator, left, right, } => format!( @@ -81,7 +81,10 @@ pub fn print_expression(expr: &JsExpr) -> String { ), JsExprData::Null => todo!(), JsExprData::Undefined => todo!(), - JsExprData::Unary { operator, value } => todo!(), + JsExprData::Unary { + op: operator, + value, + } => todo!(), JsExprData::Dot { base, name } => todo!(), JsExprData::Index { base, index } => todo!(), JsExprData::Function { @@ -89,9 +92,10 @@ pub fn print_expression(expr: &JsExpr) -> String { params, block, } => todo!(), - JsExprData::If { condition, yes, no } => todo!(), + JsExprData::Ternary { condition, yes, no } => todo!(), JsExprData::ArrowFunction { params, block } => todo!(), JsExprData::Comma { items } => todo!(), + JsExprData::Void(_) => todo!(), } } @@ -191,7 +195,7 @@ mod tests { fn operation() { assert_eq!( print_expression(&JsExprData::Binary { - operator: AnyOperator::Add, + op: AnyOperator::Add, left: Box::new(JsExprData::Number(1.0)), right: Box::new(JsExprData::Number(2.0)), }), @@ -199,7 +203,7 @@ mod tests { ); assert_eq!( print_expression(&JsExprData::Binary { - operator: AnyOperator::Sub, + op: AnyOperator::Sub, left: Box::new(JsExprData::Number(1.0)), right: Box::new(JsExprData::Number(2.0)), }), diff --git a/crates/js_printer/src/print_statement.rs b/crates/js_printer/src/print_statement.rs index 57fdb827b637716f5c05b2065c39e621eff95d62..c7d78693136d06b23070c35e9b55ab1dfa70e90f 100644 --- a/crates/js_printer/src/print_statement.rs +++ b/crates/js_printer/src/print_statement.rs @@ -1,4 +1,4 @@ -use js_ast::{JsStatement, JsStatementData}; +use js_ast::{If, JSDeclaration, JSFunctionDeclaration, JsStatement, JsStatementData}; use crate::print_expression::print_expression; @@ -8,11 +8,11 @@ pub fn print_statement(statement: &JsStatement) -> String { // JsStatementData::Export(value) => format!("export {}", print_statement(value)), JsStatementData::Export(value) => todo!(), JsStatementData::Return(value) => format!("return {};", print_expression(&value)), - JsStatementData::Declaration { + JsStatementData::Declaration(Declaration { name, init, is_constant, - } => { + }) => { format!( "{} {}={};", match is_constant { @@ -23,11 +23,11 @@ pub fn print_statement(statement: &JsStatement) -> String { print_expression(&init) ) } - JsStatementData::Function { + JsStatementData::Function(JSFunctionDeclaration { name, block, params, - } => format!( + }) => format!( "function {}({}){{{}}}", name, params.join(","), @@ -38,7 +38,7 @@ pub fn print_statement(statement: &JsStatement) -> String { .collect::>() .join("") ), - JsStatementData::If { condition, yes, no } => { + JsStatementData::If(JsIf { condition, yes, no }) => { let else_value = match no { Some(value) => format!( "else{{{}}}", diff --git a/crates/parser/parse_block.rs b/crates/parser/parse_block.rs index 4c6dc77d314740afd0a31a442a85885d18403e89..3ab34ac6cc6474d351a0a944fabaa17ad2bc2775 100644 --- a/crates/parser/parse_block.rs +++ b/crates/parser/parse_block.rs @@ -15,7 +15,11 @@ pub fn parse_block(p: &mut Parser) -> Result { let was_top_level = p.is_top_level; p.is_top_level = false; - loop { + 'outer: loop { + let is_statement_like_expression = p + .peek() + .map_or(false, |x| x.is_start_of_statement_like_expression()); + let statement = match parse_statement(p) { Ok(statement) => statement, Err(err) => { @@ -25,8 +29,9 @@ pub fn parse_block(p: &mut Parser) -> Result { }; statements.push(statement.clone()); - match p.next() { + match p.peek() { Some(Token::Semicolon) => loop { + p.next(); match p.peek() { Some(Token::ClosedBrace) => { p.next(); @@ -36,14 +41,18 @@ pub fn parse_block(p: &mut Parser) -> Result { has_implicit_return: false, }); } - _ => continue, + _ => continue 'outer, } }, Some(Token::ClosedBrace) => { - if statement.requires_semicolon() { + p.next(); + if !statement.is_expression() + && !statement.is_comment() + && is_statement_like_expression + { p.is_top_level = was_top_level; return Err(p - .error("Expected semicolon after this statement".to_string()) + .error("Expected semicolon or '}' after statement".to_string()) .into()); } @@ -54,10 +63,15 @@ pub fn parse_block(p: &mut Parser) -> Result { }); } _ => { - p.is_top_level = was_top_level; - return Err(p - .error("Expected semicolon or '}' after statement".to_string()) - .into()); + if !statement.is_expression() + && !statement.is_comment() + && is_statement_like_expression + { + p.is_top_level = was_top_level; + return Err(p + .error("Expected semicolon or '}' after statement".to_string()) + .into()); + } } }; } diff --git a/crates/parser/parse_call_arguments.rs b/crates/parser/parse_call_arguments.rs index afa16cdcfef28ab3c3dd309f6d2036b5403120bb..4bdda948b2aef6eca565194538d7c4aeac397753 100644 --- a/crates/parser/parse_call_arguments.rs +++ b/crates/parser/parse_call_arguments.rs @@ -8,6 +8,11 @@ use crate::{parse_expr::parse_expr, Parser}; pub fn parse_call_arguments(p: &mut Parser) -> Result> { let mut args = Vec::new(); + if let Some(Token::ClosedParen) = p.peek() { + _ = p.next(); + return Ok(args); + } + loop { let arg = parse_expr(p)?; args.push(arg); diff --git a/crates/parser/parse_expr.rs b/crates/parser/parse_expr.rs index 6ca753167cdabc91ac85e5845d4c1fb356ef2748..693053995ac365681335bfdabb3ebdd0f0a96b3b 100644 --- a/crates/parser/parse_expr.rs +++ b/crates/parser/parse_expr.rs @@ -1,4 +1,5 @@ -use ast::{get_binary_op_level, BinaryOperator, BinaryOperatorLevel, Expr, ExprData}; +use ast::Associativity::*; +use ast::{BinaryOperator, Expr, ExprData, OperatorLevel}; use miette::Result; use crate::parse_expr_basic::parse_expr_basic; @@ -15,14 +16,27 @@ pub fn parse_expr(p: &mut Parser) -> Result { // Parse operators loop { if let Some(token) = p.peek() { - if let Some(level) = get_binary_op_level(&token) { + if let Some(level) = BinaryOperator::from_token(&token).map(|o| o.level()) { _ = p.next(); - expr = parse_expr_binary( - p, - BinaryOperator::from_token(&token).unwrap(), - expr.clone(), - level, - )?; + match level.associativity() { + RightToLeft => { + expr = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + expr, + level, + )?; + } + LeftToRight => { + // TODO(@paperdave): TODO + expr = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + expr, + level, + )?; + } + } continue; } } @@ -37,22 +51,35 @@ fn parse_expr_binary( p: &mut Parser, op: BinaryOperator, left: Expr, - level: BinaryOperatorLevel, + level: OperatorLevel, ) -> Result { let loc = p.loc(); let mut right = parse_expr_basic(p)?; loop { if let Some(token) = p.peek() { - if let Some(next_level) = get_binary_op_level(&token) { - if next_level <= level { + if let Some(new_level) = BinaryOperator::from_token(&token).map(|o| o.level()) { + if new_level <= level { _ = p.next(); - right = parse_expr_binary( - p, - BinaryOperator::from_token(&token).unwrap(), - right, - next_level, - )?; + match new_level.associativity() { + RightToLeft => { + right = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + right, + new_level, + )?; + } + LeftToRight => { + // TODO(@paperdave): TODO + right = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + right, + new_level, + )?; + } + } continue; } } diff --git a/crates/parser/parse_expr_if.rs b/crates/parser/parse_expr_if.rs index 0efc61ee5e3b52a9d221fca3f692d782073b6c5e..5af7b44fd7fbf2383a3c3d4ed31d243168859558 100644 --- a/crates/parser/parse_expr_if.rs +++ b/crates/parser/parse_expr_if.rs @@ -1,4 +1,4 @@ -use ast::{BlockWithLoc, Expr, ExprData, If}; +use ast::{BlockOrIf, BlockWithLoc, If}; use miette::Result; use tokenizer::Token; @@ -48,21 +48,17 @@ pub fn parse_expr_if(p: &mut Parser) -> Result { match p.peek() { Some(Token::OpenBrace) => { let loc = p.loc(); - let comments = p.take_comments(); - Some(Box::new(Expr::new( - ExprData::Block(parse_block(p)?), + // let comments = p.take_comments(); + Some(BlockOrIf::Block(BlockWithLoc { + block: parse_block(p)?, loc, - comments, - ))) + })) } Some(Token::If) => { - let loc = p.loc(); - let comments = p.take_comments(); - Some(Box::new(Expr::new( - ExprData::If(parse_expr_if(p)?), - loc, - comments, - ))) + // TODO(@paperdave): Save location for the if + // let loc = p.loc(); + // let comments = p.take_comments(); + Some(BlockOrIf::If(Box::new(parse_expr_if(p)?))) } _ => return Err(p.error("Expected a block after else".to_string()).into()), } diff --git a/crates/parser/parse_fn_declaration.rs b/crates/parser/parse_fn_declaration.rs index 3b7363a36204bcd95ac287e5d7fb854cc2f06f78..32b4e3cff41688b8ee04d9c6e2cba54d01c64291 100644 --- a/crates/parser/parse_fn_declaration.rs +++ b/crates/parser/parse_fn_declaration.rs @@ -52,7 +52,10 @@ pub fn parse_fn_declaration( break; } } - Some(Token::ClosedParen) => break, + Some(Token::ClosedParen) => { + p.next(); + break; + } _ => { return Err(p .error("Expected ',' or ')' after identifier".to_string()) @@ -108,8 +111,12 @@ pub fn parse_fn_declaration( loc: loc, }) } else { + let actually = p.peek().map_or("EOF".to_string(), |x| x.to_string()); return Err(p - .error("Expected a block to follow function declaration".to_string()) + .error(format!( + "Expected a block to follow function declaration, but got {}", + actually, + )) .into()); } } diff --git a/crates/parser/parse_module.rs b/crates/parser/parse_module.rs index 9d3f8fbe8e2e9f2ce1138a72a8b1ece27dda0011..97aef8e42d12836cc0d7e6c5ecde30167e67c57f 100644 --- a/crates/parser/parse_module.rs +++ b/crates/parser/parse_module.rs @@ -11,6 +11,10 @@ pub fn parse_module(p: &mut Parser) -> Result { let file_doc_comment = maybe_read_doc_comment(p); loop { + let is_statement_like_expression = p + .peek() + .map_or(false, |x| x.is_start_of_statement_like_expression()); + let statement = parse_top_level_statement(p)?; match p.peek() { @@ -20,7 +24,7 @@ pub fn parse_module(p: &mut Parser) -> Result { } } _ => { - if statement.requires_semicolon() { + if !statement.is_comment() && is_statement_like_expression { return Err(p .error("Expected semicolon after statement".to_string()) .into()); diff --git a/crates/sema/Cargo.toml b/crates/sema/Cargo.toml index 151d91227fdbfa2b5b2120a5e072e56796233326..d9a6f7ea10286ec4418afd61c1538671280ce8b4 100644 --- a/crates/sema/Cargo.toml +++ b/crates/sema/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [lib] +path = "./sema.rs" [dependencies] miette.workspace = true diff --git a/crates/sema/src/ast_type_coersion.rs b/crates/sema/ast_type_coersion.rs similarity index 100% rename from crates/sema/src/ast_type_coersion.rs rename to crates/sema/ast_type_coersion.rs diff --git a/crates/sema/src/lib.rs b/crates/sema/sema.rs similarity index 95% rename from crates/sema/src/lib.rs rename to crates/sema/sema.rs index c7a930d635ec3bd0f58f49a10c38bc95b5d81d9e..899c5d9433436f4bf7f4f85d55a96d821cc88572 100644 --- a/crates/sema/src/lib.rs +++ b/crates/sema/sema.rs @@ -1,9 +1,11 @@ -// use ast::{ Module, Type}; -// use miette::Result; +use ast::Module; +use miette::Result; -// mod ast_type_coersion; +pub fn analyze_module(_block: &mut Module) -> Result<()> { + // + Ok(()) +} -// pub fn check_module(block: &Module) -> Result<()> { // for statement in &block.statements { // check_expression(statement)?; // } diff --git a/crates/tokenizer/Cargo.toml b/crates/tokenizer/Cargo.toml index 50effc8036b4f1705b14ec46fd16ad36622229e7..6e881084601ce5e033c79ce16949e3dc732880a2 100644 --- a/crates/tokenizer/Cargo.toml +++ b/crates/tokenizer/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [lib] +path="./tokenizer.rs" [dependencies] miette.workspace = true diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/tokenizer.rs similarity index 98% rename from crates/tokenizer/src/lib.rs rename to crates/tokenizer/tokenizer.rs index 2d906f20a2b75c81a5168c92c3d591314c8d2c18..b03a99236d2ecf981c8b866f060c73eaa9cdf97f 100644 --- a/crates/tokenizer/src/lib.rs +++ b/crates/tokenizer/tokenizer.rs @@ -76,6 +76,15 @@ pub enum Token { Dot, // . } +impl Token { + pub fn is_start_of_statement_like_expression(&self) -> bool { + match &self { + Token::If | Token::OpenBrace | Token::Fn => false, + _ => true, + } + } +} + impl std::fmt::Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let token = match self { -- 2.54.0