diff --git a/.gitignore b/.gitignore index eb5a316cbd195d26e3f768c7dd8e1b47299e17f8..4143657114f43375f7bb260327bd203355dcd140 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target +*.js diff --git a/Cargo.lock b/Cargo.lock index 426f40ae047164d1a7ae0edd8328a6bcc3bc0da0..e11d7608f4d0b1da5e46afd069efbe460f5c5b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,9 +70,9 @@ name = "ast" version = "0.1.0" dependencies = [ "miette", + "num", "thiserror", "tokenizer", - "util", ] [[package]] @@ -175,6 +175,8 @@ dependencies = [ "js_ast", "js_printer", "miette", + "parser", + "sema", "thiserror", "tokenizer", ] @@ -237,8 +239,9 @@ dependencies = [ "ast", "miette", "num", + "parser", "thiserror", - "util", + "tokenizer", ] [[package]] @@ -249,7 +252,6 @@ dependencies = [ "miette", "num", "thiserror", - "util", ] [[package]] @@ -408,6 +410,17 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "parser" +version = "0.1.0" +dependencies = [ + "ast", + "miette", + "num", + "thiserror", + "tokenizer", +] + [[package]] name = "proc-macro2" version = "1.0.76" @@ -445,6 +458,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "sema" +version = "0.1.0" +dependencies = [ + "ast", + "miette", + "num", + "thiserror", +] + [[package]] name = "smawk" version = "0.3.2" @@ -543,7 +566,6 @@ version = "0.1.0" dependencies = [ "miette", "thiserror", - "util", ] [[package]] @@ -570,10 +592,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" -[[package]] -name = "util" -version = "0.1.0" - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index e0aaabdca72cfe022aef071763d33f4ee6b7997c..a1fb3c69c966fc3a4eeefa9afb84d0c02ba31b08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,21 @@ [workspace.package] version = "0.1.0" -name = "blight" edition = "2023" license = "MIT" -authors = ["Lino Le Van"] +authors = ["Lino Le Van", "Dave Caruso"] repository = "https://github.com/lino-levan/blight" [workspace] resolver = "2" -members = ["crates/cli", "crates/util", "crates/tokenizer", "crates/ast", "crates/js_ast", "crates/js_printer"] +members = [ + "crates/ast", + "crates/sema", + "crates/cli", + "crates/js_ast", + "crates/js_printer", + "crates/parser", + "crates/tokenizer", +] [workspace.dependencies] clap = { version = "4.4.12", features = ["derive"] } diff --git a/crates/ast/Cargo.toml b/crates/ast/Cargo.toml index 2f57e0d3305382b701e02b9ba80ee44d14c7d822..fc7837e1d94befad96581f8d202330bb79e732d8 100644 --- a/crates/ast/Cargo.toml +++ b/crates/ast/Cargo.toml @@ -4,10 +4,11 @@ version = "0.1.0" edition = "2021" [lib] +path = "./lib.rs" [dependencies] miette.workspace = true thiserror.workspace = true +num.workspace = true tokenizer = { path = "../tokenizer" } -util = { path = "../util" } diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs new file mode 100644 index 0000000000000000000000000000000000000000..680c579aa79c8a3a0dd1cc19cc010ea8cd46cf7e --- /dev/null +++ b/crates/ast/ast.rs @@ -0,0 +1,247 @@ +use tokenizer::Loc; + +use crate::{ + types::{Literal, Type}, + BinaryOperator, UnaryOperator, +}; + +#[derive(Debug, Clone)] +pub enum BuiltinId { + Raw, +} + +#[derive(Debug, Clone)] +pub struct Module { + pub comment: Option, + pub statements: Vec, +} + +#[derive(Debug, Clone)] +pub struct Block { + pub statements: Vec, + pub has_implicit_return: bool, +} + +#[derive(Debug, Clone)] +pub struct Param { + pub name: String, + pub param_type: Type, +} + +#[derive(Debug, Clone)] + +pub struct VarDecl { + pub name: String, + pub data_type: Type, + pub init: Expr, + pub is_constant: bool, + pub is_pub: bool, + pub loc: Loc, + pub comment: Option, +} + +#[derive(Debug, Clone)] +pub struct FnDecl { + pub name: String, + pub return_type: Type, + pub params: Vec, + pub body: Expr, + pub is_pub: bool, + pub loc: Loc, + pub comment: Option, +} + +#[derive(Debug, Clone)] +pub struct Expr { + pub data: ExprData, + pub loc: Loc, + pub comment: Option, +} + +impl Expr { + pub fn new(data: ExprData, loc: Loc, comment: Option) -> Self { + Self { data, loc, comment } + } +} + +#[derive(Debug, Clone)] +pub struct DocComment { + pub data: String, + pub loc: Loc, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Comment { + pub data: String, +} + +impl Comment { + pub fn format(&self) -> String { + let mut lines = self.data.lines(); + let first_line = lines.next().unwrap(); + let mut result = format!("// {}", first_line); + for line in lines { + result.push_str(&format!("\n// {}", line)); + } + 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 { + pub fn format(&self) -> String { + let mut lines = self.data.lines(); + let first_line = lines.next().unwrap(); + let mut result = format!("/// {}", first_line); + for line in lines { + result.push_str(&format!("\n/// {}", line)); + } + result + } +} + +#[derive(Debug, Clone)] +pub struct BlockWithLoc { + pub block: Block, + pub loc: Loc, +} + +#[derive(Debug, Clone)] +pub struct If { + pub condition: Box, + pub yes: BlockWithLoc, + pub no: Option, +} + +#[derive(Debug, Clone)] +pub enum BlockOrIf { + Block(BlockWithLoc), + If(Box), +} + +#[derive(Debug, Clone)] +pub enum ExprData { + Block(Block), + Binary { + op: BinaryOperator, + left: Box, + right: Box, + }, + Unary { + op: UnaryOperator, + value: Box, + }, + Fn { + name: String, + params: Vec, + return_type: Type, + body: Box, + }, + Call { + base: Box, + arguments: Vec, + }, + Field { + base: Box, + field: Box, + }, + Builtin { + id: BuiltinId, + arguments: Vec, + }, + If(If), + Return(Box), + Literal(Literal), + Identifier(String), +} + +#[derive(Debug, Clone)] +pub enum Statement { + Expr(Expr), + VarDecl(VarDecl), + FnDecl(FnDecl), + Comment(Comment), +} + +#[derive(Debug, Clone)] +pub enum TopLevelStatement { + Expr(Expr), + VarDecl(VarDecl), + FnDecl(FnDecl), + Comment(Comment), +} + +impl BuiltinId { + pub fn display_name(&self) -> &'static str { + match self { + Self::Raw => "@raw", + } + } + + pub fn from_str(name: &str) -> Option { + match name { + "@raw" => Some(Self::Raw), + _ => None, + } + } +} + +impl Statement { + pub fn to_top_level(self) -> TopLevelStatement { + match self { + Statement::Expr(expr) => TopLevelStatement::Expr(expr), + Statement::VarDecl(decl) => TopLevelStatement::VarDecl(decl), + Statement::FnDecl(decl) => TopLevelStatement::FnDecl(decl), + Statement::Comment(comment) => TopLevelStatement::Comment(comment), + } + } + + /// This is stupid function im sorry + pub fn require_semicolon(&self, semicolon_hint: bool) -> bool { + match &self { + Statement::Expr(_) => !semicolon_hint, + Statement::VarDecl(_) => true, + _ => false, + } + } + + pub fn is_expression(&self) -> bool { + match &self { + Statement::Expr(_) => true, + _ => false, + } + } +} + +impl TopLevelStatement { + /// This is stupid function im sorry + pub fn require_semicolon(&self, earlier_hint: bool) -> bool { + match &self { + TopLevelStatement::Expr(_) => !earlier_hint, + TopLevelStatement::VarDecl(_) => true, + TopLevelStatement::FnDecl(_) => false, + _ => false, + } + } + + pub fn to_statement(self) -> Option { + match self { + TopLevelStatement::Expr(expr) => Some(Statement::Expr(expr)), + TopLevelStatement::VarDecl(decl) => Some(Statement::VarDecl(decl)), + TopLevelStatement::FnDecl(decl) => Some(Statement::FnDecl(decl)), + TopLevelStatement::Comment(comment) => Some(Statement::Comment(comment)), + } + } +} diff --git a/crates/ast/lib.rs b/crates/ast/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..c35262a7f11a1125135720a5efee113eb1ec8c29 --- /dev/null +++ b/crates/ast/lib.rs @@ -0,0 +1,7 @@ +mod ast; +mod operators; +mod types; + +pub use ast::*; +pub use operators::*; +pub use types::*; diff --git a/crates/ast/operators.rs b/crates/ast/operators.rs new file mode 100644 index 0000000000000000000000000000000000000000..622be4fcc24c93e55588e9e341bccc1176e1cc54 --- /dev/null +++ b/crates/ast/operators.rs @@ -0,0 +1,315 @@ +use tokenizer::Token; + +use crate::Literal; + +#[derive(Debug, Clone, PartialEq)] +pub enum BinaryOperator { + Add, + Sub, + Mul, + Div, + Pow, + Eq, + Gt, + Lt, + Gte, + Lte, + Lshift, + Rshift, + RshiftUnsigned, + BitAnd, + BitOr, + BitXor, + Mod, + NotEq, + And, + Or, + + Assign, + AssignAdd, + AssignSub, + AssignMul, + AssignDiv, + AssignPow, + AssignLshift, + AssignRshift, + AssignRshiftUnsigned, + AssignBitAnd, + AssignBitOr, + AssignBitXor, + AssignMod, + AssignAnd, + AssignOr, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum UnaryOperator { + Neg, + Not, + BitNot, +} + +impl BinaryOperator { + pub fn to_str(&self) -> &'static str { + match self { + Self::Add => "+", + Self::Sub => "-", + Self::Div => "/", + Self::Mul => "*", + Self::Pow => "**", + Self::Eq => "==", + Self::Gt => ">", + Self::Lt => "<", + Self::Gte => ">=", + Self::Lte => "<=", + Self::Lshift => "<<", + Self::Rshift => ">>", + Self::RshiftUnsigned => ">>>", + Self::BitAnd => "&", + Self::BitOr => "|", + Self::BitXor => "^", + Self::Mod => "%", + 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 { + match token { + Token::And => Some(Self::BitAnd), + Token::AndAnd => Some(Self::And), + Token::BangEqual => Some(Self::NotEq), + Token::Caret => Some(Self::BitXor), + Token::EqualEqual => Some(Self::Eq), + Token::Gt => Some(Self::Gt), + Token::GtEqual => Some(Self::Gte), + Token::GtGt => Some(Self::Rshift), + Token::GtGtGt => Some(Self::RshiftUnsigned), + Token::Lt => Some(Self::Lt), + Token::LtEqual => Some(Self::Lte), + Token::LtLt => Some(Self::Lshift), + Token::Minus => Some(Self::Sub), + Token::Or => Some(Self::BitOr), + Token::OrOr => Some(Self::Or), + Token::Percent => Some(Self::Mod), + Token::Plus => Some(Self::Add), + Token::Slash => Some(Self::Div), + Token::Star => Some(Self::Mul), + Token::StarStar => Some(Self::Pow), + _ => None, + } + } + + 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 { + BinaryOperator::Eq => Ok(Literal::Bool(left == right)), + BinaryOperator::NotEq => Ok(Literal::Bool(left != right)), + + BinaryOperator::Or | BinaryOperator::And => match (left, right) { + (Literal::Bool(a), Literal::Bool(b)) => Ok(Literal::Bool(match self { + BinaryOperator::Or => a || b, + BinaryOperator::And => a && b, + _ => unreachable!(), + })), + _ => Err(()), + }, + + BinaryOperator::Add + | BinaryOperator::Sub + | BinaryOperator::Mul + | BinaryOperator::Div + | BinaryOperator::Pow + | BinaryOperator::Gt + | BinaryOperator::Lt + | BinaryOperator::Gte + | BinaryOperator::Lte + | BinaryOperator::Lshift + | BinaryOperator::Rshift + | BinaryOperator::RshiftUnsigned + | BinaryOperator::BitAnd + | BinaryOperator::BitOr + | BinaryOperator::BitXor + | BinaryOperator::Mod => match (left, right) { + (Literal::Number(a), Literal::Number(b)) => Ok(match self { + BinaryOperator::Add => Literal::Number(a + b), + BinaryOperator::Sub => Literal::Number(a - b), + BinaryOperator::Mul => Literal::Number(a * b), + BinaryOperator::Div => Literal::Number(a / b), + BinaryOperator::Pow => Literal::Number(a.powf(b)), + BinaryOperator::Gt => Literal::Bool(a > b), + BinaryOperator::Lt => Literal::Bool(a < b), + BinaryOperator::Gte => Literal::Bool(a >= b), + BinaryOperator::Lte => Literal::Bool(a <= b), + BinaryOperator::Lshift => Literal::Number(((a as i32) << (b as i32)).into()), + BinaryOperator::Rshift => Literal::Number(((a as i32) >> (b as i32)).into()), + BinaryOperator::RshiftUnsigned => { + Literal::Number(((a as u32) >> (b as u32)).into()) + } + BinaryOperator::BitAnd => Literal::Number(((a as i32) & (b as i32)).into()), + BinaryOperator::BitOr => Literal::Number(((a as i32) | (b as i32)).into()), + BinaryOperator::BitXor => Literal::Number(((a as i32) ^ (b as i32)).into()), + BinaryOperator::Mod => Literal::Number(a % b), + _ => unreachable!(), + }), + _ => Err(()), + }, + + // Impossible condition doing assign-add to a literal + _ => Err(()), + } + } +} + +impl UnaryOperator { + 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 { + match self { + UnaryOperator::Neg => match value { + Literal::Number(a) => Ok(Literal::Number(-a)), + _ => Err(()), + }, + UnaryOperator::Not => match value { + Literal::Bool(a) => Ok(Literal::Bool(!a)), + _ => Err(()), + }, + UnaryOperator::BitNot => match value { + Literal::Number(a) => Ok(Literal::Number((!(a as i32)).into())), + _ => Err(()), + }, + } + } +} + +/// Taken from from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table +/// +/// 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, // +, - + BitwiseShift, // <<, >>, >>> + Relational, // <, <=, >, >= + Equality, // ==, != + BitwiseAnd, // & + BitwiseXor, // ^ + 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), + } + } +} + +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/ast/src/error.rs b/crates/ast/src/error.rs deleted file mode 100644 index f698cb95d6c28ff61e24bab5161acbd133992c78..0000000000000000000000000000000000000000 --- a/crates/ast/src/error.rs +++ /dev/null @@ -1,17 +0,0 @@ -use miette::Diagnostic; -use thiserror::Error; - -#[derive(Error, Diagnostic, Debug)] -#[error("AstError")] -pub struct AstError { - #[help] - error: String, -} - -impl AstError { - pub fn new(error: &'static str) -> AstError { - AstError { - error: String::from(error), - } - } -} diff --git a/crates/ast/src/lib.rs b/crates/ast/src/lib.rs deleted file mode 100644 index c4841db4e84a9184793e9563f94d5e15b3dbaa66..0000000000000000000000000000000000000000 --- a/crates/ast/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -use miette::Result; - -mod error; -mod parse_block; -mod parse_expression; -mod parse_function_call; -mod parse_statement; -mod parse_type; -mod types; - -use tokenizer::Token; - -use parse_statement::parse_statement; -use types::ParserContext; - -pub use types::{Ast, Block, Type}; - -pub fn parse(tokens: Vec) -> Result> { - let mut ctx = ParserContext::new(tokens); - let mut ast: Vec = Vec::new(); - - while !ctx.empty() { - ast.push(parse_statement(&mut ctx)?); - } - - Ok(ast) -} diff --git a/crates/ast/src/parse_block.rs b/crates/ast/src/parse_block.rs deleted file mode 100644 index c074466d00afb74240012c5d37631596e5cbc374..0000000000000000000000000000000000000000 --- a/crates/ast/src/parse_block.rs +++ /dev/null @@ -1,55 +0,0 @@ -use miette::Result; - -use tokenizer::Token; - -use super::error::AstError; -use super::parse_expression::parse_expression; -use super::parse_statement::parse_statement; -use super::types::Block; -use super::types::ParserContext; - -pub fn parse_block(tokens: &mut ParserContext) -> Result { - match tokens.next() { - Some(Token::OpenBracket) => (), - _ => return Err(AstError::new("Expected { when parsing block").into()), - } - - let mut value = Vec::new(); - let mut implicit_return = false; - - loop { - match tokens.peek() { - Some(Token::ClosedBracket) => { - tokens.next(); - break; - } - _ => (), - } - - let current_cursor = tokens.cursor; - let result = parse_statement(tokens); - - match result { - Ok(ast) => value.push(ast), - Err(_) => { - tokens.cursor = current_cursor; - value.push(parse_expression(tokens)?); - implicit_return = true; - match tokens.peek() { - Some(Token::ClosedBracket) => (), - _ => { - return Err(AstError::new( - "Expected closed bracket after expression in function body", - ) - .into()); - } - } - } - } - } - - Ok(Block { - value, - implicit_return, - }) -} diff --git a/crates/ast/src/parse_expression.rs b/crates/ast/src/parse_expression.rs deleted file mode 100644 index f71c1cd26b5070d3577eb7bcd607b2f5e4417005..0000000000000000000000000000000000000000 --- a/crates/ast/src/parse_expression.rs +++ /dev/null @@ -1,105 +0,0 @@ -use miette::Result; - -use tokenizer::Token; - -use super::error::AstError; -use super::parse_block::parse_block; -use super::parse_function_call::parse_function_call; -use super::types::{Ast, Block, ParserContext}; - -pub fn parse_expression(tokens: &mut ParserContext) -> Result { - let left = match tokens.next() { - Some(token) => match token { - Token::Number(number) => Ast::Number(number), - Token::String(string) => Ast::String(string.clone()), - Token::BuiltIn(name) => { - match tokens.peek() { - Some(Token::OpenParanthesis) => { - // FRICK we consumed the open paranthesis - tokens.back(); - parse_function_call(tokens)? - } - _ => Ast::BuiltIn(name.clone()), - } - } - Token::True => Ast::Bool(true), - Token::False => Ast::Bool(false), - Token::OpenBracket => { - // FRICK we consumed the open bracket - // we need to put the cursor back - tokens.back(); - Ast::Block(parse_block(tokens)?) - } - Token::If => { - let condition = parse_expression(tokens)?; - - let value = parse_block(tokens)?; - - let mut else_value: Option = None; - - match tokens.peek() { - Some(Token::Else) => { - tokens.next(); - match tokens.peek() { - Some(Token::If) => { - let else_expression = parse_expression(tokens)?; - else_value = Some(Block { - value: vec![else_expression], - implicit_return: true, - }); - } - Some(Token::OpenBracket) => { - else_value = Some(parse_block(tokens)?); - } - _ => { - return Err(AstError::new( - "Expected if or open bracket after else in if statement", - ) - .into()) - } - } - } - _ => (), - } - - return Ok(Ast::If { - condition: Box::new(condition), - value, - else_value, - }); - } - Token::Identifier(name) => { - match tokens.peek() { - Some(Token::OpenParanthesis) => { - // FRICK we consumed the open paranthesis - tokens.back(); - parse_function_call(tokens)? - } - _ => Ast::Identifier(name.clone()), - } - } - _ => { - println!("token: {:?}", token); - return Err(AstError::new("Expected expression, but did not get it").into()); - } - }, - None => return Err(AstError::new("Expected expression, but got nothing instead").into()), - }; - - match tokens.peek() { - Some(token) => match token { - Token::Operator(operator) => { - tokens.next(); - return Ok(Ast::Operation { - operator: operator.clone(), - left: Box::new(left), - right: Box::new(parse_expression(tokens)?), - }); - } - _ => (), - }, - _ => (), - } - - Ok(left) -} diff --git a/crates/ast/src/parse_function_call.rs b/crates/ast/src/parse_function_call.rs deleted file mode 100644 index 57c177d8c77fb7e7aeb1b1d7cbd502c7651ca133..0000000000000000000000000000000000000000 --- a/crates/ast/src/parse_function_call.rs +++ /dev/null @@ -1,56 +0,0 @@ -use miette::Result; - -use tokenizer::Token; - -use super::error::AstError; -use super::parse_expression::parse_expression; -use super::types::{Ast, ParserContext}; - -/// Parses a function call AS AN EXPRESSION -/// This does not consume the semicolon at the end of the function call -/// if it is a statement. -pub fn parse_function_call(tokens: &mut ParserContext) -> Result { - let identifier = match tokens.next() { - Some(Token::Identifier(identifier)) => Ast::Identifier(identifier), - Some(Token::BuiltIn(identifier)) => Ast::BuiltIn(identifier), - _ => return Err(AstError::new("Expected identifier").into()), - }; - - match tokens.peek() { - Some(Token::OpenParanthesis) => { - tokens.next(); - - let mut arguments = Vec::new(); - - loop { - match tokens.peek() { - Some(Token::ClosedParanthesis) => { - tokens.next(); - break; - } - _ => (), - } - arguments.push(parse_expression(tokens)?); - - match tokens.peek() { - Some(Token::Comma) => { - tokens.next(); - } - Some(Token::ClosedParanthesis) => (), - _ => { - return Err(AstError::new( - "Expected ) or , but found something else instead.", - ) - .into()) - } - } - } - - Ok(Ast::FunctionCall { - name: Box::new(identifier), - arguments, - }) - } - _ => Err(AstError::new("Unexpected identifier").into()), - } -} diff --git a/crates/ast/src/parse_statement.rs b/crates/ast/src/parse_statement.rs deleted file mode 100644 index 343aceff9eea0e9fc29c57e25fab2387e39f8b32..0000000000000000000000000000000000000000 --- a/crates/ast/src/parse_statement.rs +++ /dev/null @@ -1,261 +0,0 @@ -use miette::Result; - -use tokenizer::Token; - -use super::error::AstError; -use super::parse_block::parse_block; -use super::parse_expression::parse_expression; -use super::parse_function_call::parse_function_call; -use super::parse_type::parse_type; -use super::types::{Argument, Ast, Block, ParserContext}; - -/// Precondition: There is at least one token in the iterator -pub fn parse_statement(tokens: &mut ParserContext) -> Result { - let token = tokens.next().unwrap(); - match token { - Token::BuiltIn(name) => { - match tokens.peek() { - Some(Token::OpenParanthesis) => { - // Okay, so we messed up. We shouldn't have consumed that token. - tokens.back(); - parse_function_call(tokens) - } - _ => Ok(Ast::BuiltIn(name)), - } - } - Token::OpenBracket => { - // FRICK, we have to backtrack - tokens.back(); - Ok(Ast::Block(parse_block(tokens)?)) - } - Token::Pub => { - match tokens.peek() { - Some(Token::Let) | Some(Token::Var) | Some(Token::Function) => (), - _ => return Err(AstError::new("Expected let, var, or fn after pub").into()), - } - - let inner = parse_statement(tokens)?; - - return Ok(Ast::Public(Box::new(inner))); - } - Token::Let => { - let name = match tokens.next() { - Some(Token::Identifier(name)) => name, - _ => return Err(AstError::new("Expected identifier after var").into()), - }; - let return_type = match tokens.next() { - Some(Token::Colon) => parse_type(tokens)?, - _ => return Err(AstError::new("Expected colon after identifier").into()), - }; - let value = match tokens.next() { - Some(Token::Equals) => parse_expression(tokens)?, - _ => return Err(AstError::new("Expected equals after identifier").into()), - }; - match tokens.next() { - Some(Token::Semicolon) => (), - _ => { - return Err( - AstError::new("Expected semicolon after constant declaration").into(), - ) - } - }; - Ok(Ast::ConstantDeclaration { - name: name.clone(), - return_type, - value: Box::new(value), - }) - } - Token::Var => { - let name = match tokens.next() { - Some(Token::Identifier(name)) => name, - _ => return Err(AstError::new("Expected identifier after var").into()), - }; - let return_type = match tokens.next() { - Some(Token::Colon) => parse_type(tokens)?, - _ => return Err(AstError::new("Expected colon after identifier").into()), - }; - let value = match tokens.next() { - Some(Token::Equals) => parse_expression(tokens)?, - _ => return Err(AstError::new("Expected equals after identifier").into()), - }; - match tokens.next() { - Some(Token::Semicolon) => (), - _ => { - return Err( - AstError::new("Expected semicolon after variable declaration").into(), - ) - } - }; - Ok(Ast::VariableDeclaration { - name: name.clone(), - return_type, - value: Box::new(value), - }) - } - Token::Return => { - let expression = parse_expression(tokens)?; - - match tokens.next() { - Some(Token::Semicolon) => (), - _ => return Err(AstError::new("Expected semicolon after return statement").into()), - }; - - Ok(Ast::Return(Box::new(expression))) - } - Token::Function => { - let name = match tokens.next() { - Some(Token::Identifier(name)) => name.clone(), - _ => return Err(AstError::new("Expected identifier after fn").into()), - }; - - match tokens.next() { - Some(Token::OpenParanthesis) => (), - _ => { - return Err(AstError::new( - "Expected open paranthesis after identifier in function", - ) - .into()) - } - }; - - let mut arguments = Vec::new(); - - loop { - match tokens.peek() { - Some(Token::ClosedParanthesis) => { - tokens.next(); - break; - } - _ => (), - } - - let argument_name = match tokens.next() { - Some(Token::Identifier(name)) => name, - _ => { - return Err(AstError::new( - "Expected identifier for argument name in function", - ) - .into()) - } - }; - - let return_type = match tokens.next() { - Some(Token::Colon) => parse_type(tokens)?, - _ => return Err(AstError::new("Expected colon after identifier").into()), - }; - - match tokens.peek() { - Some(Token::Comma) => { - tokens.next(); - } - _ => (), - } - - arguments.push(Argument { - name: argument_name.clone(), - return_type, - }) - } - - let return_type = match tokens.next() { - Some(Token::RightArrow) => parse_type(tokens)?, - _ => { - return Err(AstError::new( - "Expected right arrow after argument list in function", - ) - .into()) - } - }; - - match tokens.peek() { - Some(Token::OpenBracket) => (), - _ => { - return Err(AstError::new( - "Expected open bracket after type in function declaration", - ) - .into()) - } - }; - - let block = parse_block(tokens)?; - - return Ok(Ast::Function { - name, - arguments, - return_type, - block, - }); - } - Token::If => { - let condition = parse_expression(tokens)?; - - let value = parse_block(tokens)?; - - let mut else_value: Option = None; - - match tokens.peek() { - Some(Token::Else) => { - tokens.next(); - match tokens.peek() { - Some(Token::If) => { - let else_if = parse_statement(tokens)?; - else_value = Some(Block { - value: vec![else_if], - implicit_return: false, - }); - } - Some(Token::OpenBracket) => { - else_value = Some(parse_block(tokens)?); - } - _ => { - return Err(AstError::new( - "Expected if or open bracket after else in if statement", - ) - .into()) - } - } - } - _ => (), - } - - return Ok(Ast::If { - condition: Box::new(condition), - value, - else_value, - }); - } - Token::Identifier(_) => match tokens.peek() { - Some(Token::OpenParanthesis) => { - // We need to give the identifier back to the parser - tokens.back(); - let result = parse_function_call(tokens); - match tokens.next() { - Some(Token::Semicolon) => (), - _ => { - return Err(AstError::new( - "Expected semicolon after function call statement", - ) - .into()) - } - }; - result - } - _ => Err(AstError::new("Unexpected identifier").into()), - }, - Token::Comment(comment) => Ok(Ast::Comment(comment.clone())), - Token::DocComment(comment) => { - let mut concatted_comments = comment.clone(); - loop { - match tokens.peek() { - Some(Token::DocComment(comment)) => { - concatted_comments.push_str(format!("\n{}", comment).as_str()); - tokens.next(); - } - _ => break, - } - } - Ok(Ast::DocComment(concatted_comments)) - } - _ => return Err(AstError::new("Unexpected token").into()), - } -} diff --git a/crates/ast/src/parse_type.rs b/crates/ast/src/parse_type.rs deleted file mode 100644 index 6837edec59b4d8f649ccc28c65f33362ef948c49..0000000000000000000000000000000000000000 --- a/crates/ast/src/parse_type.rs +++ /dev/null @@ -1,21 +0,0 @@ -use miette::Result; - -use tokenizer::Token; - -use super::error::AstError; -use super::types::{ParserContext, Type}; - -pub fn parse_type(tokens: &mut ParserContext) -> Result { - match tokens.next() { - Some(Token::Identifier(typ)) => match typ.as_str() { - "int" => Ok(Type::Int), - "float" => Ok(Type::Float), - "bigint" => Ok(Type::Bigint), - "bool" => Ok(Type::Bool), - "string" => Ok(Type::String), - "any" => Ok(Type::Any), - _ => return Err(AstError::new("Expected type").into()), - }, - _ => return Err(AstError::new("Expected type").into()), - } -} diff --git a/crates/ast/src/types.rs b/crates/ast/src/types.rs deleted file mode 100644 index 3500088aa85499be1b8eb25c25ed7749ff58c598..0000000000000000000000000000000000000000 --- a/crates/ast/src/types.rs +++ /dev/null @@ -1,109 +0,0 @@ -// TODO(lino-levan): Split up AST into expressions, statements, etc. - -use tokenizer::Token; -use util::Operator; - -#[derive(Debug)] -pub struct ParserContext { - pub tokens: Vec, - pub cursor: usize, -} - -impl ParserContext { - pub fn new(tokens: Vec) -> ParserContext { - ParserContext { tokens, cursor: 0 } - } - - pub fn empty(&self) -> bool { - self.cursor == self.tokens.len() - } - - pub fn back(&mut self) { - self.cursor -= 1; - } - - pub fn next(&mut self) -> Option { - if self.cursor == self.tokens.len() { - return None; - } - - let token = self.tokens[self.cursor].clone(); - self.cursor += 1; - Some(token) - } - - pub fn peek(&self) -> Option { - if self.cursor == self.tokens.len() { - return None; - } - - Some(self.tokens[self.cursor].clone()) - } -} - -#[derive(Debug, Clone)] -pub enum Type { - Int, - Float, - Bigint, - Bool, - String, - Any, -} - -#[derive(Debug, Clone)] -pub struct Block { - pub value: Vec, - pub implicit_return: bool, -} - -#[derive(Debug, Clone)] -pub struct Argument { - pub name: String, - pub return_type: Type, -} - -#[derive(Debug, Clone)] -pub enum Ast { - ConstantDeclaration { - name: String, - return_type: Type, - value: Box, - }, - VariableDeclaration { - name: String, - return_type: Type, - value: Box, - }, - Operation { - operator: Operator, - left: Box, - right: Box, - }, - Function { - name: String, - arguments: Vec, - return_type: Type, - block: Block, - }, - FunctionCall { - /// Identifier or BuiltIn - name: Box, - arguments: Vec, - }, - If { - condition: Box, - value: Block, - else_value: Option, - }, - Return(Box), - Public(Box), - Block(Block), - Number(f64), - Bool(bool), - String(String), - Identifier(String), - BuiltIn(String), - Comment(String), - DocComment(String), -} diff --git a/crates/ast/types.rs b/crates/ast/types.rs new file mode 100644 index 0000000000000000000000000000000000000000..e65220300f5e78fceec6f68eef1fdf5ef51dc984 --- /dev/null +++ b/crates/ast/types.rs @@ -0,0 +1,208 @@ +use miette::{Diagnostic, Result}; +use thiserror::Error; + +/// A value that is known at compile-time +#[derive(Debug, Clone, PartialEq)] +pub enum Literal { + Number(f64), + Bool(bool), + String(String), + Void, +} + +impl std::fmt::Display for Literal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Literal::Number(value) => write!(f, "{}", value), + Literal::Bool(value) => write!(f, "{}", value), + Literal::String(value) => write!(f, "{}", value), + Literal::Void => write!(f, "void"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Type { + /// Variables without an explicit type are assigned this until + /// the type inferencer figures out what this is. There is not a + /// keyword for this. + Infer, + /// The type assigned to literals. This allows integer literals + /// to coerce to integer or float depending on the context, but + /// prevents implicit casting of runtime values. + Literal(Literal), + /// Return value of `@raw`. All type checks are disabled. + Any, + /// The type of `void`. This is the return type of functions + /// that do not return a value. + Void, + /// noreturn is the type of 'return', aka a value that never exists. + /// unlike void, this includes functions that do not return at all. + NoReturn, + /// 32 signed integer + Int, + /// 64 signed JS float + Float, + /// Boolean + Bool, + /// JS BigInt + BigInt, + /// JS String + String, + /// A union of multiple possible types. Might be removed + Union(Vec), +} + +pub trait TypeCoerceable { + fn to_type(&self) -> Option; +} + +impl TypeCoerceable for Literal { + fn to_type(&self) -> Option { + Some(match self { + Literal::Number(_) => Type::Float, + Literal::Bool(_) => Type::Bool, + Literal::String(_) => Type::String, + Literal::Void => Type::Void, + }) + } +} + +impl TypeCoerceable for Type { + fn to_type(&self) -> Option { + Some(self.clone()) + } +} + +impl std::fmt::Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Type::Infer => write!(f, "infered type"), + Type::Any => write!(f, "opaque js value"), + Type::Literal(value) => write!(f, "{}", value), + Type::Int => write!(f, "int"), + Type::Float => write!(f, "float"), + Type::BigInt => write!(f, "bigint"), + Type::Bool => write!(f, "bool"), + Type::String => write!(f, "string"), + Type::NoReturn => write!(f, "noreturn"), + Type::Void => write!(f, "void"), + Type::Union(of) => { + for (i, t) in of.iter().enumerate() { + if i != 0 { + write!(f, " | ")?; + } + write!(f, "{}", t)?; + } + Ok(()) + } + } + } +} + +pub fn merge_types(a: Type, b: Type) -> Result { + match a { + Type::Infer => return Ok(b), + Type::Any => return Ok(Type::Any), + _ => {} + } + + match b { + Type::Infer => return Ok(a), + Type::Any => return Ok(Type::Any), + _ => {} + } + + if a == b { + return Ok(a); + } + + Err(TypeError::new(format!("TODO: Implement type merging for {} and {}", a, b)).into()) +} + +// pub fn coerce_type(target: Type, source: Type) -> Result { +// match source { +// Type::Any => return Ok(target), +// _ => {} +// } +// match target { +// Type::Infer => Ok(source), +// Type::Any => Ok(Type::Any), + +// Type::Literal(_) => unreachable!(), + +// Type::Int => match source { +// Type::Int => Ok(Type::Int), +// Type::Float => Ok(Type::Int), +// Type::Literal(value) => { +// if value.fract() != 0.0 { +// Err( +// TypeError::new(format!("Numeric Literal '{}' is not an integer", value)) +// .into(), +// ) +// } else if value > i32::MAX.into() || value < i32::MIN.into() { +// Err(TypeError::new(format!( +// "Numeric Literal '{}' is out of range for an integer ({}..{})", +// value, +// i32::MIN, +// i32::MAX +// )) +// .into()) +// } else { +// Ok(Type::Int) +// } +// } +// t => Err(TypeError::new(format!("Cannot coerce {} to int", t)).into()), +// }, + +// Type::BigInt => match source { +// Type::Int => Ok(Type::Int), +// Type::Float => Ok(Type::Int), +// Type::NumberLiteral(value) => { +// if value.fract() != 0.0 { +// Err( +// TypeError::new(format!("Numeric Literal '{}' is not an integer", value)) +// .into(), +// ) +// } else { +// Ok(Type::BigInt) +// } +// } +// t => Err(TypeError::new(format!("Cannot coerce {} to int", t)).into()), +// }, + +// Type::Float => match source { +// Type::Int => Ok(Type::Float), +// Type::Float => Ok(Type::Float), +// Type::NumberLiteral(value) => { +// if value.fract() == 0.0 && value <= f64::MAX.into() && value >= f64::MIN.into() { +// Ok(Type::Float) +// } else { +// Err(TypeError::new(format!("Numeric Literal")).into()) +// } +// } +// _ => Err(TypeError::new(format!("Cannot coerce int to float")).into()), +// }, + +// other => { +// if other == source { +// Ok(other) +// } else { +// Err(TypeError::new(format!("Cannot coerce {} to {}", source, other)).into()) +// } +// } +// } +// } + +#[derive(Error, Diagnostic, Debug)] +#[error("TypeError")] +pub struct TypeError { + #[help] + error: String, +} + +impl TypeError { + pub fn new(error: String) -> Self { + Self { error } + } +} 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 0f6618aef8ab0f73dfad007939f2c13f827f0fa5..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 @@ -14,5 +14,7 @@ thiserror.workspace = true tokenizer = { path = "../tokenizer" } ast = { path = "../ast" } +parser = { path = "../parser" } js_ast = { path = "../js_ast" } js_printer = { path = "../js_printer" } +sema = { path = "../sema" } diff --git a/crates/cli/main.rs b/crates/cli/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..dfcb80febf985e7247541887ad7a59bdd65d364f --- /dev/null +++ b/crates/cli/main.rs @@ -0,0 +1,172 @@ +use std::{io::Write, path::PathBuf}; + +use clap::{Parser, Subcommand}; +use miette::{diagnostic, Result}; + +use js_printer; +use tokenizer; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Turn debugging information on + #[arg(short, long, action = clap::ArgAction::Count)] + debug: u8, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Compiles a file into javascript + Compile { + file: PathBuf, + // Whether to output to stdout instead of a file + #[arg(long, default_value_t = false)] + stdout: bool, + /// What javascript runtime to compile to + #[arg(short, long, default_value = "deno")] + target: String, + /// Format the output javascript? + #[arg(long, default_value_t = true)] + format: bool, + }, + Run { + file: PathBuf, + /// What javascript runtime to compile to and use + #[arg(short, long, default_value = "deno")] + target: String, + }, + Astdump { + file: PathBuf, + }, +} + +fn get_runtime(runtime: &String) -> String { + match std::env::var("USER") { + Ok(user) if user == "dave" => String::from("bun"), + _ => runtime.clone(), + } +} + +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 mut blight_ast = parser::parse(tokens)?; + // 3. Semantic analysis. + sema::analyze_module(&mut blight_ast)?; + // 4. Optimize the AST + // optimize::optimize_module(&mut blight_ast); + // 5. Translate the AST into javascript + let js_ast = js_ast::blight_to_js(blight_ast); + // 6. Print the JS AST + // 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 { + // 1. Tokenize the input + let tokens = tokenizer::tokenize(content)?; + // 2. Parse the tokens into an AST + let blight_ast = parser::parse(tokens)?; + return Ok(format!("{:#?}", blight_ast)); +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Compile { + file, + stdout, + target: _, + format, + } => { + let contents = match std::fs::read_to_string(file.clone()) { + Ok(contents) => contents, + Err(e) => { + eprintln!("Failed to read {}: {}", file.display(), e); + std::process::exit(1); + } + }; + + let mut result = transpile_to_string(contents.clone())?; + + if format { + let mut formatter = std::process::Command::new("deno") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .arg("fmt") + .arg("-") + .spawn() + .expect("Failed to execute 'deno fmt' on output"); + let mut stdin = formatter.stdin.take().expect("Failed to open stdin"); + stdin + .write_all(result.as_bytes()) + .expect("Failed to write to stdin"); + stdin.flush().expect("Failed to flush stdin"); + drop(stdin); + let out = formatter + .wait_with_output() + .expect("Failed to execute 'deno fmt' on output"); + result = String::from_utf8(out.stdout).unwrap(); + } + + if stdout { + println!("{}", result); + } else { + std::fs::write(file.with_extension("js"), result).unwrap(); + } + } + Commands::Run { file, target } => { + let contents = std::fs::read_to_string(file.clone()).unwrap(); + 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(); + + let rt = get_runtime(&target); + + match rt.as_str() { + "deno" => std::process::Command::new("deno") + .arg("run") + .arg("-A") + .arg(temp_file) + .spawn() + .expect("Failed to execute process"), + "node" => std::process::Command::new("node") + .arg(temp_file) + .spawn() + .expect("Failed to execute process"), + "bun" => std::process::Command::new("bun") + .arg("run") + .arg(temp_file) + .spawn() + .expect("Failed to execute process"), + _ => { + panic!("Unknown runtime: {}", target); + } + } + .wait() + .unwrap(); + } + Commands::Astdump { file } => { + let contents = std::fs::read_to_string(file.clone()).unwrap(); + let result = astdump(contents.clone())?; + println!("{}", result); + } + } + + Ok(()) +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs deleted file mode 100644 index e04af712afecd134f30a2d7e1afdf3a700673eba..0000000000000000000000000000000000000000 --- a/crates/cli/src/main.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::path::PathBuf; - -use clap::{Parser, Subcommand}; -use miette::Result; - -use ast; -use js_printer; -use tokenizer; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - /// Turn debugging information on - #[arg(short, long, action = clap::ArgAction::Count)] - debug: u8, - - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Compiles a file into javascript - Compile { - file: PathBuf, - // Whether to output to stdout instead of a file - #[arg(long, default_value_t = false)] - stdout: bool, - /// What javascript runtime to compile to - #[arg(short, long, default_value = "deno")] - target: String, - }, - Run { - file: PathBuf, - /// What javascript runtime to compile to and use - #[arg(short, long, default_value = "deno")] - target: String, - }, -} - -fn get_runtime(runtime: &String) -> String { - match std::env::var("USER") { - Ok(user) if user == "dave" => String::from("bun"), - _ => runtime.clone(), - } -} - -pub fn transpile(content: String) -> Result { - // 1. Tokenize the input - let tokens = tokenizer::tokenize(content)?; - // 2. Parse the tokens into an AST - let blight_ast = ast::parse(tokens)?; - // 3. Type check the AST - // type_check(tree.clone())?; - // 4. Optimize the AST - // optimize(tree.clone())?; - // 5. Translate the AST into javascript - let js_ast = js_ast::translate(blight_ast)?; - // 6. Print the JS AST - let javascript = js_printer::print(js_ast); - // 7. Return the javascript - return Ok(javascript); -} - -fn main() -> Result<()> { - let cli = Cli::parse(); - - match cli.command { - Commands::Compile { - file, - stdout, - target: _, - } => { - let contents = std::fs::read_to_string(file.clone()).unwrap(); - let result = transpile(contents.clone())?; - - if stdout { - println!("{}", result); - } else { - std::fs::write(file.with_extension("js"), result).unwrap(); - } - } - Commands::Run { file, target } => { - let contents = std::fs::read_to_string(file.clone()).unwrap(); - let result = transpile(contents.clone())?; - let mut temp_file = std::env::temp_dir(); - temp_file.push("temp.js"); - std::fs::write(temp_file.clone(), result).unwrap(); - - let rt = get_runtime(&target); - - match rt.as_str() { - "deno" => std::process::Command::new("deno") - .arg("run") - .arg("-A") - .arg(temp_file) - .spawn() - .expect("Failed to execute process"), - "node" => std::process::Command::new("node") - .arg(temp_file) - .spawn() - .expect("Failed to execute process"), - "bun" => std::process::Command::new("bun") - .arg("run") - .arg(temp_file) - .spawn() - .expect("Failed to execute process"), - _ => { - panic!("Unknown runtime: {}", target); - } - } - .wait() - .unwrap(); - } - } - - Ok(()) -} diff --git a/crates/js_ast/Cargo.toml b/crates/js_ast/Cargo.toml index ae2bc9036ff37259b6f1bbd79effdf1c8e293511..68087cf32a5792a7ae7f6ecac80399eb5883fdba 100644 --- a/crates/js_ast/Cargo.toml +++ b/crates/js_ast/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [lib] +path = "./lib.rs" [dependencies] miette.workspace = true @@ -11,4 +12,5 @@ thiserror.workspace = true num.workspace = true ast = { path = "../ast" } -util = { path = "../util" } +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/block_convert_context.rs b/crates/js_ast/convert/block_convert_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..de41e77cae5f13c2d98b6215e925fd637a9bebb6 --- /dev/null +++ b/crates/js_ast/convert/block_convert_context.rs @@ -0,0 +1,244 @@ +use crate::js_ast::*; +use ast::*; +use std::collections::{HashMap, HashSet}; +use std::vec; +use tokenizer::Loc; + +use super::tree_shake_statement_list; + +/// A block convert context keeps track of state while converting non-valid JS expressions to JS +pub struct BlockConvertContext { + /// Statements in this block context + /// This may be the merged contents of multiple blocks + statements: Vec, + ids: HashSet, + next_id: u32, + scopes: Vec, + /// If the last item in `statements` is actually valid + has_last_statement: bool, +} + +struct VirtualScope { + renames: HashMap, +} + +pub struct TempId { + unmapped_name: String, +} + +pub struct StatementHandle { + index: Option, +} + +impl StatementHandle { + pub fn delete(self, ctx: &mut BlockConvertContext) { + if let Some(index) = self.index { + ctx.statements.remove(index as usize); + } + } +} + +impl TempId { + pub fn get(&self, hoist_context: &BlockConvertContext) -> String { + hoist_context.remap_identifier(&self.unmapped_name) + } +} + +impl BlockConvertContext { + pub fn new() -> Self { + Self { + statements: Vec::new(), + ids: HashSet::new(), + next_id: 0, + scopes: vec![VirtualScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + pub fn new_child(&self) -> Self { + Self { + statements: Vec::new(), + ids: self.ids.clone(), + next_id: 0, + scopes: vec![VirtualScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + /// A virtual scope is a scope that exists in blight syntax + /// but may or may not exist in blight. With these it is important to + pub fn push_virtual_scope(&mut self) { + self.scopes.push(VirtualScope { + renames: HashMap::new(), + }); + } + + /// See `push_virtual_scope` for more information on what this is. + pub fn pop_virtual_scope(&mut self) { + self.scopes.pop(); + debug_assert!(self.scopes.len() > 0); + } + + pub fn generate_temp_id(&mut self, basename: &str) -> TempId { + self.next_id += 1; + loop { + let name = format!("{}{}", basename, self.next_id); + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + self.next_id += 1; + } + } + + /// This does some extra optimizations: + /// - `let a = b;` will not be emitted but instad write a variable alias + pub fn add_statement(&mut self, statement: JsStatement) -> StatementHandle { + match statement.data { + JsStatementData::Declaration(JSDeclaration { + name, + init: + Some(JsExpr { + data: JsExprData::Identifier(init_name), + .. + }), + is_constant: true, + }) => { + self.scopes + .last_mut() + .unwrap() + .renames + .insert(name, init_name); + self.has_last_statement = false; + return StatementHandle { index: None }; + } + _ => {} + } + self.statements.push(statement); + self.has_last_statement = true; + StatementHandle { + index: Some(self.statements.len() as u32 - 1), + } + } + + /// Get the last statement if it exists. + /// If the statement was optimized away, this returns None + pub fn last_statement(&self) -> Option<&JsStatement> { + if self.has_last_statement { + Some(self.statements.last().unwrap()) + } else { + None + } + } + + pub fn generate_id(&mut self, basename: &String) -> TempId { + let mut id = 0u32; + loop { + let name = match id { + 0 => basename.clone(), + _ => format!("{}{}", basename, id), + }; + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + id += 1; + } + } + + /// Given a blight identifier, remap it to a new identifier. + pub fn remap_identifier(&self, name: &String) -> String { + for scope in self.scopes.iter().rev() { + if let Some(remapped) = scope.renames.get(name) { + return remapped.clone(); + } + } + name.clone() + } + + /// Declare an identifier in the current scope. + pub fn declare_identifier(&mut self, name: String) -> TempId { + let remapped = self.generate_id(&name); + let scope = self.scopes.last_mut().unwrap(); + self.ids.insert(remapped.unmapped_name.clone()); + scope.renames.insert(name, remapped.unmapped_name.clone()); + remapped + } + + pub fn take_last_statement(&mut self) -> Option { + if !self.has_last_statement { + return None; + } + self.has_last_statement = false; + Some(self.statements.pop().unwrap()) + } + + pub fn take_last_statement_if_expr(&mut self) -> Option { + if !self.has_last_statement { + return None; + } + match self.statements.last().unwrap().data { + JsStatementData::Expression(_) => { + self.has_last_statement = false; + match self.statements.pop().unwrap().data { + JsStatementData::Expression(expr) => Some(expr), + _ => unreachable!(), + } + } + _ => return None, + } + } +} + +pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { + JsStatement::new( + JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), + loc, + None, + ) +} + +pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { + JsExpr::new( + JsExprData::Binary { + op: BinaryOperator::Assign, + left: Box::new(JsExpr::new( + JsExprData::Identifier(target), + loc.clone(), + None, + )), + right: Box::new(expr), + }, + loc, + None, + ) +} + +impl Into for BlockConvertContext { + fn into(self) -> JsBlock { + JsBlock::new(self.into(), Loc::EMPTY) + } +} + +impl Into for BlockConvertContext { + fn into(self) -> JsModule { + JsModule { + statements: self.into(), + } + } +} + +impl Into> for BlockConvertContext { + fn into(mut self) -> Vec { + tree_shake_statement_list(&mut self.statements); + self.statements + } +} diff --git a/crates/js_ast/convert/convert_valid_block.rs b/crates/js_ast/convert/convert_valid_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb40111073d4e3ea44604c0117ed7419b879935f --- /dev/null +++ b/crates/js_ast/convert/convert_valid_block.rs @@ -0,0 +1,37 @@ +use super::convert_valid_statement_to_expr; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn convert_valid_block(ctx: &mut BlockConvertContext, block: Block, loc: Loc) -> JsExpr { + debug_assert!( + block.is_valid_js_expression(), + "must call convert_valid_block with valid js expression" + ); + + let len = block.statements.len(); + let loc = loc; + let block_expr = match len { + 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), + // a block expression with a single expression is just that expression + 1 => convert_valid_statement_to_expr(ctx, block.statements.into_iter().next().unwrap()), + // Since blocks are not valid expressions in JS, we create a + // comma expression from a block with more than one statement. + 2.. => { + let mut items = Vec::new(); + for statement in block.statements.into_iter() { + items.push(convert_valid_statement_to_expr(ctx, statement)); + } + JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) + } + }; + + // a block without implicit return is to be type void, so we wrap in a void expression + if !block.has_implicit_return && len > 0 { + JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) + } else { + block_expr + } +} diff --git a/crates/js_ast/convert/convert_valid_expr.rs b/crates/js_ast/convert/convert_valid_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ba30c694c43c60d8c543c4b64be40ac6c7a32c6 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_expr.rs @@ -0,0 +1,115 @@ +use super::convert_valid_block; +use super::BlockConvertContext; +use crate::convert::convert_valid_if; +use crate::convert::process_function::process_function_to_arrow; +use crate::convert::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; + +/// Translate a blight ast expression that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_expr(ctx: &mut BlockConvertContext, expr: Expr) -> JsExpr { + // Assert may be skipped in release builds as all failures are + // annotated with a release-checked panic + debug_assert!( + expr.is_valid_js_expression(), + "must call convert_valid_expr with valid js expression (use hoist instead)" + ); + + let loc = expr.loc; + let comment = expr.comment; + + match expr.data { + ExprData::Identifier(str) => JsExpr::new( + JsExprData::Identifier(ctx.remap_identifier(&str)), + loc, + comment, + ), + + ExprData::Literal(Literal::Bool(value)) => { + JsExpr::new(JsExprData::Bool(value), loc, comment) + } + ExprData::Literal(Literal::Number(value)) => { + JsExpr::new(JsExprData::Number(value), loc, comment) + } + ExprData::Literal(Literal::String(value)) => { + JsExpr::new(JsExprData::String(value), loc, comment) + } + ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), + + ExprData::Block(block) => convert_valid_block(ctx, block, loc), + + ExprData::Binary { op, left, right } => { + let left = convert_valid_expr(ctx, *left); + let right = convert_valid_expr(ctx, *right); + JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + loc, + comment, + ) + } + ExprData::Unary { op, value } => { + let value = convert_valid_expr(ctx, *value); + JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + loc, + comment, + ) + } + ExprData::Fn { body, params, .. } => { + process_function_to_arrow(ctx, *body, params, loc, comment) + } + ExprData::Builtin { id, arguments } => match id { + BuiltinId::Raw => { + let first_arg = arguments.iter().next().unwrap(); + // TODO(@paperdave): this string clone is stupid + let str = match first_arg.data.clone() { + ExprData::Literal(Literal::String(s)) => s, + _ => panic!("raw expects a string"), + }; + JsExpr::new(JsExprData::Raw(str), loc, comment) + } + }, + ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), + + ExprData::Call { base, arguments } => { + let base = convert_valid_expr(ctx, *base); + let arguments = arguments + .into_iter() + .map(|x| convert_valid_expr(ctx, x)) + .collect(); + JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + loc, + comment, + ) + } + + ExprData::Field { base, field } => JsExpr::new( + match field.data { + ExprData::Literal(Literal::String(name)) => JsExprData::Dot { + base: Box::new(convert_valid_expr(ctx, *base)), + name, + }, + _ => JsExprData::Index { + base: Box::new(convert_valid_expr(ctx, *base)), + index: Box::new(convert_valid_expr(ctx, *field)), + }, + }, + loc, + comment, + ), + + ExprData::Return(_) => unreachable!("convert_valid_expr called on return"), + } +} diff --git a/crates/js_ast/convert/convert_valid_if.rs b/crates/js_ast/convert/convert_valid_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..1e71398123c950b76f1f1de7e6e3088f3995fb79 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_if.rs @@ -0,0 +1,38 @@ +use super::convert_valid_block; +use super::convert_valid_expr; +use super::BlockConvertContext; +use crate::convert::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +/// Convert a valid blight if statement to a JS expression +pub fn convert_valid_if( + ctx: &mut BlockConvertContext, + expr: If, + loc: Loc, + comment: Option, +) -> JsExpr { + debug_assert!( + expr.is_valid_js_expression(), + "convert_valid_if on an invalid expression (use hoist instead)" + ); + let condition = convert_valid_expr(ctx, *expr.condition); + + let yes = convert_valid_block(ctx, expr.yes.block, expr.yes.loc); + let no = match expr.no { + Some(BlockOrIf::Block(b)) => Some(convert_valid_block(ctx, b.block, b.loc)), + Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), + None => None, + }; + + JsExpr::new( + JsExprData::Ternary { + condition: Box::new(condition), + yes: Box::new(yes), + no: no.map(Box::new), + }, + loc, + comment, + ) +} diff --git a/crates/js_ast/convert/convert_valid_statement.rs b/crates/js_ast/convert/convert_valid_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..f02fb4e407e920b50233e188a1f14fb6fbc72b24 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_statement.rs @@ -0,0 +1,32 @@ +use super::convert_valid_expr; +use super::is_valid_js_expression::*; +use super::BlockConvertContext; +use crate::js_ast::*; +use ast::*; + +/// Translate a blight ast statement that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_statement_to_expr( + ctx: &mut BlockConvertContext, + statement: Statement, +) -> JsExpr { + debug_assert!( + statement.is_valid_js_expression(), + "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" + ); + + match statement { + ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), + _ => unreachable!("attempt to convert statement to js without hoisting"), + } +} + +/// Translate a blight ast statement that is a valid JS expression, to a JS statement +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +/// +/// TODO(@paperdave): Replace this with Into for JsExpr +pub fn convert_valid_statement(ctx: &mut BlockConvertContext, statement: Statement) -> JsStatement { + let expr = convert_valid_statement_to_expr(ctx, statement); + let loc = expr.loc.clone(); + JsStatement::new(JsStatementData::Expression(expr), loc, None) +} diff --git a/crates/js_ast/convert/hoist_block_to_expr.rs b/crates/js_ast/convert/hoist_block_to_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa419cedde552f566140b6474ddf08bee0f84bbd --- /dev/null +++ b/crates/js_ast/convert/hoist_block_to_expr.rs @@ -0,0 +1,78 @@ +use super::is_valid_js_expression::*; +use super::process_statement; +use super::BlockConvertContext; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +/// This block hoist is used when we have a block in an expression context. +/// +/// All the expressions in the block are lifted above the currently +/// processed expression, and then a temporary variable is returned. +/// (or the last expression might get inlined as the return value) +/// +/// A "virtual scope" is created for this block, which handles renaming +/// situations like: +/// +/// let x = 1; +/// { +/// let x = 1; +/// } +/// call(x) +/// +/// The output JS of the above would be: +/// +/// const x = 1; +/// const x2 = 1; +/// call(x) +/// +/// For expressions that have branches, use `hoist_block_to_js_block` +pub fn hoist_block_to_expr( + ctx: &mut BlockConvertContext, + block: Block, + loc: Loc, + comment: Option, +) -> Option { + debug_assert!( + !block.is_valid_js_expression(), + "hoist_block on a valid expression, should call convert_valid_block instead" + ); + + let temporary = ctx.generate_temp_id("_block"); + + ctx.push_virtual_scope(); + + for statement in block.statements { + process_statement(ctx, statement); + } + + ctx.pop_virtual_scope(); + + // If the last statement is an expression, we need + // to assign to the temporary variable. + let result = match ctx.last_statement().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) if block.has_implicit_return => { + match ctx.take_last_statement().unwrap().data { + JsStatementData::Expression(expr) => expr, + _ => unreachable!(), + } + } + _ => JsExpr::new(JsExprData::Null, loc.clone(), None), + }; + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: temporary.get(&ctx), + init: Some(result), + is_constant: false, + }), + loc.clone(), + comment, + )); + + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&ctx)), + loc, + None, + )) +} diff --git a/crates/js_ast/convert/hoist_block_to_js_block.rs b/crates/js_ast/convert/hoist_block_to_js_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..2dd10483b209d1d999b593d2763cb4b3b9109c2c --- /dev/null +++ b/crates/js_ast/convert/hoist_block_to_js_block.rs @@ -0,0 +1,68 @@ +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +use super::*; + +/// This block hoist is used when we actually need to create a JsBlock, which +/// is needed if there is an invalid expression within a branch of code. +/// +/// For example, `if` has two branches, the 'yes' and 'no' paths. +/// When we cannot lower it as a ternary, we have to create two JsBlocks +/// +/// The `target` param is a shared temporary variable that will be assigned +/// to the result of the block. +/// +/// Since blocks can be void or noreturn, this target is not always +/// assigned to. The `did_assign` flag is used to indicate that. +/// +/// For expressions that do not have branches, use `hoist_block_to_expr` +/// +/// TODO(@paperdave): move `did_assign` into the return value +pub fn hoist_block_to_js_block( + ctx: &mut BlockConvertContext, + block: Block, + loc: Loc, + target: &TempId, + did_assign: &mut bool, +) -> JsBlock { + // A non-block will become a block with a single expression: + // _temp = expr; + // + // We will also apply this transformation to blocks that are valid + // expressions, because that is more concise. + if block.is_valid_js_expression() { + return JsBlock::new( + vec![new_js_assignment_statement( + convert_valid_block(ctx, block, loc.clone()), + target.get(ctx), + loc.clone(), + )], + loc, + ); + } + + let mut child = ctx.new_child(); + + for statement in block.statements { + if let None = process_statement(&mut child, statement) { + return child.into(); + }; + } + + // If the last statement is an expression, we need + // to assign to the temporary variable. + match child.take_last_statement_if_expr() { + Some(expr) => { + *did_assign = true; + child.add_statement(new_js_assignment_statement( + expr, + target.get(ctx), + loc.clone(), + )); + } + _ => {} + } + + child.into() +} diff --git a/crates/js_ast/convert/hoist_expr.rs b/crates/js_ast/convert/hoist_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..a79cb5a9928c6a95e0e49e9e9a18ff44ffd77646 --- /dev/null +++ b/crates/js_ast/convert/hoist_expr.rs @@ -0,0 +1,132 @@ +use super::convert_valid_expr; +use super::hoist_block_to_expr; +use super::hoist_if_expr; +use super::is_valid_js_expression::*; +use super::BlockConvertContext; +// use super::HasSideEffects; +use crate::js_ast::*; +use crate::util::is_js_identifier; +use ast::*; + +/// Pushes a decl with a generated identifier to the hoisted result +/// and returns an identifier expression for it. +pub fn hoist_expr_if_needed(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + // if !expr.has_side_effects() { + // return Some(ctx.hoist_expression_contents(expr)?); + // } + + let init = hoist_expr_contents(ctx, expr)?; + + let id = ctx.generate_temp_id("_"); + let loc = init.loc.clone(); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: id.get(&ctx), + init: Some(init), + is_constant: true, + }), + loc.clone(), + None, + )); + + Some(JsExpr::new(JsExprData::Identifier(id.get(&ctx)), loc, None)) +} + +/// Returning `None` from this means that the expression is noreturn, and that +/// the rest of the statement never occurs and can be stripped. +pub fn hoist_expr_contents(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + if expr.is_valid_js_expression() { + return Some(convert_valid_expr(ctx, expr)); + } + + match expr.data { + ExprData::Binary { op, left, right } => { + let left = hoist_expr_if_needed(ctx, *left)?; + let right = hoist_expr_if_needed(ctx, *right)?; + + Some(JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Unary { op, value } => { + let value = hoist_expr_if_needed(ctx, *value)?; + Some(JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Field { base, field } => { + let base = hoist_expr_if_needed(ctx, *base)?; + let field = hoist_expr_if_needed(ctx, *field)?; + + Some(JsExpr::new( + match field.data { + JsExprData::String(name) if is_js_identifier(name.as_str()) => { + JsExprData::Dot { + base: Box::new(base), + name, + } + } + _ => JsExprData::Index { + base: Box::new(base), + index: Box::new(field), + }, + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Block(block) => hoist_block_to_expr(ctx, block, expr.loc, expr.comment), + + ExprData::If(_) => hoist_if_expr(ctx, expr), + + ExprData::Return(expr) => { + let loc = expr.loc.clone(); + let return_value = hoist_expr_if_needed(ctx, *expr)?; + ctx.add_statement(JsStatement::new( + JsStatementData::Return(return_value), + loc, + None, + )); + None + } + + ExprData::Call { base, arguments } => { + let base = hoist_expr_if_needed(ctx, *base)?; + let arguments = arguments + .into_iter() + .map(|x| hoist_expr_if_needed(ctx, x)) + .collect::>>()?; + + Some(JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Fn { .. } + | ExprData::Builtin { .. } + | ExprData::Literal(_) + | ExprData::Identifier(_) => { + unreachable!("attempt to hoist an expression that does not require it") + } + } +} diff --git a/crates/js_ast/convert/hoist_if.rs b/crates/js_ast/convert/hoist_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..cbe2fffe3866392543f3d7c67f1fddf3b9fa44f0 --- /dev/null +++ b/crates/js_ast/convert/hoist_if.rs @@ -0,0 +1,113 @@ +use super::convert_valid_expr; +use super::hoist_block_to_js_block; +use super::hoist_expr_if_needed; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use super::TempId; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn hoist_if_expr(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + let data = match expr.data { + ExprData::If(data) => data, + _ => unreachable!("hoist_if_expr called on non-if expression"), + }; + + let temporary = ctx.generate_temp_id("_if"); + + let decl = ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: temporary.get(&ctx), + init: None, + is_constant: false, + }), + expr.loc.clone(), + None, + )); + + let condition = hoist_expr_if_needed(ctx, *data.condition)?; + let mut did_assign = false; + + let js_if = hoist_if_branch( + ctx, + data.yes, + data.no, + condition, + &mut did_assign, + &temporary, + ); + + ctx.add_statement(JsStatement::new( + JsStatementData::If(js_if), + expr.loc.clone(), + None, + )); + + if did_assign { + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&ctx)), + expr.loc, + expr.comment, + )) + } else { + decl.delete(ctx); + Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) + } +} + +pub fn hoist_if_branch( + ctx: &mut BlockConvertContext, + yes: BlockWithLoc, + no: Option, + condition: JsExpr, + did_assign: &mut bool, + temporary: &TempId, +) -> JsIf { + let yes = hoist_block_to_js_block(ctx, yes.block, yes.loc, &temporary, did_assign); + + let no = match no { + Some(BlockOrIf::Block(no)) => Some(JsElse::Block(hoist_block_to_js_block( + ctx, no.block, no.loc, &temporary, did_assign, + ))), + Some(BlockOrIf::If(else_if)) => { + // If the condition of this block has a side effect, we need to + // emit a block so that we can assign to the temporary variable. + // + // Otherwise, we can just emit the condition inline + if else_if.condition.is_valid_js_expression() { + let condition = convert_valid_expr(ctx, *else_if.condition.clone()); + Some(JsElse::If(Box::new(hoist_if_branch( + ctx, + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )))) + } else { + let mut temp_block = ctx.new_child(); + let condition = + hoist_expr_if_needed(&mut temp_block, *else_if.condition.clone()).unwrap(); + + temp_block.add_statement(JsStatement::new( + JsStatementData::If(hoist_if_branch( + ctx, + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )), + Loc::EMPTY, + None, + )); + + Some(JsElse::Block(temp_block.into())) + } + } + None => None, + }; + + JsIf { condition, yes, no } +} diff --git a/crates/js_ast/convert/is_valid_js_expression.rs b/crates/js_ast/convert/is_valid_js_expression.rs new file mode 100644 index 0000000000000000000000000000000000000000..feb2f206c2daf10b6be3f68358ad4097788adc08 --- /dev/null +++ b/crates/js_ast/convert/is_valid_js_expression.rs @@ -0,0 +1,91 @@ +use ast::*; + +/// "Valid" in this context means that the expression does not contain JS statements +/// Meaning conversion is a direct map from a blight as node into a JS one +/// +/// A valid expression should not go through any hoist logic, but if an expression +/// isn't valid, then it means that it must be split into multiple JS statements +/// inside of a block. +pub trait IsValidJSExpression { + /// Can this be lowered to a JS ast without hoisting items + /// ex: a `return` expression is not a valid JS expression + 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/convert/mod.rs b/crates/js_ast/convert/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..870912a6c10fe558c69cde35f714fd9961f9497a --- /dev/null +++ b/crates/js_ast/convert/mod.rs @@ -0,0 +1,42 @@ +mod block_convert_context; +mod convert_valid_block; +mod convert_valid_expr; +mod convert_valid_if; +mod convert_valid_statement; +mod hoist_block_to_expr; +mod hoist_block_to_js_block; +mod hoist_expr; +mod hoist_if; +mod is_valid_js_expression; +mod process_function; +mod process_statement; +mod side_effects; +mod tree_shake_statement_list; + +pub use block_convert_context::*; +pub use convert_valid_block::*; +pub use convert_valid_expr::*; +pub use convert_valid_if::*; +pub use convert_valid_statement::*; +pub use hoist_block_to_expr::*; +pub use hoist_block_to_js_block::*; +pub use hoist_expr::*; +pub use hoist_if::*; +pub use is_valid_js_expression::*; +pub use process_function::*; +pub use process_statement::*; +// pub use side_effects::*; +pub use tree_shake_statement_list::*; + +use crate::js_ast::JsModule; +use ast::Module; + +pub fn blight_to_js(blight: Module) -> JsModule { + let mut root_context = BlockConvertContext::new(); + + for statement in blight.statements { + process_top_level_statement(&mut root_context, statement).unwrap(); + } + + root_context.into() +} diff --git a/crates/js_ast/convert/process_function.rs b/crates/js_ast/convert/process_function.rs new file mode 100644 index 0000000000000000000000000000000000000000..d45090b7889d25c279447ce1dfc3bd7f3297b19e --- /dev/null +++ b/crates/js_ast/convert/process_function.rs @@ -0,0 +1,63 @@ +use super::convert_valid_expr; +use super::hoist_expr_if_needed; +use super::process_statement; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn process_function_to_arrow( + ctx: &mut BlockConvertContext, + body: Expr, + params: Vec, + loc: Loc, + comment: Option, +) -> JsExpr { + // A block that is a valid expression will just become + // that expression. We can convert multiple expressions + // into a comma expression. + let body = if body.is_valid_js_expression() { + JsArrowBody::Expr(convert_valid_expr(ctx, body)) + } else { + let mut child = ctx.new_child(); + + match body.data { + ExprData::Block(block) => { + for statement in block.statements { + if let None = process_statement(&mut child, statement) { + break; + }; + } + + if let Some(expr) = child.take_last_statement_if_expr() { + child.add_statement(JsStatement::new( + JsStatementData::Return(expr), + loc.clone(), + None, + )); + } + } + _ => { + if let Some(expr) = hoist_expr_if_needed(&mut child, body) { + child.add_statement(JsStatement::new( + JsStatementData::Return(expr), + loc.clone(), + None, + )); + } + } + } + + JsArrowBody::Block(child.into()) + }; + + JsExpr::new( + JsExprData::ArrowFunction { + params: params.into_iter().map(|x| x.name).collect(), + body: Box::new(body), + }, + loc.clone(), + comment.map(|x| x.into()), + ) +} diff --git a/crates/js_ast/convert/process_statement.rs b/crates/js_ast/convert/process_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..f2cdaabb1636d75a56592e38f641a90d48b3aa58 --- /dev/null +++ b/crates/js_ast/convert/process_statement.rs @@ -0,0 +1,95 @@ +use super::convert_valid_statement; +use super::hoist_expr_contents; +use super::process_function_to_arrow; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; + +pub fn process_statement(ctx: &mut BlockConvertContext, statement: Statement) -> Option<()> { + if statement.is_valid_js_expression() { + let result = convert_valid_statement(ctx, statement); + ctx.add_statement(result); + return Some(()); + } + + match statement { + Statement::VarDecl(decl) => { + // visit init before name, this way allows for: + // + // let a = a; + // + // and `a` will be looked up before it is re-declared. + // we will also return in the noreturn case + let init = hoist_expr_contents(ctx, decl.init)?; + + let name = ctx.declare_identifier(decl.name); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: name.get(&ctx), + init: Some(init), + is_constant: decl.is_constant, + }), + decl.loc, + decl.comment.map(|x| x.into()), + )); + + Some(()) + } + Statement::Expr(expr) => { + if let Some(expr) = hoist_expr_contents(ctx, expr) { + let loc = expr.loc.clone(); + ctx.add_statement(JsStatement::new( + JsStatementData::Expression(expr), + loc, + None, + )); + Some(()) + } else { + None + } + } + Statement::FnDecl(FnDecl { + name, + params, + body, + loc, + comment, + .. + }) => { + let name = ctx.declare_identifier(name); + + let processed = Some(process_function_to_arrow( + ctx, + body, + params, + loc.clone(), + None, + )); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: name.get(&ctx), + init: processed, + is_constant: true, + }), + loc, + comment.map(|x| x.into()), + )); + + Some(()) + } + Statement::Comment(_) => Some(()), + } +} + +pub fn process_top_level_statement( + ctx: &mut BlockConvertContext, + statement: TopLevelStatement, +) -> Option<()> { + match statement.to_statement() { + Some(stmt) => process_statement(ctx, stmt), + None => todo!("process_top_level_statement"), + } +} diff --git a/crates/js_ast/convert/side_effects.rs b/crates/js_ast/convert/side_effects.rs new file mode 100644 index 0000000000000000000000000000000000000000..5eeaea8a53069fe637943bc72add4cb5c202fcc5 --- /dev/null +++ b/crates/js_ast/convert/side_effects.rs @@ -0,0 +1,91 @@ +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::VarDecl(decl) => decl.init.has_side_effects(), + Statement::Comment(_) => false, + Statement::FnDecl(_) => 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/convert/tree_shake_statement_list.rs b/crates/js_ast/convert/tree_shake_statement_list.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7a631e29c443f71ddfe2409eb400df6ebe534cc --- /dev/null +++ b/crates/js_ast/convert/tree_shake_statement_list.rs @@ -0,0 +1,31 @@ +use crate::js_ast::*; + +/// Removes statements that are not needed from a JS block. Very limited. +/// +/// BlockConvertContext will automatically execute this when converting +/// itself into a Statement list or other data structure. +/// +/// What this does: +/// - Remove discarded literals `null` -> `` +pub fn tree_shake_statement_list(list: &mut Vec) { + let mut set_i = 0; + for i in 0..list.len() { + let stmt = list.get(i).unwrap(); + let should_keep = match &stmt.data { + JsStatementData::Expression(expr) => !expr.is_literal(), + JsStatementData::Return(_) => true, + JsStatementData::Export(_) => true, + JsStatementData::Declaration(_) => true, + JsStatementData::Function(_) => true, + JsStatementData::If(_) => true, + }; + + if should_keep { + if set_i != i { + list.swap(i, set_i) + } + set_i += 1; + } + } + list.truncate(set_i) +} diff --git a/crates/js_ast/js_ast.rs b/crates/js_ast/js_ast.rs new file mode 100644 index 0000000000000000000000000000000000000000..a580ac8502ce38df702844a319c825cd030a03ad --- /dev/null +++ b/crates/js_ast/js_ast.rs @@ -0,0 +1,190 @@ +use ast::{BinaryOperator, Comment, UnaryOperator}; +use num::bigint::BigInt; +use tokenizer::Loc; + +#[derive(Debug, PartialEq, Clone)] +pub struct JsExpr { + pub data: JsExprData, + pub loc: Loc, + pub comment: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsKV { + pub key: JsExpr, + pub value: JsExpr, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsExprData { + /// node emitted by the '@raw' building + Raw(String), + // Litearls + Number(f64), + BigInt(BigInt), + String(String), + Bool(bool), + Array(Vec), + Object(Vec), + Null, + Undefined, + + Identifier(String), + Unary { + op: UnaryOperator, + value: Box, + }, + Binary { + op: BinaryOperator, + left: Box, + right: Box, + }, + Dot { + base: Box, + name: String, + }, + Index { + base: Box, + index: Box, + }, + Call { + base: Box, + arguments: Vec, + }, + Function { + name: Option, + params: Vec, + body: Box, + }, + ArrowFunction { + params: Vec, + body: Box, + }, + Ternary { + condition: Box, + yes: Box, + no: Option>, + }, + Comma { + items: Vec, + }, + Void(Box), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsStatement { + pub data: JsStatementData, + pub loc: Loc, + 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(JSDeclaration), + Function(JSFunctionDeclaration), + If(JsIf), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsExport { + Named(Vec), + Function(Box), + Declaration(Box), + Default(JsExpr), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JSNamedExport { + pub identifier: String, + pub export_name: String, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsBlock { + pub statements: Vec, + pub loc: Loc, +} + +impl JsExpr { + 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 } + } +} + +#[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 new file mode 100644 index 0000000000000000000000000000000000000000..9d29be2c29a04ae7f9bc7701413e57ec70196e62 --- /dev/null +++ b/crates/js_ast/lib.rs @@ -0,0 +1,7 @@ +mod convert; +mod js_ast; +mod util; + +pub use convert::*; +pub use js_ast::*; +pub use util::*; diff --git a/crates/js_ast/src/lib.rs b/crates/js_ast/src/lib.rs deleted file mode 100644 index f0f738cde7b062470fb261f949c656f770af9b8a..0000000000000000000000000000000000000000 --- a/crates/js_ast/src/lib.rs +++ /dev/null @@ -1,348 +0,0 @@ -use std::collections::HashMap; - -use miette::Result; - -use ast::Ast; - -mod types; -pub use types::{JsExpression, JsStatement}; - -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: JsExpression) -> JsExpression { - self.context - .last_mut() - .unwrap() - .statements - .push(JsStatement::VariableDeclaration { - name: format!("${}", self.internal_counter), - value, - }); - self.internal_counter += 1; - JsExpression::Identifier(format!("${}", self.internal_counter - 1)) - } - - fn push(self: &mut Self, statement: JsStatement) { - self.context.last_mut().unwrap().statements.push(statement); - } - - fn peek(self: &Self) -> &JsStatement { - self.context.last().unwrap().statements.last().unwrap() - } - - 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) - } - } - - 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); - } -} - -// 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: &Ast, ctx: &mut TranslateContext) -> JsExpression { - match ast { - Ast::Number(value) => JsExpression::Number(*value), - Ast::String(value) => JsExpression::String(value.clone()), - Ast::Identifier(value) => JsExpression::Identifier(ctx.get_identifier(value.clone())), - Ast::Bool(value) => JsExpression::Bool(*value), - 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_internal_variable(JsExpression::FunctionCall { - name: Box::new(JsExpression::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() { - JsExpression::String(value) => { - return JsExpression::Raw(value.clone()); - } - _ => panic!("@raw expects a string argument"), - } - } - _ => panic!("Unknown built-in function {}", name), - }, - _ => panic!("Function name must be an identifier"), - } - } - Ast::Operation { - operator, - left, - right, - } => { - let left_handle = get_expression_handle(left, ctx); - let right_handle = get_expression_handle(right, ctx); - JsExpression::Operation { - operator: operator.clone(), - left: Box::new(left_handle), - right: Box::new(right_handle), - } - } - Ast::Block(block) => { - if !block.implicit_return { - panic!("Expression blocks must have an implicit return"); - } - ctx.push_context(); - for (i, statement) in block.value.iter().enumerate() { - if i == block.value.len() - 1 { - break; - } else { - translate_statement(statement, ctx); - } - } - let result = get_expression_handle(block.value.last().unwrap(), ctx); - let value = ctx.pop_context(); - value.iter().for_each(|x| ctx.push(x.clone())); - result - } - _ => { - panic!("unimplemented: {:?}", ast); - } - } -} - -fn translate_statement(ast: &Ast, ctx: &mut TranslateContext) { - match ast { - Ast::ConstantDeclaration { - name, - return_type: _, - value, - } => { - let handle = get_expression_handle(value, ctx); - let rename = ctx.declare_variable(name.clone()); - ctx.push(JsStatement::ConstantDeclaration { - name: rename, - value: handle, - }); - } - Ast::VariableDeclaration { - name, - return_type: _, - value, - } => { - let handle = get_expression_handle(value, ctx); - let rename = ctx.declare_variable(name.clone()); - ctx.push(JsStatement::VariableDeclaration { - name: rename, - value: handle, - }); - } - 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::VariableDeclaration { name, .. } - | JsStatement::ConstantDeclaration { name, .. } => { - let original_name = name.split("$").next().unwrap().to_string(); - ctx.push(JsStatement::Export(Box::new( - JsStatement::ConstantDeclaration { - name: original_name, - value: JsExpression::Identifier(name.clone()), - }, - ))); - } - _ => { - 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::If { - condition, - value: block, - else_value, - } => { - let handle = get_expression_handle(condition, ctx); - - 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(); - let else_value = match else_value { - Some(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); - } - } - Some(ctx.pop_context()) - } - None => None, - }; - - ctx.push(JsStatement::If { - condition: handle, - value, - else_value, - }); - } - 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(JsExpression::FunctionCall { - name: Box::new(JsExpression::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() { - JsExpression::String(value) => { - ctx.push(JsStatement::Expression(JsExpression::FunctionCall { - name: Box::new(JsExpression::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(_ast: Vec) -> Result> { - let mut ctx = TranslateContext::new(); - - for ast in _ast { - translate_statement(&ast, &mut ctx); - } - - Ok(ctx.pop_context()) -} diff --git a/crates/js_ast/src/types.rs b/crates/js_ast/src/types.rs deleted file mode 100644 index 18a2788da595867d00cc1249f0cbd40f5487f3d0..0000000000000000000000000000000000000000 --- a/crates/js_ast/src/types.rs +++ /dev/null @@ -1,54 +0,0 @@ -use num::bigint::BigInt; -use std::collections::HashMap; - -use util::Operator; - -#[derive(Debug, PartialEq, Clone)] -pub enum JsExpression { - Number(f64), - BigInt(BigInt), - String(String), - Bool(bool), - Identifier(String), - Raw(String), - Array(Vec), - Object(HashMap), - Operation { - operator: Operator, - left: Box, - right: Box, - }, - Function { - arguments: Vec, - value: Vec, - }, - FunctionCall { - name: Box, - arguments: Vec, - }, -} - -#[derive(Debug, PartialEq, Clone)] -pub enum JsStatement { - Expression(JsExpression), - Export(Box), - Return(JsExpression), - VariableDeclaration { - name: String, - value: JsExpression, - }, - ConstantDeclaration { - name: String, - value: JsExpression, - }, - Function { - name: String, - arguments: Vec, - value: Vec, - }, - If { - condition: JsExpression, - value: Vec, - else_value: Option>, - }, -} 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 0071929ae0046ab381623d16ae23e397e7f36393..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 @@ -11,4 +12,3 @@ thiserror.workspace = true num.workspace = true js_ast = { path = "../js_ast" } -util = { path = "../util" } diff --git a/crates/js_printer/lib.rs b/crates/js_printer/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..b90a4a2850196ea232831af66d36017ead017334 --- /dev/null +++ b/crates/js_printer/lib.rs @@ -0,0 +1,372 @@ +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 } => { + let wrap = false; + if wrap { + write!(writer, "(")?; + } + condition.print(writer)?; + write!(writer, "?")?; + yes.print(writer)?; + write!(writer, ":")?; + if let Some(no) = no { + no.print(writer)?; + } else { + write!(writer, "null")?; + } + if wrap { + write!(writer, ")")?; + } + Ok(()) + } + JsExprData::Comma { items } => { + let wrap = true; + if wrap { + write!(writer, "(")?; + } + for (i, item) in items.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + item.print(writer)?; + } + if wrap { + write!(writer, ")")?; + } + Ok(()) + } + JsExprData::Void(expr) => { + 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/lib.rs b/crates/js_printer/src/lib.rs deleted file mode 100644 index 49f3aa65ab7579c2a6e9a4b0470a4bc256e51557..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod print_expression; -mod print_statement; - -use print_statement::print_statement; - -use js_ast::JsStatement; - -pub fn print(forest: Vec) -> String { - let mut result = String::new(); - - for tree in forest { - result.push_str(&print_statement(&tree)); - } - - result -} diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs deleted file mode 100644 index ff87066c667b0ed8496173cb4fbe09422702e859..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/print_expression.rs +++ /dev/null @@ -1,266 +0,0 @@ -use js_ast::JsExpression; - -use crate::print_statement::print_statement; - -pub fn print_expression(statement: &JsExpression) -> String { - match statement { - JsExpression::Raw(value) => format!("{}", value), - JsExpression::Number(value) => format!("{}", value), - JsExpression::BigInt(value) => format!("{}n", value), - JsExpression::String(value) => { - let mut result = String::new(); - // TODO: This is an INCREDIBLY naive implementation of escaping. - // Also it's probably not even correct. - for char in value.chars() { - match char { - '\n' => result.push_str("\\n"), - '\r' => result.push_str("\\r"), - '\t' => result.push_str("\\t"), - '"' => result.push_str("\\\""), - '\\' => result.push_str("\\\\"), - _ => result.push(char), - } - } - format!("\"{}\"", result) - } - JsExpression::Bool(value) => format!("{}", value), - JsExpression::Identifier(value) => format!("{}", value), - JsExpression::Array(value) => format!( - "[{}]", - value - .iter() - .map(|x| print_expression(x)) - .collect::>() - .join(",") - ), - JsExpression::Object(value) => format!( - "{{{}}}", - value - .iter() - .map(|(k, v)| format!("\"{}\":{}", k, print_expression(v))) - .collect::>() - .join(",") - ), - JsExpression::Operation { - operator, - left, - right, - } => format!( - "{}{}{}", - print_expression(left), - operator.clone().to_string(), - print_expression(right) - ), - JsExpression::Function { arguments, value } => format!( - "({})=>{{{}}}", - arguments.join(","), - value - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - JsExpression::FunctionCall { name, arguments } => format!( - "{}({})", - match *name.clone() { - JsExpression::Identifier(value) => value.clone(), - JsExpression::Raw(value) => value.clone(), - _ => panic!("Function call name must be an identifier"), - }, - arguments - .iter() - .map(|x| print_expression(x)) - .collect::>() - .join(",") - ), - } -} - -#[cfg(test)] -mod tests { - use crate::print_expression::print_expression; - use js_ast::JsExpression; - use js_ast::JsStatement; - use num::{bigint::Sign, BigInt}; - use std::collections::HashMap; - use util::Operator; - - #[test] - fn number() { - assert_eq!(print_expression(&JsExpression::Number(1.0)), "1"); - assert_eq!(print_expression(&JsExpression::Number(1.5)), "1.5"); - assert_eq!( - print_expression(&JsExpression::Number(1.5e10)), - "15000000000" - ); - assert_eq!( - print_expression(&JsExpression::Number(1.5e-10)), - "0.00000000015" - ); - } - - #[test] - fn bigint() { - assert_eq!( - print_expression(&JsExpression::BigInt(BigInt::new(Sign::Plus, vec![1]))), - "1n" - ); - assert_eq!( - print_expression(&JsExpression::BigInt(BigInt::new( - Sign::Plus, - vec![10000000] - ))), - "10000000n" - ); - assert_eq!( - print_expression(&JsExpression::BigInt(BigInt::new( - Sign::Minus, - vec![10000000] - ))), - "-10000000n" - ); - } - - #[test] - fn string() { - assert_eq!( - print_expression(&JsExpression::String("".to_string())), - "\"\"" - ); - assert_eq!( - print_expression(&JsExpression::String("hello".to_string())), - "\"hello\"" - ); - assert_eq!( - print_expression(&JsExpression::String("hello\nworld".to_string())), - "\"hello\\nworld\"" - ); - } - - #[test] - fn bool() { - assert_eq!(print_expression(&JsExpression::Bool(true)), "true"); - assert_eq!(print_expression(&JsExpression::Bool(false)), "false"); - } - - #[test] - fn identifier() { - assert_eq!( - print_expression(&JsExpression::Identifier("hello".to_string())), - "hello" - ); - } - - #[test] - fn array() { - assert_eq!(print_expression(&JsExpression::Array(vec![])), "[]"); - assert_eq!( - print_expression(&JsExpression::Array(vec![JsExpression::Number(1.0)])), - "[1]" - ); - assert_eq!( - print_expression(&JsExpression::Array(vec![ - JsExpression::Number(1.0), - JsExpression::Number(2.0) - ])), - "[1,2]" - ); - } - - #[test] - fn object() { - assert_eq!( - print_expression(&JsExpression::Object(HashMap::new())), - "{}" - ); - let mut map = HashMap::new(); - map.insert("hello".to_string(), JsExpression::Number(1.0)); - assert_eq!( - print_expression(&JsExpression::Object(map)), - "{\"hello\":1}" - ); - } - - #[test] - fn operation() { - assert_eq!( - print_expression(&JsExpression::Operation { - operator: Operator::Add, - left: Box::new(JsExpression::Number(1.0)), - right: Box::new(JsExpression::Number(2.0)), - }), - "1+2" - ); - assert_eq!( - print_expression(&JsExpression::Operation { - operator: Operator::Subtract, - left: Box::new(JsExpression::Number(1.0)), - right: Box::new(JsExpression::Number(2.0)), - }), - "1-2" - ); - } - - #[test] - fn function() { - use crate::print_expression::print_expression; - use js_ast::JsExpression; - - assert_eq!( - print_expression(&JsExpression::Function { - arguments: vec![], - value: vec![], - }), - "()=>{}" - ); - assert_eq!( - print_expression(&JsExpression::Function { - arguments: vec!["a".to_string()], - value: vec![], - }), - "(a)=>{}" - ); - assert_eq!( - print_expression(&JsExpression::Function { - arguments: vec!["a".to_string(), "b".to_string()], - value: vec![], - }), - "(a,b)=>{}" - ); - assert_eq!( - print_expression(&JsExpression::Function { - arguments: vec!["a".to_string(), "b".to_string()], - value: vec![JsStatement::VariableDeclaration { - name: "a".to_string(), - value: JsExpression::Number(1.0) - }], - }), - "(a,b)=>{let a=1;}" - ); - } - - #[test] - fn function_call() { - assert_eq!( - print_expression(&JsExpression::FunctionCall { - name: Box::new(JsExpression::Identifier("hello".to_string())), - arguments: vec![], - }), - "hello()" - ); - assert_eq!( - print_expression(&JsExpression::FunctionCall { - name: Box::new(JsExpression::Identifier("hello".to_string())), - arguments: vec![JsExpression::Number(1.0)], - }), - "hello(1)" - ); - assert_eq!( - print_expression(&JsExpression::FunctionCall { - name: Box::new(JsExpression::Identifier("hello".to_string())), - arguments: vec![JsExpression::Number(1.0), JsExpression::Number(2.0)], - }), - "hello(1,2)" - ); - } -} diff --git a/crates/js_printer/src/print_statement.rs b/crates/js_printer/src/print_statement.rs deleted file mode 100644 index 4a9bf75b17a5f0fde0844473aad91c65dbb51b92..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/print_statement.rs +++ /dev/null @@ -1,145 +0,0 @@ -use js_ast::JsStatement; - -use crate::print_expression::print_expression; - -pub fn print_statement(statement: &JsStatement) -> String { - match statement { - JsStatement::Expression(value) => format!("{};", print_expression(value)), - JsStatement::Export(value) => format!("export {}", print_statement(value)), - JsStatement::Return(value) => format!("return {};", print_expression(value)), - JsStatement::VariableDeclaration { name, value } => { - format!("let {}={};", name, print_expression(value)) - } - JsStatement::ConstantDeclaration { name, value } => { - format!("const {}={};", name, print_expression(value)) - } - JsStatement::Function { - name, - arguments, - value, - } => format!( - "function {}({}){{{}}}", - name, - arguments.join(","), - value - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - JsStatement::If { - condition, - value, - else_value, - } => { - let else_value = match else_value { - Some(value) => format!( - "else{{{}}}", - value - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - None => "".to_string(), - }; - format!( - "if({}){{{}}}{}", - print_expression(condition), - value - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join(""), - else_value - ) - } - } -} - -#[cfg(test)] -mod tests { - use crate::print_statement::print_statement; - use js_ast::JsExpression; - use js_ast::JsStatement; - - #[test] - fn variable_declaration() { - assert_eq!( - print_statement(&JsStatement::VariableDeclaration { - name: "x".to_string(), - value: JsExpression::Number(1.0), - }), - "let x=1;" - ); - } - - #[test] - fn constant_declaration() { - assert_eq!( - print_statement(&JsStatement::ConstantDeclaration { - name: "x".to_string(), - value: JsExpression::Number(1.0), - }), - "const x=1;" - ); - } - - #[test] - fn function_declaration() { - assert_eq!( - print_statement(&JsStatement::Function { - name: "x".to_string(), - arguments: vec!["y".to_string()], - value: vec![JsStatement::Expression(JsExpression::Number(1.0))], - }), - "function x(y){1;}" - ); - } - - #[test] - fn expression() { - assert_eq!( - print_statement(&JsStatement::Expression(JsExpression::Number(1.0))), - "1;" - ); - } - - #[test] - fn export() { - assert_eq!( - print_statement(&JsStatement::Export(Box::new(JsStatement::Expression( - JsExpression::Number(1.0) - )))), - "export 1;" - ); - } - - #[test] - fn return_statement() { - assert_eq!( - print_statement(&JsStatement::Return(JsExpression::Number(1.0))), - "return 1;" - ); - } - - #[test] - fn if_statement() { - assert_eq!( - print_statement(&JsStatement::If { - condition: JsExpression::Number(1.0), - value: vec![JsStatement::Expression(JsExpression::Number(2.0))], - else_value: None, - }), - "if(1){2;}" - ); - assert_eq!( - print_statement(&JsStatement::If { - condition: JsExpression::Number(1.0), - value: vec![JsStatement::Expression(JsExpression::Number(2.0))], - else_value: Some(vec![JsStatement::Expression(JsExpression::Number(3.0))]), - }), - "if(1){2;}else{3;}" - ); - } -} diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..220932c2a7189d96bb8a2b807c9d0c97292a2d0a --- /dev/null +++ b/crates/parser/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "parser" +version = "0.1.0" +edition = "2021" + +[lib] +path = "./parser.rs" + +[dependencies] +miette.workspace = true +thiserror.workspace = true +num.workspace = true + +ast = { path = "../ast" } +tokenizer = { path = "../tokenizer" } diff --git a/crates/parser/parse_block.rs b/crates/parser/parse_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..4ae805a759d0dd7b58c6642c94dc83e5c821c9e4 --- /dev/null +++ b/crates/parser/parse_block.rs @@ -0,0 +1,64 @@ +use crate::{parse_statement::parse_statement, Parser}; +use ast::Block; +use miette::Result; +use tokenizer::Token; + +/// Precondition: The first token exists and is '{' +pub fn parse_block(p: &mut Parser) -> Result { + match p.next() { + Some(Token::OpenBrace) => (), + _ => unreachable!(), + } + + let mut statements = Vec::new(); + + '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) => { + return Err(err); + } + }; + statements.push(statement.clone()); + + match p.peek() { + Some(Token::Semicolon) => loop { + p.next(); + match p.peek() { + Some(Token::ClosedBrace) => { + p.next(); + return Ok(Block { + statements, + has_implicit_return: false, + }); + } + _ => continue 'outer, + } + }, + Some(Token::ClosedBrace) => { + p.next(); + if !statement.is_expression() { + return Err(p + .error("Expected semicolon or '}' after statement".to_string()) + .into()); + } + + return Ok(Block { + statements, + has_implicit_return: true, + }); + } + _ => { + if statement.require_semicolon(is_statement_like_expression) { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..4bdda948b2aef6eca565194538d7c4aeac397753 --- /dev/null +++ b/crates/parser/parse_call_arguments.rs @@ -0,0 +1,36 @@ +use ast::Expr; +use miette::Result; +use tokenizer::Token; + +use crate::{parse_expr::parse_expr, Parser}; + +/// Precondition: The '(' was already consumed +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); + + match p.peek() { + Some(Token::Comma) => { + _ = p.next(); + continue; + } + Some(Token::ClosedParen) => { + _ = p.next(); + return Ok(args); + } + _ => { + return Err(p + .error("Expected ',' or ')' after argument".to_string()) + .into()) + } + } + } +} diff --git a/crates/parser/parse_declaration.rs b/crates/parser/parse_declaration.rs new file mode 100644 index 0000000000000000000000000000000000000000..80f140076bb711e3fdf03716bbccd416096a0c35 --- /dev/null +++ b/crates/parser/parse_declaration.rs @@ -0,0 +1,50 @@ +use ast::{DocComment, Type, VarDecl}; +use miette::Result; +use tokenizer::Token; + +use crate::parse_expr::parse_expr; +use crate::parse_type::parse_type; +use crate::Parser; + +/// Precondition: The first token exists and is either 'var' or 'let' +pub fn parse_declaration( + p: &mut Parser, + is_pub: bool, + comment: Option, +) -> Result { + let loc = p.loc(); + let is_constant = match p.next() { + Some(Token::Var) => false, + Some(Token::Let) => true, + _ => unreachable!(), + }; + let name = match p.next() { + Some(Token::Identifier(name)) => name, + Some(tok) => return Err(p.error(format!("Unexpected {}", tok)).into()), + None => return Err(p.error(format!("Unexpected end of file")).into()), + }; + let return_type = match p.peek() { + Some(Token::Colon) => { + p.next(); + parse_type(p)? + } + _ => Type::Infer, + }; + let init = match p.next() { + Some(Token::Equal) => parse_expr(p)?, + _ => { + return Err(p + .error("Variable must have an initializer".to_string()) + .into()) + } + }; + Ok(VarDecl { + name: name.clone(), + data_type: return_type, + init, + is_constant, + is_pub, + comment, + loc, + }) +} diff --git a/crates/parser/parse_doc_comment.rs b/crates/parser/parse_doc_comment.rs new file mode 100644 index 0000000000000000000000000000000000000000..f62e9f32b30c867c7c0542d2902956a337f7a86a --- /dev/null +++ b/crates/parser/parse_doc_comment.rs @@ -0,0 +1,15 @@ +use ast::DocComment; +use tokenizer::Token; + +use crate::Parser; + +pub fn maybe_read_doc_comment(p: &mut Parser) -> Option { + match p.peek() { + Some(Token::DocComment(data)) => { + let loc = p.loc(); + p.next(); + Some(DocComment { data, loc }) + } + _ => None, + } +} diff --git a/crates/parser/parse_expr.rs b/crates/parser/parse_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..693053995ac365681335bfdabb3ebdd0f0a96b3b --- /dev/null +++ b/crates/parser/parse_expr.rs @@ -0,0 +1,99 @@ +use ast::Associativity::*; +use ast::{BinaryOperator, Expr, ExprData, OperatorLevel}; +use miette::Result; + +use crate::parse_expr_basic::parse_expr_basic; +use crate::Parser; + +pub fn parse_expr(p: &mut Parser) -> Result { + let mut expr = parse_expr_basic(p)?; + + // Edge case for return. This has already parsed all the way to the end of the expression + if let ExprData::Return(_) = expr.data { + return Ok(expr); + } + + // Parse operators + loop { + if let Some(token) = p.peek() { + if let Some(level) = BinaryOperator::from_token(&token).map(|o| o.level()) { + _ = p.next(); + 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; + } + } + break; + } + + Ok(expr) +} + +/// Prerequisite: `p.peek()` is an operator +fn parse_expr_binary( + p: &mut Parser, + op: BinaryOperator, + left: Expr, + level: OperatorLevel, +) -> Result { + let loc = p.loc(); + let mut right = parse_expr_basic(p)?; + + loop { + if let Some(token) = p.peek() { + if let Some(new_level) = BinaryOperator::from_token(&token).map(|o| o.level()) { + if new_level <= level { + _ = p.next(); + 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; + } + } + } + break; + } + + Ok(Expr::new( + ExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + loc, + None, + )) +} diff --git a/crates/parser/parse_expr_basic.rs b/crates/parser/parse_expr_basic.rs new file mode 100644 index 0000000000000000000000000000000000000000..efcf48fb65ecc9b0f8c8512e682ddfe7aab3d96b --- /dev/null +++ b/crates/parser/parse_expr_basic.rs @@ -0,0 +1,180 @@ +use ast::{BuiltinId, Expr, ExprData, Literal, UnaryOperator}; +use miette::Result; +use tokenizer::Token; + +use crate::{ + parse_block::parse_block, parse_call_arguments::parse_call_arguments, parse_expr::parse_expr, + parse_expr_if::parse_expr_if, Parser, +}; + +pub fn parse_expr_basic(p: &mut Parser) -> Result { + let loc = p.loc(); + let comments = p.take_comments(); + let mut expr = match p.peek() { + Some(Token::Number(number)) => { + _ = p.next(); + Expr::new( + ExprData::Literal(Literal::Number(number.clone())), + loc, + comments, + ) + } + Some(Token::String(string)) => { + _ = p.next(); + Expr::new( + ExprData::Literal(Literal::String(string.clone())), + loc, + comments, + ) + } + Some(Token::True) => { + _ = p.next(); + Expr::new(ExprData::Literal(Literal::Bool(true)), loc, comments) + } + Some(Token::False) => { + _ = p.next(); + Expr::new(ExprData::Literal(Literal::Bool(false)), loc, comments) + } + Some(Token::Void) => { + _ = p.next(); + Expr::new(ExprData::Literal(Literal::Void), loc, comments) + } + Some(Token::Identifier(name)) => { + let loc = p.loc(); + _ = p.next(); + if name.starts_with("@") { + if let Some(id) = BuiltinId::from_str(name.as_str()) { + if let Some(Token::OpenParen) = p.next() { + let arguments = parse_call_arguments(p)?; + Expr::new(ExprData::Builtin { id, arguments }, loc, comments) + } else { + return Err(p + .error_at(format!("Built-in function '{}' must be called", name), loc) + .into()); + } + } else { + return Err(p + .error_at(format!("Non-existant built-in: '{}'", name), loc) + .into()); + } + } else { + Expr::new(ExprData::Identifier(name), loc, comments) + } + } + + Some(Token::Return) => { + if p.allow_return { + return Err(p + .error("'return' cannot be used at top level".to_string()) + .into()); + } else { + _ = p.next(); + return Ok(Expr::new( + ExprData::Return(Box::new(parse_expr(p)?)), + loc, + comments, + )); + } + } + + Some(Token::If) => Expr::new(ExprData::If(parse_expr_if(p)?), loc, comments), + + Some(Token::OpenBrace) => Expr::new(ExprData::Block(parse_block(p)?), loc, comments), + + Some(Token::OpenParen) => { + _ = p.next(); + let expr = parse_expr(p)?; + match p.next() { + Some(Token::ClosedParen) => (), + Some(token) => { + return Err(p.error(format!("Expected ')', but got {:?}", token)).into()) + } + None => return Err(p.error("Unexpected end of file".to_string()).into()), + } + expr + } + + Some(t @ Token::Bang) | Some(t @ Token::Minus) | Some(t @ Token::Tilde) => { + _ = p.next(); + Expr::new( + ExprData::Unary { + op: match t { + Token::Bang => UnaryOperator::Not, + Token::Minus => UnaryOperator::Neg, + Token::Tilde => UnaryOperator::BitNot, + _ => unreachable!(), + }, + value: Box::new(parse_expr_basic(p)?), + }, + loc, + comments, + ) + } + + Some(t) => return Err(p.error(format!("Unexpected '{}'", t)).into()), + None => return Err(p.error("Unexpected end of file".to_string()).into()), + }; + + // Parse function calls and field accesses + loop { + match p.peek() { + Some(Token::Dot) => { + let loc = p.loc(); + _ = p.next(); + let field = match p.peek() { + Some(Token::Identifier(name)) => { + let loc = p.loc(); + p.next(); + Expr::new(ExprData::Literal(Literal::String(name)), loc, None) + } + _ => { + return Err(p.error("Expected identifier after dot".to_string()).into()); + } + }; + expr = Expr::new( + ExprData::Field { + base: Box::new(expr), + field: Box::new(field), + }, + loc, + None, + ); + } + Some(Token::OpenParen) => { + let loc = p.loc(); + _ = p.next(); + let arguments = parse_call_arguments(p)?; + expr = Expr::new( + ExprData::Call { + base: Box::new(expr), + arguments, + }, + loc, + None, + ); + } + Some(Token::OpenBracket) => { + let loc = p.loc(); + _ = p.next(); + let index = parse_expr(p)?; + if let Some(Token::ClosedBracket) = p.next() { + expr = Expr::new( + ExprData::Field { + base: Box::new(expr), + field: Box::new(index), + }, + loc, + None, + ); + } else { + return Err(p + .error("Expected closing bracket after index".to_string()) + .into()); + } + } + _ => break, + } + } + + Ok(expr) +} diff --git a/crates/parser/parse_expr_if.rs b/crates/parser/parse_expr_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..5af7b44fd7fbf2383a3c3d4ed31d243168859558 --- /dev/null +++ b/crates/parser/parse_expr_if.rs @@ -0,0 +1,70 @@ +use ast::{BlockOrIf, BlockWithLoc, If}; +use miette::Result; +use tokenizer::Token; + +use crate::parse_block::parse_block; +use crate::parse_expr::parse_expr; +use crate::Parser; + +/// Precondition: the first token exists and is 'if' +pub fn parse_expr_if(p: &mut Parser) -> Result { + match p.next() { + Some(Token::If) => (), + _ => unreachable!(), + } + + match p.peek() { + Some(Token::OpenBrace) => { + return Err(p + .error( + "To use a block as the condition for an if statement, wrap it in parentheses" + .to_string(), + ) + .into()); + } + _ => (), + } + + let condition = Box::new(parse_expr(p)?); + + let yes = match p.peek() { + Some(Token::OpenBrace) => { + let loc = p.loc(); + BlockWithLoc { + block: parse_block(p)?, + loc, + } + } + _ => { + return Err(p + .error("Expected a block after condition".to_string()) + .into()) + } + }; + + let no = match p.peek() { + Some(Token::Else) => { + _ = p.next(); + match p.peek() { + Some(Token::OpenBrace) => { + let loc = p.loc(); + // let comments = p.take_comments(); + Some(BlockOrIf::Block(BlockWithLoc { + block: parse_block(p)?, + loc, + })) + } + Some(Token::If) => { + // 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()), + } + } + _ => None, + }; + + Ok(If { condition, yes, no }) +} diff --git a/crates/parser/parse_fn_declaration.rs b/crates/parser/parse_fn_declaration.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1b576cec8709710e413f4f1ae9726d6ba80635f --- /dev/null +++ b/crates/parser/parse_fn_declaration.rs @@ -0,0 +1,122 @@ +use ast::{DocComment, Expr, ExprData, FnDecl, Type}; +use miette::Result; +use tokenizer::Token; + +use crate::parse_block::parse_block; +use crate::parse_expr::parse_expr; +use crate::parse_param::parse_param; +use crate::parse_type::parse_type; +use crate::Parser; + +/// Precondition: +pub fn parse_fn_declaration( + p: &mut Parser, + is_pub: bool, + comment: Option, +) -> Result { + let loc = p.loc(); + match p.next() { + Some(Token::Fn) => (), + _ => unreachable!(), + } + + let name = match p.next() { + Some(Token::Identifier(name)) => name, + _ => return Err(p.error("Expected identifier after 'fn'".to_string()).into()), + }; + + match p.peek() { + Some(Token::OpenParen) => p.next(), + _ => { + return Err(p + .error("Expected '(' after function name".to_string()) + .into()) + } + }; + + let mut params = Vec::new(); + + loop { + match p.peek() { + Some(Token::ClosedParen) => { + _ = p.next(); + break; + } + Some(Token::Identifier(_)) => { + params.push(parse_param(p)?); + + match p.peek() { + Some(Token::Comma) => { + p.next(); + if let Some(Token::ClosedParen) = p.peek() { + p.next(); + break; + } + } + Some(Token::ClosedParen) => { + p.next(); + break; + } + _ => { + let actual = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); + return Err(p + .error(format!( + "Expected ',' or ')' after identifier, got {}", + actual + )) + .into()); + } + }; + } + _ => { + return Err(p + .error("Expected ')' or identifier after '('".to_string()) + .into()) + } + } + } + + let return_type = match p.peek() { + Some(Token::RightArrow) => { + _ = p.next(); + parse_type(p)? + } + _ => Type::Infer, + }; + + let was_allow_return = p.allow_return; + p.allow_return = true; + + let body = match p.peek() { + Some(Token::OpenBrace) => { + let block_loc = p.loc(); + let block = parse_block(p)?; + Expr::new(ExprData::Block(block), block_loc, None) + } + Some(Token::BigRightArrow) => { + _ = p.next(); + parse_expr(p)? + } + _ => { + let actually = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); + return Err(p + .error(format!( + "Expected a block or '=>' to follow function declaration, but got {}", + actually, + )) + .into()); + } + }; + + p.allow_return = was_allow_return; + + Ok(FnDecl { + name, + return_type, + params, + body, + is_pub, + comment, + loc, + }) +} diff --git a/crates/parser/parse_module.rs b/crates/parser/parse_module.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3521bd641adcb0063461c9deee8e92593eff303 --- /dev/null +++ b/crates/parser/parse_module.rs @@ -0,0 +1,44 @@ +use crate::parse_doc_comment::maybe_read_doc_comment; +use crate::parse_top_level_statement::parse_top_level_statement; +use crate::Parser; +use ast::Module; +use miette::Result; +use tokenizer::Token; + +pub fn parse_module(p: &mut Parser) -> Result { + let mut statements = Vec::new(); + + 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() { + Some(Token::Semicolon) => { + while let Some(Token::Semicolon) = p.peek() { + p.next(); + } + } + _ => { + if statement.require_semicolon(is_statement_like_expression) { + return Err(p + .error("Expected semicolon after statement".to_string()) + .into()); + } + } + }; + + statements.push(statement); + + if p.empty() { + return Ok(Module { + statements, + comment: file_doc_comment, + }); + } + } +} diff --git a/crates/parser/parse_param.rs b/crates/parser/parse_param.rs new file mode 100644 index 0000000000000000000000000000000000000000..9e8f34be012b33c1811621ec434e413672df8888 --- /dev/null +++ b/crates/parser/parse_param.rs @@ -0,0 +1,30 @@ +use ast::Param; +use miette::Result; +use tokenizer::Token; + +use crate::{parse_type::parse_type, Parser}; + +/// Precondition: A token exists and is an identifier +pub fn parse_param(p: &mut Parser) -> Result { + let name = match p.next() { + Some(Token::Identifier(name)) => name.clone(), + _ => unreachable!(), + }; + + let param_type = match p.peek() { + Some(Token::Colon) => { + _ = p.next(); + parse_type(p)? + } + _ => { + return Err(p + .error("Function parameters require type annotations".to_string()) + .into()) + } + }; + + Ok(Param { + name: name.clone(), + param_type, + }) +} diff --git a/crates/parser/parse_statement.rs b/crates/parser/parse_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..537f9512571cf182854ce26e549ded5c65d729be --- /dev/null +++ b/crates/parser/parse_statement.rs @@ -0,0 +1,66 @@ +use ast::Statement; +use miette::Result; +use tokenizer::Token; + +use crate::parse_declaration::parse_declaration; +use crate::parse_doc_comment::maybe_read_doc_comment; +use crate::parse_expr::parse_expr; +use crate::parse_fn_declaration::parse_fn_declaration; +use crate::Parser; + +pub fn parse_statement(p: &mut Parser) -> Result { + if let Some(comments) = p.take_comments() { + return Ok(Statement::Comment(comments)); + } + + let m_doc_comment = maybe_read_doc_comment(p); + if let Some(doc_comment) = m_doc_comment.clone() { + if p.has_comments() { + // this implies that comments are immediately after the doc + return Err(p + .error_at(format!("Unattached doc comment"), doc_comment.loc) + .into()); + } + } + + match p.peek() { + Some(Token::DocComment(_)) => unreachable!(), + Some(Token::Pub) => { + _ = p.next(); + match p.peek() { + Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( + p, + true, + m_doc_comment, + )?)), + Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( + p, + true, + m_doc_comment, + )?)), + Some(_) => Err(p.error("Expected a declaration".to_string()).into()), + None => Err(p.error("Unexpected end of file".to_string()).into()), + } + } + Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( + p, + false, + m_doc_comment, + )?)), + Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( + p, + false, + m_doc_comment, + )?)), + Some(_) => { + if let Some(doc_comment) = m_doc_comment { + Err(p + .error_at(format!("Unattached doc comment"), doc_comment.loc) + .into()) + } else { + Ok(Statement::Expr(parse_expr(p)?)) + } + } + None => Err(p.error("Unexpected end of file".to_string()).into()), + } +} diff --git a/crates/parser/parse_top_level_statement.rs b/crates/parser/parse_top_level_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..b3383ecfa6134479f8e473c3e85f9023d58294ee --- /dev/null +++ b/crates/parser/parse_top_level_statement.rs @@ -0,0 +1,10 @@ +use ast::TopLevelStatement; +use miette::Result; + +use crate::{parse_statement::parse_statement, Parser}; + +/// Precondition: the next token exists +pub fn parse_top_level_statement(p: &mut Parser) -> Result { + // TODO: we will eventually have real statements that are exclusive to top-level + Ok(parse_statement(p)?.to_top_level()) +} diff --git a/crates/parser/parse_type.rs b/crates/parser/parse_type.rs new file mode 100644 index 0000000000000000000000000000000000000000..b3e022b0ec6eede07db734f24e5412e6b57ce4c1 --- /dev/null +++ b/crates/parser/parse_type.rs @@ -0,0 +1,26 @@ +use ast::Type; +use miette::Result; + +use tokenizer::Token; + +use crate::Parser; + +pub fn parse_type(p: &mut Parser) -> Result { + match p.next() { + Some(Token::Identifier(id)) => match id.as_str() { + "int" => Ok(Type::Int), + "float" => Ok(Type::Float), + "bigint" => Ok(Type::BigInt), + "bool" => Ok(Type::Bool), + "string" => Ok(Type::String), + invalid_type => Err(p + .error(format!( + "Invalid type '{}' (TODO: add custom type support)", + invalid_type + )) + .into()), + }, + Some(unexpected) => Err(p.error(format!("Expected type, got {}", unexpected)).into()), + None => Err(p.error("Expected type, got EOF".to_string()).into()), + } +} diff --git a/crates/parser/parser.rs b/crates/parser/parser.rs new file mode 100644 index 0000000000000000000000000000000000000000..465ce180e9bdd2c388361825407ee0d14bedbf05 --- /dev/null +++ b/crates/parser/parser.rs @@ -0,0 +1,144 @@ +mod parse_block; +mod parse_call_arguments; +mod parse_declaration; +mod parse_doc_comment; +mod parse_expr; +mod parse_expr_basic; +mod parse_expr_if; +mod parse_fn_declaration; +mod parse_module; +mod parse_param; +mod parse_statement; +mod parse_top_level_statement; +mod parse_type; + +use ast::Comment; +use ast::Module; +use miette::Diagnostic; +use miette::Result; +use parse_module::parse_module; +use thiserror::Error; +use tokenizer::tokenize; +use tokenizer::{Loc, Token}; + +pub fn parse(tokens: Vec) -> Result { + let mut p = Parser::new(tokens); + parse_module(&mut p) +} + +pub fn parse_from_string(string: String) -> Result { + let tokens = tokenize(string)?; + parse(tokens) +} + +#[derive(Error, Diagnostic, Debug)] +#[error("ParseError")] +pub struct ParseError { + #[help] + error: String, + loc: Option, +} + +impl ParseError { + pub fn new(error: String, loc: Option) -> ParseError { + ParseError { error, loc } + } +} + +#[derive(Debug)] +pub struct Parser { + /// TODO(@paperdave): switch to streaming tokenizer + one that can + /// report line and column info + tokens: Vec, + cursor: usize, + + /// Whether or not the parser is at the top level + pub allow_return: bool, + + comments: Vec, +} + +impl Parser { + pub fn new(tokens: Vec) -> Parser { + Parser { + tokens, + cursor: 0, + allow_return: false, + comments: vec![], + } + } + + pub fn empty(&self) -> bool { + self.cursor == self.tokens.len() + } + + // TODO(@paperdave): fix parser token stuff. its very messy rn + + pub fn next(&mut self) -> Option { + if self.cursor == self.tokens.len() { + return None; + } + + loop { + match self.tokens[self.cursor].clone() { + Token::Comment(comment) => { + self.comments.push(comment); + self.cursor += 1; + continue; + } + token => { + self.cursor += 1; + return Some(token); + } + } + } + } + + pub fn take_comments(&mut self) -> Option { + if self.comments.len() == 0 { + return None; + } + let comments = self.comments.clone(); + self.comments = vec![]; + Some(Comment { + data: comments.join("\n"), + }) + } + + pub fn has_comments(&mut self) -> bool { + self.peek(); + self.comments.len() > 0 + } + + pub fn peek(&mut self) -> Option { + loop { + if self.cursor == self.tokens.len() { + return None; + } + + match self.tokens[self.cursor].clone() { + Token::Comment(comment) => { + self.comments.push(comment); + self.cursor += 1; + continue; + } + token => { + return Some(token); + } + } + } + } + + pub fn error(&self, error: String) -> ParseError { + // TODO(@lino-levan): Add location information + ParseError::new(error, None) + } + + fn loc(&self) -> Loc { + return Loc {}; + } + + fn error_at(&self, error: String, loc: Loc) -> ParseError { + ParseError::new(error, Some(loc)) + } +} diff --git a/crates/sema/Cargo.toml b/crates/sema/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d9a6f7ea10286ec4418afd61c1538671280ce8b4 --- /dev/null +++ b/crates/sema/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sema" +version = "0.1.0" +edition = "2021" + +[lib] +path = "./sema.rs" + +[dependencies] +miette.workspace = true +thiserror.workspace = true +num.workspace = true + +ast = { path = "../ast" } \ No newline at end of file diff --git a/crates/sema/ast_type_coersion.rs b/crates/sema/ast_type_coersion.rs new file mode 100644 index 0000000000000000000000000000000000000000..2fab10b08f06b009feda2523d100641b89dc0d52 --- /dev/null +++ b/crates/sema/ast_type_coersion.rs @@ -0,0 +1,54 @@ +// use ast::{BinaryOperator, ExprData, Type}; + +impl TypeCoerceable for ExprData { + fn to_type(&self) -> Option { + match self { + ExprData::Block(block) => { + if (block.has_implicit_return) { + block.statements.last().unwrap().to_type() + } else { + Some(Type::Void) + } + } + ExprData::Binary { op, left, right } => { + // TODO: this likely has to do further type checking + let left_type = left.to_type(); + match op { + BinaryOperator::Add => left_type, + BinaryOperator::Sub => left_type, + BinaryOperator::Mul => left_type, + BinaryOperator::Div => left_type, + BinaryOperator::Pow => left_type, + BinaryOperator::Eq => Some(Type::Bool), + BinaryOperator::Gt => Some(Type::Bool), + BinaryOperator::Lt => Some(Type::Bool), + BinaryOperator::Gte => Some(Type::Bool), + BinaryOperator::Lte => Some(Type::Bool), + BinaryOperator::Lshift => Some(Type::Int), + BinaryOperator::Rshift => Some(Type::Int), + BinaryOperator::BitAnd => Some(Type::Int), + BinaryOperator::BitOr => Some(Type::Int), + BinaryOperator::BitXor => Some(Type::Int), + BinaryOperator::Mod => Some(Type::Int), + BinaryOperator::NotEq => Some(Type::Int), + + BinaryOperator::And => Some(Type::Bool), + BinaryOperator::Or => Some(Type::Bool), + } + } + ExprData::Unary { op, value } => todo!(), + ExprData::Function { + name, + params, + return_type, + block, + } => Some(return_type), + ExprData::Call { base, arguments } => None, // oh shit. yeah this trait isn't implementable + ExprData::Builtin { id, arguments } => todo!(), + ExprData::If(_) => todo!(), + ExprData::Return(_) => todo!(), + ExprData::Literal(_) => todo!(), + ExprData::Identifier(_) => todo!(), + } + } +} diff --git a/crates/sema/sema.rs b/crates/sema/sema.rs new file mode 100644 index 0000000000000000000000000000000000000000..899c5d9433436f4bf7f4f85d55a96d821cc88572 --- /dev/null +++ b/crates/sema/sema.rs @@ -0,0 +1,98 @@ +use ast::Module; +use miette::Result; + +pub fn analyze_module(_block: &mut Module) -> Result<()> { + // + Ok(()) +} + +// for statement in &block.statements { +// check_expression(statement)?; +// } + +// Ok(()) +// } + +// pub fn resolve_type(expr: &Ast) -> Result { +// match expr { +// Ast::Number(value) => Ok(Type::NumberLiteral(value.clone())), +// Ast::Declaration { +// name, +// decl_type, +// value, +// is_constant, +// } => Ok(Type::NoReturn), +// Ast::Binary { +// operator, +// left, +// right, +// } => merge_types(resolve_type(left)?, resolve_type(right)?), +// Ast::Function { +// name, +// arguments, +// return_type, +// block, +// } => todo!(), +// Ast::FunctionCall { name, arguments } => todo!(), +// Ast::If { +// condition, +// value, +// else_value, +// } => todo!(), +// Ast::Return(_) => Ok(Type::NoReturn), +// Ast::Public(_) => todo!(), // TODO: remove Public +// Ast::Block(_) => todo!(), +// Ast::Bool(_) => Ok(Type::Bool), +// Ast::String(_) => Ok(Type::String), +// Ast::Identifier(_) => todo!(), +// Ast::BuiltIn(_) => todo!(), + +// Ast::Comment(_) => unreachable!(), +// Ast::DocComment(_) => unreachable!(), +// } +// } + +// pub fn check_expression(expr: &Ast) -> Result<()> { +// panic!() +// match expr { +// Ast::Declaration { +// name, +// decl_type: return_type, +// value, +// is_constant, +// } => { +// check_expression(value)?; +// // coerce_type(return_type.clone(), resolve_type(value)?)?; +// } +// _ => {} +// // Ast::Operation { +// // operator, +// // left, +// // right, +// // } => Ok(()), +// // Ast::Function { +// // name, +// // arguments, +// // return_type, +// // block, +// // } => Ok(()), +// // Ast::FunctionCall { name, arguments } => Ok(()), +// // Ast::If { +// // condition, +// // value, +// // else_value, +// // } => Ok(()), +// // Ast::Return(_) => Ok(()), +// // Ast::Public(_) => Ok(()), +// // Ast::Block(_) => Ok(()), +// // Ast::Number(_) => Ok(()), +// // Ast::Bool(_) => Ok(()), +// // Ast::String(_) => Ok(()), +// // Ast::Identifier(_) => Ok(()), +// // Ast::BuiltIn(_) => Ok(()), +// // Ast::Comment(_) => Ok(()), +// // Ast::DocComment(_) => Ok(()), +// } + +// Ok(()) +// } diff --git a/crates/sema/sema_scope.rs b/crates/sema/sema_scope.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/tokenizer/Cargo.toml b/crates/tokenizer/Cargo.toml index 21243ee5fc409f648340819f1b792d721c214741..6e881084601ce5e033c79ce16949e3dc732880a2 100644 --- a/crates/tokenizer/Cargo.toml +++ b/crates/tokenizer/Cargo.toml @@ -4,9 +4,8 @@ version = "0.1.0" edition = "2021" [lib] +path="./tokenizer.rs" [dependencies] miette.workspace = true thiserror.workspace = true - -util = { path = "../util" } diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/src/lib.rs deleted file mode 100644 index 4892985ab5abfd83a772b786ba469e88436e6a7c..0000000000000000000000000000000000000000 --- a/crates/tokenizer/src/lib.rs +++ /dev/null @@ -1,227 +0,0 @@ -use miette::{Diagnostic, Result}; -use thiserror::Error; - -use util::Operator; - -#[derive(Error, Diagnostic, Debug)] -#[error("UnknownCharacter")] -struct UnknownCharacter { - #[help] - error: String, -} - -#[derive(Debug, Clone)] -pub enum Token { - // Literals - Identifier(String), - Number(f64), - String(String), - BuiltIn(String), - // Comments - Comment(String), - DocComment(String), - // Keywords - True, - False, - Let, - Var, - If, - Else, - Return, - Pub, - Function, - RightArrow, - Comma, - OpenParanthesis, - ClosedParanthesis, - OpenBracket, - ClosedBracket, - Colon, - Equals, - Semicolon, - // Operators - Operator(Operator), -} - -pub fn tokenize(content: String) -> Result> { - let mut tokens: Vec = Vec::new(); - let mut chars = content.chars().enumerate().peekable(); - - loop { - match chars.next() { - Some((_, c)) => match c { - 'a'..='z' | 'A'..='Z' | '_' | '@' => { - let mut current_token = String::new(); - current_token.push(c); - loop { - let c = chars.peek(); - if c.is_none() { - break; - } - let (_, c) = c.unwrap(); - match c { - 'a'..='z' | 'A'..='Z' | '_' | '0'..='9' => { - current_token.push(*c); - chars.next(); - } - _ => break, - } - } - - match current_token.as_str() { - "var" => tokens.push(Token::Var), - "let" => tokens.push(Token::Let), - "if" => tokens.push(Token::If), - "else" => tokens.push(Token::Else), - "pub" => tokens.push(Token::Pub), - "fn" => tokens.push(Token::Function), - "return" => tokens.push(Token::Return), - "true" => tokens.push(Token::True), - "false" => tokens.push(Token::False), - _ => { - if current_token.starts_with('@') { - tokens.push(Token::BuiltIn(current_token)); - } else { - tokens.push(Token::Identifier(current_token)) - } - } - } - } - '"' => { - let mut current_token = String::new(); - loop { - let c = chars.next(); - if c.is_none() { - break; - } - let (_, c) = c.unwrap(); - match c { - '"' => break, - _ => current_token.push(c), - } - } - tokens.push(Token::String(current_token)); - } - '0'..='9' => { - let mut current_token = String::new(); - current_token.push(c); - loop { - let c = chars.peek(); - if c.is_none() { - break; - } - let (_, c) = c.unwrap(); - match c { - '0'..='9' => { - current_token.push(*c); - chars.next(); - } - _ => break, - } - } - match current_token.parse::() { - Ok(v) => tokens.push(Token::Number(v)), - Err(_) => { - return Err(UnknownCharacter { - error: format!("Unknown character: {}", current_token), - } - .into()) - } - } - } - ':' => tokens.push(Token::Colon), - '=' => match chars.peek() { - Some((_, '=')) => { - chars.next(); - tokens.push(Token::Operator(Operator::Equals)); - } - _ => tokens.push(Token::Equals), - }, - ';' => tokens.push(Token::Semicolon), - '>' => match chars.peek() { - Some((_, '=')) => { - chars.next(); - tokens.push(Token::Operator(Operator::GreaterThanOrEquals)); - } - _ => tokens.push(Token::Operator(Operator::GreaterThan)), - }, - '<' => match chars.peek() { - Some((_, '=')) => { - chars.next(); - tokens.push(Token::Operator(Operator::LessThanOrEquals)); - } - _ => tokens.push(Token::Operator(Operator::LessThan)), - }, - '+' => tokens.push(Token::Operator(Operator::Add)), - '-' => match chars.peek() { - Some((_, '>')) => { - chars.next(); - tokens.push(Token::RightArrow); - } - _ => tokens.push(Token::Operator(Operator::Subtract)), - }, - '*' => match chars.peek() { - Some((_, '*')) => { - chars.next(); - tokens.push(Token::Operator(Operator::Power)); - } - _ => tokens.push(Token::Operator(Operator::Multiply)), - }, - '/' => { - match chars.peek() { - Some((_, '/')) => { - chars.next(); // Consume the second slash - - let is_doc_comment = match chars.peek() { - Some((_, '/')) => { - chars.next(); // Consume the third slash - true - } - _ => false, - }; - - let mut comment = String::new(); - loop { - let c = chars.next(); - if c.is_none() { - break; - } - let (_, c) = c.unwrap(); - match c { - '\n' => break, - _ => { - comment.push(c.clone()); - } - } - } - - comment = comment.trim().to_string(); - - if is_doc_comment { - tokens.push(Token::DocComment(comment)); - } else { - tokens.push(Token::Comment(comment)); - } - } - _ => tokens.push(Token::Operator(Operator::Divide)), - } - } - ',' => tokens.push(Token::Comma), - '(' => tokens.push(Token::OpenParanthesis), - ')' => tokens.push(Token::ClosedParanthesis), - '{' => tokens.push(Token::OpenBracket), - '}' => tokens.push(Token::ClosedBracket), - s if s.is_whitespace() => (), - _ => { - return Err(UnknownCharacter { - error: format!("Unsupported character: {}", c), - } - .into()) - } - }, - None => break, - } - } - - Ok(tokens) -} diff --git a/crates/tokenizer/tokenizer.rs b/crates/tokenizer/tokenizer.rs new file mode 100644 index 0000000000000000000000000000000000000000..06f2495729efddae8d1a4a3945f2805341d42201 --- /dev/null +++ b/crates/tokenizer/tokenizer.rs @@ -0,0 +1,409 @@ +use miette::{Diagnostic, Result}; +use thiserror::Error; + +#[derive(Debug, PartialEq, Clone)] +pub struct Loc {} + +impl Loc { + /// TODO(@paperdave): Remove this. every usage of this is incorrect. + pub const EMPTY: Loc = Loc {}; + + pub fn new() -> Self { + Self {} + } +} + +#[derive(Error, Diagnostic, Debug)] +#[error("UnknownCharacter")] +struct UnknownCharacter { + #[help] + error: String, +} + +#[derive(Debug, Clone)] +pub enum Token { + // Literals + Identifier(String), + Number(f64), + String(String), + // Comments + Comment(String), + DocComment(String), + // Keywords + Else, + False, + Fn, + If, + Let, + Pub, + Return, + True, + Var, + Void, + // Symbols + And, // & + AndAnd, // && + Bang, // ! + BangEqual, // != + BigRightArrow, // => + Caret, // ^ + ClosedBrace, // } + ClosedBracket, // ] + ClosedParen, // ) + Colon, // : + Comma, // , + Dot, // . + Equal, // = + EqualEqual, // == + Gt, // > + GtEqual, // >= + GtGt, // >> + GtGtGt, // >>> + Lt, // < + LtEqual, // <= + LtLt, // << + Minus, // - + OpenBrace, // { + OpenBracket, // [ + OpenParen, // ( + Or, // | + OrOr, // || + Percent, // % + Plus, // + + RightArrow, // -> + Semicolon, // ; + Slash, // / + Star, // * + StarStar, // ** + Tilde, // ~ +} + +impl Token { + pub fn is_start_of_statement_like_expression(&self) -> bool { + match &self { + Token::If | Token::OpenBrace => true, + _ => false, + } + } +} + +impl std::fmt::Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let token = match self { + Token::Identifier(_) => "identifier".to_string(), + + Token::Number(n) => n.to_string(), + Token::String(s) => format!("\"{}\"", s), + Token::Comment(_) => "comment".to_string(), + Token::DocComment(_) => "documentation comment".to_string(), + + Token::Else => "else".to_string(), + Token::False => "false".to_string(), + Token::Fn => "fn".to_string(), + Token::If => "if".to_string(), + Token::Let => "let".to_string(), + Token::Pub => "pub".to_string(), + Token::Return => "return".to_string(), + Token::True => "true".to_string(), + Token::Var => "var".to_string(), + Token::Void => "void".to_string(), + + Token::And => "&".to_string(), + Token::AndAnd => "&&".to_string(), + Token::Bang => "!".to_string(), + Token::BangEqual => "!=".to_string(), + Token::BigRightArrow => "=>".to_string(), + Token::Caret => "^".to_string(), + Token::ClosedBrace => "}".to_string(), + Token::ClosedBracket => "]".to_string(), + Token::ClosedParen => ")".to_string(), + Token::Colon => ":".to_string(), + Token::Comma => ",".to_string(), + Token::Dot => ".".to_string(), + Token::Equal => "=".to_string(), + Token::EqualEqual => "==".to_string(), + Token::Gt => ">".to_string(), + Token::GtEqual => ">=".to_string(), + Token::GtGt => ">>".to_string(), + Token::GtGtGt => ">>>".to_string(), + Token::Lt => "<".to_string(), + Token::LtEqual => "<=".to_string(), + Token::LtLt => "<<".to_string(), + Token::Minus => "-".to_string(), + Token::OpenBrace => "{".to_string(), + Token::OpenBracket => "[".to_string(), + Token::OpenParen => "(".to_string(), + Token::Or => "|".to_string(), + Token::OrOr => "||".to_string(), + Token::Percent => "%".to_string(), + Token::Plus => "+".to_string(), + Token::RightArrow => "->".to_string(), + Token::Semicolon => ";".to_string(), + Token::Slash => "/".to_string(), + Token::Star => "*".to_string(), + Token::StarStar => "**".to_string(), + Token::Tilde => "~".to_string(), + }; + + write!(f, "{}", token) + } +} + +// TODO(@paperdave): rewrite this to not use a Vec. we probably dont need it. also store Loc +pub fn tokenize(content: String) -> Result> { + let mut tokens: Vec = Vec::new(); + let mut chars = content.chars().enumerate().peekable(); + + loop { + match chars.next() { + Some((_, c)) => match c { + 'a'..='z' | 'A'..='Z' | '_' | '@' => { + let mut current_token = String::new(); + current_token.push(c); + loop { + let c = chars.peek(); + if c.is_none() { + break; + } + let (_, c) = c.unwrap(); + match c { + 'a'..='z' | 'A'..='Z' | '_' | '0'..='9' => { + current_token.push(*c); + chars.next(); + } + _ => break, + } + } + + match current_token.as_str() { + "else" => tokens.push(Token::Else), + "false" => tokens.push(Token::False), + "fn" => tokens.push(Token::Fn), + "if" => tokens.push(Token::If), + "let" => tokens.push(Token::Let), + "pub" => tokens.push(Token::Pub), + "return" => tokens.push(Token::Return), + "true" => tokens.push(Token::True), + "var" => tokens.push(Token::Var), + "void" => tokens.push(Token::Void), + _ => tokens.push(Token::Identifier(current_token)), + } + } + '"' => { + let mut current_token = String::new(); + loop { + let c = chars.next(); + if c.is_none() { + break; + } + let (_, c) = c.unwrap(); + match c { + '"' => break, + _ => current_token.push(c), + } + } + tokens.push(Token::String(current_token)); + } + '0'..='9' => { + let mut current_token = String::new(); + current_token.push(c); + loop { + let c = chars.peek(); + if c.is_none() { + break; + } + let (_, c) = c.unwrap(); + match c { + '0'..='9' => { + current_token.push(*c); + chars.next(); + } + _ => break, + } + } + match current_token.parse::() { + Ok(v) => tokens.push(Token::Number(v)), + Err(_) => { + return Err(UnknownCharacter { + error: format!("Unknown character: {}", current_token), + } + .into()) + } + } + } + ':' => tokens.push(Token::Colon), + '=' => match chars.peek() { + Some((_, '=')) => { + chars.next(); + tokens.push(Token::EqualEqual); + } + Some((_, '>')) => { + chars.next(); + tokens.push(Token::BigRightArrow); + } + _ => tokens.push(Token::Equal), + }, + ';' => tokens.push(Token::Semicolon), + '>' => match chars.peek() { + Some((_, '=')) => { + chars.next(); + tokens.push(Token::GtEqual); + } + Some((_, '>')) => { + chars.next(); + match chars.peek() { + Some((_, '>')) => { + chars.next(); + tokens.push(Token::GtGtGt); + } + _ => tokens.push(Token::GtGt), + } + } + _ => tokens.push(Token::Gt), + }, + '<' => match chars.peek() { + Some((_, '=')) => { + chars.next(); + tokens.push(Token::LtEqual); + } + Some((_, '<')) => { + chars.next(); + tokens.push(Token::LtLt); + } + _ => tokens.push(Token::Lt), + }, + '+' => tokens.push(Token::Plus), + '-' => match chars.peek() { + Some((_, '>')) => { + chars.next(); + tokens.push(Token::RightArrow); + } + _ => tokens.push(Token::Minus), + }, + '*' => match chars.peek() { + Some((_, '*')) => { + chars.next(); + tokens.push(Token::StarStar); + } + _ => tokens.push(Token::Star), + }, + '/' => { + match chars.peek() { + Some((_, '/')) => { + chars.next(); // Consume the second slash + + let is_doc_comment = match chars.peek() { + Some((_, '/')) => { + chars.next(); // Consume the third slash + true + } + _ => false, + }; + + let mut comment = String::new(); + loop { + // skip leading whitespace + while let Some((_, c)) = chars.peek() { + if !c.is_whitespace() { + break; + } + chars.next(); + } + // read the comment + while let Some((_, c)) = chars.next() { + if c == '\n' || c == '\r' { + break; + } + comment.push(c); + } + // trim trailing whitespace on line + comment = comment.trim_end().to_string(); + // skip to next non-whitespace character + while let Some((_, c)) = chars.peek() { + if !c.is_whitespace() { + break; + } + chars.next(); + } + // check if we've hit another comment + let mut tmp = chars.clone(); + + let is_another_comment = match tmp.peek() { + Some((_, '/')) => { + tmp.next(); // Consume the second slash + match tmp.peek() { + Some((_, '/')) => { + tmp.next(); // Consume the third slash + is_doc_comment + } + _ => !is_doc_comment, + } + } + _ => false, + }; + + if is_another_comment { + // apply our temporary iterator to the main iterator + comment.push('\n'); + chars = tmp; + chars.next(); + } else { + break; + } + } + + if is_doc_comment { + tokens.push(Token::DocComment(comment)); + } else { + tokens.push(Token::Comment(comment)); + } + } + _ => tokens.push(Token::Slash), + } + } + ',' => tokens.push(Token::Comma), + '(' => tokens.push(Token::OpenParen), + ')' => tokens.push(Token::ClosedParen), + '{' => tokens.push(Token::OpenBrace), + '}' => tokens.push(Token::ClosedBrace), + '[' => tokens.push(Token::OpenBracket), + ']' => tokens.push(Token::ClosedBracket), + '&' => match chars.peek() { + Some((_, '&')) => { + chars.next(); + tokens.push(Token::AndAnd); + } + _ => tokens.push(Token::And), + }, + '|' => match chars.peek() { + Some((_, '|')) => { + chars.next(); + tokens.push(Token::OrOr); + } + _ => tokens.push(Token::Or), + }, + '!' => match chars.peek() { + Some((_, '=')) => { + chars.next(); + tokens.push(Token::BangEqual); + } + _ => tokens.push(Token::Bang), + }, + '^' => tokens.push(Token::Caret), + '%' => tokens.push(Token::Percent), + '~' => tokens.push(Token::Tilde), + '.' => tokens.push(Token::Dot), + s if s.is_whitespace() => (), + _ => { + return Err(UnknownCharacter { + error: format!("Unexpected '{}' while tokenizing", c), + } + .into()) + } + }, + None => break, + } + } + + Ok(tokens) +} diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml deleted file mode 100644 index 8729b70b0ce2b0b1139b6defbbcdee89deea81cc..0000000000000000000000000000000000000000 --- a/crates/util/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "util" -version = "0.1.0" -edition = "2021" - -[lib] - -[dependencies] diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs deleted file mode 100644 index c79f4c32474b7cedc5f4b6b6acda2330b51a19c9..0000000000000000000000000000000000000000 --- a/crates/util/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -#[derive(Debug, Clone, PartialEq)] -pub enum Operator { - Add, - Subtract, - Multiply, - Divide, - Power, - Equals, - GreaterThan, - LessThan, - GreaterThanOrEquals, - LessThanOrEquals, -} - -impl Operator { - pub fn to_string(self) -> String { - String::from(match self { - Self::Add => "+", - Self::Subtract => "-", - Self::Divide => "/", - Self::Multiply => "*", - Self::Power => "**", - Self::Equals => "===", - Self::GreaterThan => ">", - Self::LessThan => "<", - Self::GreaterThanOrEquals => ">=", - Self::LessThanOrEquals => "<=", - }) - } -} diff --git a/examples/basics/control.blight b/examples/basics/control.blight index 1428905a5fddb419fb987256783c401c956355d2..f6e3bdb1db079dc14869607c8fc2821eee7a89f2 100644 --- a/examples/basics/control.blight +++ b/examples/basics/control.blight @@ -1,4 +1,4 @@ -let print: any = @raw("console.log"); +let print = @raw("console.log"); let x: bool = true; diff --git a/examples/basics/expressions.blight b/examples/basics/expressions.blight index fbc8e4c4c024dff1abf958045fd7d5a3c189bd7c..4897a92481c947df2cefd5171363c5d919241e9f 100644 --- a/examples/basics/expressions.blight +++ b/examples/basics/expressions.blight @@ -7,5 +7,5 @@ let x: int = 5 + { y }; -let print: any = @raw("console.log"); +let print = @raw("console.log"); print(x); // 10 diff --git a/examples/basics/fib.blight b/examples/basics/fib.blight index 898f001d4f163a3b4142a38a64a86fd2393b9181..bfa4b5623183aa460e1f0b79e0988f660ab4def4 100644 --- a/examples/basics/fib.blight +++ b/examples/basics/fib.blight @@ -8,5 +8,5 @@ pub fn fib(n: int) -> int { } } -let print: any = @raw("console.log"); +let print = @raw("console.log"); print(fib(10)); diff --git a/examples/basics/functions.blight b/examples/basics/functions.blight index 58cd31caa9d8c9c49aadd8609bc9ccc62bc36eb2..37ca9d3a2dce9143b9e7b2dbd50819abff151942 100644 --- a/examples/basics/functions.blight +++ b/examples/basics/functions.blight @@ -4,5 +4,5 @@ fn bingus(x: int, y: int) -> int { let result: int = bingus(10, 25); -let print: any = @raw("console.log"); +let print = @raw("console.log"); print(result); diff --git a/examples/basics/operations.blight b/examples/basics/operations.blight index 85ee1283b38b61b6840f20d8dd6f8c206d8cf7c9..55dc5693ebb9d507fd598284e1510811f34cbc03 100644 --- a/examples/basics/operations.blight +++ b/examples/basics/operations.blight @@ -1,4 +1,4 @@ let a: int = 10 - 2 / 4 * 15; -let print: any = @raw("console.log"); +let print = @raw("console.log"); print(a); diff --git a/examples/basics/shadowing.blight b/examples/basics/shadowing.blight index 2a8514581e8a102a5f67b44c22f825aca3fd9252..3e96605fa4d98e020ae275232d4cc06ed958c9c0 100644 --- a/examples/basics/shadowing.blight +++ b/examples/basics/shadowing.blight @@ -1,4 +1,4 @@ -let print: any = @raw("console.log"); +let print = @raw("console.log"); let x: int = 0; let x: int = 1; diff --git a/examples/basics/stupid.blight b/examples/basics/stupid.blight new file mode 100644 index 0000000000000000000000000000000000000000..4144819ae054ff63f524006f9d3c00e2e4793ba8 --- /dev/null +++ b/examples/basics/stupid.blight @@ -0,0 +1,2 @@ + +fn add(x: int, y: int) => x + y diff --git a/examples/basics/variables.blight b/examples/basics/variables.blight index 3bfe757ddba318fa7e40620b6a4bbbd6c3f2cf45..302be86a6474de450a084ae10406e333f2afd1b5 100644 --- a/examples/basics/variables.blight +++ b/examples/basics/variables.blight @@ -6,5 +6,5 @@ let a: int = 0; let b: float = 0; let c: bigint = 0; -let print: any = @raw("console.log"); +let print = @raw("console.log"); print(x); diff --git a/examples/basics/weird.blight b/examples/basics/weird.blight index a10c1ec7a2f55dbbe68e90ab93503ceefd4c91aa..80b5ea000738ae9763f383f35a18b2a665448958 100644 --- a/examples/basics/weird.blight +++ b/examples/basics/weird.blight @@ -9,6 +9,6 @@ fn bingus(y: bool) -> int { x } -let print: any = @raw("console.log"); +let print = @raw("console.log"); print(bingus(true)); print(bingus(false)); diff --git a/examples/interop/deno.blight b/examples/interop/deno.blight index 9f6b1157fa6bb3f64d7f1ffbdc0f086372559925..1b5fd3af6cad1c63f41f5f41f55c17d25bc98de0 100644 --- a/examples/interop/deno.blight +++ b/examples/interop/deno.blight @@ -1,4 +1,4 @@ -let writeTextFile: any = @raw("Deno.writeTextFile"); +let writeTextFile = @raw("Deno.writeTextFile"); fn x() -> string { "hello"