From c80f41a7e129fe011b7ecae276ea08cb0cff10ab Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 20 Jan 2024 09:38:23 -0800 Subject: [PATCH 1/8] swag --- .gitignore | 1 + Cargo.lock | 12 ++ crates/ast/src/error.rs | 4 + crates/ast/src/lib.rs | 6 +- crates/ast/src/parse_statement.rs | 50 ++---- crates/ast/src/parse_type.rs | 15 +- crates/ast/src/types.rs | 64 +++++-- crates/checker/Cargo.toml | 14 ++ crates/checker/src/lib.rs | 203 ++++++++++++++++++++++ crates/cli/Cargo.toml | 1 + crates/cli/src/main.rs | 43 ++++- crates/js_ast/src/lib.rs | 44 ++--- crates/js_ast/src/types.rs | 7 +- crates/js_printer/src/print_expression.rs | 2 +- crates/js_printer/src/print_statement.rs | 21 ++- crates/tokenizer/src/lib.rs | 36 ++++ examples/basics/control.blight | 2 +- examples/basics/expressions.blight | 2 +- examples/basics/fib.blight | 2 +- examples/basics/functions.blight | 2 +- examples/basics/operations.blight | 2 +- examples/basics/shadowing.blight | 2 +- examples/basics/variables.blight | 2 +- examples/basics/weird.blight | 2 +- examples/interop/deno.blight | 2 +- 25 files changed, 437 insertions(+), 104 deletions(-) create mode 100644 crates/checker/Cargo.toml create mode 100644 crates/checker/src/lib.rs 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..8317b65192cbb35945e6d398cc412aea7bcee045 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "checker" +version = "0.1.0" +dependencies = [ + "ast", + "miette", + "num", + "thiserror", + "util", +] + [[package]] name = "clap" version = "4.4.14" @@ -171,6 +182,7 @@ name = "cli" version = "0.1.0" dependencies = [ "ast", + "checker", "clap", "js_ast", "js_printer", diff --git a/crates/ast/src/error.rs b/crates/ast/src/error.rs index f698cb95d6c28ff61e24bab5161acbd133992c78..c04c6362d317fb2623d187a22a7a78dee72982ff 100644 --- a/crates/ast/src/error.rs +++ b/crates/ast/src/error.rs @@ -14,4 +14,8 @@ impl AstError { error: String::from(error), } } + + pub fn from_string(error: String) -> AstError { + AstError { error } + } } diff --git a/crates/ast/src/lib.rs b/crates/ast/src/lib.rs index c4841db4e84a9184793e9563f94d5e15b3dbaa66..3810f82f7e30940a92838648832212aeddf5443c 100644 --- a/crates/ast/src/lib.rs +++ b/crates/ast/src/lib.rs @@ -13,9 +13,9 @@ use tokenizer::Token; use parse_statement::parse_statement; use types::ParserContext; -pub use types::{Ast, Block, Type}; +pub use types::{Ast, Block, Module, Type}; -pub fn parse(tokens: Vec) -> Result> { +pub fn parse(tokens: Vec) -> Result { let mut ctx = ParserContext::new(tokens); let mut ast: Vec = Vec::new(); @@ -23,5 +23,5 @@ pub fn parse(tokens: Vec) -> Result> { ast.push(parse_statement(&mut ctx)?); } - Ok(ast) + Ok(Module { value: ast }) } diff --git a/crates/ast/src/parse_statement.rs b/crates/ast/src/parse_statement.rs index 343aceff9eea0e9fc29c57e25fab2387e39f8b32..1286c41fd08bdcc6267f855fa7b1a40df9056004 100644 --- a/crates/ast/src/parse_statement.rs +++ b/crates/ast/src/parse_statement.rs @@ -2,6 +2,8 @@ use miette::Result; use tokenizer::Token; +use crate::Type; + use super::error::AstError; use super::parse_block::parse_block; use super::parse_expression::parse_expression; @@ -38,14 +40,22 @@ pub fn parse_statement(tokens: &mut ParserContext) -> Result { return Ok(Ast::Public(Box::new(inner))); } - Token::Let => { + Token::Var | Token::Let => { + let is_constant = match token { + Token::Var => false, + Token::Let => true, + _ => unreachable!(), + }; 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 return_type = match tokens.peek() { + Some(Token::Colon) => { + tokens.next(); + parse_type(tokens)? + } + _ => Type::Infer, }; let value = match tokens.next() { Some(Token::Equals) => parse_expression(tokens)?, @@ -59,37 +69,11 @@ pub fn parse_statement(tokens: &mut ParserContext) -> Result { ) } }; - Ok(Ast::ConstantDeclaration { + Ok(Ast::Declaration { 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, + decl_type: return_type, value: Box::new(value), + is_constant, }) } Token::Return => { diff --git a/crates/ast/src/parse_type.rs b/crates/ast/src/parse_type.rs index 6837edec59b4d8f649ccc28c65f33362ef948c49..e481d0d283d7ad230ed89190f60b859134dfce4e 100644 --- a/crates/ast/src/parse_type.rs +++ b/crates/ast/src/parse_type.rs @@ -10,12 +10,19 @@ pub fn parse_type(tokens: &mut ParserContext) -> Result { Some(Token::Identifier(typ)) => match typ.as_str() { "int" => Ok(Type::Int), "float" => Ok(Type::Float), - "bigint" => Ok(Type::Bigint), + "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::from_string(format!( + "Invalid type '{}' (TODO: add custom type support)", + typ + ) + ).into()) + } }, - _ => return Err(AstError::new("Expected type").into()), + Some(unexpected) => return Err(AstError::from_string(format!("Expected type, got {}", unexpected)).into()), + None => return Err(AstError::from_string("Expected type, got EOF".to_string()).into()), } } diff --git a/crates/ast/src/types.rs b/crates/ast/src/types.rs index 3500088aa85499be1b8eb25c25ed7749ff58c598..61a84e57cd474bd406185292dfb87fa0cfeed8b3 100644 --- a/crates/ast/src/types.rs +++ b/crates/ast/src/types.rs @@ -41,14 +41,62 @@ impl ParserContext { } } -#[derive(Debug, Clone)] +#[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 numeric literals. This allows integer literals + /// to coerce to integer or float depending on the context, but prevents + /// implicit casting of runtime values. + NumberLiteral(f64), + /// Return value of `@raw`. All type checks are disabled. + Any, + /// noreturn is the type of 'return', aka a value that never exists + NoReturn, + /// 32 signed integer Int, + /// 64 signed JS float Float, - Bigint, + /// Boolean Bool, + /// JS BigInt + BigInt, + /// JS String String, - Any, + /// A union of multiple possible types. Might be removed + Union(Vec), +} + +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::NumberLiteral(_) => write!(f, "numeric literal"), + 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::Union(of) => { + for (i, t) in of.iter().enumerate() { + if i != 0 { + write!(f, " | ")?; + } + write!(f, "{}", t)?; + } + Ok(()) + } + } + } +} + +#[derive(Debug, Clone)] +pub struct Module { + pub value: Vec, } #[derive(Debug, Clone)] @@ -65,15 +113,11 @@ pub struct Argument { #[derive(Debug, Clone)] pub enum Ast { - ConstantDeclaration { + Declaration { name: String, - return_type: Type, - value: Box, - }, - VariableDeclaration { - name: String, - return_type: Type, + decl_type: Type, value: Box, + is_constant: bool, }, Operation { operator: Operator, diff --git a/crates/checker/Cargo.toml b/crates/checker/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..9911221e439591b90a010b7a36b71770a76c2305 --- /dev/null +++ b/crates/checker/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "checker" +version = "0.1.0" +edition = "2021" + +[lib] + +[dependencies] +miette.workspace = true +thiserror.workspace = true +num.workspace = true + +ast = { path = "../ast" } +util = { path = "../util" } diff --git a/crates/checker/src/lib.rs b/crates/checker/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..7f68543e0b5f8f27ec36090523bf10227dd0770a --- /dev/null +++ b/crates/checker/src/lib.rs @@ -0,0 +1,203 @@ +use ast::{Ast, Module, Type}; +use miette::Result; + +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Error, Diagnostic, Debug)] +#[error("TypeError")] +pub struct TypeError { + #[help] + error: String, +} + +impl TypeError { + pub fn new(error: String) -> Self { + Self { error } + } +} + +pub fn check_module(block: &Module) -> Result<()> { + for statement in &block.value { + 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::Operation { + 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 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!("Cannot merge {} 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::NumberLiteral(_) => unreachable!(), + + Type::Int => 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 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()) + } + } + } +} + +pub fn check_expression(expr: &Ast) -> Result<()> { + 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/cli/Cargo.toml b/crates/cli/Cargo.toml index 0f6618aef8ab0f73dfad007939f2c13f827f0fa5..a0e6f101de7d10297488257b9f58ddb06354076d 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -16,3 +16,4 @@ tokenizer = { path = "../tokenizer" } ast = { path = "../ast" } js_ast = { path = "../js_ast" } js_printer = { path = "../js_printer" } +checker = { path = "../checker" } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e04af712afecd134f30a2d7e1afdf3a700673eba..254bad1c38f34c5af9d8a13b3c5e1b3482e93cd8 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,5 +1,6 @@ -use std::path::PathBuf; +use std::{io::Write, path::PathBuf}; +use checker; use clap::{Parser, Subcommand}; use miette::Result; @@ -29,6 +30,9 @@ enum Commands { /// 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, @@ -51,11 +55,11 @@ pub fn transpile(content: String) -> Result { // 2. Parse the tokens into an AST let blight_ast = ast::parse(tokens)?; // 3. Type check the AST - // type_check(tree.clone())?; + checker::check_module(&blight_ast)?; // 4. Optimize the AST // optimize(tree.clone())?; // 5. Translate the AST into javascript - let js_ast = js_ast::translate(blight_ast)?; + let js_ast = js_ast::translate(&blight_ast)?; // 6. Print the JS AST let javascript = js_printer::print(js_ast); // 7. Return the javascript @@ -70,9 +74,38 @@ fn main() -> Result<()> { file, stdout, target: _, + format, } => { - let contents = std::fs::read_to_string(file.clone()).unwrap(); - let result = transpile(contents.clone())?; + 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(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); diff --git a/crates/js_ast/src/lib.rs b/crates/js_ast/src/lib.rs index f0f738cde7b062470fb261f949c656f770af9b8a..28685a906d0f4fa1e3083a8b6f70bc9a97e3457f 100644 --- a/crates/js_ast/src/lib.rs +++ b/crates/js_ast/src/lib.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use miette::Result; -use ast::Ast; +use ast::{Ast, Module}; mod types; pub use types::{JsExpression, JsStatement}; @@ -55,9 +55,10 @@ impl TranslateContext { .last_mut() .unwrap() .statements - .push(JsStatement::VariableDeclaration { + .push(JsStatement::Declaration { name: format!("${}", self.internal_counter), value, + is_constant: false, }); self.internal_counter += 1; JsExpression::Identifier(format!("${}", self.internal_counter - 1)) @@ -169,28 +170,18 @@ fn get_expression_handle(ast: &Ast, ctx: &mut TranslateContext) -> JsExpression fn translate_statement(ast: &Ast, ctx: &mut TranslateContext) { match ast { - Ast::ConstantDeclaration { + Ast::Declaration { name, - return_type: _, + decl_type: _, value, + is_constant, } => { 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 { + ctx.push(JsStatement::Declaration { name: rename, value: handle, + is_constant: is_constant.clone(), }); } Ast::Function { @@ -226,16 +217,13 @@ fn translate_statement(ast: &Ast, ctx: &mut TranslateContext) { 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, .. } => { + JsStatement::Function { name, .. } | JsStatement::Declaration { 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()), - }, - ))); + ctx.push(JsStatement::Export(Box::new(JsStatement::Declaration { + name: original_name, + value: JsExpression::Identifier(name.clone()), + is_constant: false, + }))); } _ => { panic!("Cannot make non-function public"); @@ -337,10 +325,10 @@ fn translate_statement(ast: &Ast, ctx: &mut TranslateContext) { } } -pub fn translate(_ast: Vec) -> Result> { +pub fn translate(module: &Module) -> Result> { let mut ctx = TranslateContext::new(); - for ast in _ast { + for ast in module.value.iter() { translate_statement(&ast, &mut ctx); } diff --git a/crates/js_ast/src/types.rs b/crates/js_ast/src/types.rs index 18a2788da595867d00cc1249f0cbd40f5487f3d0..3b5f176e4a51e7ccf14eb6fcf3be46c25577f8b2 100644 --- a/crates/js_ast/src/types.rs +++ b/crates/js_ast/src/types.rs @@ -33,13 +33,10 @@ pub enum JsStatement { Expression(JsExpression), Export(Box), Return(JsExpression), - VariableDeclaration { - name: String, - value: JsExpression, - }, - ConstantDeclaration { + Declaration { name: String, value: JsExpression, + is_constant: bool, }, Function { name: String, diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs index ff87066c667b0ed8496173cb4fbe09422702e859..e13e3ebbda35bf9db23ae4dd38c3d8cb513c65f6 100644 --- a/crates/js_printer/src/print_expression.rs +++ b/crates/js_printer/src/print_expression.rs @@ -230,7 +230,7 @@ mod tests { assert_eq!( print_expression(&JsExpression::Function { arguments: vec!["a".to_string(), "b".to_string()], - value: vec![JsStatement::VariableDeclaration { + value: vec![JsStatement::Declaration { name: "a".to_string(), value: JsExpression::Number(1.0) }], diff --git a/crates/js_printer/src/print_statement.rs b/crates/js_printer/src/print_statement.rs index 4a9bf75b17a5f0fde0844473aad91c65dbb51b92..2f4c37399b38c214fc2e8036fbb232aaf0017234 100644 --- a/crates/js_printer/src/print_statement.rs +++ b/crates/js_printer/src/print_statement.rs @@ -7,11 +7,20 @@ pub fn print_statement(statement: &JsStatement) -> String { 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::Declaration { + name, + value, + is_constant, + } => { + format!( + "{} {}={};", + match is_constant { + true => "const", + false => "let", + }, + name, + print_expression(value) + ) } JsStatement::Function { name, @@ -66,7 +75,7 @@ mod tests { #[test] fn variable_declaration() { assert_eq!( - print_statement(&JsStatement::VariableDeclaration { + print_statement(&JsStatement::Declaration { name: "x".to_string(), value: JsExpression::Number(1.0), }), diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/src/lib.rs index 4892985ab5abfd83a772b786ba469e88436e6a7c..a35ad44dd5d8122748a865188c70b94544a9fb3a 100644 --- a/crates/tokenizer/src/lib.rs +++ b/crates/tokenizer/src/lib.rs @@ -43,6 +43,42 @@ pub enum Token { Operator(Operator), } +impl std::fmt::Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let token = match self { + Token::Identifier(_) => "an identifier".to_string(), + Token::Number(n) => n.to_string(), + Token::String(s) => format!("\"{}\"", s), + Token::BuiltIn(_) => "a builtin identifier".to_string(), + Token::Comment(_) => "a comment".to_string(), + Token::DocComment(_) => "a documentation comment".to_string(), + Token::True => "true".to_string(), + Token::False => "false".to_string(), + Token::Let => "let".to_string(), + Token::Var => "var".to_string(), + Token::If => "if".to_string(), + Token::Else => "else".to_string(), + Token::Return => "return".to_string(), + Token::Pub => "pub".to_string(), + Token::Function => "fn".to_string(), + Token::RightArrow => "->".to_string(), + Token::Comma => ",".to_string(), + Token::OpenParanthesis => "(".to_string(), + Token::ClosedParanthesis => ")".to_string(), + Token::OpenBracket => "{".to_string(), + Token::ClosedBracket => "}".to_string(), + Token::Colon => ":".to_string(), + Token::Equals => "=".to_string(), + Token::Semicolon => ";".to_string(), + Token::Operator(o) => o.clone().to_string(), + }; + + write!(f, "{}", token) + } +} + + + pub fn tokenize(content: String) -> Result> { let mut tokens: Vec = Vec::new(); let mut chars = content.chars().enumerate().peekable(); 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/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" -- 2.54.0 From 7ccc33ec770f34fd73e21b4d81e5a150b1a96909 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 20 Jan 2024 16:32:51 -0800 Subject: [PATCH 2/8] rewrite the parser in rust --- Cargo.lock | 22 +- Cargo.toml | 12 +- crates/ast/Cargo.toml | 3 +- crates/ast/ast.rs | 174 ++++++++++ crates/ast/lib.rs | 7 + crates/ast/operators.rs | 171 +++++++++ crates/ast/src/error.rs | 21 -- crates/ast/src/lib.rs | 27 -- crates/ast/src/parse_block.rs | 55 --- crates/ast/src/parse_expression.rs | 105 ------ crates/ast/src/parse_function_call.rs | 56 --- crates/ast/src/parse_statement.rs | 245 ------------- crates/ast/src/parse_type.rs | 28 -- crates/ast/src/types.rs | 153 -------- crates/ast/types.rs | 208 +++++++++++ crates/checker/Cargo.toml | 3 +- crates/checker/src/ast_type_coersion.rs | 54 +++ crates/checker/src/lib.rs | 299 ++++++---------- crates/cli/Cargo.toml | 1 + crates/cli/src/main.rs | 22 +- crates/js_ast/Cargo.toml | 1 - crates/js_ast/src/lib.rs | 384 ++++++++++----------- crates/js_ast/src/types.rs | 11 +- crates/js_printer/Cargo.toml | 1 - crates/js_printer/src/print_expression.rs | 21 +- crates/parser/Cargo.toml | 15 + crates/parser/parse_block.rs | 64 ++++ crates/parser/parse_call_arguments.rs | 31 ++ crates/parser/parse_declaration.rs | 46 +++ crates/parser/parse_expr.rs | 161 +++++++++ crates/parser/parse_expr_basic.rs | 110 ++++++ crates/parser/parse_expr_if.rs | 61 ++++ crates/parser/parse_fn_declaration.rs | 111 ++++++ crates/parser/parse_module.rs | 33 ++ crates/parser/parse_param.rs | 30 ++ crates/parser/parse_statement.rs | 28 ++ crates/parser/parse_top_level_statement.rs | 10 + crates/parser/parse_type.rs | 26 ++ crates/parser/parser.rs | 104 ++++++ crates/tokenizer/Cargo.toml | 2 - crates/tokenizer/src/lib.rs | 214 ++++++++---- crates/util/Cargo.toml | 8 - crates/util/src/lib.rs | 30 -- examples/basics/control.blight | 18 +- 44 files changed, 1941 insertions(+), 1245 deletions(-) create mode 100644 crates/ast/ast.rs create mode 100644 crates/ast/lib.rs create mode 100644 crates/ast/operators.rs delete mode 100644 crates/ast/src/error.rs delete mode 100644 crates/ast/src/lib.rs delete mode 100644 crates/ast/src/parse_block.rs delete mode 100644 crates/ast/src/parse_expression.rs delete mode 100644 crates/ast/src/parse_function_call.rs delete mode 100644 crates/ast/src/parse_statement.rs delete mode 100644 crates/ast/src/parse_type.rs delete mode 100644 crates/ast/src/types.rs create mode 100644 crates/ast/types.rs create mode 100644 crates/checker/src/ast_type_coersion.rs create mode 100644 crates/parser/Cargo.toml create mode 100644 crates/parser/parse_block.rs create mode 100644 crates/parser/parse_call_arguments.rs create mode 100644 crates/parser/parse_declaration.rs create mode 100644 crates/parser/parse_expr.rs create mode 100644 crates/parser/parse_expr_basic.rs create mode 100644 crates/parser/parse_expr_if.rs create mode 100644 crates/parser/parse_fn_declaration.rs create mode 100644 crates/parser/parse_module.rs create mode 100644 crates/parser/parse_param.rs create mode 100644 crates/parser/parse_statement.rs create mode 100644 crates/parser/parse_top_level_statement.rs create mode 100644 crates/parser/parse_type.rs create mode 100644 crates/parser/parser.rs delete mode 100644 crates/util/Cargo.toml delete mode 100644 crates/util/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8317b65192cbb35945e6d398cc412aea7bcee045..0d4dfa5e9ed55431cd6d3cfba52ca941ce7660df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,9 +70,9 @@ name = "ast" version = "0.1.0" dependencies = [ "miette", + "num", "thiserror", "tokenizer", - "util", ] [[package]] @@ -134,7 +134,6 @@ dependencies = [ "miette", "num", "thiserror", - "util", ] [[package]] @@ -187,6 +186,7 @@ dependencies = [ "js_ast", "js_printer", "miette", + "parser", "thiserror", "tokenizer", ] @@ -250,7 +250,6 @@ dependencies = [ "miette", "num", "thiserror", - "util", ] [[package]] @@ -261,7 +260,6 @@ dependencies = [ "miette", "num", "thiserror", - "util", ] [[package]] @@ -420,6 +418,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" @@ -555,7 +564,6 @@ version = "0.1.0" dependencies = [ "miette", "thiserror", - "util", ] [[package]] @@ -582,10 +590,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..d786a2120adc1dbefba1c1002a3a56ec7fd9c010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,20 @@ 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/checker", + "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..972216ddbe6d10e928f7160f8f5d999983829948 --- /dev/null +++ b/crates/ast/ast.rs @@ -0,0 +1,174 @@ +use tokenizer::Loc; + +use crate::{ + types::{Literal, Type}, + BinaryOperator, UnaryOperator, +}; + +#[derive(Debug, Clone)] +pub enum BuiltinId { + Raw, +} + +#[derive(Debug, Clone)] +pub struct Module { + 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 Decl { + pub name: String, + pub decl_type: Type, + pub init: Expr, + pub is_constant: bool, + pub is_pub: bool, + pub loc: Loc, + pub comment: Option, +} + +#[derive(Debug, Clone)] +pub struct Expr { + pub data: ExprData, + pub loc: Loc, +} + +impl Expr { + pub fn new(data: ExprData, loc: Loc) -> Self { + Self { data, loc } + } +} + +#[derive(Debug, Clone)] +pub struct DocComment { + pub data: String, + pub loc: Loc, +} + +#[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 ExprData { + Block(Block), + Binary { + op: BinaryOperator, + left: Box, + right: Box, + }, + Unary { + op: UnaryOperator, + value: Box, + }, + Fn { + name: String, + params: Vec, + return_type: Type, + block: Block, + }, + 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), + Decl(Decl), +} + +#[derive(Debug, Clone)] +pub enum TopLevelStatement { + Expr(Expr), + Decl(Decl), +} + +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 requires_semicolon(&self) -> bool { + match &self { + Statement::Expr(expr) => match expr.data { + ExprData::Block(_) => false, + ExprData::If(_) => false, + _ => true, + }, + _ => false, + } + } + + pub fn to_top_level(self) -> TopLevelStatement { + match self { + Statement::Expr(expr) => TopLevelStatement::Expr(expr), + Statement::Decl(decl) => TopLevelStatement::Decl(decl), + } + } +} + +impl TopLevelStatement { + pub fn requires_semicolon(&self) -> bool { + match &self { + TopLevelStatement::Expr(expr) => match expr.data { + ExprData::Block(_) => false, + ExprData::If(_) => false, + _ => true, + }, + _ => false, + } + } + + pub fn to_statement(self) -> Statement { + match self { + TopLevelStatement::Expr(expr) => Statement::Expr(expr), + TopLevelStatement::Decl(decl) => Statement::Decl(decl), + } + } +} 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..f7c7d4d9ffb2783bb85444d8de71dfed4c6ce2c2 --- /dev/null +++ b/crates/ast/operators.rs @@ -0,0 +1,171 @@ +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, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum UnaryOperator { + Neg, + Not, + BitNot, +} + +impl BinaryOperator { + pub fn to_string(&self) -> String { + String::from(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 => "||", + }) + } + + 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, + } + } + + /// 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(()), + }, + } + } +} + +impl UnaryOperator { + pub fn to_string(self) -> String { + String::from(match self { + Self::Neg => "-", + Self::Not => "!", + Self::BitNot => "~", + }) + } + + 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(()), + }, + } + } +} diff --git a/crates/ast/src/error.rs b/crates/ast/src/error.rs deleted file mode 100644 index c04c6362d317fb2623d187a22a7a78dee72982ff..0000000000000000000000000000000000000000 --- a/crates/ast/src/error.rs +++ /dev/null @@ -1,21 +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), - } - } - - pub fn from_string(error: String) -> AstError { - AstError { error } - } -} diff --git a/crates/ast/src/lib.rs b/crates/ast/src/lib.rs deleted file mode 100644 index 3810f82f7e30940a92838648832212aeddf5443c..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, Module, 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(Module { value: 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 1286c41fd08bdcc6267f855fa7b1a40df9056004..0000000000000000000000000000000000000000 --- a/crates/ast/src/parse_statement.rs +++ /dev/null @@ -1,245 +0,0 @@ -use miette::Result; - -use tokenizer::Token; - -use crate::Type; - -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::Var | Token::Let => { - let is_constant = match token { - Token::Var => false, - Token::Let => true, - _ => unreachable!(), - }; - let name = match tokens.next() { - Some(Token::Identifier(name)) => name, - _ => return Err(AstError::new("Expected identifier after var").into()), - }; - let return_type = match tokens.peek() { - Some(Token::Colon) => { - tokens.next(); - parse_type(tokens)? - } - _ => Type::Infer, - }; - 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::Declaration { - name: name.clone(), - decl_type: return_type, - value: Box::new(value), - is_constant, - }) - } - 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 e481d0d283d7ad230ed89190f60b859134dfce4e..0000000000000000000000000000000000000000 --- a/crates/ast/src/parse_type.rs +++ /dev/null @@ -1,28 +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), - _ => { - return Err( - AstError::from_string(format!( - "Invalid type '{}' (TODO: add custom type support)", - typ - ) - ).into()) - } - }, - Some(unexpected) => return Err(AstError::from_string(format!("Expected type, got {}", unexpected)).into()), - None => return Err(AstError::from_string("Expected type, got EOF".to_string()).into()), - } -} diff --git a/crates/ast/src/types.rs b/crates/ast/src/types.rs deleted file mode 100644 index 61a84e57cd474bd406185292dfb87fa0cfeed8b3..0000000000000000000000000000000000000000 --- a/crates/ast/src/types.rs +++ /dev/null @@ -1,153 +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, 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 numeric literals. This allows integer literals - /// to coerce to integer or float depending on the context, but prevents - /// implicit casting of runtime values. - NumberLiteral(f64), - /// Return value of `@raw`. All type checks are disabled. - Any, - /// noreturn is the type of 'return', aka a value that never exists - 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), -} - -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::NumberLiteral(_) => write!(f, "numeric literal"), - 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::Union(of) => { - for (i, t) in of.iter().enumerate() { - if i != 0 { - write!(f, " | ")?; - } - write!(f, "{}", t)?; - } - Ok(()) - } - } - } -} - -#[derive(Debug, Clone)] -pub struct Module { - pub value: Vec, -} - -#[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 { - Declaration { - name: String, - decl_type: Type, - value: Box, - is_constant: bool, - }, - 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/checker/Cargo.toml b/crates/checker/Cargo.toml index 9911221e439591b90a010b7a36b71770a76c2305..35bcf3fac1c683c696223e428ba9f59ff2bb8c75 100644 --- a/crates/checker/Cargo.toml +++ b/crates/checker/Cargo.toml @@ -10,5 +10,4 @@ miette.workspace = true thiserror.workspace = true num.workspace = true -ast = { path = "../ast" } -util = { path = "../util" } +ast = { path = "../ast" } \ No newline at end of file diff --git a/crates/checker/src/ast_type_coersion.rs b/crates/checker/src/ast_type_coersion.rs new file mode 100644 index 0000000000000000000000000000000000000000..411442e62cbed296913068bdf7011e02e4eb4601 --- /dev/null +++ b/crates/checker/src/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/checker/src/lib.rs b/crates/checker/src/lib.rs index 7f68543e0b5f8f27ec36090523bf10227dd0770a..c7a930d635ec3bd0f58f49a10c38bc95b5d81d9e 100644 --- a/crates/checker/src/lib.rs +++ b/crates/checker/src/lib.rs @@ -1,203 +1,96 @@ -use ast::{Ast, Module, Type}; -use miette::Result; - -use miette::Diagnostic; -use thiserror::Error; - -#[derive(Error, Diagnostic, Debug)] -#[error("TypeError")] -pub struct TypeError { - #[help] - error: String, -} - -impl TypeError { - pub fn new(error: String) -> Self { - Self { error } - } -} - -pub fn check_module(block: &Module) -> Result<()> { - for statement in &block.value { - 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::Operation { - 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 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!("Cannot merge {} 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::NumberLiteral(_) => unreachable!(), - - Type::Int => 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 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()) - } - } - } -} - -pub fn check_expression(expr: &Ast) -> Result<()> { - 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(()) -} +// use ast::{ Module, Type}; +// use miette::Result; + +// mod ast_type_coersion; + +// pub fn check_module(block: &Module) -> Result<()> { +// 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/cli/Cargo.toml b/crates/cli/Cargo.toml index a0e6f101de7d10297488257b9f58ddb06354076d..779b647427e55d42b654c970a4baf081f2551ed1 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -14,6 +14,7 @@ thiserror.workspace = true tokenizer = { path = "../tokenizer" } ast = { path = "../ast" } +parser = { path = "../parser" } js_ast = { path = "../js_ast" } js_printer = { path = "../js_printer" } checker = { path = "../checker" } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 254bad1c38f34c5af9d8a13b3c5e1b3482e93cd8..4bd03112c4df413316fd9acfe40662d71d26abaf 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,10 +1,8 @@ use std::{io::Write, path::PathBuf}; -use checker; use clap::{Parser, Subcommand}; use miette::Result; -use ast; use js_printer; use tokenizer; @@ -40,6 +38,9 @@ enum Commands { #[arg(short, long, default_value = "deno")] target: String, }, + Astdump { + file: PathBuf, + }, } fn get_runtime(runtime: &String) -> String { @@ -53,9 +54,9 @@ 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)?; + let blight_ast = parser::parse(tokens)?; // 3. Type check the AST - checker::check_module(&blight_ast)?; + // checker::check_module(&blight_ast)?; // 4. Optimize the AST // optimize(tree.clone())?; // 5. Translate the AST into javascript @@ -66,6 +67,14 @@ pub fn transpile(content: String) -> Result { return Ok(javascript); } +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(); @@ -145,6 +154,11 @@ fn main() -> Result<()> { .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/js_ast/Cargo.toml b/crates/js_ast/Cargo.toml index ae2bc9036ff37259b6f1bbd79effdf1c8e293511..dbbaf2f2c722dce465082656b9ac56a078b9730e 100644 --- a/crates/js_ast/Cargo.toml +++ b/crates/js_ast/Cargo.toml @@ -11,4 +11,3 @@ thiserror.workspace = true num.workspace = true ast = { path = "../ast" } -util = { path = "../util" } diff --git a/crates/js_ast/src/lib.rs b/crates/js_ast/src/lib.rs index 28685a906d0f4fa1e3083a8b6f70bc9a97e3457f..d0b9dd88049ab72f5f7b4e50cd2d9a480294a454 100644 --- a/crates/js_ast/src/lib.rs +++ b/crates/js_ast/src/lib.rs @@ -1,9 +1,8 @@ use std::collections::HashMap; +use ast::{Expr, ExprData, Literal, Module, Statement}; use miette::Result; -use ast::{Ast, Module}; - mod types; pub use types::{JsExpression, JsStatement}; @@ -99,68 +98,69 @@ impl TranslateContext { // 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 } => { +fn get_expression_handle(ast: &Expr, ctx: &mut TranslateContext) -> JsExpression { + match ast.data.clone() { + ExprData::Literal(Literal::String(value)) => JsExpression::String(value), + ExprData::Literal(Literal::Number(value)) => JsExpression::Number(value), + ExprData::Literal(Literal::Void) => JsExpression::Identifier("null".to_string()), + ExprData::Literal(Literal::Bool(value)) => JsExpression::Identifier(if value { + "true".to_string() + } else { + "false".to_string() + }), + + ExprData::Identifier(value) => JsExpression::Identifier(ctx.get_identifier(value.clone())), + ExprData::Call { base, 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"), + handles.push(get_expression_handle(&argument, ctx)); } + let base = get_expression_handle(&base, ctx); + ctx.push_internal_variable(JsExpression::Call { + base: Box::new(base), + arguments: handles, + }) } - 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(), + ExprData::Builtin { id, arguments } => match id { + ast::BuiltinId::Raw => { + if arguments.len() != 1 { + panic!("@raw expects exactly one argument"); + } + match arguments.get(0).unwrap() { + Expr { + data: ExprData::Literal(Literal::String(value)), + .. + } => JsExpression::Raw(value.clone()), + _ => panic!("@raw expects a string literal"), + } + } + }, + ExprData::Binary { op, left, right } => { + let left_handle = get_expression_handle(&left, ctx); + let right_handle = get_expression_handle(&right, ctx); + JsExpression::Binary { + operator: op.clone(), left: Box::new(left_handle), right: Box::new(right_handle), } } - Ast::Block(block) => { - if !block.implicit_return { + ExprData::Block(block) => { + if !block.has_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 { + for (i, statement) in block.statements.iter().enumerate() { + if i == block.statements.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 + todo!() + // let result = get_expression_handle(block.statements.last().unwrap(), ctx); + // let value = ctx.pop_context(); + // value.iter().for_each(|x| ctx.push(x.clone())); + // result } _ => { panic!("unimplemented: {:?}", ast); @@ -168,168 +168,164 @@ fn get_expression_handle(ast: &Ast, ctx: &mut TranslateContext) -> JsExpression } } -fn translate_statement(ast: &Ast, ctx: &mut TranslateContext) { +fn translate_statement(ast: &Statement, ctx: &mut TranslateContext) { match ast { - Ast::Declaration { - name, - decl_type: _, - value, - is_constant, - } => { - let handle = get_expression_handle(value, ctx); - let rename = ctx.declare_variable(name.clone()); + Statement::Decl(decl) => { + let handle = get_expression_handle(&decl.init, ctx); + let rename = ctx.declare_variable(decl.name.clone()); ctx.push(JsStatement::Declaration { name: rename, value: handle, - is_constant: is_constant.clone(), + is_constant: decl.is_constant.clone(), }); } - 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(); + Statement::Expr(_) => todo!(), + // 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_context(); + // for (i, statement) in block.value.iter().enumerate() { + // if block.implicit_return && i == block.value.len() - 1 { + // let handle = get_expression_handle(statement, ctx); + // ctx.push(JsStatement::Return(handle)); + // } else { + // translate_statement(statement, ctx); + // } + // } + // let value = ctx.pop_context(); - ctx.push(JsStatement::Function { - name: rename, - arguments, - value, - }); - } - Ast::Public(value) => { - translate_statement(value, ctx); - // I'm sorry, I just don't know how to get the original name properly... my bad - match ctx.peek() { - JsStatement::Function { name, .. } | JsStatement::Declaration { name, .. } => { - let original_name = name.split("$").next().unwrap().to_string(); - ctx.push(JsStatement::Export(Box::new(JsStatement::Declaration { - name: original_name, - value: JsExpression::Identifier(name.clone()), - is_constant: false, - }))); - } - _ => { - 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(JsStatement::Function { + // name: rename, + // arguments, + // value, + // }); + // } + // Ast::Public(value) => { + // translate_statement(value, ctx); + // // I'm sorry, I just don't know how to get the original name properly... my bad + // match ctx.peek() { + // JsStatement::Function { name, .. } | JsStatement::Declaration { name, .. } => { + // let original_name = name.split("$").next().unwrap().to_string(); + // ctx.push(JsStatement::Export(Box::new(JsStatement::Declaration { + // name: original_name, + // value: JsExpression::Identifier(name.clone()), + // is_constant: true, + // }))); + // } + // _ => { + // panic!("Cannot make non-function public"); + // } + // } + // } + // Ast::Return(value) => { + // let handle = get_expression_handle(value, ctx); + // ctx.push(JsStatement::Return(handle)); + // } + // Ast::Block(block) => { + // ctx.push_context(); + // for (i, statement) in block.value.iter().enumerate() { + // if block.implicit_return && i == block.value.len() - 1 { + // let handle = get_expression_handle(statement, ctx); + // ctx.push(JsStatement::Return(handle)); + // } else { + // translate_statement(statement, ctx); + // } + // } + // let value = ctx.pop_context(); + // value.iter().for_each(|x| ctx.push(x.clone())); + // } + // Ast::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_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); - } + // 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::Call { + // 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::Call { + // 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(module: &Module) -> Result> { let mut ctx = TranslateContext::new(); - for ast in module.value.iter() { - translate_statement(&ast, &mut ctx); + for ast in module.statements.iter() { + translate_statement(&ast.clone().to_statement(), &mut ctx); } Ok(ctx.pop_context()) diff --git a/crates/js_ast/src/types.rs b/crates/js_ast/src/types.rs index 3b5f176e4a51e7ccf14eb6fcf3be46c25577f8b2..f53fb3f665655d739d189bd01a1ae334579b7167 100644 --- a/crates/js_ast/src/types.rs +++ b/crates/js_ast/src/types.rs @@ -1,8 +1,7 @@ +use ast::BinaryOperator; use num::bigint::BigInt; use std::collections::HashMap; -use util::Operator; - #[derive(Debug, PartialEq, Clone)] pub enum JsExpression { Number(f64), @@ -13,8 +12,8 @@ pub enum JsExpression { Raw(String), Array(Vec), Object(HashMap), - Operation { - operator: Operator, + Binary { + operator: BinaryOperator, left: Box, right: Box, }, @@ -22,8 +21,8 @@ pub enum JsExpression { arguments: Vec, value: Vec, }, - FunctionCall { - name: Box, + Call { + base: Box, arguments: Vec, }, } diff --git a/crates/js_printer/Cargo.toml b/crates/js_printer/Cargo.toml index 0071929ae0046ab381623d16ae23e397e7f36393..ec58b9490b3338b6f12f18906971afbdadb9f4ca 100644 --- a/crates/js_printer/Cargo.toml +++ b/crates/js_printer/Cargo.toml @@ -11,4 +11,3 @@ thiserror.workspace = true num.workspace = true js_ast = { path = "../js_ast" } -util = { path = "../util" } diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs index e13e3ebbda35bf9db23ae4dd38c3d8cb513c65f6..15810d077d963fe78e1c45c6c4247403dce9677a 100644 --- a/crates/js_printer/src/print_expression.rs +++ b/crates/js_printer/src/print_expression.rs @@ -41,7 +41,7 @@ pub fn print_expression(statement: &JsExpression) -> String { .collect::>() .join(",") ), - JsExpression::Operation { + JsExpression::Binary { operator, left, right, @@ -60,9 +60,9 @@ pub fn print_expression(statement: &JsExpression) -> String { .collect::>() .join("") ), - JsExpression::FunctionCall { name, arguments } => format!( + JsExpression::Call { base, arguments } => format!( "{}({})", - match *name.clone() { + match *base.clone() { JsExpression::Identifier(value) => value.clone(), JsExpression::Raw(value) => value.clone(), _ => panic!("Function call name must be an identifier"), @@ -83,7 +83,6 @@ mod tests { use js_ast::JsStatement; use num::{bigint::Sign, BigInt}; use std::collections::HashMap; - use util::Operator; #[test] fn number() { @@ -184,16 +183,16 @@ mod tests { #[test] fn operation() { assert_eq!( - print_expression(&JsExpression::Operation { - operator: Operator::Add, + print_expression(&JsExpression::Binary { + operator: AnyOperator::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, + print_expression(&JsExpression::Binary { + operator: AnyOperator::Sub, left: Box::new(JsExpression::Number(1.0)), right: Box::new(JsExpression::Number(2.0)), }), @@ -242,21 +241,21 @@ mod tests { #[test] fn function_call() { assert_eq!( - print_expression(&JsExpression::FunctionCall { + print_expression(&JsExpression::Call { name: Box::new(JsExpression::Identifier("hello".to_string())), arguments: vec![], }), "hello()" ); assert_eq!( - print_expression(&JsExpression::FunctionCall { + print_expression(&JsExpression::Call { name: Box::new(JsExpression::Identifier("hello".to_string())), arguments: vec![JsExpression::Number(1.0)], }), "hello(1)" ); assert_eq!( - print_expression(&JsExpression::FunctionCall { + print_expression(&JsExpression::Call { name: Box::new(JsExpression::Identifier("hello".to_string())), arguments: vec![JsExpression::Number(1.0), JsExpression::Number(2.0)], }), 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..4c6dc77d314740afd0a31a442a85885d18403e89 --- /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(); + + let was_top_level = p.is_top_level; + p.is_top_level = false; + + loop { + let statement = match parse_statement(p) { + Ok(statement) => statement, + Err(err) => { + p.is_top_level = was_top_level; + return Err(err); + } + }; + statements.push(statement.clone()); + + match p.next() { + Some(Token::Semicolon) => loop { + match p.peek() { + Some(Token::ClosedBrace) => { + p.next(); + p.is_top_level = was_top_level; + return Ok(Block { + statements, + has_implicit_return: false, + }); + } + _ => continue, + } + }, + Some(Token::ClosedBrace) => { + if statement.requires_semicolon() { + p.is_top_level = was_top_level; + return Err(p + .error("Expected semicolon after this statement".to_string()) + .into()); + } + + p.is_top_level = was_top_level; + return Ok(Block { + statements, + has_implicit_return: true, + }); + } + _ => { + p.is_top_level = was_top_level; + return Err(p + .error("Expected semicolon or '}' after statement".to_string()) + .into()); + } + }; + } +} diff --git a/crates/parser/parse_call_arguments.rs b/crates/parser/parse_call_arguments.rs new file mode 100644 index 0000000000000000000000000000000000000000..afa16cdcfef28ab3c3dd309f6d2036b5403120bb --- /dev/null +++ b/crates/parser/parse_call_arguments.rs @@ -0,0 +1,31 @@ +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(); + + 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..3fbfc4e8ef2e6d7ee91926c2de05acbc3d46e679 --- /dev/null +++ b/crates/parser/parse_declaration.rs @@ -0,0 +1,46 @@ +use ast::{Decl, Type}; +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) -> 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(Decl { + name: name.clone(), + decl_type: return_type, + init, + is_constant, + is_pub, + comment: None, + loc, + }) +} diff --git a/crates/parser/parse_expr.rs b/crates/parser/parse_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..98d098d3c847c174c0a9ca6a375cc038fa6d2b55 --- /dev/null +++ b/crates/parser/parse_expr.rs @@ -0,0 +1,161 @@ +use ast::{BinaryOperator, Expr, ExprData, Literal}; +use miette::Result; +use tokenizer::Token; + +use crate::{ + parse_call_arguments::parse_call_arguments, parse_expr_basic::parse_expr_basic, Parser, +}; + +/// Derived from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table + +#[derive(PartialEq, PartialOrd)] +enum Level { + Prefix, // +, -, !, ~ + Exponentiation, // ** + Multiplicative, // *, /, % + Additive, // +, - + BitwiseShift, // <<, >>, >>> + Relational, // <, <=, >, >= + Equality, // ==, != + BitwiseAnd, // & + BitwiseXor, // ^ + BitwiseOr, // | + LogicalAnd, // && + LogicalOr, // ||, ?? +} + +pub fn get_postfix_op_level(token: &Token) -> Option { + match token { + Token::StarStar => Some(Level::Exponentiation), + Token::Star | Token::Slash | Token::Percent => Some(Level::Multiplicative), + Token::Plus | Token::Minus => Some(Level::Additive), + Token::LtLt | Token::GtGt | Token::GtGtGt => Some(Level::BitwiseShift), + Token::Gt | Token::GtEqual | Token::Lt | Token::LtEqual => Some(Level::Relational), + Token::EqualEqual | Token::BangEqual => Some(Level::Equality), + Token::And => Some(Level::BitwiseAnd), + Token::Caret => Some(Level::BitwiseXor), + Token::Or => Some(Level::BitwiseOr), + Token::AndAnd => Some(Level::LogicalAnd), + Token::OrOr => Some(Level::LogicalOr), + _ => None, + } +} + +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 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) + } + _ => { + 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, + ); + } + 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, + ); + } + 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, + ); + } else { + return Err(p + .error("Expected closing bracket after index".to_string()) + .into()); + } + } + _ => break, + } + } + + // Parse operators + loop { + if let Some(token) = p.peek() { + if let Some(level) = get_postfix_op_level(&token) { + _ = p.next(); + expr = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + expr.clone(), + level, + )?; + continue; + } + } + break; + } + + Ok(expr) +} + +/// Prerequisite: `p.peek()` is an operator +fn parse_expr_binary(p: &mut Parser, op: BinaryOperator, left: Expr, level: Level) -> Result { + let loc = p.loc(); + let mut right = parse_expr_basic(p)?; + + loop { + if let Some(token) = p.peek() { + if let Some(next_level) = get_postfix_op_level(&token) { + if next_level <= level { + _ = p.next(); + right = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + right, + next_level, + )?; + continue; + } + } + } + break; + } + + Ok(Expr::new( + ExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + loc, + )) +} diff --git a/crates/parser/parse_expr_basic.rs b/crates/parser/parse_expr_basic.rs new file mode 100644 index 0000000000000000000000000000000000000000..8cc9c8409c33d5a4a1ae08550f4cf60ea63a8a76 --- /dev/null +++ b/crates/parser/parse_expr_basic.rs @@ -0,0 +1,110 @@ +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(); + match p.peek() { + Some(Token::Number(number)) => { + _ = p.next(); + Ok(Expr::new( + ExprData::Literal(Literal::Number(number.clone())), + loc, + )) + } + Some(Token::String(string)) => { + _ = p.next(); + Ok(Expr::new( + ExprData::Literal(Literal::String(string.clone())), + loc, + )) + } + Some(Token::True) => { + _ = p.next(); + Ok(Expr::new(ExprData::Literal(Literal::Bool(true)), loc)) + } + Some(Token::False) => { + _ = p.next(); + Ok(Expr::new(ExprData::Literal(Literal::Bool(false)), loc)) + } + Some(Token::Void) => { + _ = p.next(); + Ok(Expr::new(ExprData::Literal(Literal::Void), loc)) + } + 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)?; + return Ok(Expr::new(ExprData::Builtin { id, arguments }, loc)); + } 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 { + Ok(Expr::new(ExprData::Identifier(name), loc)) + } + } + + Some(Token::Return) => { + if p.is_top_level { + 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)); + } + } + + Some(Token::If) => Ok(Expr::new(ExprData::If(parse_expr_if(p)?), loc)), + + Some(Token::OpenBrace) => Ok(Expr::new(ExprData::Block(parse_block(p)?), loc)), + + Some(Token::OpenParen) => { + _ = p.next(); + let expr = parse_expr_basic(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()), + } + Ok(expr) + } + + Some(t @ Token::Bang) | Some(t @ Token::Minus) | Some(t @ Token::Tilde) => { + _ = p.next(); + Ok(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, + )) + } + + Some(t) => Err(p.error(format!("Unexpected '{}'", t)).into()), + None => Err(p.error("Unexpected end of file".to_string()).into()), + } +} diff --git a/crates/parser/parse_expr_if.rs b/crates/parser/parse_expr_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..a956585da772e01c03c550745f8b33687c79356f --- /dev/null +++ b/crates/parser/parse_expr_if.rs @@ -0,0 +1,61 @@ +use ast::{BlockWithLoc, If}; +use miette::Result; +use tokenizer::Token; + +use crate::{parse_block::parse_block, parse_expr::parse_expr, 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(); + Some(BlockWithLoc { + block: parse_block(p)?, + loc, + }) + } + _ => 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..4b7405c6bf3a57ab3b0bd50e9a2e26f8b12bd664 --- /dev/null +++ b/crates/parser/parse_fn_declaration.rs @@ -0,0 +1,111 @@ +use ast::{Decl, Expr, ExprData, Type}; +use miette::Result; +use tokenizer::Token; + +use crate::parse_block::parse_block; +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) -> 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) => break, + _ => { + return Err(p + .error("Expected ',' or ')' after identifier".to_string()) + .into()) + } + }; + } + _ => { + return Err(p + .error("Expected ')' or identifier after '('".to_string()) + .into()) + } + } + + match p.peek() { + Some(Token::Comma) => p.next(), + Some(Token::ClosedParen) => { + _ = p.next(); + break; + } + _ => { + return Err(p + .error("Expected ',' or ')' after identifier".to_string()) + .into()) + } + }; + } + + let return_type = match p.peek() { + Some(Token::RightArrow) => { + _ = p.next(); + parse_type(p)? + } + _ => Type::Infer, + }; + + if let Some(Token::OpenBrace) = p.peek() { + let block_loc = p.loc(); + let block = parse_block(p)?; + let function_body = ExprData::Fn { + name: name.clone(), + params, + return_type, + block, + }; + Ok(Decl { + name, + decl_type: Type::Any, + init: Expr::new(function_body, block_loc), + is_constant: true, + is_pub, + comment: None, + loc: loc, + }) + } else { + return Err(p + .error("Expected a block to follow function declaration".to_string()) + .into()); + } +} diff --git a/crates/parser/parse_module.rs b/crates/parser/parse_module.rs new file mode 100644 index 0000000000000000000000000000000000000000..9ff66244156f39002142e205e7bd4a4ed6f73cd4 --- /dev/null +++ b/crates/parser/parse_module.rs @@ -0,0 +1,33 @@ +use crate::{parse_top_level_statement::parse_top_level_statement, Parser}; +use ast::Module; +use miette::Result; +use tokenizer::Token; + +pub fn parse_module(tokens: &mut Parser) -> Result { + let mut statements = Vec::new(); + + loop { + let statement = parse_top_level_statement(tokens)?; + + match tokens.peek() { + Some(Token::Semicolon) => { + while let Some(Token::Semicolon) = tokens.peek() { + tokens.next(); + } + } + _ => { + if statement.requires_semicolon() { + return Err(tokens + .error("Expected semicolon after statement".to_string()) + .into()); + } + } + }; + + statements.push(statement); + + if tokens.empty() { + return Ok(Module { statements }); + } + } +} 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..c3da1e4f219af484a5af82b0d45c35f68444c92e --- /dev/null +++ b/crates/parser/parse_statement.rs @@ -0,0 +1,28 @@ +use ast::Statement; +use miette::Result; +use tokenizer::Token; + +use crate::parse_declaration::parse_declaration; +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 { + match p.peek() { + Some(Token::Pub) => { + _ = p.next(); + match p.peek() { + Some(Token::Var) | Some(Token::Let) => { + Ok(Statement::Decl(parse_declaration(p, true)?)) + } + Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration(p, true)?)), + 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::Decl(parse_declaration(p, false)?)), + Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration(p, false)?)), + Some(_) => 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..0dda281df5e97957e5a57c1ef5261894b56034a0 --- /dev/null +++ b/crates/parser/parser.rs @@ -0,0 +1,104 @@ +mod parse_block; +mod parse_call_arguments; +mod parse_declaration; +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 tokenizer::{Loc, Token}; + +use miette::Diagnostic; +use thiserror::Error; + +use ast::Module; +use miette::Result; +use parse_module::parse_module; +use tokenizer::tokenize; + +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 { + pub tokens: Vec, + pub cursor: usize, + + /// Whether or not the parser is at the top level + pub is_top_level: bool, +} + +impl Parser { + pub fn new(tokens: Vec) -> Parser { + Parser { + tokens, + cursor: 0, + is_top_level: true, + } + } + + 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()) + } + + 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/tokenizer/Cargo.toml b/crates/tokenizer/Cargo.toml index 21243ee5fc409f648340819f1b792d721c214741..50effc8036b4f1705b14ec46fd16ad36622229e7 100644 --- a/crates/tokenizer/Cargo.toml +++ b/crates/tokenizer/Cargo.toml @@ -8,5 +8,3 @@ edition = "2021" [dependencies] miette.workspace = true thiserror.workspace = true - -util = { path = "../util" } diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/src/lib.rs index a35ad44dd5d8122748a865188c70b94544a9fb3a..d536ccd14cf9a7bcd522e139e2f8f60d727786ad 100644 --- a/crates/tokenizer/src/lib.rs +++ b/crates/tokenizer/src/lib.rs @@ -1,7 +1,8 @@ use miette::{Diagnostic, Result}; use thiserror::Error; -use util::Operator; +#[derive(Debug, Clone)] +pub struct Loc {} #[derive(Error, Diagnostic, Debug)] #[error("UnknownCharacter")] @@ -16,69 +17,118 @@ pub enum Token { Identifier(String), Number(f64), String(String), - BuiltIn(String), // Comments Comment(String), DocComment(String), // Keywords - True, + Else, False, - Let, - Var, + Fn, If, - Else, - Return, + Let, Pub, - Function, - RightArrow, - Comma, - OpenParanthesis, - ClosedParanthesis, - OpenBracket, - ClosedBracket, - Colon, - Equals, - Semicolon, - // Operators - Operator(Operator), + Return, + True, + Var, + Void, + // Symbols + And, // & + AndAnd, // && + Bang, // ! + BangEqual, // != + Caret, // ^ + ClosedBrace, // } + ClosedBracket, // ] + ClosedParen, // ) + Colon, // : + Comma, // , + Equal, // = + EqualEqual, // == + Gt, // > + GtEqual, // >= + GtGt, // >> + GtGtGt, // >>> + Lt, // < + LtEqual, // <= + LtLt, // << + Minus, // - + OpenBrace, // { + OpenBracket, // [ + OpenParen, // ( + Or, // | + OrOr, // || + Percent, // % + Plus, // + + RightArrow, // -> + Semicolon, // ; + Slash, // / + Star, // * + StarStar, // ** + Tilde, // ~ + Dot, // . } impl std::fmt::Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let token = match self { Token::Identifier(_) => "an identifier".to_string(), + Token::Number(n) => n.to_string(), Token::String(s) => format!("\"{}\"", s), - Token::BuiltIn(_) => "a builtin identifier".to_string(), Token::Comment(_) => "a comment".to_string(), Token::DocComment(_) => "a documentation comment".to_string(), - Token::True => "true".to_string(), + + Token::Else => "else".to_string(), Token::False => "false".to_string(), - Token::Let => "let".to_string(), - Token::Var => "var".to_string(), + Token::Fn => "fn".to_string(), Token::If => "if".to_string(), - Token::Else => "else".to_string(), - Token::Return => "return".to_string(), + Token::Let => "let".to_string(), Token::Pub => "pub".to_string(), - Token::Function => "fn".to_string(), - Token::RightArrow => "->".to_string(), - Token::Comma => ",".to_string(), - Token::OpenParanthesis => "(".to_string(), - Token::ClosedParanthesis => ")".to_string(), - Token::OpenBracket => "{".to_string(), - Token::ClosedBracket => "}".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::Caret => "^".to_string(), + Token::ClosedBrace => "}".to_string(), + Token::ClosedBracket => "]".to_string(), + Token::ClosedParen => ")".to_string(), Token::Colon => ":".to_string(), - Token::Equals => "=".to_string(), + Token::Comma => ",".to_string(), + Token::Equal => "=".to_string(), + Token::EqualEqual => "==".to_string(), + Token::Gt => ">".to_string(), + Token::GtEqual => ">=".to_string(), + Token::GtGt => ">>".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::Operator(o) => o.clone().to_string(), + Token::Slash => "/".to_string(), + Token::Star => "*".to_string(), + Token::StarStar => "**".to_string(), + Token::Tilde => "~".to_string(), + Token::Dot => ".".to_string(), + Token::GtGtGt => ">>>".to_string(), }; write!(f, "{}", token) } } - - pub fn tokenize(content: String) -> Result> { let mut tokens: Vec = Vec::new(); let mut chars = content.chars().enumerate().peekable(); @@ -105,22 +155,17 @@ pub fn tokenize(content: String) -> Result> { } 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), + "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), - "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)) - } - } + "var" => tokens.push(Token::Var), + "void" => tokens.push(Token::Void), + _ => tokens.push(Token::Identifier(current_token)), } } '"' => { @@ -169,39 +214,53 @@ pub fn tokenize(content: String) -> Result> { '=' => match chars.peek() { Some((_, '=')) => { chars.next(); - tokens.push(Token::Operator(Operator::Equals)); + tokens.push(Token::EqualEqual); } - _ => tokens.push(Token::Equals), + _ => tokens.push(Token::Equal), }, ';' => tokens.push(Token::Semicolon), '>' => match chars.peek() { Some((_, '=')) => { chars.next(); - tokens.push(Token::Operator(Operator::GreaterThanOrEquals)); + tokens.push(Token::GtEqual); } - _ => tokens.push(Token::Operator(Operator::GreaterThan)), + 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::Operator(Operator::LessThanOrEquals)); + tokens.push(Token::LtEqual); } - _ => tokens.push(Token::Operator(Operator::LessThan)), + Some((_, '<')) => { + chars.next(); + tokens.push(Token::LtLt); + } + _ => tokens.push(Token::Lt), }, - '+' => tokens.push(Token::Operator(Operator::Add)), + '+' => tokens.push(Token::Plus), '-' => match chars.peek() { Some((_, '>')) => { chars.next(); tokens.push(Token::RightArrow); } - _ => tokens.push(Token::Operator(Operator::Subtract)), + _ => tokens.push(Token::Minus), }, '*' => match chars.peek() { Some((_, '*')) => { chars.next(); - tokens.push(Token::Operator(Operator::Power)); + tokens.push(Token::StarStar); } - _ => tokens.push(Token::Operator(Operator::Multiply)), + _ => tokens.push(Token::Star), }, '/' => { match chars.peek() { @@ -239,18 +298,45 @@ pub fn tokenize(content: String) -> Result> { tokens.push(Token::Comment(comment)); } } - _ => tokens.push(Token::Operator(Operator::Divide)), + _ => tokens.push(Token::Slash), } } ',' => tokens.push(Token::Comma), - '(' => tokens.push(Token::OpenParanthesis), - ')' => tokens.push(Token::ClosedParanthesis), - '{' => tokens.push(Token::OpenBracket), - '}' => tokens.push(Token::ClosedBracket), + '(' => 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!("Unsupported character: {}", c), + error: format!("Unexpected '{}' while tokenizing", c), } .into()) } 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 f6e3bdb1db079dc14869607c8fc2821eee7a89f2..579f551f87fa6dc390eb6fd92390ef066f287c36 100644 --- a/examples/basics/control.blight +++ b/examples/basics/control.blight @@ -1,17 +1 @@ -let print = @raw("console.log"); - -let x: bool = true; - -if x { - print("x is true!"); -} - -let y: int = 3; - -if y == 1 { - print("y is 1!"); -} else if y == 2 { - print("y is 2!"); -} else { - print("y is not 1 or 2!"); -} +let x: bool = 1 * 3 + 2 / 2 == 4 + 2; -- 2.54.0 From 17c6ef0255b85120d2bdcae9f48ecd8c88c3c247 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 20 Jan 2024 16:34:20 -0800 Subject: [PATCH 3/8] revert my changes to control.blight --- examples/basics/control.blight | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/basics/control.blight b/examples/basics/control.blight index 579f551f87fa6dc390eb6fd92390ef066f287c36..f6e3bdb1db079dc14869607c8fc2821eee7a89f2 100644 --- a/examples/basics/control.blight +++ b/examples/basics/control.blight @@ -1 +1,17 @@ -let x: bool = 1 * 3 + 2 / 2 == 4 + 2; +let print = @raw("console.log"); + +let x: bool = true; + +if x { + print("x is true!"); +} + +let y: int = 3; + +if y == 1 { + print("y is 1!"); +} else if y == 2 { + print("y is 2!"); +} else { + print("y is not 1 or 2!"); +} -- 2.54.0 From c89bf26922f4448e6f0cb6015f4627792841deab Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 20 Jan 2024 19:08:18 -0800 Subject: [PATCH 4/8] parser changes --- Cargo.lock | 22 ++-- Cargo.toml | 2 +- crates/ast/ast.rs | 41 ++++++- crates/ast/operators.rs | 34 ++++++ crates/cli/Cargo.toml | 2 +- crates/cli/src/main.rs | 2 +- crates/js_ast/src/lib.rs | 1 + crates/parser/parse_declaration.rs | 12 +- crates/parser/parse_doc_comment.rs | 15 +++ crates/parser/parse_expr.rs | 113 ++---------------- crates/parser/parse_expr_basic.rs | 110 +++++++++++++---- crates/parser/parse_expr_if.rs | 4 +- crates/parser/parse_fn_declaration.rs | 14 ++- crates/parser/parse_module.rs | 25 ++-- crates/parser/parse_statement.rs | 44 ++++++- crates/parser/parser.rs | 72 ++++++++--- crates/{checker => sema}/Cargo.toml | 2 +- .../src/ast_type_coersion.rs | 0 crates/{checker => sema}/src/lib.rs | 0 crates/tokenizer/src/lib.rs | 64 +++++++--- examples/basics/comments.blight | 11 +- 21 files changed, 388 insertions(+), 202 deletions(-) create mode 100644 crates/parser/parse_doc_comment.rs rename crates/{checker => sema}/Cargo.toml (90%) rename crates/{checker => sema}/src/ast_type_coersion.rs (100%) rename crates/{checker => sema}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 0d4dfa5e9ed55431cd6d3cfba52ca941ce7660df..4c8ddfacf5557906451853c5edb4394a656e2af5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,16 +126,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "checker" -version = "0.1.0" -dependencies = [ - "ast", - "miette", - "num", - "thiserror", -] - [[package]] name = "clap" version = "4.4.14" @@ -181,12 +171,12 @@ name = "cli" version = "0.1.0" dependencies = [ "ast", - "checker", "clap", "js_ast", "js_printer", "miette", "parser", + "sema", "thiserror", "tokenizer", ] @@ -466,6 +456,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "sema" +version = "0.1.0" +dependencies = [ + "ast", + "miette", + "num", + "thiserror", +] + [[package]] name = "smawk" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index d786a2120adc1dbefba1c1002a3a56ec7fd9c010..fdcf94c7291631b43fbac96e4a24885312c0d65e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/lino-levan/blight" resolver = "2" members = [ "crates/ast", - "crates/checker", + "crates/sema", "crates/cli", "crates/js_ast", "crates/js_printer", diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs index 972216ddbe6d10e928f7160f8f5d999983829948..bf9cd16e386e9321b7bd123de01cb21d622daa9a 100644 --- a/crates/ast/ast.rs +++ b/crates/ast/ast.rs @@ -12,6 +12,7 @@ pub enum BuiltinId { #[derive(Debug, Clone)] pub struct Module { + pub comment: Option, pub statements: Vec, } @@ -31,7 +32,7 @@ pub struct Param { pub struct Decl { pub name: String, - pub decl_type: Type, + pub data_type: Type, pub init: Expr, pub is_constant: bool, pub is_pub: bool, @@ -43,11 +44,12 @@ pub struct Decl { pub struct Expr { pub data: ExprData, pub loc: Loc, + pub comment: Option, } impl Expr { - pub fn new(data: ExprData, loc: Loc) -> Self { - Self { data, loc } + pub fn new(data: ExprData, loc: Loc, comment: Option) -> Self { + Self { data, loc, comment } } } @@ -57,6 +59,35 @@ pub struct DocComment { pub loc: Loc, } +#[derive(Debug, 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 + } +} + +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, @@ -110,12 +141,14 @@ pub enum ExprData { pub enum Statement { Expr(Expr), Decl(Decl), + Comment(Comment), } #[derive(Debug, Clone)] pub enum TopLevelStatement { Expr(Expr), Decl(Decl), + Comment(Comment), } impl BuiltinId { @@ -149,6 +182,7 @@ impl Statement { match self { Statement::Expr(expr) => TopLevelStatement::Expr(expr), Statement::Decl(decl) => TopLevelStatement::Decl(decl), + Statement::Comment(comment) => TopLevelStatement::Comment(comment), } } } @@ -169,6 +203,7 @@ impl TopLevelStatement { match self { TopLevelStatement::Expr(expr) => Statement::Expr(expr), TopLevelStatement::Decl(decl) => Statement::Decl(decl), + TopLevelStatement::Comment(comment) => Statement::Comment(comment), } } } diff --git a/crates/ast/operators.rs b/crates/ast/operators.rs index f7c7d4d9ffb2783bb85444d8de71dfed4c6ce2c2..18dd5ae68a5961b785e815adca22dd9c71418907 100644 --- a/crates/ast/operators.rs +++ b/crates/ast/operators.rs @@ -169,3 +169,37 @@ impl UnaryOperator { } } } + +/// Taken from from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table +#[derive(PartialEq, PartialOrd)] +pub enum BinaryOperatorLevel { + Exponentiation, // ** + Multiplicative, // *, /, % + Additive, // +, - + BitwiseShift, // <<, >>, >>> + Relational, // <, <=, >, >= + Equality, // ==, != + BitwiseAnd, // & + BitwiseXor, // ^ + BitwiseOr, // | + LogicalAnd, // && + LogicalOr, // ||, ?? +} + +pub fn get_binary_op_level(token: &Token) -> Option { + use BinaryOperatorLevel::*; + match token { + Token::StarStar => Some(Exponentiation), + Token::Star | Token::Slash | Token::Percent => Some(Multiplicative), + Token::Plus | Token::Minus => Some(Additive), + Token::LtLt | Token::GtGt | Token::GtGtGt => Some(BitwiseShift), + Token::Gt | Token::GtEqual | Token::Lt | Token::LtEqual => Some(Relational), + Token::EqualEqual | Token::BangEqual => Some(Equality), + Token::And => Some(BitwiseAnd), + Token::Caret => Some(BitwiseXor), + Token::Or => Some(BitwiseOr), + Token::AndAnd => Some(LogicalAnd), + Token::OrOr => Some(LogicalOr), + _ => None, + } +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 779b647427e55d42b654c970a4baf081f2551ed1..8c257d214dad91041d7de4f1e29bc1ee6641750c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -17,4 +17,4 @@ ast = { path = "../ast" } parser = { path = "../parser" } js_ast = { path = "../js_ast" } js_printer = { path = "../js_printer" } -checker = { path = "../checker" } +sema = { path = "../sema" } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 4bd03112c4df413316fd9acfe40662d71d26abaf..00945e42be7c877440cc6f4466dfba7fad622f9c 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -56,7 +56,7 @@ pub fn transpile(content: String) -> Result { // 2. Parse the tokens into an AST let blight_ast = parser::parse(tokens)?; // 3. Type check the AST - // checker::check_module(&blight_ast)?; + // sema::check_module(&blight_ast)?; // 4. Optimize the AST // optimize(tree.clone())?; // 5. Translate the AST into javascript diff --git a/crates/js_ast/src/lib.rs b/crates/js_ast/src/lib.rs index d0b9dd88049ab72f5f7b4e50cd2d9a480294a454..57a3e02c1bcea90ebbd0ed2d0cbc470aac67610c 100644 --- a/crates/js_ast/src/lib.rs +++ b/crates/js_ast/src/lib.rs @@ -180,6 +180,7 @@ fn translate_statement(ast: &Statement, ctx: &mut TranslateContext) { }); } Statement::Expr(_) => todo!(), + Statement::Comment(_) => todo!(), // Ast::Function { // name, // arguments, diff --git a/crates/parser/parse_declaration.rs b/crates/parser/parse_declaration.rs index 3fbfc4e8ef2e6d7ee91926c2de05acbc3d46e679..9e5176ed38fa2d414c15e048774d8012b62a0f49 100644 --- a/crates/parser/parse_declaration.rs +++ b/crates/parser/parse_declaration.rs @@ -1,4 +1,4 @@ -use ast::{Decl, Type}; +use ast::{Decl, DocComment, Type}; use miette::Result; use tokenizer::Token; @@ -7,7 +7,11 @@ 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) -> Result { +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, @@ -36,11 +40,11 @@ pub fn parse_declaration(p: &mut Parser, is_pub: bool) -> Result { }; Ok(Decl { name: name.clone(), - decl_type: return_type, + data_type: return_type, init, is_constant, is_pub, - comment: None, + 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 index 98d098d3c847c174c0a9ca6a375cc038fa6d2b55..6ca753167cdabc91ac85e5845d4c1fb356ef2748 100644 --- a/crates/parser/parse_expr.rs +++ b/crates/parser/parse_expr.rs @@ -1,45 +1,8 @@ -use ast::{BinaryOperator, Expr, ExprData, Literal}; +use ast::{get_binary_op_level, BinaryOperator, BinaryOperatorLevel, Expr, ExprData}; use miette::Result; -use tokenizer::Token; -use crate::{ - parse_call_arguments::parse_call_arguments, parse_expr_basic::parse_expr_basic, Parser, -}; - -/// Derived from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table - -#[derive(PartialEq, PartialOrd)] -enum Level { - Prefix, // +, -, !, ~ - Exponentiation, // ** - Multiplicative, // *, /, % - Additive, // +, - - BitwiseShift, // <<, >>, >>> - Relational, // <, <=, >, >= - Equality, // ==, != - BitwiseAnd, // & - BitwiseXor, // ^ - BitwiseOr, // | - LogicalAnd, // && - LogicalOr, // ||, ?? -} - -pub fn get_postfix_op_level(token: &Token) -> Option { - match token { - Token::StarStar => Some(Level::Exponentiation), - Token::Star | Token::Slash | Token::Percent => Some(Level::Multiplicative), - Token::Plus | Token::Minus => Some(Level::Additive), - Token::LtLt | Token::GtGt | Token::GtGtGt => Some(Level::BitwiseShift), - Token::Gt | Token::GtEqual | Token::Lt | Token::LtEqual => Some(Level::Relational), - Token::EqualEqual | Token::BangEqual => Some(Level::Equality), - Token::And => Some(Level::BitwiseAnd), - Token::Caret => Some(Level::BitwiseXor), - Token::Or => Some(Level::BitwiseOr), - Token::AndAnd => Some(Level::LogicalAnd), - Token::OrOr => Some(Level::LogicalOr), - _ => None, - } -} +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)?; @@ -49,68 +12,10 @@ pub fn parse_expr(p: &mut Parser) -> Result { return Ok(expr); } - // 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) - } - _ => { - 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, - ); - } - 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, - ); - } - 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, - ); - } else { - return Err(p - .error("Expected closing bracket after index".to_string()) - .into()); - } - } - _ => break, - } - } - // Parse operators loop { if let Some(token) = p.peek() { - if let Some(level) = get_postfix_op_level(&token) { + if let Some(level) = get_binary_op_level(&token) { _ = p.next(); expr = parse_expr_binary( p, @@ -128,13 +33,18 @@ pub fn parse_expr(p: &mut Parser) -> Result { } /// Prerequisite: `p.peek()` is an operator -fn parse_expr_binary(p: &mut Parser, op: BinaryOperator, left: Expr, level: Level) -> Result { +fn parse_expr_binary( + p: &mut Parser, + op: BinaryOperator, + left: Expr, + level: BinaryOperatorLevel, +) -> Result { let loc = p.loc(); let mut right = parse_expr_basic(p)?; loop { if let Some(token) = p.peek() { - if let Some(next_level) = get_postfix_op_level(&token) { + if let Some(next_level) = get_binary_op_level(&token) { if next_level <= level { _ = p.next(); right = parse_expr_binary( @@ -157,5 +67,6 @@ fn parse_expr_binary(p: &mut Parser, op: BinaryOperator, left: Expr, level: Leve right: Box::new(right), }, loc, + None, )) } diff --git a/crates/parser/parse_expr_basic.rs b/crates/parser/parse_expr_basic.rs index 8cc9c8409c33d5a4a1ae08550f4cf60ea63a8a76..d1949c82fc65ad390e482f92707eb95b24415346 100644 --- a/crates/parser/parse_expr_basic.rs +++ b/crates/parser/parse_expr_basic.rs @@ -9,32 +9,35 @@ use crate::{ pub fn parse_expr_basic(p: &mut Parser) -> Result { let loc = p.loc(); - match p.peek() { + let comments = p.take_comments(); + let mut expr = match p.peek() { Some(Token::Number(number)) => { _ = p.next(); - Ok(Expr::new( + Expr::new( ExprData::Literal(Literal::Number(number.clone())), loc, - )) + comments, + ) } Some(Token::String(string)) => { _ = p.next(); - Ok(Expr::new( + Expr::new( ExprData::Literal(Literal::String(string.clone())), loc, - )) + comments, + ) } Some(Token::True) => { _ = p.next(); - Ok(Expr::new(ExprData::Literal(Literal::Bool(true)), loc)) + Expr::new(ExprData::Literal(Literal::Bool(true)), loc, comments) } Some(Token::False) => { _ = p.next(); - Ok(Expr::new(ExprData::Literal(Literal::Bool(false)), loc)) + Expr::new(ExprData::Literal(Literal::Bool(false)), loc, comments) } Some(Token::Void) => { _ = p.next(); - Ok(Expr::new(ExprData::Literal(Literal::Void), loc)) + Expr::new(ExprData::Literal(Literal::Void), loc, comments) } Some(Token::Identifier(name)) => { let loc = p.loc(); @@ -43,7 +46,7 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result { if let Some(id) = BuiltinId::from_str(name.as_str()) { if let Some(Token::OpenParen) = p.next() { let arguments = parse_call_arguments(p)?; - return Ok(Expr::new(ExprData::Builtin { id, arguments }, loc)); + Expr::new(ExprData::Builtin { id, arguments }, loc, comments) } else { return Err(p .error_at(format!("Built-in function '{}' must be called", name), loc) @@ -55,7 +58,7 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result { .into()); } } else { - Ok(Expr::new(ExprData::Identifier(name), loc)) + Expr::new(ExprData::Identifier(name), loc, comments) } } @@ -66,18 +69,21 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result { .into()); } else { _ = p.next(); - - return Ok(Expr::new(ExprData::Return(Box::new(parse_expr(p)?)), loc)); + return Ok(Expr::new( + ExprData::Return(Box::new(parse_expr(p)?)), + loc, + comments, + )); } } - Some(Token::If) => Ok(Expr::new(ExprData::If(parse_expr_if(p)?), loc)), + Some(Token::If) => Expr::new(ExprData::If(parse_expr_if(p)?), loc, comments), - Some(Token::OpenBrace) => Ok(Expr::new(ExprData::Block(parse_block(p)?), loc)), + Some(Token::OpenBrace) => Expr::new(ExprData::Block(parse_block(p)?), loc, comments), Some(Token::OpenParen) => { _ = p.next(); - let expr = parse_expr_basic(p)?; + let expr = parse_expr(p)?; match p.next() { Some(Token::ClosedParen) => (), Some(token) => { @@ -85,12 +91,12 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result { } None => return Err(p.error("Unexpected end of file".to_string()).into()), } - Ok(expr) + expr } Some(t @ Token::Bang) | Some(t @ Token::Minus) | Some(t @ Token::Tilde) => { _ = p.next(); - Ok(Expr::new( + Expr::new( ExprData::Unary { op: match t { Token::Bang => UnaryOperator::Not, @@ -101,10 +107,74 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result { value: Box::new(parse_expr_basic(p)?), }, loc, - )) + comments, + ) } - Some(t) => Err(p.error(format!("Unexpected '{}'", t)).into()), - None => Err(p.error("Unexpected end of file".to_string()).into()), + 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 index a956585da772e01c03c550745f8b33687c79356f..d480a09d7d30075449793e398b15c3103f06e751 100644 --- a/crates/parser/parse_expr_if.rs +++ b/crates/parser/parse_expr_if.rs @@ -2,7 +2,9 @@ use ast::{BlockWithLoc, If}; use miette::Result; use tokenizer::Token; -use crate::{parse_block::parse_block, parse_expr::parse_expr, Parser}; +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 { diff --git a/crates/parser/parse_fn_declaration.rs b/crates/parser/parse_fn_declaration.rs index 4b7405c6bf3a57ab3b0bd50e9a2e26f8b12bd664..3b7363a36204bcd95ac287e5d7fb854cc2f06f78 100644 --- a/crates/parser/parse_fn_declaration.rs +++ b/crates/parser/parse_fn_declaration.rs @@ -1,4 +1,4 @@ -use ast::{Decl, Expr, ExprData, Type}; +use ast::{Decl, DocComment, Expr, ExprData, Type}; use miette::Result; use tokenizer::Token; @@ -8,7 +8,11 @@ use crate::parse_type::parse_type; use crate::Parser; /// Precondition: -pub fn parse_fn_declaration(p: &mut Parser, is_pub: bool) -> Result { +pub fn parse_fn_declaration( + p: &mut Parser, + is_pub: bool, + comment: Option, +) -> Result { let loc = p.loc(); match p.next() { Some(Token::Fn) => (), @@ -96,11 +100,11 @@ pub fn parse_fn_declaration(p: &mut Parser, is_pub: bool) -> Result { }; Ok(Decl { name, - decl_type: Type::Any, - init: Expr::new(function_body, block_loc), + data_type: Type::Any, + init: Expr::new(function_body, block_loc, None), is_constant: true, is_pub, - comment: None, + comment, loc: loc, }) } else { diff --git a/crates/parser/parse_module.rs b/crates/parser/parse_module.rs index 9ff66244156f39002142e205e7bd4a4ed6f73cd4..9d3f8fbe8e2e9f2ce1138a72a8b1ece27dda0011 100644 --- a/crates/parser/parse_module.rs +++ b/crates/parser/parse_module.rs @@ -1,23 +1,27 @@ -use crate::{parse_top_level_statement::parse_top_level_statement, Parser}; +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(tokens: &mut Parser) -> Result { +pub fn parse_module(p: &mut Parser) -> Result { let mut statements = Vec::new(); + let file_doc_comment = maybe_read_doc_comment(p); + loop { - let statement = parse_top_level_statement(tokens)?; + let statement = parse_top_level_statement(p)?; - match tokens.peek() { + match p.peek() { Some(Token::Semicolon) => { - while let Some(Token::Semicolon) = tokens.peek() { - tokens.next(); + while let Some(Token::Semicolon) = p.peek() { + p.next(); } } _ => { if statement.requires_semicolon() { - return Err(tokens + return Err(p .error("Expected semicolon after statement".to_string()) .into()); } @@ -26,8 +30,11 @@ pub fn parse_module(tokens: &mut Parser) -> Result { statements.push(statement); - if tokens.empty() { - return Ok(Module { statements }); + if p.empty() { + return Ok(Module { + statements, + comment: file_doc_comment, + }); } } } diff --git a/crates/parser/parse_statement.rs b/crates/parser/parse_statement.rs index c3da1e4f219af484a5af82b0d45c35f68444c92e..a135c1d4e6de8d3d17c0f0b3d7b03fa3f62c4859 100644 --- a/crates/parser/parse_statement.rs +++ b/crates/parser/parse_statement.rs @@ -3,26 +3,60 @@ 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::Decl(parse_declaration(p, true)?)) + Ok(Statement::Decl(parse_declaration(p, true, m_doc_comment)?)) } - Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration(p, true)?)), + Some(Token::Fn) => Ok(Statement::Decl(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::Decl(parse_declaration(p, false)?)), - Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration(p, false)?)), - Some(_) => Ok(Statement::Expr(parse_expr(p)?)), + Some(Token::Var) | Some(Token::Let) => { + Ok(Statement::Decl(parse_declaration(p, false, m_doc_comment)?)) + } + Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration( + 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/parser.rs b/crates/parser/parser.rs index 0dda281df5e97957e5a57c1ef5261894b56034a0..44f7cdd6f8750dee21ec88a0c746afc2b60f03ae 100644 --- a/crates/parser/parser.rs +++ b/crates/parser/parser.rs @@ -1,6 +1,7 @@ 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; @@ -11,15 +12,14 @@ mod parse_statement; mod parse_top_level_statement; mod parse_type; -use tokenizer::{Loc, Token}; - -use miette::Diagnostic; -use thiserror::Error; - +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); @@ -47,11 +47,15 @@ impl ParseError { #[derive(Debug)] pub struct Parser { - pub tokens: Vec, - pub cursor: usize, + /// 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 is_top_level: bool, + + comments: Vec, } impl Parser { @@ -60,6 +64,7 @@ impl Parser { tokens, cursor: 0, is_top_level: true, + comments: vec![], } } @@ -67,26 +72,61 @@ impl Parser { self.cursor == self.tokens.len() } - pub fn back(&mut self) { - self.cursor -= 1; - } + // TODO(@paperdave): fix parser token stuff. its very messy rn 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) + 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 peek(&self) -> Option { - if self.cursor == self.tokens.len() { + 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"), + }) + } - Some(self.tokens[self.cursor].clone()) + 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 { diff --git a/crates/checker/Cargo.toml b/crates/sema/Cargo.toml similarity index 90% rename from crates/checker/Cargo.toml rename to crates/sema/Cargo.toml index 35bcf3fac1c683c696223e428ba9f59ff2bb8c75..151d91227fdbfa2b5b2120a5e072e56796233326 100644 --- a/crates/checker/Cargo.toml +++ b/crates/sema/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "checker" +name = "sema" version = "0.1.0" edition = "2021" diff --git a/crates/checker/src/ast_type_coersion.rs b/crates/sema/src/ast_type_coersion.rs similarity index 100% rename from crates/checker/src/ast_type_coersion.rs rename to crates/sema/src/ast_type_coersion.rs diff --git a/crates/checker/src/lib.rs b/crates/sema/src/lib.rs similarity index 100% rename from crates/checker/src/lib.rs rename to crates/sema/src/lib.rs diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/src/lib.rs index d536ccd14cf9a7bcd522e139e2f8f60d727786ad..5b2095c6d430acbe3f687f6324b22018a349a6dd 100644 --- a/crates/tokenizer/src/lib.rs +++ b/crates/tokenizer/src/lib.rs @@ -71,12 +71,12 @@ pub enum Token { impl std::fmt::Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let token = match self { - Token::Identifier(_) => "an identifier".to_string(), + Token::Identifier(_) => "identifier".to_string(), Token::Number(n) => n.to_string(), Token::String(s) => format!("\"{}\"", s), - Token::Comment(_) => "a comment".to_string(), - Token::DocComment(_) => "a documentation comment".to_string(), + Token::Comment(_) => "comment".to_string(), + Token::DocComment(_) => "documentation comment".to_string(), Token::Else => "else".to_string(), Token::False => "false".to_string(), @@ -129,6 +129,7 @@ impl std::fmt::Display for 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(); @@ -277,21 +278,56 @@ pub fn tokenize(content: String) -> Result> { let mut comment = String::new(); loop { - let c = chars.next(); - if c.is_none() { + // 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; } - 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 { diff --git a/examples/basics/comments.blight b/examples/basics/comments.blight index cef4db9adcbbb9a7735efe8c97e0bea4e4a39b12..773d675b9cc0a27ff6d2419988e72893c7a09f62 100644 --- a/examples/basics/comments.blight +++ b/examples/basics/comments.blight @@ -1,9 +1,2 @@ -// this is a regular comment -let x: int = 0; - -/// this is a doc comment -let y: int = 0; - -/// Doc comments can be multiple lines. -/// This can be done by simply adding another triple-slash to the next line. -let z: int = 0; +2 // yapping + + 2; \ No newline at end of file -- 2.54.0 From ff04a973b4a12303ecf15c34fbf2bedf8deda9d5 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 20 Jan 2024 21:15:12 -0800 Subject: [PATCH 5/8] it compiles again --- Cargo.lock | 1 + crates/ast/ast.rs | 57 ++- crates/js_ast/Cargo.toml | 2 + crates/js_ast/js_ast.rs | 273 ++++++++++++++ crates/js_ast/lib.rs | 411 ++++++++++++++++++++++ crates/js_ast/src/lib.rs | 333 ------------------ crates/js_ast/src/types.rs | 50 --- crates/js_printer/src/print_expression.rs | 175 ++++----- crates/js_printer/src/print_statement.rs | 41 ++- crates/parser/parse_expr_if.rs | 19 +- crates/tokenizer/src/lib.rs | 10 +- examples/basics/comments.blight | 11 +- 12 files changed, 886 insertions(+), 497 deletions(-) create mode 100644 crates/js_ast/js_ast.rs create mode 100644 crates/js_ast/lib.rs delete mode 100644 crates/js_ast/src/lib.rs delete mode 100644 crates/js_ast/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 4c8ddfacf5557906451853c5edb4394a656e2af5..03588a003888ec3e8fa82a3bacbbed69357e14c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,7 @@ dependencies = [ "miette", "num", "thiserror", + "tokenizer", ] [[package]] diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs index bf9cd16e386e9321b7bd123de01cb21d622daa9a..123a54fe5567f5ce9fab3273f1c98753a84ab2ac 100644 --- a/crates/ast/ast.rs +++ b/crates/ast/ast.rs @@ -59,7 +59,7 @@ pub struct DocComment { pub loc: Loc, } -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Clone)] pub struct Comment { pub data: String, } @@ -98,7 +98,7 @@ pub struct BlockWithLoc { pub struct If { pub condition: Box, pub yes: BlockWithLoc, - pub no: Option, + pub no: Option>, } #[derive(Debug, Clone)] @@ -166,6 +166,52 @@ impl BuiltinId { } } +impl ExprData { + pub fn is_valid_js_expression(&self) -> bool { + match self { + // A block containing just expressions can be an expression + ExprData::Block(block) => block.is_valid_js_expression(), + ExprData::If(expr) => expr.is_valid_js_expression(), + ExprData::Return(_) => false, + ExprData::Builtin { id, .. } => match id { + BuiltinId::Raw => true, + }, + _ => true, + } + } +} + +impl Block { + pub fn is_valid_js_expression(&self) -> bool { + self.statements + .iter() + .all(|statement| statement.is_valid_js_expression()) + } +} + +impl Expr { + pub fn is_valid_js_expression(&self) -> bool { + self.data.is_valid_js_expression() + } +} + +impl If { + pub fn is_valid_js_expression(&self) -> bool { + self.condition.is_valid_js_expression() + && self.yes.is_valid_js_expression() + && match &self.no { + Some(no) => no.is_valid_js_expression(), + None => true, + } + } +} + +impl BlockWithLoc { + pub fn is_valid_js_expression(&self) -> bool { + self.block.is_valid_js_expression() + } +} + impl Statement { pub fn requires_semicolon(&self) -> bool { match &self { @@ -178,6 +224,13 @@ impl Statement { } } + pub fn is_valid_js_expression(&self) -> bool { + match self { + Statement::Expr(expr) => expr.data.is_valid_js_expression(), + _ => false, + } + } + pub fn to_top_level(self) -> TopLevelStatement { match self { Statement::Expr(expr) => TopLevelStatement::Expr(expr), diff --git a/crates/js_ast/Cargo.toml b/crates/js_ast/Cargo.toml index dbbaf2f2c722dce465082656b9ac56a078b9730e..f7b5438968013029fa069bd2d9040847c5454392 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,3 +12,4 @@ thiserror.workspace = true num.workspace = true ast = { path = "../ast" } +tokenizer = { path = "../tokenizer" } # TODO: move Loc out of the tokenizer diff --git a/crates/js_ast/js_ast.rs b/crates/js_ast/js_ast.rs new file mode 100644 index 0000000000000000000000000000000000000000..bc68f6e919939807b35f6a473819937156393548 --- /dev/null +++ b/crates/js_ast/js_ast.rs @@ -0,0 +1,273 @@ +use ast::{BinaryOperator, BlockWithLoc, BuiltinId, Comment, Expr, 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 { + operator: UnaryOperator, + value: Box, + }, + Binary { + operator: BinaryOperator, + left: Box, + right: Box, + }, + Dot { + base: Box, + name: String, + }, + Index { + base: Box, + index: Box, + }, + Call { + base: Box, + arguments: Vec, + }, + Function { + name: String, + params: Vec, + block: Vec, + }, + If { + condition: Box, + yes: Vec, + no: Option>, + }, + ArrowFunction { + params: Vec, + block: Vec, + }, + Comma { + items: Vec, + }, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsStatement { + pub data: JsStatementData, + pub loc: Loc, + pub comment: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsStatementData { + Expression(JsExpr), + Return(JsExpr), + Export(JsExport), + Declaration { + name: String, + init: JsExpr, + is_constant: bool, + }, + Function { + name: String, + params: Vec, + block: JsBlock, + }, + If { + condition: JsExpr, + yes: JsBlock, + no: Option, + }, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsExport { + Named(Vec), + Default(JsExpr), + Equals(JsExpr), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsBlock { + pub statements: Vec, + pub loc: Loc, +} + +impl JsExpr { + pub fn from_blight(expr: &Expr) -> Option { + use ast::ExprData::*; + + let loc = &expr.loc; + let comment = &expr.comment; + + match &expr.data { + Block(block) => Self::comma_expr_from_blight_block(&BlockWithLoc { + block: block.clone(), + loc: loc.clone(), + }), + Binary { op, left, right } => { + let left = Self::from_blight(&*left)?; + let right = Self::from_blight(&*right)?; + Some(JsExpr::new( + JsExprData::Binary { + operator: op.clone(), + left: Box::new(left), + right: Box::new(right), + }, + loc.clone(), + comment.clone(), + )) + } + Unary { op, value } => { + let value = Self::from_blight(&*value)?; + Some(JsExpr::new( + JsExprData::Unary { + operator: op.clone(), + value: Box::new(value), + }, + loc.clone(), + comment.clone(), + )) + } + Fn { + name, + params, + return_type, + block, + } => { + let block = Self::comma_expr_from_blight_block(&BlockWithLoc { + block: block.clone(), + loc: loc.clone(), + })?; + Some(JsExpr::new( + JsExprData::Function { + name: name.clone(), + params: vec![], + block: vec![JsStatement::new( + JsStatementData::Expression(block), + loc.clone(), + comment.clone(), + )], + }, + loc.clone(), + comment.clone(), + )) + } + Builtin { id, arguments } => match id { + BuiltinId::Raw => { + let mut args = arguments.iter(); + let arg = args.next().unwrap(); + let arg = match &arg.data { + Literal(ast::Literal::String(s)) => s, + _ => panic!("raw expects a string"), + }; + Some(JsExpr::new( + JsExprData::Raw(arg.clone()), + loc.clone(), + comment.clone(), + )) + } + }, + If(expr) => { + let condition = Self::from_blight(&expr.condition)?; + let yes = Self::comma_expr_from_blight_block(&expr.yes)?; + let no = match &expr.no { + Some(no) => Some(Self::from_blight(&no)?), + None => None, + }; + Some(JsExpr::new( + JsExprData::If { + condition: Box::new(condition), + yes: vec![yes], + no: no.map(Box::new), + }, + loc.clone(), + comment.clone(), + )) + } + + Call { base, arguments } => todo!(), + Field { base, field } => todo!(), + + Return(_) => None, + + Literal(_) => todo!(), + Identifier(_) => todo!(), + // Literal(ast::Literal::Number(n)) => { + // Some(JsExpr::new(JsExprData::Number(n), loc.clone(), comment)) + // } + // Literal(ast::Literal::Bool(b)) => Some(JsExpr::new(JsExprData::Bool(b), loc, comment)), + // Literal(ast::Literal::String(s)) => { + // Some(JsExpr::new(JsExprData::String(s), loc, comment)) + // } + // Literal(ast::Literal::Void) => Some(JsExpr::new(JsExprData::Null, loc, comment)), + // Identifier(id) => Some(JsExpr::new(JsExprData::Identifier(id), loc, comment)), + } + } + + /// Since blocks are not valid expressions in JS, this creates a + /// comma expression from a block with more than one statement. + /// + /// Not possible to create an ast => None + pub fn comma_expr_from_blight_block(b: &ast::BlockWithLoc) -> Option { + if !b.block.has_implicit_return { + return Some(JsExpr::new(JsExprData::Null, b.loc.clone(), None)); + } + // a block expression with a single statement is just that statement + if b.block.statements.len() == 1 && b.block.has_implicit_return { + match b.block.statements.first().unwrap() { + ast::Statement::Expr(expr) => Self::from_blight(expr), + _ => return None, + }; + } + let mut statements = Vec::new(); + for statement in &b.block.statements { + match statement { + ast::Statement::Expr(expr) => { + if let Some(expr) = Self::from_blight(&expr) { + statements.push(expr); + } + } + _ => return None, + } + } + // a block without implicit return is to be type void, so we append null + if !b.block.has_implicit_return { + statements.push(JsExpr::new(JsExprData::Null, b.loc.clone(), None)); + } + Some(JsExpr::new( + JsExprData::Comma { items: statements }, + b.loc.clone(), + None, + )) + } + + pub fn new(data: JsExprData, loc: Loc, comment: Option) -> Self { + Self { data, loc, comment } + } +} + +impl JsStatement { + pub fn new(data: JsStatementData, loc: Loc, comment: Option) -> Self { + Self { data, loc, comment } + } +} diff --git a/crates/js_ast/lib.rs b/crates/js_ast/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..0c27a7e5204b91d8e1acb6f61f4ec69f044d4cbd --- /dev/null +++ b/crates/js_ast/lib.rs @@ -0,0 +1,411 @@ +use std::collections::HashMap; + +use ast::{BlockWithLoc, Expr, ExprData, Literal, Module, Statement}; +pub use js_ast::*; +use miette::Result; +use tokenizer::Loc; + +mod js_ast; + +struct StackContext { + statements: Vec, + /// Variables that are declared in this context. + /// The number is the number of times the variable has been declared. + /// This is used to allow for shadowing + variable_map: HashMap, + block: u32, +} + +impl StackContext { + fn new(block: u32) -> Self { + StackContext { + statements: Vec::new(), + variable_map: HashMap::new(), + block, + } + } +} + +struct TranslateContext { + context: Vec, + internal_counter: u32, + block_counter: u32, +} + +impl TranslateContext { + fn new() -> Self { + TranslateContext { + context: vec![StackContext::new(0)], + internal_counter: 0, + block_counter: 0, + } + } + + fn push_context(self: &mut Self) { + self.block_counter += 1; + self.context.push(StackContext::new(self.block_counter)); + } + + fn pop_context(self: &mut Self) -> Vec { + self.context.pop().unwrap().statements + } + + fn push_internal_variable(self: &mut Self, value: JsExpr) -> JsExpr { + let name = format!("${}", self.internal_counter); + let loc = value.loc.clone(); + self.context + .last_mut() + .unwrap() + .statements + .push(JsStatement::new( + JsStatementData::Declaration { + name: name.clone(), + init: value, + is_constant: false, + }, + loc, + None, + )); + self.internal_counter += 1; + JsExpr::new(JsExprData::Identifier(name), Loc::EMPTY, None) + } + + 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); + } +} + +fn get_block_handle(b: &ast::BlockWithLoc, ctx: &mut TranslateContext) -> JsExpr { + // :) + if b.is_valid_js_expression() { + return JsExpr::comma_expr_from_blight_block(b).unwrap(); + } + + ctx.push_context(); + for (i, statement) in b.block.statements.iter().enumerate() { + if i == b.block.statements.len() - 1 { + break; + } else { + translate_statement(statement, ctx); + } + } + let result = match b.block.statements.last().unwrap() { + ast::Statement::Expr(expr) => get_expression_handle(expr, ctx), + stmt => { + translate_statement(stmt, ctx); + JsExpr::new(JsExprData::Null, Loc::EMPTY, None) + } + }; + let value = ctx.pop_context(); + value.iter().for_each(|x| ctx.push(x.clone())); + result +} + +// basically, expressions are really hard to translate +// in blight, everything is an expression +// in js, very few things are expressions +// here, we recursively expand out expressions into statements +// we then store the result of all the expressions in a variable +// and then return the name of that variable +fn get_expression_handle(ast: &Expr, ctx: &mut TranslateContext) -> JsExpr { + match ast.data.clone() { + ExprData::Literal(Literal::String(value)) => JsExpr::new( + JsExprData::String(value), + ast.loc.clone(), + ast.comment.clone(), + ), + ExprData::Literal(Literal::Number(value)) => JsExpr::new( + JsExprData::Number(value), + ast.loc.clone(), + ast.comment.clone(), + ), + ExprData::Literal(Literal::Void) => { + JsExpr::new(JsExprData::Null, ast.loc.clone(), ast.comment.clone()) + } + ExprData::Literal(Literal::Bool(b)) => { + JsExpr::new(JsExprData::Bool(b), ast.loc.clone(), ast.comment.clone()) + } + + ExprData::Identifier(value) => JsExpr::new( + JsExprData::Identifier(ctx.get_identifier(value.clone())), + ast.loc.clone(), + ast.comment.clone(), + ), + ExprData::Call { base, arguments } => { + let mut handles = Vec::new(); + for argument in arguments { + handles.push(get_expression_handle(&argument, ctx)); + } + let base = get_expression_handle(&base, ctx); + ctx.push_internal_variable(JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments: handles, + }, + ast.loc.clone(), + ast.comment.clone(), + )) + } + ExprData::Builtin { id, arguments } => match id { + ast::BuiltinId::Raw => { + if arguments.len() != 1 { + panic!("@raw expects exactly one argument"); + } + match arguments.get(0).unwrap() { + Expr { + data: ExprData::Literal(Literal::String(value)), + .. + } => JsExpr::new( + JsExprData::Raw(value.clone()), + ast.loc.clone(), + ast.comment.clone(), + ), + _ => panic!("@raw expects a string literal"), + } + } + }, + ExprData::Binary { op, left, right } => { + let left_handle = get_expression_handle(&left, ctx); + let right_handle = get_expression_handle(&right, ctx); + JsExpr::new( + JsExprData::Binary { + operator: op.clone(), + left: Box::new(left_handle), + right: Box::new(right_handle), + }, + ast.loc.clone(), + ast.comment.clone(), + ) + } + + ExprData::Unary { op, value } => { + let value_handle = get_expression_handle(&value, ctx); + JsExpr::new( + JsExprData::Unary { + operator: op.clone(), + value: Box::new(value_handle), + }, + ast.loc.clone(), + ast.comment.clone(), + ) + } + + ExprData::Field { base, field } => { + let base = Box::new(get_expression_handle(&base, ctx)); + + match field.data { + ExprData::Literal(Literal::String(name)) => JsExpr::new( + JsExprData::Dot { base, name }, + ast.loc.clone(), + ast.comment.clone(), + ), + _ => JsExpr::new( + JsExprData::Index { + base, + index: Box::new(get_expression_handle(&field, ctx)), + }, + ast.loc.clone(), + ast.comment.clone(), + ), + } + } + + ExprData::Block(block) => get_block_handle( + &BlockWithLoc { + block, + loc: ast.loc.clone(), + }, + ctx, + ), + + ExprData::If(expr) => { + let handle = get_expression_handle(&expr.condition, ctx); + + let yes = get_block_handle(&expr.yes, ctx); + let no = match expr.no { + Some(block) => Some(get_expression_handle(&block, ctx)), + None => None, + }; + + // ctx.push(JsStatement::new( + // JsStatementData::If { + // condition: handle, + // yes, + // no, + // }, + // ast.loc, + // ast.comment, + // )); + + // panic!() + handle + } + ExprData::Fn { + name, + params, + return_type, + block, + } => todo!(), + ExprData::Return(_) => todo!(), + } +} + +fn translate_statement(ast: &Statement, ctx: &mut TranslateContext) { + match ast { + Statement::Decl(decl) => { + let handle = get_expression_handle(&decl.init, ctx); + let rename = ctx.declare_variable(decl.name.clone()); + ctx.push(JsStatement::new( + JsStatementData::Declaration { + name: rename, + init: handle, + is_constant: decl.is_constant.clone(), + }, + decl.loc.clone(), + None, + )); + } + Statement::Expr(expr) => { + let h = get_expression_handle(expr, ctx); + ctx.push(JsStatement::new( + JsStatementData::Expression(h), + expr.loc.clone(), + None, + )) + } + Statement::Comment(_) => {} // Ast::Function { + // name, + // arguments, + // return_type: _, + // block, + // } => { + // let rename = ctx.declare_variable(name.clone()); + // let arguments = arguments + // .iter() + // .map(|x| ctx.declare_variable(x.name.clone())) + // .collect(); + + // ctx.push_context(); + // for (i, statement) in block.value.iter().enumerate() { + // if block.implicit_return && i == block.value.len() - 1 { + // let handle = get_expression_handle(statement, ctx); + // ctx.push(JsStatement::Return(handle)); + // } else { + // translate_statement(statement, ctx); + // } + // } + // let value = ctx.pop_context(); + + // ctx.push(JsStatement::Function { + // name: rename, + // arguments, + // value, + // }); + // } + // Ast::Public(value) => { + // translate_statement(value, ctx); + // // I'm sorry, I just don't know how to get the original name properly... my bad + // match ctx.peek() { + // JsStatement::Function { name, .. } | JsStatement::Declaration { name, .. } => { + // let original_name = name.split("$").next().unwrap().to_string(); + // ctx.push(JsStatement::Export(Box::new(JsStatement::Declaration { + // name: original_name, + // value: JsExpr::Identifier(name.clone()), + // is_constant: true, + // }))); + // } + // _ => { + // panic!("Cannot make non-function public"); + // } + // } + // } + // Ast::Return(value) => { + // let handle = get_expression_handle(value, ctx); + // ctx.push(JsStatement::Return(handle)); + // } + // Ast::Block(block) => { + // ctx.push_context(); + // for (i, statement) in block.value.iter().enumerate() { + // if block.implicit_return && i == block.value.len() - 1 { + // let handle = get_expression_handle(statement, ctx); + // ctx.push(JsStatement::Return(handle)); + // } else { + // translate_statement(statement, ctx); + // } + // } + // let value = ctx.pop_context(); + // value.iter().for_each(|x| ctx.push(x.clone())); + // } + + // Ast::FunctionCall { name, arguments } => { + // let mut handles = Vec::new(); + // for argument in arguments { + // handles.push(get_expression_handle(argument, ctx)); + // } + // match *name.clone() { + // Ast::Identifier(name) => { + // ctx.push(JsStatement::Expression(JsExpr::Call { + // name: Box::new(JsExpr::Identifier(ctx.get_identifier(name.clone()))), + // arguments: handles, + // })); + // } + // Ast::BuiltIn(name) => match name.as_str() { + // "@raw" => { + // if handles.len() != 1 { + // panic!("@raw expects exactly one argument"); + // } + // match handles.get(0).unwrap() { + // JsExpr::String(value) => { + // ctx.push(JsStatement::Expression(JsExpr::Call { + // name: Box::new(JsExpr::Raw(value.clone())), + // arguments: handles, + // })); + // } + // _ => panic!("@raw expects a string argument"), + // } + // } + // _ => panic!("Unknown built-in function {}", name), + // }, + // _ => panic!("Function name must be an identifier"), + // } + // } + // Ast::Comment(_) | Ast::DocComment(_) => {} + // _ => { + // panic!("unimplemented: {:?}", ast); + // } + } +} + +pub fn translate(module: &Module) -> Result> { + let mut ctx = TranslateContext::new(); + + for ast in module.statements.iter() { + translate_statement(&ast.clone().to_statement(), &mut ctx); + } + + Ok(ctx.pop_context()) +} diff --git a/crates/js_ast/src/lib.rs b/crates/js_ast/src/lib.rs deleted file mode 100644 index 57a3e02c1bcea90ebbd0ed2d0cbc470aac67610c..0000000000000000000000000000000000000000 --- a/crates/js_ast/src/lib.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::collections::HashMap; - -use ast::{Expr, ExprData, Literal, Module, Statement}; -use miette::Result; - -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::Declaration { - name: format!("${}", self.internal_counter), - value, - is_constant: false, - }); - 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: &Expr, ctx: &mut TranslateContext) -> JsExpression { - match ast.data.clone() { - ExprData::Literal(Literal::String(value)) => JsExpression::String(value), - ExprData::Literal(Literal::Number(value)) => JsExpression::Number(value), - ExprData::Literal(Literal::Void) => JsExpression::Identifier("null".to_string()), - ExprData::Literal(Literal::Bool(value)) => JsExpression::Identifier(if value { - "true".to_string() - } else { - "false".to_string() - }), - - ExprData::Identifier(value) => JsExpression::Identifier(ctx.get_identifier(value.clone())), - ExprData::Call { base, arguments } => { - let mut handles = Vec::new(); - for argument in arguments { - handles.push(get_expression_handle(&argument, ctx)); - } - let base = get_expression_handle(&base, ctx); - ctx.push_internal_variable(JsExpression::Call { - base: Box::new(base), - arguments: handles, - }) - } - ExprData::Builtin { id, arguments } => match id { - ast::BuiltinId::Raw => { - if arguments.len() != 1 { - panic!("@raw expects exactly one argument"); - } - match arguments.get(0).unwrap() { - Expr { - data: ExprData::Literal(Literal::String(value)), - .. - } => JsExpression::Raw(value.clone()), - _ => panic!("@raw expects a string literal"), - } - } - }, - ExprData::Binary { op, left, right } => { - let left_handle = get_expression_handle(&left, ctx); - let right_handle = get_expression_handle(&right, ctx); - JsExpression::Binary { - operator: op.clone(), - left: Box::new(left_handle), - right: Box::new(right_handle), - } - } - ExprData::Block(block) => { - if !block.has_implicit_return { - panic!("Expression blocks must have an implicit return"); - } - ctx.push_context(); - for (i, statement) in block.statements.iter().enumerate() { - if i == block.statements.len() - 1 { - break; - } else { - translate_statement(statement, ctx); - } - } - todo!() - // let result = get_expression_handle(block.statements.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: &Statement, ctx: &mut TranslateContext) { - match ast { - Statement::Decl(decl) => { - let handle = get_expression_handle(&decl.init, ctx); - let rename = ctx.declare_variable(decl.name.clone()); - ctx.push(JsStatement::Declaration { - name: rename, - value: handle, - is_constant: decl.is_constant.clone(), - }); - } - Statement::Expr(_) => todo!(), - Statement::Comment(_) => todo!(), - // Ast::Function { - // name, - // arguments, - // return_type: _, - // block, - // } => { - // let rename = ctx.declare_variable(name.clone()); - // let arguments = arguments - // .iter() - // .map(|x| ctx.declare_variable(x.name.clone())) - // .collect(); - - // ctx.push_context(); - // for (i, statement) in block.value.iter().enumerate() { - // if block.implicit_return && i == block.value.len() - 1 { - // let handle = get_expression_handle(statement, ctx); - // ctx.push(JsStatement::Return(handle)); - // } else { - // translate_statement(statement, ctx); - // } - // } - // let value = ctx.pop_context(); - - // ctx.push(JsStatement::Function { - // name: rename, - // arguments, - // value, - // }); - // } - // Ast::Public(value) => { - // translate_statement(value, ctx); - // // I'm sorry, I just don't know how to get the original name properly... my bad - // match ctx.peek() { - // JsStatement::Function { name, .. } | JsStatement::Declaration { name, .. } => { - // let original_name = name.split("$").next().unwrap().to_string(); - // ctx.push(JsStatement::Export(Box::new(JsStatement::Declaration { - // name: original_name, - // value: JsExpression::Identifier(name.clone()), - // is_constant: true, - // }))); - // } - // _ => { - // panic!("Cannot make non-function public"); - // } - // } - // } - // Ast::Return(value) => { - // let handle = get_expression_handle(value, ctx); - // ctx.push(JsStatement::Return(handle)); - // } - // Ast::Block(block) => { - // ctx.push_context(); - // for (i, statement) in block.value.iter().enumerate() { - // if block.implicit_return && i == block.value.len() - 1 { - // let handle = get_expression_handle(statement, ctx); - // ctx.push(JsStatement::Return(handle)); - // } else { - // translate_statement(statement, ctx); - // } - // } - // let value = ctx.pop_context(); - // value.iter().for_each(|x| ctx.push(x.clone())); - // } - // Ast::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::Call { - // 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::Call { - // 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(module: &Module) -> Result> { - let mut ctx = TranslateContext::new(); - - for ast in module.statements.iter() { - translate_statement(&ast.clone().to_statement(), &mut ctx); - } - - Ok(ctx.pop_context()) -} diff --git a/crates/js_ast/src/types.rs b/crates/js_ast/src/types.rs deleted file mode 100644 index f53fb3f665655d739d189bd01a1ae334579b7167..0000000000000000000000000000000000000000 --- a/crates/js_ast/src/types.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ast::BinaryOperator; -use num::bigint::BigInt; -use std::collections::HashMap; - -#[derive(Debug, PartialEq, Clone)] -pub enum JsExpression { - Number(f64), - BigInt(BigInt), - String(String), - Bool(bool), - Identifier(String), - Raw(String), - Array(Vec), - Object(HashMap), - Binary { - operator: BinaryOperator, - left: Box, - right: Box, - }, - Function { - arguments: Vec, - value: Vec, - }, - Call { - base: Box, - arguments: Vec, - }, -} - -#[derive(Debug, PartialEq, Clone)] -pub enum JsStatement { - Expression(JsExpression), - Export(Box), - Return(JsExpression), - Declaration { - name: String, - value: JsExpression, - is_constant: bool, - }, - Function { - name: String, - arguments: Vec, - value: Vec, - }, - If { - condition: JsExpression, - value: Vec, - else_value: Option>, - }, -} diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs index 15810d077d963fe78e1c45c6c4247403dce9677a..26122cadfe2a55e259a05322fb103b3a5f64e9c9 100644 --- a/crates/js_printer/src/print_expression.rs +++ b/crates/js_printer/src/print_expression.rs @@ -1,13 +1,13 @@ -use js_ast::JsExpression; +use js_ast::{JsExpr, JsExprData}; 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) => { +pub fn print_expression(expr: &JsExpr) -> String { + match &expr.data { + JsExprData::Raw(value) => format!("{}", value), + JsExprData::Number(value) => format!("{}", value), + JsExprData::BigInt(value) => format!("{}n", value), + JsExprData::String(value) => { let mut result = String::new(); // TODO: This is an INCREDIBLY naive implementation of escaping. // Also it's probably not even correct. @@ -23,9 +23,9 @@ pub fn print_expression(statement: &JsExpression) -> String { } format!("\"{}\"", result) } - JsExpression::Bool(value) => format!("{}", value), - JsExpression::Identifier(value) => format!("{}", value), - JsExpression::Array(value) => format!( + JsExprData::Bool(value) => format!("{}", value), + JsExprData::Identifier(value) => format!("{}", value), + JsExprData::Array(value) => format!( "[{}]", value .iter() @@ -33,67 +33,83 @@ pub fn print_expression(statement: &JsExpression) -> String { .collect::>() .join(",") ), - JsExpression::Object(value) => format!( + JsExprData::Object(value) => format!( "{{{}}}", value .iter() - .map(|(k, v)| format!("\"{}\":{}", k, print_expression(v))) + .map(|entry| format!( + "{}:{}", + print_expression(&entry.key), + print_expression(&entry.value) + )) .collect::>() .join(",") ), - JsExpression::Binary { + JsExprData::Binary { operator, left, right, } => format!( "{}{}{}", - print_expression(left), + print_expression(&left), operator.clone().to_string(), - print_expression(right) + print_expression(&right) ), - JsExpression::Function { arguments, value } => format!( - "({})=>{{{}}}", - arguments.join(","), - value - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - JsExpression::Call { base, arguments } => format!( + // JsExprData::Function { + // arguments, + // value, + // name: _, + // params: _, + // block, + // } => format!( + // "({})=>{{{}}}", + // arguments.join(","), + // value + // .iter() + // .map(|x| format!("{}", print_statement(x))) + // .collect::>() + // .join("") + // ), + JsExprData::Call { base, arguments } => format!( "{}({})", - match *base.clone() { - JsExpression::Identifier(value) => value.clone(), - JsExpression::Raw(value) => value.clone(), - _ => panic!("Function call name must be an identifier"), - }, + print_expression(&base), arguments .iter() .map(|x| print_expression(x)) .collect::>() .join(",") ), + JsExprData::Null => todo!(), + JsExprData::Undefined => todo!(), + JsExprData::Unary { operator, value } => todo!(), + JsExprData::Dot { base, name } => todo!(), + JsExprData::Index { base, index } => todo!(), + JsExprData::Function { + name, + params, + block, + } => todo!(), + JsExprData::If { condition, yes, no } => todo!(), + JsExprData::ArrowFunction { params, block } => todo!(), + JsExprData::Comma { items } => todo!(), } } #[cfg(test)] mod tests { use crate::print_expression::print_expression; - use js_ast::JsExpression; + use js_ast::JsExprData; use js_ast::JsStatement; use num::{bigint::Sign, BigInt}; use std::collections::HashMap; #[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(&JsExprData::Number(1.0)), "1"); + assert_eq!(print_expression(&JsExprData::Number(1.5)), "1.5"); + assert_eq!(print_expression(&JsExprData::Number(1.5e10)), "15000000000"); assert_eq!( - print_expression(&JsExpression::Number(1.5e10)), - "15000000000" - ); - assert_eq!( - print_expression(&JsExpression::Number(1.5e-10)), + print_expression(&JsExprData::Number(1.5e-10)), "0.00000000015" ); } @@ -101,18 +117,15 @@ mod tests { #[test] fn bigint() { assert_eq!( - print_expression(&JsExpression::BigInt(BigInt::new(Sign::Plus, vec![1]))), + print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![1]))), "1n" ); assert_eq!( - print_expression(&JsExpression::BigInt(BigInt::new( - Sign::Plus, - vec![10000000] - ))), + print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![10000000]))), "10000000n" ); assert_eq!( - print_expression(&JsExpression::BigInt(BigInt::new( + print_expression(&JsExprData::BigInt(BigInt::new( Sign::Minus, vec![10000000] ))), @@ -123,44 +136,44 @@ mod tests { #[test] fn string() { assert_eq!( - print_expression(&JsExpression::String("".to_string())), + print_expression(&JsExprData::String("".to_string())), "\"\"" ); assert_eq!( - print_expression(&JsExpression::String("hello".to_string())), + print_expression(&JsExprData::String("hello".to_string())), "\"hello\"" ); assert_eq!( - print_expression(&JsExpression::String("hello\nworld".to_string())), + print_expression(&JsExprData::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"); + assert_eq!(print_expression(&JsExprData::Bool(true)), "true"); + assert_eq!(print_expression(&JsExprData::Bool(false)), "false"); } #[test] fn identifier() { assert_eq!( - print_expression(&JsExpression::Identifier("hello".to_string())), + print_expression(&JsExprData::Identifier("hello".to_string())), "hello" ); } #[test] fn array() { - assert_eq!(print_expression(&JsExpression::Array(vec![])), "[]"); + assert_eq!(print_expression(&JsExprData::Array(vec![])), "[]"); assert_eq!( - print_expression(&JsExpression::Array(vec![JsExpression::Number(1.0)])), + print_expression(&JsExprData::Array(vec![JsExprData::Number(1.0)])), "[1]" ); assert_eq!( - print_expression(&JsExpression::Array(vec![ - JsExpression::Number(1.0), - JsExpression::Number(2.0) + print_expression(&JsExprData::Array(vec![ + JsExprData::Number(1.0), + JsExprData::Number(2.0) ])), "[1,2]" ); @@ -168,33 +181,27 @@ mod tests { #[test] fn object() { - assert_eq!( - print_expression(&JsExpression::Object(HashMap::new())), - "{}" - ); + assert_eq!(print_expression(&JsExprData::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}" - ); + map.insert("hello".to_string(), JsExprData::Number(1.0)); + assert_eq!(print_expression(&JsExprData::Object(map)), "{\"hello\":1}"); } #[test] fn operation() { assert_eq!( - print_expression(&JsExpression::Binary { + print_expression(&JsExprData::Binary { operator: AnyOperator::Add, - left: Box::new(JsExpression::Number(1.0)), - right: Box::new(JsExpression::Number(2.0)), + left: Box::new(JsExprData::Number(1.0)), + right: Box::new(JsExprData::Number(2.0)), }), "1+2" ); assert_eq!( - print_expression(&JsExpression::Binary { + print_expression(&JsExprData::Binary { operator: AnyOperator::Sub, - left: Box::new(JsExpression::Number(1.0)), - right: Box::new(JsExpression::Number(2.0)), + left: Box::new(JsExprData::Number(1.0)), + right: Box::new(JsExprData::Number(2.0)), }), "1-2" ); @@ -203,35 +210,35 @@ mod tests { #[test] fn function() { use crate::print_expression::print_expression; - use js_ast::JsExpression; + use js_ast::JsExprData; assert_eq!( - print_expression(&JsExpression::Function { + print_expression(&JsExprData::Function { arguments: vec![], value: vec![], }), "()=>{}" ); assert_eq!( - print_expression(&JsExpression::Function { + print_expression(&JsExprData::Function { arguments: vec!["a".to_string()], value: vec![], }), "(a)=>{}" ); assert_eq!( - print_expression(&JsExpression::Function { + print_expression(&JsExprData::Function { arguments: vec!["a".to_string(), "b".to_string()], value: vec![], }), "(a,b)=>{}" ); assert_eq!( - print_expression(&JsExpression::Function { + print_expression(&JsExprData::Function { arguments: vec!["a".to_string(), "b".to_string()], value: vec![JsStatement::Declaration { name: "a".to_string(), - value: JsExpression::Number(1.0) + value: JsExprData::Number(1.0) }], }), "(a,b)=>{let a=1;}" @@ -241,23 +248,23 @@ mod tests { #[test] fn function_call() { assert_eq!( - print_expression(&JsExpression::Call { - name: Box::new(JsExpression::Identifier("hello".to_string())), + print_expression(&JsExprData::Call { + name: Box::new(JsExprData::Identifier("hello".to_string())), arguments: vec![], }), "hello()" ); assert_eq!( - print_expression(&JsExpression::Call { - name: Box::new(JsExpression::Identifier("hello".to_string())), - arguments: vec![JsExpression::Number(1.0)], + print_expression(&JsExprData::Call { + name: Box::new(JsExprData::Identifier("hello".to_string())), + arguments: vec![JsExprData::Number(1.0)], }), "hello(1)" ); assert_eq!( - print_expression(&JsExpression::Call { - name: Box::new(JsExpression::Identifier("hello".to_string())), - arguments: vec![JsExpression::Number(1.0), JsExpression::Number(2.0)], + print_expression(&JsExprData::Call { + name: Box::new(JsExprData::Identifier("hello".to_string())), + arguments: vec![JsExprData::Number(1.0), JsExprData::Number(2.0)], }), "hello(1,2)" ); diff --git a/crates/js_printer/src/print_statement.rs b/crates/js_printer/src/print_statement.rs index 2f4c37399b38c214fc2e8036fbb232aaf0017234..57fdb827b637716f5c05b2065c39e621eff95d62 100644 --- a/crates/js_printer/src/print_statement.rs +++ b/crates/js_printer/src/print_statement.rs @@ -1,15 +1,16 @@ -use js_ast::JsStatement; +use js_ast::{JsStatement, JsStatementData}; 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::Declaration { + match &statement.data { + JsStatementData::Expression(value) => format!("{};", print_expression(&value)), + // JsStatementData::Export(value) => format!("export {}", print_statement(value)), + JsStatementData::Export(value) => todo!(), + JsStatementData::Return(value) => format!("return {};", print_expression(&value)), + JsStatementData::Declaration { name, - value, + init, is_constant, } => { format!( @@ -19,32 +20,30 @@ pub fn print_statement(statement: &JsStatement) -> String { false => "let", }, name, - print_expression(value) + print_expression(&init) ) } - JsStatement::Function { + JsStatementData::Function { name, - arguments, - value, + block, + params, } => format!( "function {}({}){{{}}}", name, - arguments.join(","), - value + params.join(","), + block + .statements .iter() .map(|x| format!("{}", print_statement(x))) .collect::>() .join("") ), - JsStatement::If { - condition, - value, - else_value, - } => { - let else_value = match else_value { + JsStatementData::If { condition, yes, no } => { + let else_value = match no { Some(value) => format!( "else{{{}}}", value + .statements .iter() .map(|x| format!("{}", print_statement(x))) .collect::>() @@ -54,8 +53,8 @@ pub fn print_statement(statement: &JsStatement) -> String { }; format!( "if({}){{{}}}{}", - print_expression(condition), - value + print_expression(&condition), + yes.statements .iter() .map(|x| format!("{}", print_statement(x))) .collect::>() diff --git a/crates/parser/parse_expr_if.rs b/crates/parser/parse_expr_if.rs index d480a09d7d30075449793e398b15c3103f06e751..0efc61ee5e3b52a9d221fca3f692d782073b6c5e 100644 --- a/crates/parser/parse_expr_if.rs +++ b/crates/parser/parse_expr_if.rs @@ -1,4 +1,4 @@ -use ast::{BlockWithLoc, If}; +use ast::{BlockWithLoc, Expr, ExprData, If}; use miette::Result; use tokenizer::Token; @@ -48,10 +48,21 @@ pub fn parse_expr_if(p: &mut Parser) -> Result { match p.peek() { Some(Token::OpenBrace) => { let loc = p.loc(); - Some(BlockWithLoc { - block: parse_block(p)?, + let comments = p.take_comments(); + Some(Box::new(Expr::new( + ExprData::Block(parse_block(p)?), loc, - }) + comments, + ))) + } + Some(Token::If) => { + let loc = p.loc(); + let comments = p.take_comments(); + Some(Box::new(Expr::new( + ExprData::If(parse_expr_if(p)?), + loc, + comments, + ))) } _ => return Err(p.error("Expected a block after else".to_string()).into()), } diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/src/lib.rs index 5b2095c6d430acbe3f687f6324b22018a349a6dd..2d906f20a2b75c81a5168c92c3d591314c8d2c18 100644 --- a/crates/tokenizer/src/lib.rs +++ b/crates/tokenizer/src/lib.rs @@ -1,9 +1,17 @@ use miette::{Diagnostic, Result}; use thiserror::Error; -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Clone)] pub struct Loc {} +impl Loc { + pub const EMPTY: Loc = Loc {}; + + pub fn new() -> Self { + Self {} + } +} + #[derive(Error, Diagnostic, Debug)] #[error("UnknownCharacter")] struct UnknownCharacter { diff --git a/examples/basics/comments.blight b/examples/basics/comments.blight index 773d675b9cc0a27ff6d2419988e72893c7a09f62..cef4db9adcbbb9a7735efe8c97e0bea4e4a39b12 100644 --- a/examples/basics/comments.blight +++ b/examples/basics/comments.blight @@ -1,2 +1,9 @@ -2 // yapping - + 2; \ No newline at end of file +// this is a regular comment +let x: int = 0; + +/// this is a doc comment +let y: int = 0; + +/// Doc comments can be multiple lines. +/// This can be done by simply adding another triple-slash to the next line. +let z: int = 0; -- 2.54.0 From c164266ac2d05ed0a0f7ab09b8db76b5b81eb964 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 22 Jan 2024 00:32:12 -0800 Subject: [PATCH 6/8] finish rewriting the project in rust --- Cargo.lock | 1 + Cargo.toml | 1 - crates/ast/ast.rs | 104 ++- crates/ast/operators.rs | 156 ++++- crates/cli/Cargo.lock | 532 --------------- crates/cli/Cargo.toml | 2 +- crates/cli/{src => }/main.rs | 31 +- crates/js_ast/Cargo.toml | 1 + crates/js_ast/convert_valid_expr.rs | 204 ++++++ crates/js_ast/has_side_effects.rs | 90 +++ crates/js_ast/hoist.rs | 614 ++++++++++++++++++ crates/js_ast/is_valid_js_expression.rs | 85 +++ crates/js_ast/js_ast.rs | 276 +++----- crates/js_ast/lib.rs | 421 +----------- crates/js_ast/util.rs | 17 + crates/js_printer/Cargo.toml | 1 + crates/js_printer/lib.rs | 358 ++++++++++ crates/js_printer/printer_tests.rs | 53 ++ crates/js_printer/src/print_expression.rs | 14 +- crates/js_printer/src/print_statement.rs | 12 +- crates/parser/parse_block.rs | 32 +- crates/parser/parse_call_arguments.rs | 5 + crates/parser/parse_expr.rs | 61 +- crates/parser/parse_expr_if.rs | 22 +- crates/parser/parse_fn_declaration.rs | 11 +- crates/parser/parse_module.rs | 6 +- crates/sema/Cargo.toml | 1 + crates/sema/{src => }/ast_type_coersion.rs | 0 crates/sema/{src/lib.rs => sema.rs} | 10 +- crates/tokenizer/Cargo.toml | 1 + crates/tokenizer/{src/lib.rs => tokenizer.rs} | 9 + 31 files changed, 1861 insertions(+), 1270 deletions(-) delete mode 100644 crates/cli/Cargo.lock rename crates/cli/{src => }/main.rs (85%) create mode 100644 crates/js_ast/convert_valid_expr.rs create mode 100644 crates/js_ast/has_side_effects.rs create mode 100644 crates/js_ast/hoist.rs create mode 100644 crates/js_ast/is_valid_js_expression.rs create mode 100644 crates/js_ast/util.rs create mode 100644 crates/js_printer/lib.rs create mode 100644 crates/js_printer/printer_tests.rs rename crates/sema/{src => }/ast_type_coersion.rs (100%) rename crates/sema/{src/lib.rs => sema.rs} (95%) rename crates/tokenizer/{src/lib.rs => tokenizer.rs} (98%) diff --git a/Cargo.lock b/Cargo.lock index 03588a003888ec3e8fa82a3bacbbed69357e14c5..e11d7608f4d0b1da5e46afd069efbe460f5c5b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ dependencies = [ "ast", "miette", "num", + "parser", "thiserror", "tokenizer", ] diff --git a/Cargo.toml b/Cargo.toml index fdcf94c7291631b43fbac96e4a24885312c0d65e..a1fb3c69c966fc3a4eeefa9afb84d0c02ba31b08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace.package] version = "0.1.0" -name = "blight" edition = "2023" license = "MIT" authors = ["Lino Le Van", "Dave Caruso"] diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs index 123a54fe5567f5ce9fab3273f1c98753a84ab2ac..46b9e6c60da841ff84f4af137d19f12dd3a34252 100644 --- a/crates/ast/ast.rs +++ b/crates/ast/ast.rs @@ -74,6 +74,19 @@ impl Comment { } result } + + pub fn to_doc_comment(self, loc: Loc) -> DocComment { + DocComment { + data: self.data, + loc, + } + } +} + +impl Into for DocComment { + fn into(self) -> Comment { + Comment { data: self.data } + } } impl DocComment { @@ -98,7 +111,13 @@ pub struct BlockWithLoc { pub struct If { pub condition: Box, pub yes: BlockWithLoc, - pub no: Option>, + pub no: Option, +} + +#[derive(Debug, Clone)] +pub enum BlockOrIf { + Block(BlockWithLoc), + If(Box), } #[derive(Debug, Clone)] @@ -166,67 +185,10 @@ impl BuiltinId { } } -impl ExprData { - pub fn is_valid_js_expression(&self) -> bool { - match self { - // A block containing just expressions can be an expression - ExprData::Block(block) => block.is_valid_js_expression(), - ExprData::If(expr) => expr.is_valid_js_expression(), - ExprData::Return(_) => false, - ExprData::Builtin { id, .. } => match id { - BuiltinId::Raw => true, - }, - _ => true, - } - } -} - -impl Block { - pub fn is_valid_js_expression(&self) -> bool { - self.statements - .iter() - .all(|statement| statement.is_valid_js_expression()) - } -} - -impl Expr { - pub fn is_valid_js_expression(&self) -> bool { - self.data.is_valid_js_expression() - } -} - -impl If { - pub fn is_valid_js_expression(&self) -> bool { - self.condition.is_valid_js_expression() - && self.yes.is_valid_js_expression() - && match &self.no { - Some(no) => no.is_valid_js_expression(), - None => true, - } - } -} - -impl BlockWithLoc { - pub fn is_valid_js_expression(&self) -> bool { - self.block.is_valid_js_expression() - } -} - impl Statement { - pub fn requires_semicolon(&self) -> bool { + pub fn is_expression(&self) -> bool { match &self { - Statement::Expr(expr) => match expr.data { - ExprData::Block(_) => false, - ExprData::If(_) => false, - _ => true, - }, - _ => false, - } - } - - pub fn is_valid_js_expression(&self) -> bool { - match self { - Statement::Expr(expr) => expr.data.is_valid_js_expression(), + Statement::Expr(_) => true, _ => false, } } @@ -238,16 +200,19 @@ impl Statement { Statement::Comment(comment) => TopLevelStatement::Comment(comment), } } + + pub fn is_comment(&self) -> bool { + match self { + Statement::Comment(_) => true, + _ => false, + } + } } impl TopLevelStatement { - pub fn requires_semicolon(&self) -> bool { + pub fn is_expression(&self) -> bool { match &self { - TopLevelStatement::Expr(expr) => match expr.data { - ExprData::Block(_) => false, - ExprData::If(_) => false, - _ => true, - }, + TopLevelStatement::Expr(_) => true, _ => false, } } @@ -259,4 +224,11 @@ impl TopLevelStatement { TopLevelStatement::Comment(comment) => Statement::Comment(comment), } } + + pub fn is_comment(&self) -> bool { + match self { + TopLevelStatement::Comment(_) => true, + _ => false, + } + } } diff --git a/crates/ast/operators.rs b/crates/ast/operators.rs index 18dd5ae68a5961b785e815adca22dd9c71418907..622be4fcc24c93e55588e9e341bccc1176e1cc54 100644 --- a/crates/ast/operators.rs +++ b/crates/ast/operators.rs @@ -24,6 +24,22 @@ pub enum BinaryOperator { NotEq, And, Or, + + Assign, + AssignAdd, + AssignSub, + AssignMul, + AssignDiv, + AssignPow, + AssignLshift, + AssignRshift, + AssignRshiftUnsigned, + AssignBitAnd, + AssignBitOr, + AssignBitXor, + AssignMod, + AssignAnd, + AssignOr, } #[derive(Debug, Clone, PartialEq)] @@ -34,8 +50,8 @@ pub enum UnaryOperator { } impl BinaryOperator { - pub fn to_string(&self) -> String { - String::from(match self { + pub fn to_str(&self) -> &'static str { + match self { Self::Add => "+", Self::Sub => "-", Self::Div => "/", @@ -56,7 +72,26 @@ impl BinaryOperator { Self::NotEq => "!=", Self::And => "&&", Self::Or => "||", - }) + Self::AssignAdd => "+=", + Self::AssignSub => "-=", + Self::AssignDiv => "/=", + Self::AssignMul => "*=", + Self::AssignPow => "**=", + Self::Assign => "=", + Self::AssignLshift => "<<=", + Self::AssignRshift => ">>=", + Self::AssignRshiftUnsigned => ">>>=", + Self::AssignBitAnd => "&=", + Self::AssignBitOr => "|=", + Self::AssignBitXor => "^=", + Self::AssignMod => "%=", + Self::AssignAnd => "&&=", + Self::AssignOr => "||=", + } + } + + pub fn to_string(&self) -> String { + String::from(self.to_str()) } pub fn from_token(token: &Token) -> Option { @@ -85,6 +120,10 @@ impl BinaryOperator { } } + pub fn level(&self) -> OperatorLevel { + OperatorLevel::from(self) + } + /// Error case of this means 'types do not allow this operation' pub fn perform(&self, left: Literal, right: Literal) -> Result { match self { @@ -139,17 +178,24 @@ impl BinaryOperator { }), _ => Err(()), }, + + // Impossible condition doing assign-add to a literal + _ => Err(()), } } } impl UnaryOperator { - pub fn to_string(self) -> String { - String::from(match self { + pub fn to_str(&self) -> &'static str { + match self { Self::Neg => "-", Self::Not => "!", Self::BitNot => "~", - }) + } + } + + pub fn to_string(&self) -> String { + String::from(self.to_str()) } pub fn perform(self, value: Literal) -> Result { @@ -171,8 +217,17 @@ impl UnaryOperator { } /// Taken from from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table -#[derive(PartialEq, PartialOrd)] -pub enum BinaryOperatorLevel { +/// +/// Blight doesn't have all of these operators, but the +/// printer uses it to determine where to put parens. +#[derive(PartialEq, PartialOrd, Debug)] +pub enum OperatorLevel { + Accessor, // e. e[] e() + New, // new + Postfix, // expr++ expr-- + Prefix, // ++e, --e, !e, ~e, +e, -e, typeof, void, delete + + // these are the binary operators Exponentiation, // ** Multiplicative, // *, /, % Additive, // +, - @@ -184,22 +239,77 @@ pub enum BinaryOperatorLevel { BitwiseOr, // | LogicalAnd, // && LogicalOr, // ||, ?? + + AssignmentAndOther, // =, +=, -=, *=, /=, %=, / <<=, >>=, >>>=, &=, ^=, |= + Comma, // , +} + +pub enum Associativity { + LeftToRight, + RightToLeft, +} + +impl OperatorLevel { + pub fn associativity(&self) -> Associativity { + use Associativity::*; + use OperatorLevel::*; + + match self { + Accessor => LeftToRight, + Exponentiation => RightToLeft, + Multiplicative => LeftToRight, + Additive => LeftToRight, + BitwiseShift => LeftToRight, + Relational => LeftToRight, + Equality => LeftToRight, + BitwiseAnd => LeftToRight, + BitwiseXor => LeftToRight, + BitwiseOr => LeftToRight, + LogicalAnd => LeftToRight, + LogicalOr => LeftToRight, + AssignmentAndOther => RightToLeft, + Comma => LeftToRight, + + // These do not make sense to have an associativity + // You will likely not run into this if you do + // BinaryOperator::level(o).associativity() + New | Postfix | Prefix => unreachable!("{:?} does not have an associativity", self), + } + } } -pub fn get_binary_op_level(token: &Token) -> Option { - use BinaryOperatorLevel::*; - match token { - Token::StarStar => Some(Exponentiation), - Token::Star | Token::Slash | Token::Percent => Some(Multiplicative), - Token::Plus | Token::Minus => Some(Additive), - Token::LtLt | Token::GtGt | Token::GtGtGt => Some(BitwiseShift), - Token::Gt | Token::GtEqual | Token::Lt | Token::LtEqual => Some(Relational), - Token::EqualEqual | Token::BangEqual => Some(Equality), - Token::And => Some(BitwiseAnd), - Token::Caret => Some(BitwiseXor), - Token::Or => Some(BitwiseOr), - Token::AndAnd => Some(LogicalAnd), - Token::OrOr => Some(LogicalOr), - _ => None, +impl From<&BinaryOperator> for OperatorLevel { + fn from(operator: &BinaryOperator) -> Self { + use BinaryOperator::*; + use OperatorLevel::*; + + match operator { + Add | Sub => Additive, + Mul | Div | Mod => Multiplicative, + Pow => Exponentiation, + Gt | Lt | Gte | Lte => Relational, + Lshift | Rshift | RshiftUnsigned => BitwiseShift, + BitAnd => BitwiseAnd, + BitOr => BitwiseOr, + BitXor => BitwiseXor, + Eq | NotEq => Equality, + And => LogicalAnd, + Or => LogicalOr, + + Assign | AssignAdd | AssignSub | AssignMul | AssignDiv | AssignPow | AssignLshift + | AssignRshift | AssignRshiftUnsigned | AssignBitAnd | AssignBitOr | AssignBitXor + | AssignMod | AssignAnd | AssignOr => AssignmentAndOther, + } + } +} + +impl From for OperatorLevel { + fn from(operator: UnaryOperator) -> Self { + use OperatorLevel::*; + use UnaryOperator::*; + + match operator { + Neg | Not | BitNot => Prefix, + } } } diff --git a/crates/cli/Cargo.lock b/crates/cli/Cargo.lock deleted file mode 100644 index bea6bbd39b14927d56ffb803015103f3cd74364f..0000000000000000000000000000000000000000 --- a/crates/cli/Cargo.lock +++ /dev/null @@ -1,532 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "anstream" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anstyle-parse" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "blight" -version = "0.1.0" -dependencies = [ - "clap", - "miette", - "thiserror", -] - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "4.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "is-terminal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - -[[package]] -name = "is_ci" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" - -[[package]] -name = "libc" -version = "0.2.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" - -[[package]] -name = "linux-raw-sys" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" - -[[package]] -name = "memchr" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" - -[[package]] -name = "miette" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" -dependencies = [ - "backtrace", - "backtrace-ext", - "is-terminal", - "miette-derive", - "once_cell", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "proc-macro2" -version = "1.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustix" -version = "0.38.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "supports-color" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" -dependencies = [ - "is-terminal", - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "supports-unicode" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "syn" -version = "2.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d27c2c202598d05175a6dd3af46824b7f747f8d8e9b14c623f19fa5069735d" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 8c257d214dad91041d7de4f1e29bc1ee6641750c..00920ac78a6265cf80755f257b26c88e56c2d8dd 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [[bin]] name = "blight" -path = "./src/main.rs" +path = "./main.rs" [dependencies] clap.workspace = true diff --git a/crates/cli/src/main.rs b/crates/cli/main.rs similarity index 85% rename from crates/cli/src/main.rs rename to crates/cli/main.rs index 00945e42be7c877440cc6f4466dfba7fad622f9c..dfcb80febf985e7247541887ad7a59bdd65d364f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/main.rs @@ -1,7 +1,7 @@ use std::{io::Write, path::PathBuf}; use clap::{Parser, Subcommand}; -use miette::Result; +use miette::{diagnostic, Result}; use js_printer; use tokenizer; @@ -50,21 +50,28 @@ fn get_runtime(runtime: &String) -> String { } } -pub fn transpile(content: String) -> Result { +pub fn transpile(content: String, writer: &mut dyn Write) -> Result<()> { // 1. Tokenize the input let tokens = tokenizer::tokenize(content)?; // 2. Parse the tokens into an AST - let blight_ast = parser::parse(tokens)?; - // 3. Type check the AST - // sema::check_module(&blight_ast)?; + let mut blight_ast = parser::parse(tokens)?; + // 3. Semantic analysis. + sema::analyze_module(&mut blight_ast)?; // 4. Optimize the AST - // optimize(tree.clone())?; + // optimize::optimize_module(&mut blight_ast); // 5. Translate the AST into javascript - let js_ast = js_ast::translate(&blight_ast)?; + let js_ast = js_ast::blight_to_js(blight_ast); // 6. Print the JS AST - let javascript = js_printer::print(js_ast); - // 7. Return the javascript - return Ok(javascript); + // TODO(@paperdave): use std::io::Write instead of a string + js_printer::Print::print(&js_ast, writer) + .map_err(|e| diagnostic!("Error while printing: {}", e))?; + Ok(()) +} + +pub fn transpile_to_string(content: String) -> Result { + let mut buffer = Vec::new(); + transpile(content, &mut buffer)?; + Ok(String::from_utf8(buffer).unwrap()) } pub fn astdump(content: String) -> Result { @@ -93,7 +100,7 @@ fn main() -> Result<()> { } }; - let mut result = transpile(contents.clone())?; + let mut result = transpile_to_string(contents.clone())?; if format { let mut formatter = std::process::Command::new("deno") @@ -124,7 +131,7 @@ fn main() -> Result<()> { } Commands::Run { file, target } => { let contents = std::fs::read_to_string(file.clone()).unwrap(); - let result = transpile(contents.clone())?; + let result = transpile_to_string(contents.clone())?; let mut temp_file = std::env::temp_dir(); temp_file.push("temp.js"); std::fs::write(temp_file.clone(), result).unwrap(); diff --git a/crates/js_ast/Cargo.toml b/crates/js_ast/Cargo.toml index f7b5438968013029fa069bd2d9040847c5454392..68087cf32a5792a7ae7f6ecac80399eb5883fdba 100644 --- a/crates/js_ast/Cargo.toml +++ b/crates/js_ast/Cargo.toml @@ -13,3 +13,4 @@ num.workspace = true ast = { path = "../ast" } tokenizer = { path = "../tokenizer" } # TODO: move Loc out of the tokenizer +parser = { path = "../parser" } # TODO: move Loc out of the tokenizer diff --git a/crates/js_ast/convert_valid_expr.rs b/crates/js_ast/convert_valid_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..90e13ddc003e4066d45034cba29c8b3f4d438432 --- /dev/null +++ b/crates/js_ast/convert_valid_expr.rs @@ -0,0 +1,204 @@ +//! "Valid" in this context means that the expression does not contain JS statements +//! Meaning conversion is a direct map from a blight as node into a JS one +//! +//! See `is_valid_js_expression` for more details on what qualifies +//! +//! For other nodes, it must hoist statements to the nearest JS block, +//! is done in the `hoist_*` functions in `host.rs`. + +use ast::{BlockOrIf, Comment, ExprData, If}; +use ast::{BlockWithLoc, BuiltinId, Expr, Literal, Statement}; +use tokenizer::Loc; + +use crate::is_valid_js_expression::IsValidJSExpression; +use crate::js_ast::*; +use crate::HoistContext; + +/// Translate a blight ast expression that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_expr(ctx: &mut HoistContext, expr: Expr) -> JsExpr { + // Assert may be skipped in release builds as all failures are + // annotated with a release-checked panic + debug_assert!( + expr.is_valid_js_expression(), + "must call convert_valid_expr with valid js expression (use hoist instead)" + ); + + let loc = expr.loc; + let comment = expr.comment; + + match expr.data { + ExprData::Identifier(str) => JsExpr::new( + JsExprData::Identifier(ctx.remap_identifier(&str)), + loc, + comment, + ), + + ExprData::Literal(Literal::Bool(value)) => { + JsExpr::new(JsExprData::Bool(value), loc, comment) + } + ExprData::Literal(Literal::Number(value)) => { + JsExpr::new(JsExprData::Number(value), loc, comment) + } + ExprData::Literal(Literal::String(value)) => { + JsExpr::new(JsExprData::String(value), loc, comment) + } + ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), + + ExprData::Block(block) => convert_valid_block(ctx, BlockWithLoc { block, loc }), + + ExprData::Binary { op, left, right } => { + let left = convert_valid_expr(ctx, *left); + let right = convert_valid_expr(ctx, *right); + JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + loc, + comment, + ) + } + ExprData::Unary { op, value } => { + let value = convert_valid_expr(ctx, *value); + JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + loc, + comment, + ) + } + ExprData::Fn { block, params, .. } => JsExpr::new( + JsExprData::ArrowFunction { + params: params.into_iter().map(|x| x.name).collect(), + body: Box::new(ctx.hoist_block_to_arrow_fn_body(BlockWithLoc { + block, + loc: loc.clone(), + })), + }, + loc, + comment, + ), + ExprData::Builtin { id, arguments } => match id { + BuiltinId::Raw => { + let first_arg = arguments.iter().next().unwrap(); + // TODO(@paperdave): this string clone is stupid + let str = match first_arg.data.clone() { + ExprData::Literal(Literal::String(s)) => s, + _ => panic!("raw expects a string"), + }; + JsExpr::new(JsExprData::Raw(str), loc, comment) + } + }, + ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), + + ExprData::Call { base, arguments } => { + let base = convert_valid_expr(ctx, *base); + let arguments = arguments + .into_iter() + .map(|x| convert_valid_expr(ctx, x)) + .collect(); + JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + loc, + comment, + ) + } + + ExprData::Field { base, field } => JsExpr::new( + match field.data { + ExprData::Literal(Literal::String(name)) => JsExprData::Dot { + base: Box::new(convert_valid_expr(ctx, *base)), + name, + }, + _ => JsExprData::Index { + base: Box::new(convert_valid_expr(ctx, *base)), + index: Box::new(convert_valid_expr(ctx, *field)), + }, + }, + loc, + comment, + ), + + ExprData::Return(_) => unreachable!("convert_valid_expr does not support return"), + } +} + +pub fn convert_valid_block(ctx: &mut HoistContext, b: BlockWithLoc) -> JsExpr { + debug_assert!( + b.is_valid_js_expression(), + "must call convert_valid_block with valid js expression" + ); + + let len = b.block.statements.len(); + let loc = b.loc; + let block_expr = match len { + 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), + // a block expression with a single expression is just that expression + 1 => convert_valid_statement_to_expr(ctx, b.block.statements.into_iter().next().unwrap()), + // Since blocks are not valid expressions in JS, we create a + // comma expression from a block with more than one statement. + 2.. => { + let mut items = Vec::new(); + for statement in b.block.statements.into_iter() { + items.push(convert_valid_statement_to_expr(ctx, statement)); + } + JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) + } + }; + + // a block without implicit return is to be type void, so we wrap in a void expression + if !b.block.has_implicit_return && len > 0 { + JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) + } else { + block_expr + } +} + +pub fn convert_valid_statement_to_expr(ctx: &mut HoistContext, statement: Statement) -> JsExpr { + debug_assert!( + statement.is_valid_js_expression(), + "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" + ); + + match statement { + ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), + _ => unreachable!("attempt to convert statement to js without hoisting"), + } +} + +pub fn convert_valid_statement(ctx: &mut HoistContext, statement: Statement) -> JsStatement { + let expr = convert_valid_statement_to_expr(ctx, statement); + let loc = expr.loc.clone(); + JsStatement::new(JsStatementData::Expression(expr), loc, None) +} + +pub fn convert_valid_if( + ctx: &mut HoistContext, + expr: If, + loc: Loc, + comment: Option, +) -> JsExpr { + let condition = convert_valid_expr(ctx, *expr.condition); + let yes = convert_valid_block(ctx, expr.yes); + let no = match expr.no { + Some(BlockOrIf::Block(block)) => Some(convert_valid_block(ctx, block)), + Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), + None => None, + }; + JsExpr::new( + JsExprData::Ternary { + condition: Box::new(condition), + yes: Box::new(yes), + no: no.map(Box::new), + }, + loc, + comment, + ) +} diff --git a/crates/js_ast/has_side_effects.rs b/crates/js_ast/has_side_effects.rs new file mode 100644 index 0000000000000000000000000000000000000000..81c16730f2cd157a8f1b9236588bea0a05854a96 --- /dev/null +++ b/crates/js_ast/has_side_effects.rs @@ -0,0 +1,90 @@ +use ast::*; + +pub trait HasSideEffects { + /// Can this item be removed from the AST without changing the program's behavior? + fn has_side_effects(&self) -> bool; +} + +impl HasSideEffects for ExprData { + fn has_side_effects(&self) -> bool { + match self { + ExprData::Literal(_) => false, + ExprData::Identifier(_) => false, + + ExprData::Block(block) => block.has_side_effects(), + ExprData::Binary { left, right, .. } => { + left.has_side_effects() || right.has_side_effects() + } + ExprData::Unary { value, .. } => value.has_side_effects(), + ExprData::If(ifexpr) => ifexpr.has_side_effects(), + + // note: a function *declaration* has no side effects, but the call *can* + ExprData::Fn { .. } => false, + + // TODO(@paperdave): implement detection of pure functions + ExprData::Call { base, arguments } => { + base.has_side_effects() || arguments.iter().any(|x| x.has_side_effects()) + } + + // note: we assume field access has side effects, ie: a getter doesn't throw + ExprData::Field { base, field } => base.has_side_effects() || field.has_side_effects(), + + // note: we assume the code in @raw always has side effects + ExprData::Builtin { id, .. } => match id { + BuiltinId::Raw => true, + }, + + ExprData::Return(_) => true, + } + } +} + +impl HasSideEffects for Statement { + fn has_side_effects(&self) -> bool { + match self { + Statement::Expr(expr) => expr.has_side_effects(), + Statement::Decl(decl) => decl.init.has_side_effects(), + Statement::Comment(_) => false, + } + } +} + +impl HasSideEffects for Block { + fn has_side_effects(&self) -> bool { + self.statements + .iter() + .any(|statement| statement.has_side_effects()) + } +} + +impl HasSideEffects for Expr { + fn has_side_effects(&self) -> bool { + self.data.has_side_effects() + } +} + +impl HasSideEffects for If { + fn has_side_effects(&self) -> bool { + self.condition.has_side_effects() + || self.yes.has_side_effects() + || match &self.no { + Some(no) => no.has_side_effects(), + None => false, + } + } +} + +impl HasSideEffects for BlockOrIf { + fn has_side_effects(&self) -> bool { + match self { + BlockOrIf::Block(block) => block.has_side_effects(), + BlockOrIf::If(if_) => if_.has_side_effects(), + } + } +} + +impl HasSideEffects for BlockWithLoc { + fn has_side_effects(&self) -> bool { + self.block.has_side_effects() + } +} diff --git a/crates/js_ast/hoist.rs b/crates/js_ast/hoist.rs new file mode 100644 index 0000000000000000000000000000000000000000..5351588aec2aae5653dcdebb524e2503df15aa9e --- /dev/null +++ b/crates/js_ast/hoist.rs @@ -0,0 +1,614 @@ +use std::collections::{HashMap, HashSet}; +use std::vec; + +use ast::{BinaryOperator, BlockOrIf, BlockWithLoc, Expr, Statement}; +use tokenizer::Loc; + +use crate::*; + +pub struct HoistContext { + pub statements: Vec, + pub ids: HashSet, + next_id: u32, + scopes: Vec, + has_last_statement: bool, +} + +struct HoistScope { + renames: HashMap, +} + +pub struct TempId { + unmapped_name: String, +} + +impl TempId { + pub fn get(&self, hoist_context: &HoistContext) -> String { + hoist_context.remap_identifier(&self.unmapped_name) + } +} + +impl HoistContext { + pub fn new() -> Self { + Self { + statements: Vec::new(), + ids: HashSet::new(), + next_id: 0, + scopes: vec![HoistScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + pub fn new_child(&self) -> Self { + Self { + statements: Vec::new(), + ids: self.ids.clone(), + next_id: 0, + scopes: vec![HoistScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + /// A virtual scope is a scope that exists in blight syntax + /// but may or may not exist in blight. With these it is important to + pub fn push_virtual_scope(&mut self) { + self.scopes.push(HoistScope { + renames: HashMap::new(), + }); + } + + /// See `push_virtual_scope` for more information on what this is. + pub fn pop_virtual_scope(&mut self) { + self.scopes.pop(); + } + + pub fn generate_temp_id(&mut self, basename: &str) -> TempId { + self.next_id += 1; + loop { + let name = format!("{}{}", basename, self.next_id); + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + self.next_id += 1; + } + } + + /// This does some extra optimizations: + /// - `let a = b;` will not be emitted but instad write a variable alias + pub fn add_statement(&mut self, statement: JsStatement) { + match statement.data { + JsStatementData::Declaration(JSDeclaration { + name, + init: + Some(JsExpr { + data: JsExprData::Identifier(init_name), + .. + }), + is_constant: true, + }) => { + self.scopes + .last_mut() + .unwrap() + .renames + .insert(name, init_name); + self.has_last_statement = false; + return; + } + _ => {} + } + self.statements.push(statement); + self.has_last_statement = true; + } + + /// Get the last statement if it exists. + /// If the statement was optimized away, this returns None + pub fn last_statement(&self) -> Option<&JsStatement> { + if self.has_last_statement { + Some(self.statements.last().unwrap()) + } else { + None + } + } + + pub fn generate_id(&mut self, basename: &String) -> TempId { + let mut id = 0u32; + loop { + let name = match id { + 0 => basename.clone(), + _ => format!("{}{}", basename, id), + }; + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + id += 1; + } + } + + /// Given a blight identifier, remap it to a new identifier. + pub fn remap_identifier(&self, name: &String) -> String { + for scope in self.scopes.iter().rev() { + if let Some(remapped) = scope.renames.get(name) { + return remapped.clone(); + } + } + name.clone() + } + + /// Declare an identifier in the current scope. + pub fn declare_identifier(&mut self, name: String) -> TempId { + let remapped = self.generate_id(&name); + let scope = self.scopes.last_mut().unwrap(); + self.ids.insert(remapped.unmapped_name.clone()); + scope.renames.insert(name, remapped.unmapped_name.clone()); + remapped + } + + pub fn process_statement(&mut self, statement: Statement) -> Option<()> { + if statement.is_valid_js_expression() { + let result = convert_valid_statement(self, statement); + if !result.is_literal() { + self.add_statement(result); + } + return Some(()); + } + + match statement { + Statement::Decl(decl) => { + // visit init before name, this way allows for: + // + // let a = a; + // + // and `a` will be looked up before it is re-declared. + if let Some(init) = self.hoist_expression_contents(decl.init) { + let name = self.declare_identifier(decl.name); + + self.add_statement(JsStatement::new( + JsStatementData::Declaration(crate::JSDeclaration { + name: name.get(&self), + init: Some(init), + is_constant: decl.is_constant, + }), + decl.loc, + decl.comment.map(|x| x.into()), + )); + Some(()) + } else { + None + } + } + Statement::Expr(expr) => { + if let Some(expr) = self.hoist_expression_contents(expr) { + let loc = expr.loc.clone(); + if !expr.is_literal() { + self.add_statement(JsStatement::new( + JsStatementData::Expression(expr), + loc, + None, + )) + } + Some(()) + } else { + None + } + } + Statement::Comment(_) => Some(()), + } + } + + /// Pushes a decl with a generated identifier to the hoisted result + /// and returns an identifier expression for it. + pub fn hoist_expression_to_temporary(&mut self, expr: Expr) -> Option { + // TODO + // if !expr.has_side_effects() { + // return Some(self.hoist_expression_contents(expr)?); + // } + + let init = self.hoist_expression_contents(expr)?; + + let id = self.generate_temp_id("_temp"); + let loc = init.loc.clone(); + + self.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: id.get(&self), + init: Some(init), + is_constant: true, + }), + loc.clone(), + None, + )); + + Some(JsExpr::new( + JsExprData::Identifier(id.get(&self)), + loc, + None, + )) + } + + // Returning `None` from this means that the expression is noreturn, and that + // the rest of the statement never occurs and can be stripped. + pub fn hoist_expression_contents(&mut self, expr: Expr) -> Option { + if expr.is_valid_js_expression() { + return Some(convert_valid_expr(self, expr)); + } + + match expr.data { + ast::ExprData::Binary { op, left, right } => { + let left = self.hoist_expression_to_temporary(*left)?; + let right = self.hoist_expression_to_temporary(*right)?; + + Some(JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Unary { op, value } => { + let value = self.hoist_expression_to_temporary(*value)?; + Some(JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Field { base, field } => { + let base = self.hoist_expression_to_temporary(*base)?; + let field = self.hoist_expression_to_temporary(*field)?; + + Some(JsExpr::new( + match field.data { + JsExprData::String(name) if is_js_identifier(name.as_str()) => { + JsExprData::Dot { + base: Box::new(base), + name, + } + } + _ => JsExprData::Index { + base: Box::new(base), + index: Box::new(field), + }, + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Block(block) => { + let temporary = self.generate_temp_id("_block"); + let result = self.hoist_block_to_js_block_in_same_context(BlockWithLoc { + block, + loc: expr.loc.clone(), + }); + + self.add_statement(JsStatement::new( + JsStatementData::Declaration(crate::JSDeclaration { + name: temporary.get(&self), + init: Some(result), + is_constant: false, + }), + expr.loc.clone(), + None, + )); + + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&self)), + expr.loc, + expr.comment, + )) + } + + ast::ExprData::If(data) => { + let temporary = self.generate_temp_id("_if"); + + let decl_index = self.statements.len(); + self.add_statement(JsStatement::new( + JsStatementData::Declaration(crate::JSDeclaration { + name: temporary.get(&self), + init: None, + is_constant: false, + }), + expr.loc.clone(), + None, + )); + + let condition = self.hoist_expression_to_temporary(*data.condition)?; + let mut did_assign = false; + + let js_if = + self.hoist_js_if(data.yes, data.no, condition, &mut did_assign, &temporary)?; + + self.add_statement(JsStatement::new( + JsStatementData::If(js_if), + expr.loc.clone(), + None, + )); + + if did_assign { + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&self)), + expr.loc, + expr.comment, + )) + } else { + self.statements.remove(decl_index); + Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) + } + } + + ast::ExprData::Return(expr) => { + let loc = expr.loc.clone(); + let return_value = self.hoist_expression_to_temporary(*expr)?; + self.add_statement(JsStatement::new( + JsStatementData::Return(return_value), + loc, + None, + )); + None + } + + ast::ExprData::Call { base, arguments } => { + let base = self.hoist_expression_to_temporary(*base)?; + let arguments = arguments + .into_iter() + .map(|x| self.hoist_expression_to_temporary(x)) + .collect::>>()?; + + Some(JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + expr.loc, + expr.comment, + )) + } + + ast::ExprData::Fn { .. } + | ast::ExprData::Builtin { .. } + | ast::ExprData::Literal(_) + | ast::ExprData::Identifier(_) => { + unreachable!("attempt to hoist an expression that does not require it") + } + } + } + + pub fn hoist_block_to_js_block( + &mut self, + b: BlockWithLoc, + temporary: &String, + did_assign: &mut bool, + ) -> Option { + let loc = b.loc.clone(); + + // A non-block will become a block with a single expression: + // _temp = expr; + // + // We will also apply this transformation to blocks that are valid + // expressions, because that is more concise. + if b.is_valid_js_expression() { + return Some(JsBlock::new( + vec![new_js_assignment_statement( + convert_valid_block(self, b), + temporary.clone(), + loc.clone(), + )], + loc, + )); + } + + let mut child = self.new_child(); + + for statement in b.block.statements { + if let None = child.process_statement(statement) { + return None; + }; + } + + // If the last statement is an expression, we need + // to assign to the temporary variable. + match child.statements.last().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) => { + *did_assign = true; + + let last = child.statements.pop().unwrap(); + let value = match last { + JsStatement { + data: JsStatementData::Expression(expr), + .. + } => expr, + _ => { + self.statements.push(last); + JsExpr::new(JsExprData::Null, loc.clone(), None) + } + }; + child.statements.push(new_js_assignment_statement( + value, + temporary.clone(), + loc.clone(), + )); + } + _ => {} + } + + Some(JsBlock::new(child.statements, loc)) + } + + pub fn hoist_block_to_arrow_fn_body(&mut self, b: BlockWithLoc) -> JsArrowBody { + let loc = b.loc.clone(); + + // A block that is a valid expression will just become + // that expression. We can convert multiple expressions + // into a comma expression. + if b.is_valid_js_expression() { + return JsArrowBody::Expr(convert_valid_block(self, b)); + } + + let mut child = self.new_child(); + + for statement in b.block.statements { + if let None = child.process_statement(statement) { + break; + }; + } + + // If the last statement is an expression, + // we need to return that expression. + match child.statements.last().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) => { + let last = child.statements.pop().unwrap(); + let value = match last { + JsStatement { + data: JsStatementData::Expression(expr), + .. + } => expr, + _ => { + self.statements.push(last); + JsExpr::new(JsExprData::Null, loc.clone(), None) + } + }; + child.statements.push(JsStatement::new( + JsStatementData::Return(value), + loc.clone(), + None, + )); + } + _ => {} + } + + JsArrowBody::Block(JsBlock::new(child.statements, loc)) + } + + pub fn hoist_block_to_js_block_in_same_context(&mut self, b: BlockWithLoc) -> JsExpr { + let loc = b.loc.clone(); + self.push_virtual_scope(); + + debug_assert!(!b.block.is_valid_js_expression()); + + for statement in b.block.statements { + self.process_statement(statement); + } + + self.pop_virtual_scope(); + + // If the last statement is an expression, we need + // to assign to the temporary variable. + match self.last_statement().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) if b.block.has_implicit_return => { + let last = self.statements.pop().unwrap(); + match last { + JsStatement { + data: JsStatementData::Expression(expr), + .. + } => expr, + _ => { + self.statements.push(last); + JsExpr::new(JsExprData::Null, loc.clone(), None) + } + } + } + _ => JsExpr::new(JsExprData::Null, loc.clone(), None), + } + } + + pub fn hoist_js_if( + &mut self, + yes: BlockWithLoc, + no: Option, + condition: JsExpr, + did_assign: &mut bool, + temporary: &TempId, + ) -> Option { + let yes = self.hoist_block_to_js_block(yes, &temporary.get(&self), did_assign)?; + + let no = match no { + Some(BlockOrIf::Block(no)) => Some(JsElse::Block(self.hoist_block_to_js_block( + no, + &temporary.get(&self), + did_assign, + )?)), + Some(BlockOrIf::If(else_if)) => { + // If the condition of this block has a side effect, we need to + // emit a block so that we can assign to the temporary variable. + // + // Otherwise, we can just emit the condition inline + if else_if.condition.is_valid_js_expression() { + let condition = convert_valid_expr(self, *else_if.condition.clone()); + Some(JsElse::If(Box::new(self.hoist_js_if( + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )?))) + } else { + let mut temp_block = self.new_child(); + let condition = + temp_block.hoist_expression_to_temporary(*else_if.condition.clone())?; + + temp_block.add_statement(JsStatement::new( + JsStatementData::If(self.hoist_js_if( + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )?), + Loc::EMPTY, + None, + )); + + Some(JsElse::Block(JsBlock::new( + temp_block.statements, + Loc::EMPTY, + ))) + } + } + None => None, + }; + + Some(JsIf { condition, yes, no }) + } +} + +pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { + JsStatement::new( + JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), + loc, + None, + ) +} + +pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { + JsExpr::new( + JsExprData::Binary { + op: BinaryOperator::Assign, + left: Box::new(JsExpr::new( + JsExprData::Identifier(target), + loc.clone(), + None, + )), + right: Box::new(expr), + }, + loc, + None, + ) +} diff --git a/crates/js_ast/is_valid_js_expression.rs b/crates/js_ast/is_valid_js_expression.rs new file mode 100644 index 0000000000000000000000000000000000000000..382b8712e3f33dc10e713d382e8859352e74f8f6 --- /dev/null +++ b/crates/js_ast/is_valid_js_expression.rs @@ -0,0 +1,85 @@ +use ast::*; + +pub trait IsValidJSExpression { + /// Can this be lowered to a JS ast without hoisting items + /// ex: a `return` expression is not a valid JS expression + fn is_valid_js_expression(&self) -> bool; +} + +impl IsValidJSExpression for ExprData { + fn is_valid_js_expression(&self) -> bool { + match self { + // A block containing just expressions can be an expression + ExprData::Block(block) => block.is_valid_js_expression(), + ExprData::If(expr) => expr.is_valid_js_expression(), + ExprData::Return(_) => false, + ExprData::Builtin { id, .. } => match id { + BuiltinId::Raw => true, + }, + ExprData::Binary { left, right, .. } => { + left.is_valid_js_expression() && right.is_valid_js_expression() + } + ExprData::Unary { value, .. } => value.is_valid_js_expression(), + ExprData::Fn { .. } => true, + ExprData::Call { base, arguments } => { + base.is_valid_js_expression() + && arguments + .iter() + .all(|argument| argument.is_valid_js_expression()) + } + ExprData::Field { base, field } => { + base.is_valid_js_expression() && field.is_valid_js_expression() + } + ExprData::Literal(_) | ExprData::Identifier(_) => true, + } + } +} + +impl IsValidJSExpression for Block { + fn is_valid_js_expression(&self) -> bool { + self.statements + .iter() + .all(|statement| statement.is_valid_js_expression()) + } +} + +impl IsValidJSExpression for Expr { + fn is_valid_js_expression(&self) -> bool { + self.data.is_valid_js_expression() + } +} + +impl IsValidJSExpression for If { + fn is_valid_js_expression(&self) -> bool { + self.condition.is_valid_js_expression() + && self.yes.is_valid_js_expression() + && match &self.no { + Some(no) => no.is_valid_js_expression(), + None => true, + } + } +} + +impl IsValidJSExpression for BlockOrIf { + fn is_valid_js_expression(&self) -> bool { + match self { + BlockOrIf::Block(block) => block.is_valid_js_expression(), + BlockOrIf::If(if_) => if_.is_valid_js_expression(), + } + } +} + +impl IsValidJSExpression for BlockWithLoc { + fn is_valid_js_expression(&self) -> bool { + self.block.is_valid_js_expression() + } +} + +impl IsValidJSExpression for Statement { + fn is_valid_js_expression(&self) -> bool { + match self { + Statement::Expr(expr) => expr.data.is_valid_js_expression(), + _ => false, + } + } +} diff --git a/crates/js_ast/js_ast.rs b/crates/js_ast/js_ast.rs index bc68f6e919939807b35f6a473819937156393548..6b6c2520d16f139d927522898af6f4bdee367b30 100644 --- a/crates/js_ast/js_ast.rs +++ b/crates/js_ast/js_ast.rs @@ -1,4 +1,4 @@ -use ast::{BinaryOperator, BlockWithLoc, BuiltinId, Comment, Expr, UnaryOperator}; +use ast::{BinaryOperator, Comment, UnaryOperator}; use num::bigint::BigInt; use tokenizer::Loc; @@ -31,11 +31,11 @@ pub enum JsExprData { Identifier(String), Unary { - operator: UnaryOperator, + op: UnaryOperator, value: Box, }, Binary { - operator: BinaryOperator, + op: BinaryOperator, left: Box, right: Box, }, @@ -52,22 +52,23 @@ pub enum JsExprData { arguments: Vec, }, Function { - name: String, + name: Option, params: Vec, - block: Vec, + body: Box, }, - If { + ArrowFunction { + params: Vec, + body: Box, + }, + Ternary { condition: Box, - yes: Vec, + yes: Box, no: Option>, }, - ArrowFunction { - params: Vec, - block: Vec, - }, Comma { items: Vec, }, + Void(Box), } #[derive(Debug, PartialEq, Clone)] @@ -77,33 +78,56 @@ pub struct JsStatement { pub comment: Option, } +#[derive(Debug, PartialEq, Clone)] +pub struct JSFunctionDeclaration { + pub name: String, + pub params: Vec, + pub block: JsBlock, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JSDeclaration { + pub name: String, + pub init: Option, + pub is_constant: bool, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsIf { + pub condition: JsExpr, + pub yes: JsBlock, + pub no: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsElse { + Expr(JsExpr), + Block(JsBlock), + If(Box), +} + #[derive(Debug, PartialEq, Clone)] pub enum JsStatementData { Expression(JsExpr), Return(JsExpr), Export(JsExport), - Declaration { - name: String, - init: JsExpr, - is_constant: bool, - }, - Function { - name: String, - params: Vec, - block: JsBlock, - }, - If { - condition: JsExpr, - yes: JsBlock, - no: Option, - }, + Declaration(JSDeclaration), + Function(JSFunctionDeclaration), + If(JsIf), } #[derive(Debug, PartialEq, Clone)] pub enum JsExport { - Named(Vec), + Named(Vec), + Function(Box), + Declaration(Box), Default(JsExpr), - Equals(JsExpr), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JSNamedExport { + pub identifier: String, + pub export_name: String, } #[derive(Debug, PartialEq, Clone)] @@ -113,161 +137,61 @@ pub struct JsBlock { } impl JsExpr { - pub fn from_blight(expr: &Expr) -> Option { - use ast::ExprData::*; - - let loc = &expr.loc; - let comment = &expr.comment; - - match &expr.data { - Block(block) => Self::comma_expr_from_blight_block(&BlockWithLoc { - block: block.clone(), - loc: loc.clone(), - }), - Binary { op, left, right } => { - let left = Self::from_blight(&*left)?; - let right = Self::from_blight(&*right)?; - Some(JsExpr::new( - JsExprData::Binary { - operator: op.clone(), - left: Box::new(left), - right: Box::new(right), - }, - loc.clone(), - comment.clone(), - )) - } - Unary { op, value } => { - let value = Self::from_blight(&*value)?; - Some(JsExpr::new( - JsExprData::Unary { - operator: op.clone(), - value: Box::new(value), - }, - loc.clone(), - comment.clone(), - )) - } - Fn { - name, - params, - return_type, - block, - } => { - let block = Self::comma_expr_from_blight_block(&BlockWithLoc { - block: block.clone(), - loc: loc.clone(), - })?; - Some(JsExpr::new( - JsExprData::Function { - name: name.clone(), - params: vec![], - block: vec![JsStatement::new( - JsStatementData::Expression(block), - loc.clone(), - comment.clone(), - )], - }, - loc.clone(), - comment.clone(), - )) - } - Builtin { id, arguments } => match id { - BuiltinId::Raw => { - let mut args = arguments.iter(); - let arg = args.next().unwrap(); - let arg = match &arg.data { - Literal(ast::Literal::String(s)) => s, - _ => panic!("raw expects a string"), - }; - Some(JsExpr::new( - JsExprData::Raw(arg.clone()), - loc.clone(), - comment.clone(), - )) - } - }, - If(expr) => { - let condition = Self::from_blight(&expr.condition)?; - let yes = Self::comma_expr_from_blight_block(&expr.yes)?; - let no = match &expr.no { - Some(no) => Some(Self::from_blight(&no)?), - None => None, - }; - Some(JsExpr::new( - JsExprData::If { - condition: Box::new(condition), - yes: vec![yes], - no: no.map(Box::new), - }, - loc.clone(), - comment.clone(), - )) - } - - Call { base, arguments } => todo!(), - Field { base, field } => todo!(), - - Return(_) => None, - - Literal(_) => todo!(), - Identifier(_) => todo!(), - // Literal(ast::Literal::Number(n)) => { - // Some(JsExpr::new(JsExprData::Number(n), loc.clone(), comment)) - // } - // Literal(ast::Literal::Bool(b)) => Some(JsExpr::new(JsExprData::Bool(b), loc, comment)), - // Literal(ast::Literal::String(s)) => { - // Some(JsExpr::new(JsExprData::String(s), loc, comment)) - // } - // Literal(ast::Literal::Void) => Some(JsExpr::new(JsExprData::Null, loc, comment)), - // Identifier(id) => Some(JsExpr::new(JsExprData::Identifier(id), loc, comment)), - } - } - - /// Since blocks are not valid expressions in JS, this creates a - /// comma expression from a block with more than one statement. - /// - /// Not possible to create an ast => None - pub fn comma_expr_from_blight_block(b: &ast::BlockWithLoc) -> Option { - if !b.block.has_implicit_return { - return Some(JsExpr::new(JsExprData::Null, b.loc.clone(), None)); - } - // a block expression with a single statement is just that statement - if b.block.statements.len() == 1 && b.block.has_implicit_return { - match b.block.statements.first().unwrap() { - ast::Statement::Expr(expr) => Self::from_blight(expr), - _ => return None, - }; - } - let mut statements = Vec::new(); - for statement in &b.block.statements { - match statement { - ast::Statement::Expr(expr) => { - if let Some(expr) = Self::from_blight(&expr) { - statements.push(expr); - } - } - _ => return None, - } - } - // a block without implicit return is to be type void, so we append null - if !b.block.has_implicit_return { - statements.push(JsExpr::new(JsExprData::Null, b.loc.clone(), None)); - } - Some(JsExpr::new( - JsExprData::Comma { items: statements }, - b.loc.clone(), - None, - )) - } - pub fn new(data: JsExprData, loc: Loc, comment: Option) -> Self { Self { data, loc, comment } } + + pub fn is_literal(&self) -> bool { + match &self.data { + JsExprData::Number(_) => true, + JsExprData::BigInt(_) => true, + JsExprData::String(_) => true, + JsExprData::Bool(_) => true, + JsExprData::Null => true, + JsExprData::Undefined => true, + _ => false, + } + } +} + +impl JsExprData { + /// Helper for testing + pub fn str(value: &str) -> Self { + Self::String(value.to_string()) + } + + /// Helper for testing + pub fn str_id(value: &str) -> Self { + Self::Identifier(value.to_string()) + } +} + +impl JsBlock { + pub fn new(statements: Vec, loc: Loc) -> Self { + Self { statements, loc } + } } impl JsStatement { pub fn new(data: JsStatementData, loc: Loc, comment: Option) -> Self { Self { data, loc, comment } } + + pub(crate) fn is_literal(&self) -> bool { + match &self.data { + JsStatementData::Expression(expr) => expr.is_literal(), + _ => false, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum JsArrowBody { + Block(JsBlock), + Expr(JsExpr), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsModule { + pub statements: Vec, } diff --git a/crates/js_ast/lib.rs b/crates/js_ast/lib.rs index 0c27a7e5204b91d8e1acb6f61f4ec69f044d4cbd..c915dc7b4c9d8e4c28d637b834f070ec2164e7b7 100644 --- a/crates/js_ast/lib.rs +++ b/crates/js_ast/lib.rs @@ -1,411 +1,28 @@ -use std::collections::HashMap; - -use ast::{BlockWithLoc, Expr, ExprData, Literal, Module, Statement}; -pub use js_ast::*; -use miette::Result; -use tokenizer::Loc; - +mod convert_valid_expr; +mod has_side_effects; +mod hoist; +mod is_valid_js_expression; mod js_ast; +mod util; -struct StackContext { - statements: Vec, - /// Variables that are declared in this context. - /// The number is the number of times the variable has been declared. - /// This is used to allow for shadowing - variable_map: HashMap, - block: u32, -} - -impl StackContext { - fn new(block: u32) -> Self { - StackContext { - statements: Vec::new(), - variable_map: HashMap::new(), - block, - } - } -} - -struct TranslateContext { - context: Vec, - internal_counter: u32, - block_counter: u32, -} - -impl TranslateContext { - fn new() -> Self { - TranslateContext { - context: vec![StackContext::new(0)], - internal_counter: 0, - block_counter: 0, - } - } - - fn push_context(self: &mut Self) { - self.block_counter += 1; - self.context.push(StackContext::new(self.block_counter)); - } - - fn pop_context(self: &mut Self) -> Vec { - self.context.pop().unwrap().statements - } - - fn push_internal_variable(self: &mut Self, value: JsExpr) -> JsExpr { - let name = format!("${}", self.internal_counter); - let loc = value.loc.clone(); - self.context - .last_mut() - .unwrap() - .statements - .push(JsStatement::new( - JsStatementData::Declaration { - name: name.clone(), - init: value, - is_constant: false, - }, - loc, - None, - )); - self.internal_counter += 1; - JsExpr::new(JsExprData::Identifier(name), Loc::EMPTY, None) - } +use ast::Module; - fn push(self: &mut Self, statement: JsStatement) { - self.context.last_mut().unwrap().statements.push(statement); - } +pub use convert_valid_expr::*; +pub use hoist::*; +pub use is_valid_js_expression::*; +pub use js_ast::*; +pub use util::*; - // fn peek(self: &Self) -> &JsStatement { - // self.context.last().unwrap().statements.last().unwrap() - // } +pub fn blight_to_js(blight: Module) -> JsModule { + let mut root_context = HoistContext::new(); - fn declare_variable(self: &mut Self, name: String) -> String { - let context = self.context.last_mut().unwrap(); - if let Some(value) = context.variable_map.get_mut(name.as_str()) { - *value += 1; - format!("{}${}${}", name, value, context.block) - } else { - context.variable_map.insert(name.to_string(), 0); - format!("{}$0${}", name, context.block) - } + for statement in blight.statements { + if let None = root_context.process_statement(statement.to_statement()) { + unreachable!("top level return is disallowed by the parser") + }; } - fn get_identifier(self: &Self, name: String) -> String { - for context in self.context.iter().rev() { - if let Some(value) = context.variable_map.get(name.as_str()) { - return format!("{}${}${}", name, value, context.block); - } - } - panic!("Variable {} not found", name); + JsModule { + statements: root_context.statements, } } - -fn get_block_handle(b: &ast::BlockWithLoc, ctx: &mut TranslateContext) -> JsExpr { - // :) - if b.is_valid_js_expression() { - return JsExpr::comma_expr_from_blight_block(b).unwrap(); - } - - ctx.push_context(); - for (i, statement) in b.block.statements.iter().enumerate() { - if i == b.block.statements.len() - 1 { - break; - } else { - translate_statement(statement, ctx); - } - } - let result = match b.block.statements.last().unwrap() { - ast::Statement::Expr(expr) => get_expression_handle(expr, ctx), - stmt => { - translate_statement(stmt, ctx); - JsExpr::new(JsExprData::Null, Loc::EMPTY, None) - } - }; - let value = ctx.pop_context(); - value.iter().for_each(|x| ctx.push(x.clone())); - result -} - -// basically, expressions are really hard to translate -// in blight, everything is an expression -// in js, very few things are expressions -// here, we recursively expand out expressions into statements -// we then store the result of all the expressions in a variable -// and then return the name of that variable -fn get_expression_handle(ast: &Expr, ctx: &mut TranslateContext) -> JsExpr { - match ast.data.clone() { - ExprData::Literal(Literal::String(value)) => JsExpr::new( - JsExprData::String(value), - ast.loc.clone(), - ast.comment.clone(), - ), - ExprData::Literal(Literal::Number(value)) => JsExpr::new( - JsExprData::Number(value), - ast.loc.clone(), - ast.comment.clone(), - ), - ExprData::Literal(Literal::Void) => { - JsExpr::new(JsExprData::Null, ast.loc.clone(), ast.comment.clone()) - } - ExprData::Literal(Literal::Bool(b)) => { - JsExpr::new(JsExprData::Bool(b), ast.loc.clone(), ast.comment.clone()) - } - - ExprData::Identifier(value) => JsExpr::new( - JsExprData::Identifier(ctx.get_identifier(value.clone())), - ast.loc.clone(), - ast.comment.clone(), - ), - ExprData::Call { base, arguments } => { - let mut handles = Vec::new(); - for argument in arguments { - handles.push(get_expression_handle(&argument, ctx)); - } - let base = get_expression_handle(&base, ctx); - ctx.push_internal_variable(JsExpr::new( - JsExprData::Call { - base: Box::new(base), - arguments: handles, - }, - ast.loc.clone(), - ast.comment.clone(), - )) - } - ExprData::Builtin { id, arguments } => match id { - ast::BuiltinId::Raw => { - if arguments.len() != 1 { - panic!("@raw expects exactly one argument"); - } - match arguments.get(0).unwrap() { - Expr { - data: ExprData::Literal(Literal::String(value)), - .. - } => JsExpr::new( - JsExprData::Raw(value.clone()), - ast.loc.clone(), - ast.comment.clone(), - ), - _ => panic!("@raw expects a string literal"), - } - } - }, - ExprData::Binary { op, left, right } => { - let left_handle = get_expression_handle(&left, ctx); - let right_handle = get_expression_handle(&right, ctx); - JsExpr::new( - JsExprData::Binary { - operator: op.clone(), - left: Box::new(left_handle), - right: Box::new(right_handle), - }, - ast.loc.clone(), - ast.comment.clone(), - ) - } - - ExprData::Unary { op, value } => { - let value_handle = get_expression_handle(&value, ctx); - JsExpr::new( - JsExprData::Unary { - operator: op.clone(), - value: Box::new(value_handle), - }, - ast.loc.clone(), - ast.comment.clone(), - ) - } - - ExprData::Field { base, field } => { - let base = Box::new(get_expression_handle(&base, ctx)); - - match field.data { - ExprData::Literal(Literal::String(name)) => JsExpr::new( - JsExprData::Dot { base, name }, - ast.loc.clone(), - ast.comment.clone(), - ), - _ => JsExpr::new( - JsExprData::Index { - base, - index: Box::new(get_expression_handle(&field, ctx)), - }, - ast.loc.clone(), - ast.comment.clone(), - ), - } - } - - ExprData::Block(block) => get_block_handle( - &BlockWithLoc { - block, - loc: ast.loc.clone(), - }, - ctx, - ), - - ExprData::If(expr) => { - let handle = get_expression_handle(&expr.condition, ctx); - - let yes = get_block_handle(&expr.yes, ctx); - let no = match expr.no { - Some(block) => Some(get_expression_handle(&block, ctx)), - None => None, - }; - - // ctx.push(JsStatement::new( - // JsStatementData::If { - // condition: handle, - // yes, - // no, - // }, - // ast.loc, - // ast.comment, - // )); - - // panic!() - handle - } - ExprData::Fn { - name, - params, - return_type, - block, - } => todo!(), - ExprData::Return(_) => todo!(), - } -} - -fn translate_statement(ast: &Statement, ctx: &mut TranslateContext) { - match ast { - Statement::Decl(decl) => { - let handle = get_expression_handle(&decl.init, ctx); - let rename = ctx.declare_variable(decl.name.clone()); - ctx.push(JsStatement::new( - JsStatementData::Declaration { - name: rename, - init: handle, - is_constant: decl.is_constant.clone(), - }, - decl.loc.clone(), - None, - )); - } - Statement::Expr(expr) => { - let h = get_expression_handle(expr, ctx); - ctx.push(JsStatement::new( - JsStatementData::Expression(h), - expr.loc.clone(), - None, - )) - } - Statement::Comment(_) => {} // Ast::Function { - // name, - // arguments, - // return_type: _, - // block, - // } => { - // let rename = ctx.declare_variable(name.clone()); - // let arguments = arguments - // .iter() - // .map(|x| ctx.declare_variable(x.name.clone())) - // .collect(); - - // ctx.push_context(); - // for (i, statement) in block.value.iter().enumerate() { - // if block.implicit_return && i == block.value.len() - 1 { - // let handle = get_expression_handle(statement, ctx); - // ctx.push(JsStatement::Return(handle)); - // } else { - // translate_statement(statement, ctx); - // } - // } - // let value = ctx.pop_context(); - - // ctx.push(JsStatement::Function { - // name: rename, - // arguments, - // value, - // }); - // } - // Ast::Public(value) => { - // translate_statement(value, ctx); - // // I'm sorry, I just don't know how to get the original name properly... my bad - // match ctx.peek() { - // JsStatement::Function { name, .. } | JsStatement::Declaration { name, .. } => { - // let original_name = name.split("$").next().unwrap().to_string(); - // ctx.push(JsStatement::Export(Box::new(JsStatement::Declaration { - // name: original_name, - // value: JsExpr::Identifier(name.clone()), - // is_constant: true, - // }))); - // } - // _ => { - // panic!("Cannot make non-function public"); - // } - // } - // } - // Ast::Return(value) => { - // let handle = get_expression_handle(value, ctx); - // ctx.push(JsStatement::Return(handle)); - // } - // Ast::Block(block) => { - // ctx.push_context(); - // for (i, statement) in block.value.iter().enumerate() { - // if block.implicit_return && i == block.value.len() - 1 { - // let handle = get_expression_handle(statement, ctx); - // ctx.push(JsStatement::Return(handle)); - // } else { - // translate_statement(statement, ctx); - // } - // } - // let value = ctx.pop_context(); - // value.iter().for_each(|x| ctx.push(x.clone())); - // } - - // Ast::FunctionCall { name, arguments } => { - // let mut handles = Vec::new(); - // for argument in arguments { - // handles.push(get_expression_handle(argument, ctx)); - // } - // match *name.clone() { - // Ast::Identifier(name) => { - // ctx.push(JsStatement::Expression(JsExpr::Call { - // name: Box::new(JsExpr::Identifier(ctx.get_identifier(name.clone()))), - // arguments: handles, - // })); - // } - // Ast::BuiltIn(name) => match name.as_str() { - // "@raw" => { - // if handles.len() != 1 { - // panic!("@raw expects exactly one argument"); - // } - // match handles.get(0).unwrap() { - // JsExpr::String(value) => { - // ctx.push(JsStatement::Expression(JsExpr::Call { - // name: Box::new(JsExpr::Raw(value.clone())), - // arguments: handles, - // })); - // } - // _ => panic!("@raw expects a string argument"), - // } - // } - // _ => panic!("Unknown built-in function {}", name), - // }, - // _ => panic!("Function name must be an identifier"), - // } - // } - // Ast::Comment(_) | Ast::DocComment(_) => {} - // _ => { - // panic!("unimplemented: {:?}", ast); - // } - } -} - -pub fn translate(module: &Module) -> Result> { - let mut ctx = TranslateContext::new(); - - for ast in module.statements.iter() { - translate_statement(&ast.clone().to_statement(), &mut ctx); - } - - Ok(ctx.pop_context()) -} diff --git a/crates/js_ast/util.rs b/crates/js_ast/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..234ee189e91921b0e25bcc7a1e717ca485459d4c --- /dev/null +++ b/crates/js_ast/util.rs @@ -0,0 +1,17 @@ +/// TODO(@paperdave): handle the rest of the identifiers +pub fn is_js_identifier(s: &str) -> bool { + if s.is_empty() { + return false; + } + let mut chars = s.chars(); + let first = chars.next().unwrap(); + if !first.is_ascii_alphabetic() && first != '_' { + return false; + } + for c in chars { + if !c.is_ascii_alphanumeric() && c != '_' { + return false; + } + } + true +} diff --git a/crates/js_printer/Cargo.toml b/crates/js_printer/Cargo.toml index ec58b9490b3338b6f12f18906971afbdadb9f4ca..785143495163bac04e1b4dee49fe808c70aa7d15 100644 --- a/crates/js_printer/Cargo.toml +++ b/crates/js_printer/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [lib] +path = "./lib.rs" [dependencies] miette.workspace = true diff --git a/crates/js_printer/lib.rs b/crates/js_printer/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..4ea0d8c03ae395ff34e563a248dbeceaefce9b55 --- /dev/null +++ b/crates/js_printer/lib.rs @@ -0,0 +1,358 @@ +mod printer_tests; + +use js_ast::*; +use std::io::{Result, Write}; + +pub trait Print { + fn print(&self, writer: &mut dyn Write) -> Result<()>; + + fn print_to_string(&self) -> String { + let mut buffer = Vec::new(); + self.print(&mut buffer).unwrap(); + String::from_utf8(buffer).unwrap() + } +} + +// the printer: + +fn print_module_clause_identifier(writer: &mut dyn Write, export_name: &str) -> Result<()> { + // TODO(@paperdave): Escape invalid identifiers + if js_ast::is_js_identifier(export_name) { + write!(writer, "{}", export_name) + } else { + todo!("call JSON.stringify on identifier") + } +} + +fn print_js_string(writer: &mut dyn Write, string: &str) -> Result<()> { + write!(writer, "\"")?; + // TODO(@paperdave): Instead of writing each character, this should write + // chunks between the escapes instead of calling write! on each character individually. + for c in string.chars() { + match c { + '\n' => write!(writer, "\\n")?, + '\r' => write!(writer, "\\r")?, + '\t' => write!(writer, "\\t")?, + '"' => write!(writer, "\\\"")?, + '\\' => write!(writer, "\\\\")?, + '\u{0008}' => write!(writer, "\\b")?, + '\u{000C}' => write!(writer, "\\f")?, + '\u{0000}'..='\u{001F}' => write!(writer, "\\u{:04x}", c as u32)?, + // TODO(@paperdave): I think there are some larger code points that need to be escaped? + _ => write!(writer, "{}", c)?, + } + } + write!(writer, "\"") +} + +impl Print for JsStatement { + fn print(&self, w: &mut dyn Write) -> Result<()> { + match &self.data { + js_ast::JsStatementData::Expression(expr) => { + expr.print(w)?; + write!(w, ";") + } + js_ast::JsStatementData::Return(expr) => { + write!(w, "return ")?; + expr.print(w) + } + js_ast::JsStatementData::Export(export) => export.print(w), + js_ast::JsStatementData::Declaration(decl) => decl.print(w), + js_ast::JsStatementData::Function(function) => function.print(w), + js_ast::JsStatementData::If(js_if) => js_if.print(w), + } + } +} + +impl Print for JsExport { + fn print(&self, w: &mut dyn Write) -> Result<()> { + match self { + JsExport::Named(exports) => { + write!(w, "export{{")?; + for (i, export) in exports.iter().enumerate() { + if i > 0 { + write!(w, ",")?; + } + export.print(w)?; + } + Ok(()) + } + JsExport::Function(function) => { + write!(w, "export ")?; + function.print(w) + } + JsExport::Declaration(declaration) => { + write!(w, "export ")?; + declaration.print(w) + } + JsExport::Default(expr) => { + write!(w, "export default ")?; + expr.print(w) + } + } + } +} + +impl Print for JSNamedExport { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + if self.export_name == self.identifier { + write!(writer, "{}", self.identifier) + } else { + write!(writer, "{} as ", self.identifier)?; + print_module_clause_identifier(writer, &self.export_name) + } + } +} + +impl Print for JSDeclaration { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!( + writer, + "{} {}", + if self.is_constant { "const" } else { "let" }, + self.name, + )?; + if let Some(init) = &self.init { + write!(writer, " = ")?; + init.print(writer)?; + } + write!(writer, ";")?; + Ok(()) + } +} + +impl Print for JsExpr { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + self.data.print(writer) + } +} + +impl Print for JsExprData { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match &self { + JsExprData::Raw(data) => write!(writer, "{}", data), + JsExprData::Number(number) => write!(writer, "{}", number), + JsExprData::BigInt(bigint) => write!(writer, "{}", bigint), + JsExprData::String(string) => print_js_string(writer, string.as_str()), + JsExprData::Bool(bool) => write!(writer, "{}", bool), + JsExprData::Null => write!(writer, "null"), + JsExprData::Undefined => write!(writer, "undefined"), + JsExprData::Identifier(str) => write!(writer, "{}", str), + JsExprData::Array(items) => { + write!(writer, "[")?; + for (i, item) in items.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + item.print(writer)?; + } + write!(writer, "]") + } + JsExprData::Object(items) => { + write!(writer, "{{")?; + for (i, kv) in items.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + + kv.print(writer)?; + } + write!(writer, "}}") + } + JsExprData::Unary { op, value } => { + write!(writer, "{}", op.to_str())?; + value.print(writer) + } + JsExprData::Binary { op, left, right } => { + // TODO: figure out when we dont need to wrap + let wrap = true; + if wrap { + write!(writer, "(")?; + } + left.print(writer)?; + write!(writer, "{}", op.to_str())?; + right.print(writer)?; + if wrap { + write!(writer, ")")?; + } + Ok(()) + } + JsExprData::Dot { base, name } => { + base.print(writer)?; + write!(writer, ".{}", name) + } + JsExprData::Index { base, index } => { + base.print(writer)?; + write!(writer, "[")?; + index.print(writer)?; + write!(writer, "]") + } + JsExprData::Call { base, arguments } => { + base.print(writer)?; + write!(writer, "(")?; + for (i, argument) in arguments.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + argument.print(writer)?; + } + write!(writer, ")") + } + JsExprData::Function { body, name, params } => { + write!(writer, "function ")?; + if let Some(name) = name { + write!(writer, "{}", name)?; + } + write!(writer, "(")?; + for (i, param) in params.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + write!(writer, "{}", param)?; + } + write!(writer, ")")?; + body.print(writer) + } + JsExprData::ArrowFunction { body, params } => { + let print_parens = params.len() != 1; + if print_parens { + write!(writer, "(")?; + } + for (i, param) in params.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + write!(writer, "{}", param)?; + } + if print_parens { + write!(writer, ")")?; + } + write!(writer, "=>")?; + body.print(writer) + } + JsExprData::Ternary { condition, yes, no } => { + condition.print(writer)?; + write!(writer, "?")?; + yes.print(writer)?; + write!(writer, ":")?; + if let Some(no) = no { + no.print(writer)?; + } else { + write!(writer, "null")?; + } + Ok(()) + } + JsExprData::Comma { items } => { + for (i, item) in items.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + item.print(writer)?; + } + Ok(()) + } + JsExprData::Void(expr) => { + write!(writer, "void ")?; + expr.print(writer) + } + } + } +} + +impl Print for JSFunctionDeclaration { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!(writer, "function {}", self.name)?; + write!(writer, "(")?; + for (i, param) in self.params.iter().enumerate() { + if i > 0 { + write!(writer, ",")?; + } + write!(writer, "{}", param)?; + } + write!(writer, ")")?; + self.block.print(writer) + } +} + +impl Print for JsBlock { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!(writer, "{{")?; + for statement in self.statements.iter() { + statement.print(writer)?; + } + write!(writer, "}}") + } +} + +impl Print for JsIf { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + write!(writer, "if(")?; + self.condition.print(writer)?; + write!(writer, ")")?; + self.yes.print(writer)?; + if let Some(no) = &self.no { + no.print(writer)?; + } + Ok(()) + } +} + +impl Print for JsElse { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match self { + JsElse::If(js_if) => { + write!(writer, "else ")?; + js_if.print(writer) + } + JsElse::Block(block) => { + write!(writer, "else")?; + block.print(writer) + } + JsElse::Expr(expr) => { + write!(writer, "else ")?; + expr.print(writer) + } + } + } +} + +impl Print for JsKV { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match &self.key.data { + JsExprData::String(string) => { + if is_js_identifier(string) { + write!(writer, "{}", string)?; + } else { + print_js_string(writer, string.as_str())?; + } + } + _ => { + write!(writer, "[")?; + self.key.print(writer)?; + write!(writer, "]")?; + } + } + write!(writer, ":")?; + self.key.print(writer)?; + write!(writer, ":")?; + self.value.print(writer) + } +} + +impl Print for JsModule { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + for statement in self.statements.iter() { + statement.print(writer)?; + } + Ok(()) + } +} + +impl Print for JsArrowBody { + fn print(&self, writer: &mut dyn Write) -> Result<()> { + match self { + JsArrowBody::Block(block) => block.print(writer), + JsArrowBody::Expr(expr) => expr.print(writer), + } + } +} diff --git a/crates/js_printer/printer_tests.rs b/crates/js_printer/printer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..d69ab247dffe40609ed2892eca79fedb3c4f1a3a --- /dev/null +++ b/crates/js_printer/printer_tests.rs @@ -0,0 +1,53 @@ +#![cfg(test)] +use crate::Print; + +use js_ast::JsExprData as E; +use num::{bigint::Sign, BigInt}; + +macro_rules! assert_printed { + ($value:expr, $expected:expr) => { + assert_eq!($value.print_to_string(), $expected); + }; +} + +#[test] +fn number() { + assert_printed!(E::Number(1.0), "1"); + assert_printed!(E::Number(1.5), "1.5"); + assert_printed!(E::Number(1.5e10), "15000000000"); + assert_printed!(E::Number(1.5e-10), "0.00000000015"); + assert_printed!(E::Number(-1.0), "-1"); + assert_printed!(E::Number(-1.5), "-1.5"); + assert_printed!(E::Number(0f64), "0"); +} + +#[test] +fn bigint() { + assert_printed!(E::BigInt(BigInt::new(Sign::Plus, vec![1])), "1n"); + assert_printed!( + E::BigInt(BigInt::new(Sign::Plus, vec![10000000])), + "10000000n" + ); + assert_printed!( + E::BigInt(BigInt::new(Sign::Minus, vec![10000000])), + "-10000000n" + ); +} + +#[test] +fn boolean() { + assert_printed!(E::Bool(true), "true"); + assert_printed!(E::Bool(false), "false"); +} + +#[test] +fn string() { + assert_printed!(E::str(""), "\"\""); + assert_printed!(E::str("hello"), "\"hello\""); + assert_printed!(E::str("hello\nworld"), "\"hello\\nworld\""); +} + +#[test] +fn identifier() { + assert_printed!(E::str_id("hello"), "hello"); +} diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs index 26122cadfe2a55e259a05322fb103b3a5f64e9c9..a27468c1c49ea1ee5ce13fd99450b645fe3fdd16 100644 --- a/crates/js_printer/src/print_expression.rs +++ b/crates/js_printer/src/print_expression.rs @@ -46,7 +46,7 @@ pub fn print_expression(expr: &JsExpr) -> String { .join(",") ), JsExprData::Binary { - operator, + op: operator, left, right, } => format!( @@ -81,7 +81,10 @@ pub fn print_expression(expr: &JsExpr) -> String { ), JsExprData::Null => todo!(), JsExprData::Undefined => todo!(), - JsExprData::Unary { operator, value } => todo!(), + JsExprData::Unary { + op: operator, + value, + } => todo!(), JsExprData::Dot { base, name } => todo!(), JsExprData::Index { base, index } => todo!(), JsExprData::Function { @@ -89,9 +92,10 @@ pub fn print_expression(expr: &JsExpr) -> String { params, block, } => todo!(), - JsExprData::If { condition, yes, no } => todo!(), + JsExprData::Ternary { condition, yes, no } => todo!(), JsExprData::ArrowFunction { params, block } => todo!(), JsExprData::Comma { items } => todo!(), + JsExprData::Void(_) => todo!(), } } @@ -191,7 +195,7 @@ mod tests { fn operation() { assert_eq!( print_expression(&JsExprData::Binary { - operator: AnyOperator::Add, + op: AnyOperator::Add, left: Box::new(JsExprData::Number(1.0)), right: Box::new(JsExprData::Number(2.0)), }), @@ -199,7 +203,7 @@ mod tests { ); assert_eq!( print_expression(&JsExprData::Binary { - operator: AnyOperator::Sub, + op: AnyOperator::Sub, left: Box::new(JsExprData::Number(1.0)), right: Box::new(JsExprData::Number(2.0)), }), diff --git a/crates/js_printer/src/print_statement.rs b/crates/js_printer/src/print_statement.rs index 57fdb827b637716f5c05b2065c39e621eff95d62..c7d78693136d06b23070c35e9b55ab1dfa70e90f 100644 --- a/crates/js_printer/src/print_statement.rs +++ b/crates/js_printer/src/print_statement.rs @@ -1,4 +1,4 @@ -use js_ast::{JsStatement, JsStatementData}; +use js_ast::{If, JSDeclaration, JSFunctionDeclaration, JsStatement, JsStatementData}; use crate::print_expression::print_expression; @@ -8,11 +8,11 @@ pub fn print_statement(statement: &JsStatement) -> String { // JsStatementData::Export(value) => format!("export {}", print_statement(value)), JsStatementData::Export(value) => todo!(), JsStatementData::Return(value) => format!("return {};", print_expression(&value)), - JsStatementData::Declaration { + JsStatementData::Declaration(Declaration { name, init, is_constant, - } => { + }) => { format!( "{} {}={};", match is_constant { @@ -23,11 +23,11 @@ pub fn print_statement(statement: &JsStatement) -> String { print_expression(&init) ) } - JsStatementData::Function { + JsStatementData::Function(JSFunctionDeclaration { name, block, params, - } => format!( + }) => format!( "function {}({}){{{}}}", name, params.join(","), @@ -38,7 +38,7 @@ pub fn print_statement(statement: &JsStatement) -> String { .collect::>() .join("") ), - JsStatementData::If { condition, yes, no } => { + JsStatementData::If(JsIf { condition, yes, no }) => { let else_value = match no { Some(value) => format!( "else{{{}}}", diff --git a/crates/parser/parse_block.rs b/crates/parser/parse_block.rs index 4c6dc77d314740afd0a31a442a85885d18403e89..3ab34ac6cc6474d351a0a944fabaa17ad2bc2775 100644 --- a/crates/parser/parse_block.rs +++ b/crates/parser/parse_block.rs @@ -15,7 +15,11 @@ pub fn parse_block(p: &mut Parser) -> Result { let was_top_level = p.is_top_level; p.is_top_level = false; - loop { + 'outer: loop { + let is_statement_like_expression = p + .peek() + .map_or(false, |x| x.is_start_of_statement_like_expression()); + let statement = match parse_statement(p) { Ok(statement) => statement, Err(err) => { @@ -25,8 +29,9 @@ pub fn parse_block(p: &mut Parser) -> Result { }; statements.push(statement.clone()); - match p.next() { + match p.peek() { Some(Token::Semicolon) => loop { + p.next(); match p.peek() { Some(Token::ClosedBrace) => { p.next(); @@ -36,14 +41,18 @@ pub fn parse_block(p: &mut Parser) -> Result { has_implicit_return: false, }); } - _ => continue, + _ => continue 'outer, } }, Some(Token::ClosedBrace) => { - if statement.requires_semicolon() { + p.next(); + if !statement.is_expression() + && !statement.is_comment() + && is_statement_like_expression + { p.is_top_level = was_top_level; return Err(p - .error("Expected semicolon after this statement".to_string()) + .error("Expected semicolon or '}' after statement".to_string()) .into()); } @@ -54,10 +63,15 @@ pub fn parse_block(p: &mut Parser) -> Result { }); } _ => { - p.is_top_level = was_top_level; - return Err(p - .error("Expected semicolon or '}' after statement".to_string()) - .into()); + if !statement.is_expression() + && !statement.is_comment() + && is_statement_like_expression + { + p.is_top_level = was_top_level; + return Err(p + .error("Expected semicolon or '}' after statement".to_string()) + .into()); + } } }; } diff --git a/crates/parser/parse_call_arguments.rs b/crates/parser/parse_call_arguments.rs index afa16cdcfef28ab3c3dd309f6d2036b5403120bb..4bdda948b2aef6eca565194538d7c4aeac397753 100644 --- a/crates/parser/parse_call_arguments.rs +++ b/crates/parser/parse_call_arguments.rs @@ -8,6 +8,11 @@ use crate::{parse_expr::parse_expr, Parser}; pub fn parse_call_arguments(p: &mut Parser) -> Result> { let mut args = Vec::new(); + if let Some(Token::ClosedParen) = p.peek() { + _ = p.next(); + return Ok(args); + } + loop { let arg = parse_expr(p)?; args.push(arg); diff --git a/crates/parser/parse_expr.rs b/crates/parser/parse_expr.rs index 6ca753167cdabc91ac85e5845d4c1fb356ef2748..693053995ac365681335bfdabb3ebdd0f0a96b3b 100644 --- a/crates/parser/parse_expr.rs +++ b/crates/parser/parse_expr.rs @@ -1,4 +1,5 @@ -use ast::{get_binary_op_level, BinaryOperator, BinaryOperatorLevel, Expr, ExprData}; +use ast::Associativity::*; +use ast::{BinaryOperator, Expr, ExprData, OperatorLevel}; use miette::Result; use crate::parse_expr_basic::parse_expr_basic; @@ -15,14 +16,27 @@ pub fn parse_expr(p: &mut Parser) -> Result { // Parse operators loop { if let Some(token) = p.peek() { - if let Some(level) = get_binary_op_level(&token) { + if let Some(level) = BinaryOperator::from_token(&token).map(|o| o.level()) { _ = p.next(); - expr = parse_expr_binary( - p, - BinaryOperator::from_token(&token).unwrap(), - expr.clone(), - level, - )?; + match level.associativity() { + RightToLeft => { + expr = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + expr, + level, + )?; + } + LeftToRight => { + // TODO(@paperdave): TODO + expr = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + expr, + level, + )?; + } + } continue; } } @@ -37,22 +51,35 @@ fn parse_expr_binary( p: &mut Parser, op: BinaryOperator, left: Expr, - level: BinaryOperatorLevel, + level: OperatorLevel, ) -> Result { let loc = p.loc(); let mut right = parse_expr_basic(p)?; loop { if let Some(token) = p.peek() { - if let Some(next_level) = get_binary_op_level(&token) { - if next_level <= level { + if let Some(new_level) = BinaryOperator::from_token(&token).map(|o| o.level()) { + if new_level <= level { _ = p.next(); - right = parse_expr_binary( - p, - BinaryOperator::from_token(&token).unwrap(), - right, - next_level, - )?; + match new_level.associativity() { + RightToLeft => { + right = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + right, + new_level, + )?; + } + LeftToRight => { + // TODO(@paperdave): TODO + right = parse_expr_binary( + p, + BinaryOperator::from_token(&token).unwrap(), + right, + new_level, + )?; + } + } continue; } } diff --git a/crates/parser/parse_expr_if.rs b/crates/parser/parse_expr_if.rs index 0efc61ee5e3b52a9d221fca3f692d782073b6c5e..5af7b44fd7fbf2383a3c3d4ed31d243168859558 100644 --- a/crates/parser/parse_expr_if.rs +++ b/crates/parser/parse_expr_if.rs @@ -1,4 +1,4 @@ -use ast::{BlockWithLoc, Expr, ExprData, If}; +use ast::{BlockOrIf, BlockWithLoc, If}; use miette::Result; use tokenizer::Token; @@ -48,21 +48,17 @@ pub fn parse_expr_if(p: &mut Parser) -> Result { match p.peek() { Some(Token::OpenBrace) => { let loc = p.loc(); - let comments = p.take_comments(); - Some(Box::new(Expr::new( - ExprData::Block(parse_block(p)?), + // let comments = p.take_comments(); + Some(BlockOrIf::Block(BlockWithLoc { + block: parse_block(p)?, loc, - comments, - ))) + })) } Some(Token::If) => { - let loc = p.loc(); - let comments = p.take_comments(); - Some(Box::new(Expr::new( - ExprData::If(parse_expr_if(p)?), - loc, - comments, - ))) + // TODO(@paperdave): Save location for the if + // let loc = p.loc(); + // let comments = p.take_comments(); + Some(BlockOrIf::If(Box::new(parse_expr_if(p)?))) } _ => return Err(p.error("Expected a block after else".to_string()).into()), } diff --git a/crates/parser/parse_fn_declaration.rs b/crates/parser/parse_fn_declaration.rs index 3b7363a36204bcd95ac287e5d7fb854cc2f06f78..32b4e3cff41688b8ee04d9c6e2cba54d01c64291 100644 --- a/crates/parser/parse_fn_declaration.rs +++ b/crates/parser/parse_fn_declaration.rs @@ -52,7 +52,10 @@ pub fn parse_fn_declaration( break; } } - Some(Token::ClosedParen) => break, + Some(Token::ClosedParen) => { + p.next(); + break; + } _ => { return Err(p .error("Expected ',' or ')' after identifier".to_string()) @@ -108,8 +111,12 @@ pub fn parse_fn_declaration( loc: loc, }) } else { + let actually = p.peek().map_or("EOF".to_string(), |x| x.to_string()); return Err(p - .error("Expected a block to follow function declaration".to_string()) + .error(format!( + "Expected a block to follow function declaration, but got {}", + actually, + )) .into()); } } diff --git a/crates/parser/parse_module.rs b/crates/parser/parse_module.rs index 9d3f8fbe8e2e9f2ce1138a72a8b1ece27dda0011..97aef8e42d12836cc0d7e6c5ecde30167e67c57f 100644 --- a/crates/parser/parse_module.rs +++ b/crates/parser/parse_module.rs @@ -11,6 +11,10 @@ pub fn parse_module(p: &mut Parser) -> Result { let file_doc_comment = maybe_read_doc_comment(p); loop { + let is_statement_like_expression = p + .peek() + .map_or(false, |x| x.is_start_of_statement_like_expression()); + let statement = parse_top_level_statement(p)?; match p.peek() { @@ -20,7 +24,7 @@ pub fn parse_module(p: &mut Parser) -> Result { } } _ => { - if statement.requires_semicolon() { + if !statement.is_comment() && is_statement_like_expression { return Err(p .error("Expected semicolon after statement".to_string()) .into()); diff --git a/crates/sema/Cargo.toml b/crates/sema/Cargo.toml index 151d91227fdbfa2b5b2120a5e072e56796233326..d9a6f7ea10286ec4418afd61c1538671280ce8b4 100644 --- a/crates/sema/Cargo.toml +++ b/crates/sema/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [lib] +path = "./sema.rs" [dependencies] miette.workspace = true diff --git a/crates/sema/src/ast_type_coersion.rs b/crates/sema/ast_type_coersion.rs similarity index 100% rename from crates/sema/src/ast_type_coersion.rs rename to crates/sema/ast_type_coersion.rs diff --git a/crates/sema/src/lib.rs b/crates/sema/sema.rs similarity index 95% rename from crates/sema/src/lib.rs rename to crates/sema/sema.rs index c7a930d635ec3bd0f58f49a10c38bc95b5d81d9e..899c5d9433436f4bf7f4f85d55a96d821cc88572 100644 --- a/crates/sema/src/lib.rs +++ b/crates/sema/sema.rs @@ -1,9 +1,11 @@ -// use ast::{ Module, Type}; -// use miette::Result; +use ast::Module; +use miette::Result; -// mod ast_type_coersion; +pub fn analyze_module(_block: &mut Module) -> Result<()> { + // + Ok(()) +} -// pub fn check_module(block: &Module) -> Result<()> { // for statement in &block.statements { // check_expression(statement)?; // } diff --git a/crates/tokenizer/Cargo.toml b/crates/tokenizer/Cargo.toml index 50effc8036b4f1705b14ec46fd16ad36622229e7..6e881084601ce5e033c79ce16949e3dc732880a2 100644 --- a/crates/tokenizer/Cargo.toml +++ b/crates/tokenizer/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [lib] +path="./tokenizer.rs" [dependencies] miette.workspace = true diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/tokenizer.rs similarity index 98% rename from crates/tokenizer/src/lib.rs rename to crates/tokenizer/tokenizer.rs index 2d906f20a2b75c81a5168c92c3d591314c8d2c18..b03a99236d2ecf981c8b866f060c73eaa9cdf97f 100644 --- a/crates/tokenizer/src/lib.rs +++ b/crates/tokenizer/tokenizer.rs @@ -76,6 +76,15 @@ pub enum Token { Dot, // . } +impl Token { + pub fn is_start_of_statement_like_expression(&self) -> bool { + match &self { + Token::If | Token::OpenBrace | Token::Fn => false, + _ => true, + } + } +} + impl std::fmt::Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let token = match self { -- 2.54.0 From 42c364acbbbd54bf8afefa0ff82034928e83f06e Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 22 Jan 2024 02:14:22 -0800 Subject: [PATCH 7/8] fix some things --- crates/ast/ast.rs | 28 +++------ crates/js_ast/hoist.rs | 98 ++++++++++++++++++----------- crates/js_ast/js_ast.rs | 7 --- crates/parser/parse_block.rs | 10 +-- crates/parser/parse_module.rs | 2 +- crates/sema/ast_type_coersion.rs | 102 +++++++++++++++---------------- crates/sema/sema_scope.rs | 0 crates/tokenizer/tokenizer.rs | 2 +- 8 files changed, 125 insertions(+), 124 deletions(-) create mode 100644 crates/sema/sema_scope.rs diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs index 46b9e6c60da841ff84f4af137d19f12dd3a34252..bdda2b34cd68a35b8c640bd1b18f1c6988ed6c5b 100644 --- a/crates/ast/ast.rs +++ b/crates/ast/ast.rs @@ -186,13 +186,6 @@ impl BuiltinId { } impl Statement { - pub fn is_expression(&self) -> bool { - match &self { - Statement::Expr(_) => true, - _ => false, - } - } - pub fn to_top_level(self) -> TopLevelStatement { match self { Statement::Expr(expr) => TopLevelStatement::Expr(expr), @@ -201,18 +194,22 @@ impl Statement { } } - pub fn is_comment(&self) -> bool { - match self { - Statement::Comment(_) => true, + /// This is stupid function im sorry + pub fn require_semicolon(&self, semicolon_hint: bool) -> bool { + match &self { + Statement::Expr(_) => !semicolon_hint, + Statement::Decl(_) => true, _ => false, } } } impl TopLevelStatement { - pub fn is_expression(&self) -> bool { + /// This is stupid function im sorry + pub fn require_semicolon(&self, earlier_hint: bool) -> bool { match &self { - TopLevelStatement::Expr(_) => true, + TopLevelStatement::Expr(_) => !earlier_hint, + TopLevelStatement::Decl(_) => true, _ => false, } } @@ -224,11 +221,4 @@ impl TopLevelStatement { TopLevelStatement::Comment(comment) => Statement::Comment(comment), } } - - pub fn is_comment(&self) -> bool { - match self { - TopLevelStatement::Comment(_) => true, - _ => false, - } - } } diff --git a/crates/js_ast/hoist.rs b/crates/js_ast/hoist.rs index 5351588aec2aae5653dcdebb524e2503df15aa9e..dfe289b9e0560009e5c5c475f686ecec097ddd0f 100644 --- a/crates/js_ast/hoist.rs +++ b/crates/js_ast/hoist.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::vec; use ast::{BinaryOperator, BlockOrIf, BlockWithLoc, Expr, Statement}; +use has_side_effects::HasSideEffects; use tokenizer::Loc; use crate::*; @@ -156,9 +157,7 @@ impl HoistContext { pub fn process_statement(&mut self, statement: Statement) -> Option<()> { if statement.is_valid_js_expression() { let result = convert_valid_statement(self, statement); - if !result.is_literal() { - self.add_statement(result); - } + self.add_statement(result); return Some(()); } @@ -189,13 +188,11 @@ impl HoistContext { Statement::Expr(expr) => { if let Some(expr) = self.hoist_expression_contents(expr) { let loc = expr.loc.clone(); - if !expr.is_literal() { - self.add_statement(JsStatement::new( - JsStatementData::Expression(expr), - loc, - None, - )) - } + self.add_statement(JsStatement::new( + JsStatementData::Expression(expr), + loc, + None, + )); Some(()) } else { None @@ -209,9 +206,9 @@ impl HoistContext { /// and returns an identifier expression for it. pub fn hoist_expression_to_temporary(&mut self, expr: Expr) -> Option { // TODO - // if !expr.has_side_effects() { - // return Some(self.hoist_expression_contents(expr)?); - // } + if !expr.has_side_effects() { + return Some(self.hoist_expression_contents(expr)?); + } let init = self.hoist_expression_contents(expr)?; @@ -334,7 +331,7 @@ impl HoistContext { let mut did_assign = false; let js_if = - self.hoist_js_if(data.yes, data.no, condition, &mut did_assign, &temporary)?; + self.hoist_js_if(data.yes, data.no, condition, &mut did_assign, &temporary); self.add_statement(JsStatement::new( JsStatementData::If(js_if), @@ -396,7 +393,7 @@ impl HoistContext { b: BlockWithLoc, temporary: &String, did_assign: &mut bool, - ) -> Option { + ) -> JsBlock { let loc = b.loc.clone(); // A non-block will become a block with a single expression: @@ -405,21 +402,22 @@ impl HoistContext { // We will also apply this transformation to blocks that are valid // expressions, because that is more concise. if b.is_valid_js_expression() { - return Some(JsBlock::new( + return JsBlock::new( vec![new_js_assignment_statement( convert_valid_block(self, b), temporary.clone(), loc.clone(), )], loc, - )); + ); } let mut child = self.new_child(); for statement in b.block.statements { if let None = child.process_statement(statement) { - return None; + tree_shake_statement_list(&mut child.statements); + return JsBlock::new(child.statements, loc); }; } @@ -449,7 +447,9 @@ impl HoistContext { _ => {} } - Some(JsBlock::new(child.statements, loc)) + tree_shake_statement_list(&mut child.statements); + + JsBlock::new(child.statements, loc) } pub fn hoist_block_to_arrow_fn_body(&mut self, b: BlockWithLoc) -> JsArrowBody { @@ -494,6 +494,8 @@ impl HoistContext { _ => {} } + tree_shake_statement_list(&mut child.statements); + JsArrowBody::Block(JsBlock::new(child.statements, loc)) } @@ -513,16 +515,9 @@ impl HoistContext { // to assign to the temporary variable. match self.last_statement().map(|x| &x.data) { Some(JsStatementData::Expression(_)) if b.block.has_implicit_return => { - let last = self.statements.pop().unwrap(); - match last { - JsStatement { - data: JsStatementData::Expression(expr), - .. - } => expr, - _ => { - self.statements.push(last); - JsExpr::new(JsExprData::Null, loc.clone(), None) - } + match self.statements.pop().unwrap().data { + JsStatementData::Expression(expr) => expr, + _ => unreachable!(), } } _ => JsExpr::new(JsExprData::Null, loc.clone(), None), @@ -536,15 +531,15 @@ impl HoistContext { condition: JsExpr, did_assign: &mut bool, temporary: &TempId, - ) -> Option { - let yes = self.hoist_block_to_js_block(yes, &temporary.get(&self), did_assign)?; + ) -> JsIf { + let yes = self.hoist_block_to_js_block(yes, &temporary.get(&self), did_assign); let no = match no { Some(BlockOrIf::Block(no)) => Some(JsElse::Block(self.hoist_block_to_js_block( no, &temporary.get(&self), did_assign, - )?)), + ))), Some(BlockOrIf::If(else_if)) => { // If the condition of this block has a side effect, we need to // emit a block so that we can assign to the temporary variable. @@ -558,11 +553,12 @@ impl HoistContext { condition, did_assign, temporary, - )?))) + )))) } else { let mut temp_block = self.new_child(); - let condition = - temp_block.hoist_expression_to_temporary(*else_if.condition.clone())?; + let condition = temp_block + .hoist_expression_to_temporary(*else_if.condition.clone()) + .unwrap(); temp_block.add_statement(JsStatement::new( JsStatementData::If(self.hoist_js_if( @@ -571,11 +567,13 @@ impl HoistContext { condition, did_assign, temporary, - )?), + )), Loc::EMPTY, None, )); + tree_shake_statement_list(&mut temp_block.statements); + Some(JsElse::Block(JsBlock::new( temp_block.statements, Loc::EMPTY, @@ -585,7 +583,7 @@ impl HoistContext { None => None, }; - Some(JsIf { condition, yes, no }) + JsIf { condition, yes, no } } } @@ -612,3 +610,29 @@ pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr None, ) } + +/// Remove +/// - Discarded literals +pub fn tree_shake_statement_list(list: &mut Vec) { + let mut set_i = 0; + for i in 0..list.len() { + let stmt = list.get(i).unwrap(); + let should_keep = match &stmt.data { + JsStatementData::Expression(expr) => !expr.is_literal(), + JsStatementData::Return(_) => true, + JsStatementData::Export(_) => true, + JsStatementData::Declaration(_) => true, + JsStatementData::Function(_) => true, + JsStatementData::If(_) => true, + // + }; + + if should_keep { + if set_i != i { + list.swap(i, set_i) + } + set_i += 1; + } + } + list.truncate(set_i) +} diff --git a/crates/js_ast/js_ast.rs b/crates/js_ast/js_ast.rs index 6b6c2520d16f139d927522898af6f4bdee367b30..a580ac8502ce38df702844a319c825cd030a03ad 100644 --- a/crates/js_ast/js_ast.rs +++ b/crates/js_ast/js_ast.rs @@ -176,13 +176,6 @@ impl JsStatement { pub fn new(data: JsStatementData, loc: Loc, comment: Option) -> Self { Self { data, loc, comment } } - - pub(crate) fn is_literal(&self) -> bool { - match &self.data { - JsStatementData::Expression(expr) => expr.is_literal(), - _ => false, - } - } } #[derive(Debug, PartialEq, Clone)] diff --git a/crates/parser/parse_block.rs b/crates/parser/parse_block.rs index 3ab34ac6cc6474d351a0a944fabaa17ad2bc2775..a9ad16ed402ac0509be65f1a95ce8a4bd76ec980 100644 --- a/crates/parser/parse_block.rs +++ b/crates/parser/parse_block.rs @@ -46,10 +46,7 @@ pub fn parse_block(p: &mut Parser) -> Result { }, Some(Token::ClosedBrace) => { p.next(); - if !statement.is_expression() - && !statement.is_comment() - && is_statement_like_expression - { + if statement.require_semicolon(is_statement_like_expression) { p.is_top_level = was_top_level; return Err(p .error("Expected semicolon or '}' after statement".to_string()) @@ -63,10 +60,7 @@ pub fn parse_block(p: &mut Parser) -> Result { }); } _ => { - if !statement.is_expression() - && !statement.is_comment() - && is_statement_like_expression - { + if statement.require_semicolon(is_statement_like_expression) { p.is_top_level = was_top_level; return Err(p .error("Expected semicolon or '}' after statement".to_string()) diff --git a/crates/parser/parse_module.rs b/crates/parser/parse_module.rs index 97aef8e42d12836cc0d7e6c5ecde30167e67c57f..f3521bd641adcb0063461c9deee8e92593eff303 100644 --- a/crates/parser/parse_module.rs +++ b/crates/parser/parse_module.rs @@ -24,7 +24,7 @@ pub fn parse_module(p: &mut Parser) -> Result { } } _ => { - if !statement.is_comment() && is_statement_like_expression { + if statement.require_semicolon(is_statement_like_expression) { return Err(p .error("Expected semicolon after statement".to_string()) .into()); diff --git a/crates/sema/ast_type_coersion.rs b/crates/sema/ast_type_coersion.rs index 411442e62cbed296913068bdf7011e02e4eb4601..2fab10b08f06b009feda2523d100641b89dc0d52 100644 --- a/crates/sema/ast_type_coersion.rs +++ b/crates/sema/ast_type_coersion.rs @@ -1,54 +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), +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!(), -// } -// } -// } + 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_scope.rs b/crates/sema/sema_scope.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/tokenizer/tokenizer.rs b/crates/tokenizer/tokenizer.rs index b03a99236d2ecf981c8b866f060c73eaa9cdf97f..aee421347f0e4a1a7a044043c4748acaadfe5360 100644 --- a/crates/tokenizer/tokenizer.rs +++ b/crates/tokenizer/tokenizer.rs @@ -79,7 +79,7 @@ pub enum Token { impl Token { pub fn is_start_of_statement_like_expression(&self) -> bool { match &self { - Token::If | Token::OpenBrace | Token::Fn => false, + Token::If | Token::OpenBrace => false, _ => true, } } -- 2.54.0 From cd0400a1a24ca1bd96fb883589958dabece01154 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 22 Jan 2024 04:42:33 -0800 Subject: [PATCH 8/8] organize the converter --- crates/ast/ast.rs | 45 +- .../js_ast/convert/block_convert_context.rs | 244 +++++++ crates/js_ast/convert/convert_valid_block.rs | 37 + crates/js_ast/convert/convert_valid_expr.rs | 115 ++++ crates/js_ast/convert/convert_valid_if.rs | 38 ++ .../js_ast/convert/convert_valid_statement.rs | 32 + crates/js_ast/convert/hoist_block_to_expr.rs | 78 +++ .../js_ast/convert/hoist_block_to_js_block.rs | 68 ++ crates/js_ast/convert/hoist_expr.rs | 132 ++++ crates/js_ast/convert/hoist_if.rs | 113 ++++ .../{ => convert}/is_valid_js_expression.rs | 6 + crates/js_ast/convert/mod.rs | 42 ++ crates/js_ast/convert/process_function.rs | 63 ++ crates/js_ast/convert/process_statement.rs | 95 +++ .../side_effects.rs} | 3 +- .../convert/tree_shake_statement_list.rs | 31 + crates/js_ast/convert_valid_expr.rs | 204 ------ crates/js_ast/hoist.rs | 638 ------------------ crates/js_ast/lib.rs | 25 +- crates/js_printer/lib.rs | 14 + crates/js_printer/src/lib.rs | 16 - crates/js_printer/src/print_expression.rs | 276 -------- crates/js_printer/src/print_statement.rs | 153 ----- crates/parser/parse_block.rs | 10 +- crates/parser/parse_declaration.rs | 6 +- crates/parser/parse_expr_basic.rs | 2 +- crates/parser/parse_fn_declaration.rs | 88 +-- crates/parser/parse_statement.rs | 20 +- crates/parser/parser.rs | 4 +- crates/tokenizer/tokenizer.rs | 17 +- examples/basics/stupid.blight | 2 + 31 files changed, 1223 insertions(+), 1394 deletions(-) create mode 100644 crates/js_ast/convert/block_convert_context.rs create mode 100644 crates/js_ast/convert/convert_valid_block.rs create mode 100644 crates/js_ast/convert/convert_valid_expr.rs create mode 100644 crates/js_ast/convert/convert_valid_if.rs create mode 100644 crates/js_ast/convert/convert_valid_statement.rs create mode 100644 crates/js_ast/convert/hoist_block_to_expr.rs create mode 100644 crates/js_ast/convert/hoist_block_to_js_block.rs create mode 100644 crates/js_ast/convert/hoist_expr.rs create mode 100644 crates/js_ast/convert/hoist_if.rs rename crates/js_ast/{ => convert}/is_valid_js_expression.rs (88%) create mode 100644 crates/js_ast/convert/mod.rs create mode 100644 crates/js_ast/convert/process_function.rs create mode 100644 crates/js_ast/convert/process_statement.rs rename crates/js_ast/{has_side_effects.rs => convert/side_effects.rs} (95%) create mode 100644 crates/js_ast/convert/tree_shake_statement_list.rs delete mode 100644 crates/js_ast/convert_valid_expr.rs delete mode 100644 crates/js_ast/hoist.rs delete mode 100644 crates/js_printer/src/lib.rs delete mode 100644 crates/js_printer/src/print_expression.rs delete mode 100644 crates/js_printer/src/print_statement.rs create mode 100644 examples/basics/stupid.blight diff --git a/crates/ast/ast.rs b/crates/ast/ast.rs index bdda2b34cd68a35b8c640bd1b18f1c6988ed6c5b..680c579aa79c8a3a0dd1cc19cc010ea8cd46cf7e 100644 --- a/crates/ast/ast.rs +++ b/crates/ast/ast.rs @@ -30,7 +30,7 @@ pub struct Param { #[derive(Debug, Clone)] -pub struct Decl { +pub struct VarDecl { pub name: String, pub data_type: Type, pub init: Expr, @@ -40,6 +40,17 @@ pub struct Decl { pub comment: Option, } +#[derive(Debug, Clone)] +pub struct FnDecl { + pub name: String, + pub return_type: Type, + pub params: Vec, + pub body: Expr, + pub is_pub: bool, + pub loc: Loc, + pub comment: Option, +} + #[derive(Debug, Clone)] pub struct Expr { pub data: ExprData, @@ -136,7 +147,7 @@ pub enum ExprData { name: String, params: Vec, return_type: Type, - block: Block, + body: Box, }, Call { base: Box, @@ -159,14 +170,16 @@ pub enum ExprData { #[derive(Debug, Clone)] pub enum Statement { Expr(Expr), - Decl(Decl), + VarDecl(VarDecl), + FnDecl(FnDecl), Comment(Comment), } #[derive(Debug, Clone)] pub enum TopLevelStatement { Expr(Expr), - Decl(Decl), + VarDecl(VarDecl), + FnDecl(FnDecl), Comment(Comment), } @@ -189,7 +202,8 @@ impl Statement { pub fn to_top_level(self) -> TopLevelStatement { match self { Statement::Expr(expr) => TopLevelStatement::Expr(expr), - Statement::Decl(decl) => TopLevelStatement::Decl(decl), + Statement::VarDecl(decl) => TopLevelStatement::VarDecl(decl), + Statement::FnDecl(decl) => TopLevelStatement::FnDecl(decl), Statement::Comment(comment) => TopLevelStatement::Comment(comment), } } @@ -198,7 +212,14 @@ impl Statement { pub fn require_semicolon(&self, semicolon_hint: bool) -> bool { match &self { Statement::Expr(_) => !semicolon_hint, - Statement::Decl(_) => true, + Statement::VarDecl(_) => true, + _ => false, + } + } + + pub fn is_expression(&self) -> bool { + match &self { + Statement::Expr(_) => true, _ => false, } } @@ -209,16 +230,18 @@ impl TopLevelStatement { pub fn require_semicolon(&self, earlier_hint: bool) -> bool { match &self { TopLevelStatement::Expr(_) => !earlier_hint, - TopLevelStatement::Decl(_) => true, + TopLevelStatement::VarDecl(_) => true, + TopLevelStatement::FnDecl(_) => false, _ => false, } } - pub fn to_statement(self) -> Statement { + pub fn to_statement(self) -> Option { match self { - TopLevelStatement::Expr(expr) => Statement::Expr(expr), - TopLevelStatement::Decl(decl) => Statement::Decl(decl), - TopLevelStatement::Comment(comment) => Statement::Comment(comment), + TopLevelStatement::Expr(expr) => Some(Statement::Expr(expr)), + TopLevelStatement::VarDecl(decl) => Some(Statement::VarDecl(decl)), + TopLevelStatement::FnDecl(decl) => Some(Statement::FnDecl(decl)), + TopLevelStatement::Comment(comment) => Some(Statement::Comment(comment)), } } } diff --git a/crates/js_ast/convert/block_convert_context.rs b/crates/js_ast/convert/block_convert_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..de41e77cae5f13c2d98b6215e925fd637a9bebb6 --- /dev/null +++ b/crates/js_ast/convert/block_convert_context.rs @@ -0,0 +1,244 @@ +use crate::js_ast::*; +use ast::*; +use std::collections::{HashMap, HashSet}; +use std::vec; +use tokenizer::Loc; + +use super::tree_shake_statement_list; + +/// A block convert context keeps track of state while converting non-valid JS expressions to JS +pub struct BlockConvertContext { + /// Statements in this block context + /// This may be the merged contents of multiple blocks + statements: Vec, + ids: HashSet, + next_id: u32, + scopes: Vec, + /// If the last item in `statements` is actually valid + has_last_statement: bool, +} + +struct VirtualScope { + renames: HashMap, +} + +pub struct TempId { + unmapped_name: String, +} + +pub struct StatementHandle { + index: Option, +} + +impl StatementHandle { + pub fn delete(self, ctx: &mut BlockConvertContext) { + if let Some(index) = self.index { + ctx.statements.remove(index as usize); + } + } +} + +impl TempId { + pub fn get(&self, hoist_context: &BlockConvertContext) -> String { + hoist_context.remap_identifier(&self.unmapped_name) + } +} + +impl BlockConvertContext { + pub fn new() -> Self { + Self { + statements: Vec::new(), + ids: HashSet::new(), + next_id: 0, + scopes: vec![VirtualScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + pub fn new_child(&self) -> Self { + Self { + statements: Vec::new(), + ids: self.ids.clone(), + next_id: 0, + scopes: vec![VirtualScope { + renames: HashMap::new(), + }], + has_last_statement: false, + } + } + + /// A virtual scope is a scope that exists in blight syntax + /// but may or may not exist in blight. With these it is important to + pub fn push_virtual_scope(&mut self) { + self.scopes.push(VirtualScope { + renames: HashMap::new(), + }); + } + + /// See `push_virtual_scope` for more information on what this is. + pub fn pop_virtual_scope(&mut self) { + self.scopes.pop(); + debug_assert!(self.scopes.len() > 0); + } + + pub fn generate_temp_id(&mut self, basename: &str) -> TempId { + self.next_id += 1; + loop { + let name = format!("{}{}", basename, self.next_id); + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + self.next_id += 1; + } + } + + /// This does some extra optimizations: + /// - `let a = b;` will not be emitted but instad write a variable alias + pub fn add_statement(&mut self, statement: JsStatement) -> StatementHandle { + match statement.data { + JsStatementData::Declaration(JSDeclaration { + name, + init: + Some(JsExpr { + data: JsExprData::Identifier(init_name), + .. + }), + is_constant: true, + }) => { + self.scopes + .last_mut() + .unwrap() + .renames + .insert(name, init_name); + self.has_last_statement = false; + return StatementHandle { index: None }; + } + _ => {} + } + self.statements.push(statement); + self.has_last_statement = true; + StatementHandle { + index: Some(self.statements.len() as u32 - 1), + } + } + + /// Get the last statement if it exists. + /// If the statement was optimized away, this returns None + pub fn last_statement(&self) -> Option<&JsStatement> { + if self.has_last_statement { + Some(self.statements.last().unwrap()) + } else { + None + } + } + + pub fn generate_id(&mut self, basename: &String) -> TempId { + let mut id = 0u32; + loop { + let name = match id { + 0 => basename.clone(), + _ => format!("{}{}", basename, id), + }; + if !self.ids.contains(&name) { + self.ids.insert(name.clone()); + return TempId { + unmapped_name: name, + }; + } + id += 1; + } + } + + /// Given a blight identifier, remap it to a new identifier. + pub fn remap_identifier(&self, name: &String) -> String { + for scope in self.scopes.iter().rev() { + if let Some(remapped) = scope.renames.get(name) { + return remapped.clone(); + } + } + name.clone() + } + + /// Declare an identifier in the current scope. + pub fn declare_identifier(&mut self, name: String) -> TempId { + let remapped = self.generate_id(&name); + let scope = self.scopes.last_mut().unwrap(); + self.ids.insert(remapped.unmapped_name.clone()); + scope.renames.insert(name, remapped.unmapped_name.clone()); + remapped + } + + pub fn take_last_statement(&mut self) -> Option { + if !self.has_last_statement { + return None; + } + self.has_last_statement = false; + Some(self.statements.pop().unwrap()) + } + + pub fn take_last_statement_if_expr(&mut self) -> Option { + if !self.has_last_statement { + return None; + } + match self.statements.last().unwrap().data { + JsStatementData::Expression(_) => { + self.has_last_statement = false; + match self.statements.pop().unwrap().data { + JsStatementData::Expression(expr) => Some(expr), + _ => unreachable!(), + } + } + _ => return None, + } + } +} + +pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { + JsStatement::new( + JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), + loc, + None, + ) +} + +pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { + JsExpr::new( + JsExprData::Binary { + op: BinaryOperator::Assign, + left: Box::new(JsExpr::new( + JsExprData::Identifier(target), + loc.clone(), + None, + )), + right: Box::new(expr), + }, + loc, + None, + ) +} + +impl Into for BlockConvertContext { + fn into(self) -> JsBlock { + JsBlock::new(self.into(), Loc::EMPTY) + } +} + +impl Into for BlockConvertContext { + fn into(self) -> JsModule { + JsModule { + statements: self.into(), + } + } +} + +impl Into> for BlockConvertContext { + fn into(mut self) -> Vec { + tree_shake_statement_list(&mut self.statements); + self.statements + } +} diff --git a/crates/js_ast/convert/convert_valid_block.rs b/crates/js_ast/convert/convert_valid_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb40111073d4e3ea44604c0117ed7419b879935f --- /dev/null +++ b/crates/js_ast/convert/convert_valid_block.rs @@ -0,0 +1,37 @@ +use super::convert_valid_statement_to_expr; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn convert_valid_block(ctx: &mut BlockConvertContext, block: Block, loc: Loc) -> JsExpr { + debug_assert!( + block.is_valid_js_expression(), + "must call convert_valid_block with valid js expression" + ); + + let len = block.statements.len(); + let loc = loc; + let block_expr = match len { + 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), + // a block expression with a single expression is just that expression + 1 => convert_valid_statement_to_expr(ctx, block.statements.into_iter().next().unwrap()), + // Since blocks are not valid expressions in JS, we create a + // comma expression from a block with more than one statement. + 2.. => { + let mut items = Vec::new(); + for statement in block.statements.into_iter() { + items.push(convert_valid_statement_to_expr(ctx, statement)); + } + JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) + } + }; + + // a block without implicit return is to be type void, so we wrap in a void expression + if !block.has_implicit_return && len > 0 { + JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) + } else { + block_expr + } +} diff --git a/crates/js_ast/convert/convert_valid_expr.rs b/crates/js_ast/convert/convert_valid_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ba30c694c43c60d8c543c4b64be40ac6c7a32c6 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_expr.rs @@ -0,0 +1,115 @@ +use super::convert_valid_block; +use super::BlockConvertContext; +use crate::convert::convert_valid_if; +use crate::convert::process_function::process_function_to_arrow; +use crate::convert::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; + +/// Translate a blight ast expression that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_expr(ctx: &mut BlockConvertContext, expr: Expr) -> JsExpr { + // Assert may be skipped in release builds as all failures are + // annotated with a release-checked panic + debug_assert!( + expr.is_valid_js_expression(), + "must call convert_valid_expr with valid js expression (use hoist instead)" + ); + + let loc = expr.loc; + let comment = expr.comment; + + match expr.data { + ExprData::Identifier(str) => JsExpr::new( + JsExprData::Identifier(ctx.remap_identifier(&str)), + loc, + comment, + ), + + ExprData::Literal(Literal::Bool(value)) => { + JsExpr::new(JsExprData::Bool(value), loc, comment) + } + ExprData::Literal(Literal::Number(value)) => { + JsExpr::new(JsExprData::Number(value), loc, comment) + } + ExprData::Literal(Literal::String(value)) => { + JsExpr::new(JsExprData::String(value), loc, comment) + } + ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), + + ExprData::Block(block) => convert_valid_block(ctx, block, loc), + + ExprData::Binary { op, left, right } => { + let left = convert_valid_expr(ctx, *left); + let right = convert_valid_expr(ctx, *right); + JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + loc, + comment, + ) + } + ExprData::Unary { op, value } => { + let value = convert_valid_expr(ctx, *value); + JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + loc, + comment, + ) + } + ExprData::Fn { body, params, .. } => { + process_function_to_arrow(ctx, *body, params, loc, comment) + } + ExprData::Builtin { id, arguments } => match id { + BuiltinId::Raw => { + let first_arg = arguments.iter().next().unwrap(); + // TODO(@paperdave): this string clone is stupid + let str = match first_arg.data.clone() { + ExprData::Literal(Literal::String(s)) => s, + _ => panic!("raw expects a string"), + }; + JsExpr::new(JsExprData::Raw(str), loc, comment) + } + }, + ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), + + ExprData::Call { base, arguments } => { + let base = convert_valid_expr(ctx, *base); + let arguments = arguments + .into_iter() + .map(|x| convert_valid_expr(ctx, x)) + .collect(); + JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + loc, + comment, + ) + } + + ExprData::Field { base, field } => JsExpr::new( + match field.data { + ExprData::Literal(Literal::String(name)) => JsExprData::Dot { + base: Box::new(convert_valid_expr(ctx, *base)), + name, + }, + _ => JsExprData::Index { + base: Box::new(convert_valid_expr(ctx, *base)), + index: Box::new(convert_valid_expr(ctx, *field)), + }, + }, + loc, + comment, + ), + + ExprData::Return(_) => unreachable!("convert_valid_expr called on return"), + } +} diff --git a/crates/js_ast/convert/convert_valid_if.rs b/crates/js_ast/convert/convert_valid_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..1e71398123c950b76f1f1de7e6e3088f3995fb79 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_if.rs @@ -0,0 +1,38 @@ +use super::convert_valid_block; +use super::convert_valid_expr; +use super::BlockConvertContext; +use crate::convert::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +/// Convert a valid blight if statement to a JS expression +pub fn convert_valid_if( + ctx: &mut BlockConvertContext, + expr: If, + loc: Loc, + comment: Option, +) -> JsExpr { + debug_assert!( + expr.is_valid_js_expression(), + "convert_valid_if on an invalid expression (use hoist instead)" + ); + let condition = convert_valid_expr(ctx, *expr.condition); + + let yes = convert_valid_block(ctx, expr.yes.block, expr.yes.loc); + let no = match expr.no { + Some(BlockOrIf::Block(b)) => Some(convert_valid_block(ctx, b.block, b.loc)), + Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), + None => None, + }; + + JsExpr::new( + JsExprData::Ternary { + condition: Box::new(condition), + yes: Box::new(yes), + no: no.map(Box::new), + }, + loc, + comment, + ) +} diff --git a/crates/js_ast/convert/convert_valid_statement.rs b/crates/js_ast/convert/convert_valid_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..f02fb4e407e920b50233e188a1f14fb6fbc72b24 --- /dev/null +++ b/crates/js_ast/convert/convert_valid_statement.rs @@ -0,0 +1,32 @@ +use super::convert_valid_expr; +use super::is_valid_js_expression::*; +use super::BlockConvertContext; +use crate::js_ast::*; +use ast::*; + +/// Translate a blight ast statement that is a valid JS expression, to a JS expression +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +pub fn convert_valid_statement_to_expr( + ctx: &mut BlockConvertContext, + statement: Statement, +) -> JsExpr { + debug_assert!( + statement.is_valid_js_expression(), + "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" + ); + + match statement { + ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), + _ => unreachable!("attempt to convert statement to js without hoisting"), + } +} + +/// Translate a blight ast statement that is a valid JS expression, to a JS statement +/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` +/// +/// TODO(@paperdave): Replace this with Into for JsExpr +pub fn convert_valid_statement(ctx: &mut BlockConvertContext, statement: Statement) -> JsStatement { + let expr = convert_valid_statement_to_expr(ctx, statement); + let loc = expr.loc.clone(); + JsStatement::new(JsStatementData::Expression(expr), loc, None) +} diff --git a/crates/js_ast/convert/hoist_block_to_expr.rs b/crates/js_ast/convert/hoist_block_to_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa419cedde552f566140b6474ddf08bee0f84bbd --- /dev/null +++ b/crates/js_ast/convert/hoist_block_to_expr.rs @@ -0,0 +1,78 @@ +use super::is_valid_js_expression::*; +use super::process_statement; +use super::BlockConvertContext; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +/// This block hoist is used when we have a block in an expression context. +/// +/// All the expressions in the block are lifted above the currently +/// processed expression, and then a temporary variable is returned. +/// (or the last expression might get inlined as the return value) +/// +/// A "virtual scope" is created for this block, which handles renaming +/// situations like: +/// +/// let x = 1; +/// { +/// let x = 1; +/// } +/// call(x) +/// +/// The output JS of the above would be: +/// +/// const x = 1; +/// const x2 = 1; +/// call(x) +/// +/// For expressions that have branches, use `hoist_block_to_js_block` +pub fn hoist_block_to_expr( + ctx: &mut BlockConvertContext, + block: Block, + loc: Loc, + comment: Option, +) -> Option { + debug_assert!( + !block.is_valid_js_expression(), + "hoist_block on a valid expression, should call convert_valid_block instead" + ); + + let temporary = ctx.generate_temp_id("_block"); + + ctx.push_virtual_scope(); + + for statement in block.statements { + process_statement(ctx, statement); + } + + ctx.pop_virtual_scope(); + + // If the last statement is an expression, we need + // to assign to the temporary variable. + let result = match ctx.last_statement().map(|x| &x.data) { + Some(JsStatementData::Expression(_)) if block.has_implicit_return => { + match ctx.take_last_statement().unwrap().data { + JsStatementData::Expression(expr) => expr, + _ => unreachable!(), + } + } + _ => JsExpr::new(JsExprData::Null, loc.clone(), None), + }; + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: temporary.get(&ctx), + init: Some(result), + is_constant: false, + }), + loc.clone(), + comment, + )); + + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&ctx)), + loc, + None, + )) +} diff --git a/crates/js_ast/convert/hoist_block_to_js_block.rs b/crates/js_ast/convert/hoist_block_to_js_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..2dd10483b209d1d999b593d2763cb4b3b9109c2c --- /dev/null +++ b/crates/js_ast/convert/hoist_block_to_js_block.rs @@ -0,0 +1,68 @@ +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +use super::*; + +/// This block hoist is used when we actually need to create a JsBlock, which +/// is needed if there is an invalid expression within a branch of code. +/// +/// For example, `if` has two branches, the 'yes' and 'no' paths. +/// When we cannot lower it as a ternary, we have to create two JsBlocks +/// +/// The `target` param is a shared temporary variable that will be assigned +/// to the result of the block. +/// +/// Since blocks can be void or noreturn, this target is not always +/// assigned to. The `did_assign` flag is used to indicate that. +/// +/// For expressions that do not have branches, use `hoist_block_to_expr` +/// +/// TODO(@paperdave): move `did_assign` into the return value +pub fn hoist_block_to_js_block( + ctx: &mut BlockConvertContext, + block: Block, + loc: Loc, + target: &TempId, + did_assign: &mut bool, +) -> JsBlock { + // A non-block will become a block with a single expression: + // _temp = expr; + // + // We will also apply this transformation to blocks that are valid + // expressions, because that is more concise. + if block.is_valid_js_expression() { + return JsBlock::new( + vec![new_js_assignment_statement( + convert_valid_block(ctx, block, loc.clone()), + target.get(ctx), + loc.clone(), + )], + loc, + ); + } + + let mut child = ctx.new_child(); + + for statement in block.statements { + if let None = process_statement(&mut child, statement) { + return child.into(); + }; + } + + // If the last statement is an expression, we need + // to assign to the temporary variable. + match child.take_last_statement_if_expr() { + Some(expr) => { + *did_assign = true; + child.add_statement(new_js_assignment_statement( + expr, + target.get(ctx), + loc.clone(), + )); + } + _ => {} + } + + child.into() +} diff --git a/crates/js_ast/convert/hoist_expr.rs b/crates/js_ast/convert/hoist_expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..a79cb5a9928c6a95e0e49e9e9a18ff44ffd77646 --- /dev/null +++ b/crates/js_ast/convert/hoist_expr.rs @@ -0,0 +1,132 @@ +use super::convert_valid_expr; +use super::hoist_block_to_expr; +use super::hoist_if_expr; +use super::is_valid_js_expression::*; +use super::BlockConvertContext; +// use super::HasSideEffects; +use crate::js_ast::*; +use crate::util::is_js_identifier; +use ast::*; + +/// Pushes a decl with a generated identifier to the hoisted result +/// and returns an identifier expression for it. +pub fn hoist_expr_if_needed(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + // if !expr.has_side_effects() { + // return Some(ctx.hoist_expression_contents(expr)?); + // } + + let init = hoist_expr_contents(ctx, expr)?; + + let id = ctx.generate_temp_id("_"); + let loc = init.loc.clone(); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: id.get(&ctx), + init: Some(init), + is_constant: true, + }), + loc.clone(), + None, + )); + + Some(JsExpr::new(JsExprData::Identifier(id.get(&ctx)), loc, None)) +} + +/// Returning `None` from this means that the expression is noreturn, and that +/// the rest of the statement never occurs and can be stripped. +pub fn hoist_expr_contents(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + if expr.is_valid_js_expression() { + return Some(convert_valid_expr(ctx, expr)); + } + + match expr.data { + ExprData::Binary { op, left, right } => { + let left = hoist_expr_if_needed(ctx, *left)?; + let right = hoist_expr_if_needed(ctx, *right)?; + + Some(JsExpr::new( + JsExprData::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Unary { op, value } => { + let value = hoist_expr_if_needed(ctx, *value)?; + Some(JsExpr::new( + JsExprData::Unary { + op, + value: Box::new(value), + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Field { base, field } => { + let base = hoist_expr_if_needed(ctx, *base)?; + let field = hoist_expr_if_needed(ctx, *field)?; + + Some(JsExpr::new( + match field.data { + JsExprData::String(name) if is_js_identifier(name.as_str()) => { + JsExprData::Dot { + base: Box::new(base), + name, + } + } + _ => JsExprData::Index { + base: Box::new(base), + index: Box::new(field), + }, + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Block(block) => hoist_block_to_expr(ctx, block, expr.loc, expr.comment), + + ExprData::If(_) => hoist_if_expr(ctx, expr), + + ExprData::Return(expr) => { + let loc = expr.loc.clone(); + let return_value = hoist_expr_if_needed(ctx, *expr)?; + ctx.add_statement(JsStatement::new( + JsStatementData::Return(return_value), + loc, + None, + )); + None + } + + ExprData::Call { base, arguments } => { + let base = hoist_expr_if_needed(ctx, *base)?; + let arguments = arguments + .into_iter() + .map(|x| hoist_expr_if_needed(ctx, x)) + .collect::>>()?; + + Some(JsExpr::new( + JsExprData::Call { + base: Box::new(base), + arguments, + }, + expr.loc, + expr.comment, + )) + } + + ExprData::Fn { .. } + | ExprData::Builtin { .. } + | ExprData::Literal(_) + | ExprData::Identifier(_) => { + unreachable!("attempt to hoist an expression that does not require it") + } + } +} diff --git a/crates/js_ast/convert/hoist_if.rs b/crates/js_ast/convert/hoist_if.rs new file mode 100644 index 0000000000000000000000000000000000000000..cbe2fffe3866392543f3d7c67f1fddf3b9fa44f0 --- /dev/null +++ b/crates/js_ast/convert/hoist_if.rs @@ -0,0 +1,113 @@ +use super::convert_valid_expr; +use super::hoist_block_to_js_block; +use super::hoist_expr_if_needed; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use super::TempId; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn hoist_if_expr(ctx: &mut BlockConvertContext, expr: Expr) -> Option { + let data = match expr.data { + ExprData::If(data) => data, + _ => unreachable!("hoist_if_expr called on non-if expression"), + }; + + let temporary = ctx.generate_temp_id("_if"); + + let decl = ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: temporary.get(&ctx), + init: None, + is_constant: false, + }), + expr.loc.clone(), + None, + )); + + let condition = hoist_expr_if_needed(ctx, *data.condition)?; + let mut did_assign = false; + + let js_if = hoist_if_branch( + ctx, + data.yes, + data.no, + condition, + &mut did_assign, + &temporary, + ); + + ctx.add_statement(JsStatement::new( + JsStatementData::If(js_if), + expr.loc.clone(), + None, + )); + + if did_assign { + Some(JsExpr::new( + JsExprData::Identifier(temporary.get(&ctx)), + expr.loc, + expr.comment, + )) + } else { + decl.delete(ctx); + Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) + } +} + +pub fn hoist_if_branch( + ctx: &mut BlockConvertContext, + yes: BlockWithLoc, + no: Option, + condition: JsExpr, + did_assign: &mut bool, + temporary: &TempId, +) -> JsIf { + let yes = hoist_block_to_js_block(ctx, yes.block, yes.loc, &temporary, did_assign); + + let no = match no { + Some(BlockOrIf::Block(no)) => Some(JsElse::Block(hoist_block_to_js_block( + ctx, no.block, no.loc, &temporary, did_assign, + ))), + Some(BlockOrIf::If(else_if)) => { + // If the condition of this block has a side effect, we need to + // emit a block so that we can assign to the temporary variable. + // + // Otherwise, we can just emit the condition inline + if else_if.condition.is_valid_js_expression() { + let condition = convert_valid_expr(ctx, *else_if.condition.clone()); + Some(JsElse::If(Box::new(hoist_if_branch( + ctx, + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )))) + } else { + let mut temp_block = ctx.new_child(); + let condition = + hoist_expr_if_needed(&mut temp_block, *else_if.condition.clone()).unwrap(); + + temp_block.add_statement(JsStatement::new( + JsStatementData::If(hoist_if_branch( + ctx, + else_if.yes, + else_if.no, + condition, + did_assign, + temporary, + )), + Loc::EMPTY, + None, + )); + + Some(JsElse::Block(temp_block.into())) + } + } + None => None, + }; + + JsIf { condition, yes, no } +} diff --git a/crates/js_ast/is_valid_js_expression.rs b/crates/js_ast/convert/is_valid_js_expression.rs similarity index 88% rename from crates/js_ast/is_valid_js_expression.rs rename to crates/js_ast/convert/is_valid_js_expression.rs index 382b8712e3f33dc10e713d382e8859352e74f8f6..feb2f206c2daf10b6be3f68358ad4097788adc08 100644 --- a/crates/js_ast/is_valid_js_expression.rs +++ b/crates/js_ast/convert/is_valid_js_expression.rs @@ -1,5 +1,11 @@ use ast::*; +/// "Valid" in this context means that the expression does not contain JS statements +/// Meaning conversion is a direct map from a blight as node into a JS one +/// +/// A valid expression should not go through any hoist logic, but if an expression +/// isn't valid, then it means that it must be split into multiple JS statements +/// inside of a block. pub trait IsValidJSExpression { /// Can this be lowered to a JS ast without hoisting items /// ex: a `return` expression is not a valid JS expression diff --git a/crates/js_ast/convert/mod.rs b/crates/js_ast/convert/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..870912a6c10fe558c69cde35f714fd9961f9497a --- /dev/null +++ b/crates/js_ast/convert/mod.rs @@ -0,0 +1,42 @@ +mod block_convert_context; +mod convert_valid_block; +mod convert_valid_expr; +mod convert_valid_if; +mod convert_valid_statement; +mod hoist_block_to_expr; +mod hoist_block_to_js_block; +mod hoist_expr; +mod hoist_if; +mod is_valid_js_expression; +mod process_function; +mod process_statement; +mod side_effects; +mod tree_shake_statement_list; + +pub use block_convert_context::*; +pub use convert_valid_block::*; +pub use convert_valid_expr::*; +pub use convert_valid_if::*; +pub use convert_valid_statement::*; +pub use hoist_block_to_expr::*; +pub use hoist_block_to_js_block::*; +pub use hoist_expr::*; +pub use hoist_if::*; +pub use is_valid_js_expression::*; +pub use process_function::*; +pub use process_statement::*; +// pub use side_effects::*; +pub use tree_shake_statement_list::*; + +use crate::js_ast::JsModule; +use ast::Module; + +pub fn blight_to_js(blight: Module) -> JsModule { + let mut root_context = BlockConvertContext::new(); + + for statement in blight.statements { + process_top_level_statement(&mut root_context, statement).unwrap(); + } + + root_context.into() +} diff --git a/crates/js_ast/convert/process_function.rs b/crates/js_ast/convert/process_function.rs new file mode 100644 index 0000000000000000000000000000000000000000..d45090b7889d25c279447ce1dfc3bd7f3297b19e --- /dev/null +++ b/crates/js_ast/convert/process_function.rs @@ -0,0 +1,63 @@ +use super::convert_valid_expr; +use super::hoist_expr_if_needed; +use super::process_statement; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; +use tokenizer::Loc; + +pub fn process_function_to_arrow( + ctx: &mut BlockConvertContext, + body: Expr, + params: Vec, + loc: Loc, + comment: Option, +) -> JsExpr { + // A block that is a valid expression will just become + // that expression. We can convert multiple expressions + // into a comma expression. + let body = if body.is_valid_js_expression() { + JsArrowBody::Expr(convert_valid_expr(ctx, body)) + } else { + let mut child = ctx.new_child(); + + match body.data { + ExprData::Block(block) => { + for statement in block.statements { + if let None = process_statement(&mut child, statement) { + break; + }; + } + + if let Some(expr) = child.take_last_statement_if_expr() { + child.add_statement(JsStatement::new( + JsStatementData::Return(expr), + loc.clone(), + None, + )); + } + } + _ => { + if let Some(expr) = hoist_expr_if_needed(&mut child, body) { + child.add_statement(JsStatement::new( + JsStatementData::Return(expr), + loc.clone(), + None, + )); + } + } + } + + JsArrowBody::Block(child.into()) + }; + + JsExpr::new( + JsExprData::ArrowFunction { + params: params.into_iter().map(|x| x.name).collect(), + body: Box::new(body), + }, + loc.clone(), + comment.map(|x| x.into()), + ) +} diff --git a/crates/js_ast/convert/process_statement.rs b/crates/js_ast/convert/process_statement.rs new file mode 100644 index 0000000000000000000000000000000000000000..f2cdaabb1636d75a56592e38f641a90d48b3aa58 --- /dev/null +++ b/crates/js_ast/convert/process_statement.rs @@ -0,0 +1,95 @@ +use super::convert_valid_statement; +use super::hoist_expr_contents; +use super::process_function_to_arrow; +use super::BlockConvertContext; +use super::IsValidJSExpression; +use crate::js_ast::*; +use ast::*; + +pub fn process_statement(ctx: &mut BlockConvertContext, statement: Statement) -> Option<()> { + if statement.is_valid_js_expression() { + let result = convert_valid_statement(ctx, statement); + ctx.add_statement(result); + return Some(()); + } + + match statement { + Statement::VarDecl(decl) => { + // visit init before name, this way allows for: + // + // let a = a; + // + // and `a` will be looked up before it is re-declared. + // we will also return in the noreturn case + let init = hoist_expr_contents(ctx, decl.init)?; + + let name = ctx.declare_identifier(decl.name); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: name.get(&ctx), + init: Some(init), + is_constant: decl.is_constant, + }), + decl.loc, + decl.comment.map(|x| x.into()), + )); + + Some(()) + } + Statement::Expr(expr) => { + if let Some(expr) = hoist_expr_contents(ctx, expr) { + let loc = expr.loc.clone(); + ctx.add_statement(JsStatement::new( + JsStatementData::Expression(expr), + loc, + None, + )); + Some(()) + } else { + None + } + } + Statement::FnDecl(FnDecl { + name, + params, + body, + loc, + comment, + .. + }) => { + let name = ctx.declare_identifier(name); + + let processed = Some(process_function_to_arrow( + ctx, + body, + params, + loc.clone(), + None, + )); + + ctx.add_statement(JsStatement::new( + JsStatementData::Declaration(JSDeclaration { + name: name.get(&ctx), + init: processed, + is_constant: true, + }), + loc, + comment.map(|x| x.into()), + )); + + Some(()) + } + Statement::Comment(_) => Some(()), + } +} + +pub fn process_top_level_statement( + ctx: &mut BlockConvertContext, + statement: TopLevelStatement, +) -> Option<()> { + match statement.to_statement() { + Some(stmt) => process_statement(ctx, stmt), + None => todo!("process_top_level_statement"), + } +} diff --git a/crates/js_ast/has_side_effects.rs b/crates/js_ast/convert/side_effects.rs similarity index 95% rename from crates/js_ast/has_side_effects.rs rename to crates/js_ast/convert/side_effects.rs index 81c16730f2cd157a8f1b9236588bea0a05854a96..5eeaea8a53069fe637943bc72add4cb5c202fcc5 100644 --- a/crates/js_ast/has_side_effects.rs +++ b/crates/js_ast/convert/side_effects.rs @@ -43,8 +43,9 @@ impl HasSideEffects for Statement { fn has_side_effects(&self) -> bool { match self { Statement::Expr(expr) => expr.has_side_effects(), - Statement::Decl(decl) => decl.init.has_side_effects(), + Statement::VarDecl(decl) => decl.init.has_side_effects(), Statement::Comment(_) => false, + Statement::FnDecl(_) => false, } } } diff --git a/crates/js_ast/convert/tree_shake_statement_list.rs b/crates/js_ast/convert/tree_shake_statement_list.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7a631e29c443f71ddfe2409eb400df6ebe534cc --- /dev/null +++ b/crates/js_ast/convert/tree_shake_statement_list.rs @@ -0,0 +1,31 @@ +use crate::js_ast::*; + +/// Removes statements that are not needed from a JS block. Very limited. +/// +/// BlockConvertContext will automatically execute this when converting +/// itself into a Statement list or other data structure. +/// +/// What this does: +/// - Remove discarded literals `null` -> `` +pub fn tree_shake_statement_list(list: &mut Vec) { + let mut set_i = 0; + for i in 0..list.len() { + let stmt = list.get(i).unwrap(); + let should_keep = match &stmt.data { + JsStatementData::Expression(expr) => !expr.is_literal(), + JsStatementData::Return(_) => true, + JsStatementData::Export(_) => true, + JsStatementData::Declaration(_) => true, + JsStatementData::Function(_) => true, + JsStatementData::If(_) => true, + }; + + if should_keep { + if set_i != i { + list.swap(i, set_i) + } + set_i += 1; + } + } + list.truncate(set_i) +} diff --git a/crates/js_ast/convert_valid_expr.rs b/crates/js_ast/convert_valid_expr.rs deleted file mode 100644 index 90e13ddc003e4066d45034cba29c8b3f4d438432..0000000000000000000000000000000000000000 --- a/crates/js_ast/convert_valid_expr.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! "Valid" in this context means that the expression does not contain JS statements -//! Meaning conversion is a direct map from a blight as node into a JS one -//! -//! See `is_valid_js_expression` for more details on what qualifies -//! -//! For other nodes, it must hoist statements to the nearest JS block, -//! is done in the `hoist_*` functions in `host.rs`. - -use ast::{BlockOrIf, Comment, ExprData, If}; -use ast::{BlockWithLoc, BuiltinId, Expr, Literal, Statement}; -use tokenizer::Loc; - -use crate::is_valid_js_expression::IsValidJSExpression; -use crate::js_ast::*; -use crate::HoistContext; - -/// Translate a blight ast expression that is a valid JS expression, to a JS expression -/// Precondition: it is a valid JS expression, check this with `is_valid_js_expression` -pub fn convert_valid_expr(ctx: &mut HoistContext, expr: Expr) -> JsExpr { - // Assert may be skipped in release builds as all failures are - // annotated with a release-checked panic - debug_assert!( - expr.is_valid_js_expression(), - "must call convert_valid_expr with valid js expression (use hoist instead)" - ); - - let loc = expr.loc; - let comment = expr.comment; - - match expr.data { - ExprData::Identifier(str) => JsExpr::new( - JsExprData::Identifier(ctx.remap_identifier(&str)), - loc, - comment, - ), - - ExprData::Literal(Literal::Bool(value)) => { - JsExpr::new(JsExprData::Bool(value), loc, comment) - } - ExprData::Literal(Literal::Number(value)) => { - JsExpr::new(JsExprData::Number(value), loc, comment) - } - ExprData::Literal(Literal::String(value)) => { - JsExpr::new(JsExprData::String(value), loc, comment) - } - ExprData::Literal(Literal::Void) => JsExpr::new(JsExprData::Null, loc, comment), - - ExprData::Block(block) => convert_valid_block(ctx, BlockWithLoc { block, loc }), - - ExprData::Binary { op, left, right } => { - let left = convert_valid_expr(ctx, *left); - let right = convert_valid_expr(ctx, *right); - JsExpr::new( - JsExprData::Binary { - op, - left: Box::new(left), - right: Box::new(right), - }, - loc, - comment, - ) - } - ExprData::Unary { op, value } => { - let value = convert_valid_expr(ctx, *value); - JsExpr::new( - JsExprData::Unary { - op, - value: Box::new(value), - }, - loc, - comment, - ) - } - ExprData::Fn { block, params, .. } => JsExpr::new( - JsExprData::ArrowFunction { - params: params.into_iter().map(|x| x.name).collect(), - body: Box::new(ctx.hoist_block_to_arrow_fn_body(BlockWithLoc { - block, - loc: loc.clone(), - })), - }, - loc, - comment, - ), - ExprData::Builtin { id, arguments } => match id { - BuiltinId::Raw => { - let first_arg = arguments.iter().next().unwrap(); - // TODO(@paperdave): this string clone is stupid - let str = match first_arg.data.clone() { - ExprData::Literal(Literal::String(s)) => s, - _ => panic!("raw expects a string"), - }; - JsExpr::new(JsExprData::Raw(str), loc, comment) - } - }, - ExprData::If(expr) => convert_valid_if(ctx, expr, loc, comment), - - ExprData::Call { base, arguments } => { - let base = convert_valid_expr(ctx, *base); - let arguments = arguments - .into_iter() - .map(|x| convert_valid_expr(ctx, x)) - .collect(); - JsExpr::new( - JsExprData::Call { - base: Box::new(base), - arguments, - }, - loc, - comment, - ) - } - - ExprData::Field { base, field } => JsExpr::new( - match field.data { - ExprData::Literal(Literal::String(name)) => JsExprData::Dot { - base: Box::new(convert_valid_expr(ctx, *base)), - name, - }, - _ => JsExprData::Index { - base: Box::new(convert_valid_expr(ctx, *base)), - index: Box::new(convert_valid_expr(ctx, *field)), - }, - }, - loc, - comment, - ), - - ExprData::Return(_) => unreachable!("convert_valid_expr does not support return"), - } -} - -pub fn convert_valid_block(ctx: &mut HoistContext, b: BlockWithLoc) -> JsExpr { - debug_assert!( - b.is_valid_js_expression(), - "must call convert_valid_block with valid js expression" - ); - - let len = b.block.statements.len(); - let loc = b.loc; - let block_expr = match len { - 0 => JsExpr::new(JsExprData::Null, loc.clone(), None), - // a block expression with a single expression is just that expression - 1 => convert_valid_statement_to_expr(ctx, b.block.statements.into_iter().next().unwrap()), - // Since blocks are not valid expressions in JS, we create a - // comma expression from a block with more than one statement. - 2.. => { - let mut items = Vec::new(); - for statement in b.block.statements.into_iter() { - items.push(convert_valid_statement_to_expr(ctx, statement)); - } - JsExpr::new(JsExprData::Comma { items }, loc.clone(), None) - } - }; - - // a block without implicit return is to be type void, so we wrap in a void expression - if !b.block.has_implicit_return && len > 0 { - JsExpr::new(JsExprData::Void(Box::new(block_expr)), loc, None) - } else { - block_expr - } -} - -pub fn convert_valid_statement_to_expr(ctx: &mut HoistContext, statement: Statement) -> JsExpr { - debug_assert!( - statement.is_valid_js_expression(), - "convert_valid_statement_to_expr on an invalid expression (use hoist instead)" - ); - - match statement { - ast::Statement::Expr(expr) => convert_valid_expr(ctx, expr), - _ => unreachable!("attempt to convert statement to js without hoisting"), - } -} - -pub fn convert_valid_statement(ctx: &mut HoistContext, statement: Statement) -> JsStatement { - let expr = convert_valid_statement_to_expr(ctx, statement); - let loc = expr.loc.clone(); - JsStatement::new(JsStatementData::Expression(expr), loc, None) -} - -pub fn convert_valid_if( - ctx: &mut HoistContext, - expr: If, - loc: Loc, - comment: Option, -) -> JsExpr { - let condition = convert_valid_expr(ctx, *expr.condition); - let yes = convert_valid_block(ctx, expr.yes); - let no = match expr.no { - Some(BlockOrIf::Block(block)) => Some(convert_valid_block(ctx, block)), - Some(BlockOrIf::If(else_if)) => Some(convert_valid_if(ctx, *else_if, Loc::EMPTY, None)), - None => None, - }; - JsExpr::new( - JsExprData::Ternary { - condition: Box::new(condition), - yes: Box::new(yes), - no: no.map(Box::new), - }, - loc, - comment, - ) -} diff --git a/crates/js_ast/hoist.rs b/crates/js_ast/hoist.rs deleted file mode 100644 index dfe289b9e0560009e5c5c475f686ecec097ddd0f..0000000000000000000000000000000000000000 --- a/crates/js_ast/hoist.rs +++ /dev/null @@ -1,638 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::vec; - -use ast::{BinaryOperator, BlockOrIf, BlockWithLoc, Expr, Statement}; -use has_side_effects::HasSideEffects; -use tokenizer::Loc; - -use crate::*; - -pub struct HoistContext { - pub statements: Vec, - pub ids: HashSet, - next_id: u32, - scopes: Vec, - has_last_statement: bool, -} - -struct HoistScope { - renames: HashMap, -} - -pub struct TempId { - unmapped_name: String, -} - -impl TempId { - pub fn get(&self, hoist_context: &HoistContext) -> String { - hoist_context.remap_identifier(&self.unmapped_name) - } -} - -impl HoistContext { - pub fn new() -> Self { - Self { - statements: Vec::new(), - ids: HashSet::new(), - next_id: 0, - scopes: vec![HoistScope { - renames: HashMap::new(), - }], - has_last_statement: false, - } - } - - pub fn new_child(&self) -> Self { - Self { - statements: Vec::new(), - ids: self.ids.clone(), - next_id: 0, - scopes: vec![HoistScope { - renames: HashMap::new(), - }], - has_last_statement: false, - } - } - - /// A virtual scope is a scope that exists in blight syntax - /// but may or may not exist in blight. With these it is important to - pub fn push_virtual_scope(&mut self) { - self.scopes.push(HoistScope { - renames: HashMap::new(), - }); - } - - /// See `push_virtual_scope` for more information on what this is. - pub fn pop_virtual_scope(&mut self) { - self.scopes.pop(); - } - - pub fn generate_temp_id(&mut self, basename: &str) -> TempId { - self.next_id += 1; - loop { - let name = format!("{}{}", basename, self.next_id); - if !self.ids.contains(&name) { - self.ids.insert(name.clone()); - return TempId { - unmapped_name: name, - }; - } - self.next_id += 1; - } - } - - /// This does some extra optimizations: - /// - `let a = b;` will not be emitted but instad write a variable alias - pub fn add_statement(&mut self, statement: JsStatement) { - match statement.data { - JsStatementData::Declaration(JSDeclaration { - name, - init: - Some(JsExpr { - data: JsExprData::Identifier(init_name), - .. - }), - is_constant: true, - }) => { - self.scopes - .last_mut() - .unwrap() - .renames - .insert(name, init_name); - self.has_last_statement = false; - return; - } - _ => {} - } - self.statements.push(statement); - self.has_last_statement = true; - } - - /// Get the last statement if it exists. - /// If the statement was optimized away, this returns None - pub fn last_statement(&self) -> Option<&JsStatement> { - if self.has_last_statement { - Some(self.statements.last().unwrap()) - } else { - None - } - } - - pub fn generate_id(&mut self, basename: &String) -> TempId { - let mut id = 0u32; - loop { - let name = match id { - 0 => basename.clone(), - _ => format!("{}{}", basename, id), - }; - if !self.ids.contains(&name) { - self.ids.insert(name.clone()); - return TempId { - unmapped_name: name, - }; - } - id += 1; - } - } - - /// Given a blight identifier, remap it to a new identifier. - pub fn remap_identifier(&self, name: &String) -> String { - for scope in self.scopes.iter().rev() { - if let Some(remapped) = scope.renames.get(name) { - return remapped.clone(); - } - } - name.clone() - } - - /// Declare an identifier in the current scope. - pub fn declare_identifier(&mut self, name: String) -> TempId { - let remapped = self.generate_id(&name); - let scope = self.scopes.last_mut().unwrap(); - self.ids.insert(remapped.unmapped_name.clone()); - scope.renames.insert(name, remapped.unmapped_name.clone()); - remapped - } - - pub fn process_statement(&mut self, statement: Statement) -> Option<()> { - if statement.is_valid_js_expression() { - let result = convert_valid_statement(self, statement); - self.add_statement(result); - return Some(()); - } - - match statement { - Statement::Decl(decl) => { - // visit init before name, this way allows for: - // - // let a = a; - // - // and `a` will be looked up before it is re-declared. - if let Some(init) = self.hoist_expression_contents(decl.init) { - let name = self.declare_identifier(decl.name); - - self.add_statement(JsStatement::new( - JsStatementData::Declaration(crate::JSDeclaration { - name: name.get(&self), - init: Some(init), - is_constant: decl.is_constant, - }), - decl.loc, - decl.comment.map(|x| x.into()), - )); - Some(()) - } else { - None - } - } - Statement::Expr(expr) => { - if let Some(expr) = self.hoist_expression_contents(expr) { - let loc = expr.loc.clone(); - self.add_statement(JsStatement::new( - JsStatementData::Expression(expr), - loc, - None, - )); - Some(()) - } else { - None - } - } - Statement::Comment(_) => Some(()), - } - } - - /// Pushes a decl with a generated identifier to the hoisted result - /// and returns an identifier expression for it. - pub fn hoist_expression_to_temporary(&mut self, expr: Expr) -> Option { - // TODO - if !expr.has_side_effects() { - return Some(self.hoist_expression_contents(expr)?); - } - - let init = self.hoist_expression_contents(expr)?; - - let id = self.generate_temp_id("_temp"); - let loc = init.loc.clone(); - - self.add_statement(JsStatement::new( - JsStatementData::Declaration(JSDeclaration { - name: id.get(&self), - init: Some(init), - is_constant: true, - }), - loc.clone(), - None, - )); - - Some(JsExpr::new( - JsExprData::Identifier(id.get(&self)), - loc, - None, - )) - } - - // Returning `None` from this means that the expression is noreturn, and that - // the rest of the statement never occurs and can be stripped. - pub fn hoist_expression_contents(&mut self, expr: Expr) -> Option { - if expr.is_valid_js_expression() { - return Some(convert_valid_expr(self, expr)); - } - - match expr.data { - ast::ExprData::Binary { op, left, right } => { - let left = self.hoist_expression_to_temporary(*left)?; - let right = self.hoist_expression_to_temporary(*right)?; - - Some(JsExpr::new( - JsExprData::Binary { - op, - left: Box::new(left), - right: Box::new(right), - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Unary { op, value } => { - let value = self.hoist_expression_to_temporary(*value)?; - Some(JsExpr::new( - JsExprData::Unary { - op, - value: Box::new(value), - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Field { base, field } => { - let base = self.hoist_expression_to_temporary(*base)?; - let field = self.hoist_expression_to_temporary(*field)?; - - Some(JsExpr::new( - match field.data { - JsExprData::String(name) if is_js_identifier(name.as_str()) => { - JsExprData::Dot { - base: Box::new(base), - name, - } - } - _ => JsExprData::Index { - base: Box::new(base), - index: Box::new(field), - }, - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Block(block) => { - let temporary = self.generate_temp_id("_block"); - let result = self.hoist_block_to_js_block_in_same_context(BlockWithLoc { - block, - loc: expr.loc.clone(), - }); - - self.add_statement(JsStatement::new( - JsStatementData::Declaration(crate::JSDeclaration { - name: temporary.get(&self), - init: Some(result), - is_constant: false, - }), - expr.loc.clone(), - None, - )); - - Some(JsExpr::new( - JsExprData::Identifier(temporary.get(&self)), - expr.loc, - expr.comment, - )) - } - - ast::ExprData::If(data) => { - let temporary = self.generate_temp_id("_if"); - - let decl_index = self.statements.len(); - self.add_statement(JsStatement::new( - JsStatementData::Declaration(crate::JSDeclaration { - name: temporary.get(&self), - init: None, - is_constant: false, - }), - expr.loc.clone(), - None, - )); - - let condition = self.hoist_expression_to_temporary(*data.condition)?; - let mut did_assign = false; - - let js_if = - self.hoist_js_if(data.yes, data.no, condition, &mut did_assign, &temporary); - - self.add_statement(JsStatement::new( - JsStatementData::If(js_if), - expr.loc.clone(), - None, - )); - - if did_assign { - Some(JsExpr::new( - JsExprData::Identifier(temporary.get(&self)), - expr.loc, - expr.comment, - )) - } else { - self.statements.remove(decl_index); - Some(JsExpr::new(JsExprData::Null, expr.loc, expr.comment)) - } - } - - ast::ExprData::Return(expr) => { - let loc = expr.loc.clone(); - let return_value = self.hoist_expression_to_temporary(*expr)?; - self.add_statement(JsStatement::new( - JsStatementData::Return(return_value), - loc, - None, - )); - None - } - - ast::ExprData::Call { base, arguments } => { - let base = self.hoist_expression_to_temporary(*base)?; - let arguments = arguments - .into_iter() - .map(|x| self.hoist_expression_to_temporary(x)) - .collect::>>()?; - - Some(JsExpr::new( - JsExprData::Call { - base: Box::new(base), - arguments, - }, - expr.loc, - expr.comment, - )) - } - - ast::ExprData::Fn { .. } - | ast::ExprData::Builtin { .. } - | ast::ExprData::Literal(_) - | ast::ExprData::Identifier(_) => { - unreachable!("attempt to hoist an expression that does not require it") - } - } - } - - pub fn hoist_block_to_js_block( - &mut self, - b: BlockWithLoc, - temporary: &String, - did_assign: &mut bool, - ) -> JsBlock { - let loc = b.loc.clone(); - - // A non-block will become a block with a single expression: - // _temp = expr; - // - // We will also apply this transformation to blocks that are valid - // expressions, because that is more concise. - if b.is_valid_js_expression() { - return JsBlock::new( - vec![new_js_assignment_statement( - convert_valid_block(self, b), - temporary.clone(), - loc.clone(), - )], - loc, - ); - } - - let mut child = self.new_child(); - - for statement in b.block.statements { - if let None = child.process_statement(statement) { - tree_shake_statement_list(&mut child.statements); - return JsBlock::new(child.statements, loc); - }; - } - - // If the last statement is an expression, we need - // to assign to the temporary variable. - match child.statements.last().map(|x| &x.data) { - Some(JsStatementData::Expression(_)) => { - *did_assign = true; - - let last = child.statements.pop().unwrap(); - let value = match last { - JsStatement { - data: JsStatementData::Expression(expr), - .. - } => expr, - _ => { - self.statements.push(last); - JsExpr::new(JsExprData::Null, loc.clone(), None) - } - }; - child.statements.push(new_js_assignment_statement( - value, - temporary.clone(), - loc.clone(), - )); - } - _ => {} - } - - tree_shake_statement_list(&mut child.statements); - - JsBlock::new(child.statements, loc) - } - - pub fn hoist_block_to_arrow_fn_body(&mut self, b: BlockWithLoc) -> JsArrowBody { - let loc = b.loc.clone(); - - // A block that is a valid expression will just become - // that expression. We can convert multiple expressions - // into a comma expression. - if b.is_valid_js_expression() { - return JsArrowBody::Expr(convert_valid_block(self, b)); - } - - let mut child = self.new_child(); - - for statement in b.block.statements { - if let None = child.process_statement(statement) { - break; - }; - } - - // If the last statement is an expression, - // we need to return that expression. - match child.statements.last().map(|x| &x.data) { - Some(JsStatementData::Expression(_)) => { - let last = child.statements.pop().unwrap(); - let value = match last { - JsStatement { - data: JsStatementData::Expression(expr), - .. - } => expr, - _ => { - self.statements.push(last); - JsExpr::new(JsExprData::Null, loc.clone(), None) - } - }; - child.statements.push(JsStatement::new( - JsStatementData::Return(value), - loc.clone(), - None, - )); - } - _ => {} - } - - tree_shake_statement_list(&mut child.statements); - - JsArrowBody::Block(JsBlock::new(child.statements, loc)) - } - - pub fn hoist_block_to_js_block_in_same_context(&mut self, b: BlockWithLoc) -> JsExpr { - let loc = b.loc.clone(); - self.push_virtual_scope(); - - debug_assert!(!b.block.is_valid_js_expression()); - - for statement in b.block.statements { - self.process_statement(statement); - } - - self.pop_virtual_scope(); - - // If the last statement is an expression, we need - // to assign to the temporary variable. - match self.last_statement().map(|x| &x.data) { - Some(JsStatementData::Expression(_)) if b.block.has_implicit_return => { - match self.statements.pop().unwrap().data { - JsStatementData::Expression(expr) => expr, - _ => unreachable!(), - } - } - _ => JsExpr::new(JsExprData::Null, loc.clone(), None), - } - } - - pub fn hoist_js_if( - &mut self, - yes: BlockWithLoc, - no: Option, - condition: JsExpr, - did_assign: &mut bool, - temporary: &TempId, - ) -> JsIf { - let yes = self.hoist_block_to_js_block(yes, &temporary.get(&self), did_assign); - - let no = match no { - Some(BlockOrIf::Block(no)) => Some(JsElse::Block(self.hoist_block_to_js_block( - no, - &temporary.get(&self), - did_assign, - ))), - Some(BlockOrIf::If(else_if)) => { - // If the condition of this block has a side effect, we need to - // emit a block so that we can assign to the temporary variable. - // - // Otherwise, we can just emit the condition inline - if else_if.condition.is_valid_js_expression() { - let condition = convert_valid_expr(self, *else_if.condition.clone()); - Some(JsElse::If(Box::new(self.hoist_js_if( - else_if.yes, - else_if.no, - condition, - did_assign, - temporary, - )))) - } else { - let mut temp_block = self.new_child(); - let condition = temp_block - .hoist_expression_to_temporary(*else_if.condition.clone()) - .unwrap(); - - temp_block.add_statement(JsStatement::new( - JsStatementData::If(self.hoist_js_if( - else_if.yes, - else_if.no, - condition, - did_assign, - temporary, - )), - Loc::EMPTY, - None, - )); - - tree_shake_statement_list(&mut temp_block.statements); - - Some(JsElse::Block(JsBlock::new( - temp_block.statements, - Loc::EMPTY, - ))) - } - } - None => None, - }; - - JsIf { condition, yes, no } - } -} - -pub fn new_js_assignment_statement(expr: JsExpr, target: String, loc: Loc) -> JsStatement { - JsStatement::new( - JsStatementData::Expression(new_js_assignment_expr(expr, target, loc.clone())), - loc, - None, - ) -} - -pub fn new_js_assignment_expr(expr: JsExpr, target: String, loc: Loc) -> JsExpr { - JsExpr::new( - JsExprData::Binary { - op: BinaryOperator::Assign, - left: Box::new(JsExpr::new( - JsExprData::Identifier(target), - loc.clone(), - None, - )), - right: Box::new(expr), - }, - loc, - None, - ) -} - -/// Remove -/// - Discarded literals -pub fn tree_shake_statement_list(list: &mut Vec) { - let mut set_i = 0; - for i in 0..list.len() { - let stmt = list.get(i).unwrap(); - let should_keep = match &stmt.data { - JsStatementData::Expression(expr) => !expr.is_literal(), - JsStatementData::Return(_) => true, - JsStatementData::Export(_) => true, - JsStatementData::Declaration(_) => true, - JsStatementData::Function(_) => true, - JsStatementData::If(_) => true, - // - }; - - if should_keep { - if set_i != i { - list.swap(i, set_i) - } - set_i += 1; - } - } - list.truncate(set_i) -} diff --git a/crates/js_ast/lib.rs b/crates/js_ast/lib.rs index c915dc7b4c9d8e4c28d637b834f070ec2164e7b7..9d29be2c29a04ae7f9bc7701413e57ec70196e62 100644 --- a/crates/js_ast/lib.rs +++ b/crates/js_ast/lib.rs @@ -1,28 +1,7 @@ -mod convert_valid_expr; -mod has_side_effects; -mod hoist; -mod is_valid_js_expression; +mod convert; mod js_ast; mod util; -use ast::Module; - -pub use convert_valid_expr::*; -pub use hoist::*; -pub use is_valid_js_expression::*; +pub use convert::*; pub use js_ast::*; pub use util::*; - -pub fn blight_to_js(blight: Module) -> JsModule { - let mut root_context = HoistContext::new(); - - for statement in blight.statements { - if let None = root_context.process_statement(statement.to_statement()) { - unreachable!("top level return is disallowed by the parser") - }; - } - - JsModule { - statements: root_context.statements, - } -} diff --git a/crates/js_printer/lib.rs b/crates/js_printer/lib.rs index 4ea0d8c03ae395ff34e563a248dbeceaefce9b55..b90a4a2850196ea232831af66d36017ead017334 100644 --- a/crates/js_printer/lib.rs +++ b/crates/js_printer/lib.rs @@ -231,6 +231,10 @@ impl Print for JsExprData { body.print(writer) } JsExprData::Ternary { condition, yes, no } => { + let wrap = false; + if wrap { + write!(writer, "(")?; + } condition.print(writer)?; write!(writer, "?")?; yes.print(writer)?; @@ -240,15 +244,25 @@ impl Print for JsExprData { } else { write!(writer, "null")?; } + if wrap { + write!(writer, ")")?; + } Ok(()) } JsExprData::Comma { items } => { + let wrap = true; + if wrap { + write!(writer, "(")?; + } for (i, item) in items.iter().enumerate() { if i > 0 { write!(writer, ",")?; } item.print(writer)?; } + if wrap { + write!(writer, ")")?; + } Ok(()) } JsExprData::Void(expr) => { diff --git a/crates/js_printer/src/lib.rs b/crates/js_printer/src/lib.rs deleted file mode 100644 index 49f3aa65ab7579c2a6e9a4b0470a4bc256e51557..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod print_expression; -mod print_statement; - -use print_statement::print_statement; - -use js_ast::JsStatement; - -pub fn print(forest: Vec) -> String { - let mut result = String::new(); - - for tree in forest { - result.push_str(&print_statement(&tree)); - } - - result -} diff --git a/crates/js_printer/src/print_expression.rs b/crates/js_printer/src/print_expression.rs deleted file mode 100644 index a27468c1c49ea1ee5ce13fd99450b645fe3fdd16..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/print_expression.rs +++ /dev/null @@ -1,276 +0,0 @@ -use js_ast::{JsExpr, JsExprData}; - -use crate::print_statement::print_statement; - -pub fn print_expression(expr: &JsExpr) -> String { - match &expr.data { - JsExprData::Raw(value) => format!("{}", value), - JsExprData::Number(value) => format!("{}", value), - JsExprData::BigInt(value) => format!("{}n", value), - JsExprData::String(value) => { - let mut result = String::new(); - // TODO: This is an INCREDIBLY naive implementation of escaping. - // Also it's probably not even correct. - for char in value.chars() { - match char { - '\n' => result.push_str("\\n"), - '\r' => result.push_str("\\r"), - '\t' => result.push_str("\\t"), - '"' => result.push_str("\\\""), - '\\' => result.push_str("\\\\"), - _ => result.push(char), - } - } - format!("\"{}\"", result) - } - JsExprData::Bool(value) => format!("{}", value), - JsExprData::Identifier(value) => format!("{}", value), - JsExprData::Array(value) => format!( - "[{}]", - value - .iter() - .map(|x| print_expression(x)) - .collect::>() - .join(",") - ), - JsExprData::Object(value) => format!( - "{{{}}}", - value - .iter() - .map(|entry| format!( - "{}:{}", - print_expression(&entry.key), - print_expression(&entry.value) - )) - .collect::>() - .join(",") - ), - JsExprData::Binary { - op: operator, - left, - right, - } => format!( - "{}{}{}", - print_expression(&left), - operator.clone().to_string(), - print_expression(&right) - ), - // JsExprData::Function { - // arguments, - // value, - // name: _, - // params: _, - // block, - // } => format!( - // "({})=>{{{}}}", - // arguments.join(","), - // value - // .iter() - // .map(|x| format!("{}", print_statement(x))) - // .collect::>() - // .join("") - // ), - JsExprData::Call { base, arguments } => format!( - "{}({})", - print_expression(&base), - arguments - .iter() - .map(|x| print_expression(x)) - .collect::>() - .join(",") - ), - JsExprData::Null => todo!(), - JsExprData::Undefined => todo!(), - JsExprData::Unary { - op: operator, - value, - } => todo!(), - JsExprData::Dot { base, name } => todo!(), - JsExprData::Index { base, index } => todo!(), - JsExprData::Function { - name, - params, - block, - } => todo!(), - JsExprData::Ternary { condition, yes, no } => todo!(), - JsExprData::ArrowFunction { params, block } => todo!(), - JsExprData::Comma { items } => todo!(), - JsExprData::Void(_) => todo!(), - } -} - -#[cfg(test)] -mod tests { - use crate::print_expression::print_expression; - use js_ast::JsExprData; - use js_ast::JsStatement; - use num::{bigint::Sign, BigInt}; - use std::collections::HashMap; - - #[test] - fn number() { - assert_eq!(print_expression(&JsExprData::Number(1.0)), "1"); - assert_eq!(print_expression(&JsExprData::Number(1.5)), "1.5"); - assert_eq!(print_expression(&JsExprData::Number(1.5e10)), "15000000000"); - assert_eq!( - print_expression(&JsExprData::Number(1.5e-10)), - "0.00000000015" - ); - } - - #[test] - fn bigint() { - assert_eq!( - print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![1]))), - "1n" - ); - assert_eq!( - print_expression(&JsExprData::BigInt(BigInt::new(Sign::Plus, vec![10000000]))), - "10000000n" - ); - assert_eq!( - print_expression(&JsExprData::BigInt(BigInt::new( - Sign::Minus, - vec![10000000] - ))), - "-10000000n" - ); - } - - #[test] - fn string() { - assert_eq!( - print_expression(&JsExprData::String("".to_string())), - "\"\"" - ); - assert_eq!( - print_expression(&JsExprData::String("hello".to_string())), - "\"hello\"" - ); - assert_eq!( - print_expression(&JsExprData::String("hello\nworld".to_string())), - "\"hello\\nworld\"" - ); - } - - #[test] - fn bool() { - assert_eq!(print_expression(&JsExprData::Bool(true)), "true"); - assert_eq!(print_expression(&JsExprData::Bool(false)), "false"); - } - - #[test] - fn identifier() { - assert_eq!( - print_expression(&JsExprData::Identifier("hello".to_string())), - "hello" - ); - } - - #[test] - fn array() { - assert_eq!(print_expression(&JsExprData::Array(vec![])), "[]"); - assert_eq!( - print_expression(&JsExprData::Array(vec![JsExprData::Number(1.0)])), - "[1]" - ); - assert_eq!( - print_expression(&JsExprData::Array(vec![ - JsExprData::Number(1.0), - JsExprData::Number(2.0) - ])), - "[1,2]" - ); - } - - #[test] - fn object() { - assert_eq!(print_expression(&JsExprData::Object(HashMap::new())), "{}"); - let mut map = HashMap::new(); - map.insert("hello".to_string(), JsExprData::Number(1.0)); - assert_eq!(print_expression(&JsExprData::Object(map)), "{\"hello\":1}"); - } - - #[test] - fn operation() { - assert_eq!( - print_expression(&JsExprData::Binary { - op: AnyOperator::Add, - left: Box::new(JsExprData::Number(1.0)), - right: Box::new(JsExprData::Number(2.0)), - }), - "1+2" - ); - assert_eq!( - print_expression(&JsExprData::Binary { - op: AnyOperator::Sub, - left: Box::new(JsExprData::Number(1.0)), - right: Box::new(JsExprData::Number(2.0)), - }), - "1-2" - ); - } - - #[test] - fn function() { - use crate::print_expression::print_expression; - use js_ast::JsExprData; - - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec![], - value: vec![], - }), - "()=>{}" - ); - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec!["a".to_string()], - value: vec![], - }), - "(a)=>{}" - ); - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec!["a".to_string(), "b".to_string()], - value: vec![], - }), - "(a,b)=>{}" - ); - assert_eq!( - print_expression(&JsExprData::Function { - arguments: vec!["a".to_string(), "b".to_string()], - value: vec![JsStatement::Declaration { - name: "a".to_string(), - value: JsExprData::Number(1.0) - }], - }), - "(a,b)=>{let a=1;}" - ); - } - - #[test] - fn function_call() { - assert_eq!( - print_expression(&JsExprData::Call { - name: Box::new(JsExprData::Identifier("hello".to_string())), - arguments: vec![], - }), - "hello()" - ); - assert_eq!( - print_expression(&JsExprData::Call { - name: Box::new(JsExprData::Identifier("hello".to_string())), - arguments: vec![JsExprData::Number(1.0)], - }), - "hello(1)" - ); - assert_eq!( - print_expression(&JsExprData::Call { - name: Box::new(JsExprData::Identifier("hello".to_string())), - arguments: vec![JsExprData::Number(1.0), JsExprData::Number(2.0)], - }), - "hello(1,2)" - ); - } -} diff --git a/crates/js_printer/src/print_statement.rs b/crates/js_printer/src/print_statement.rs deleted file mode 100644 index c7d78693136d06b23070c35e9b55ab1dfa70e90f..0000000000000000000000000000000000000000 --- a/crates/js_printer/src/print_statement.rs +++ /dev/null @@ -1,153 +0,0 @@ -use js_ast::{If, JSDeclaration, JSFunctionDeclaration, JsStatement, JsStatementData}; - -use crate::print_expression::print_expression; - -pub fn print_statement(statement: &JsStatement) -> String { - match &statement.data { - JsStatementData::Expression(value) => format!("{};", print_expression(&value)), - // JsStatementData::Export(value) => format!("export {}", print_statement(value)), - JsStatementData::Export(value) => todo!(), - JsStatementData::Return(value) => format!("return {};", print_expression(&value)), - JsStatementData::Declaration(Declaration { - name, - init, - is_constant, - }) => { - format!( - "{} {}={};", - match is_constant { - true => "const", - false => "let", - }, - name, - print_expression(&init) - ) - } - JsStatementData::Function(JSFunctionDeclaration { - name, - block, - params, - }) => format!( - "function {}({}){{{}}}", - name, - params.join(","), - block - .statements - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - JsStatementData::If(JsIf { condition, yes, no }) => { - let else_value = match no { - Some(value) => format!( - "else{{{}}}", - value - .statements - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join("") - ), - None => "".to_string(), - }; - format!( - "if({}){{{}}}{}", - print_expression(&condition), - yes.statements - .iter() - .map(|x| format!("{}", print_statement(x))) - .collect::>() - .join(""), - else_value - ) - } - } -} - -#[cfg(test)] -mod tests { - use crate::print_statement::print_statement; - use js_ast::JsExpression; - use js_ast::JsStatement; - - #[test] - fn variable_declaration() { - assert_eq!( - print_statement(&JsStatement::Declaration { - name: "x".to_string(), - value: JsExpression::Number(1.0), - }), - "let x=1;" - ); - } - - #[test] - fn constant_declaration() { - assert_eq!( - print_statement(&JsStatement::ConstantDeclaration { - name: "x".to_string(), - value: JsExpression::Number(1.0), - }), - "const x=1;" - ); - } - - #[test] - fn function_declaration() { - assert_eq!( - print_statement(&JsStatement::Function { - name: "x".to_string(), - arguments: vec!["y".to_string()], - value: vec![JsStatement::Expression(JsExpression::Number(1.0))], - }), - "function x(y){1;}" - ); - } - - #[test] - fn expression() { - assert_eq!( - print_statement(&JsStatement::Expression(JsExpression::Number(1.0))), - "1;" - ); - } - - #[test] - fn export() { - assert_eq!( - print_statement(&JsStatement::Export(Box::new(JsStatement::Expression( - JsExpression::Number(1.0) - )))), - "export 1;" - ); - } - - #[test] - fn return_statement() { - assert_eq!( - print_statement(&JsStatement::Return(JsExpression::Number(1.0))), - "return 1;" - ); - } - - #[test] - fn if_statement() { - assert_eq!( - print_statement(&JsStatement::If { - condition: JsExpression::Number(1.0), - value: vec![JsStatement::Expression(JsExpression::Number(2.0))], - else_value: None, - }), - "if(1){2;}" - ); - assert_eq!( - print_statement(&JsStatement::If { - condition: JsExpression::Number(1.0), - value: vec![JsStatement::Expression(JsExpression::Number(2.0))], - else_value: Some(vec![JsStatement::Expression(JsExpression::Number(3.0))]), - }), - "if(1){2;}else{3;}" - ); - } -} diff --git a/crates/parser/parse_block.rs b/crates/parser/parse_block.rs index a9ad16ed402ac0509be65f1a95ce8a4bd76ec980..4ae805a759d0dd7b58c6642c94dc83e5c821c9e4 100644 --- a/crates/parser/parse_block.rs +++ b/crates/parser/parse_block.rs @@ -12,9 +12,6 @@ pub fn parse_block(p: &mut Parser) -> Result { let mut statements = Vec::new(); - let was_top_level = p.is_top_level; - p.is_top_level = false; - 'outer: loop { let is_statement_like_expression = p .peek() @@ -23,7 +20,6 @@ pub fn parse_block(p: &mut Parser) -> Result { let statement = match parse_statement(p) { Ok(statement) => statement, Err(err) => { - p.is_top_level = was_top_level; return Err(err); } }; @@ -35,7 +31,6 @@ pub fn parse_block(p: &mut Parser) -> Result { match p.peek() { Some(Token::ClosedBrace) => { p.next(); - p.is_top_level = was_top_level; return Ok(Block { statements, has_implicit_return: false, @@ -46,14 +41,12 @@ pub fn parse_block(p: &mut Parser) -> Result { }, Some(Token::ClosedBrace) => { p.next(); - if statement.require_semicolon(is_statement_like_expression) { - p.is_top_level = was_top_level; + if !statement.is_expression() { return Err(p .error("Expected semicolon or '}' after statement".to_string()) .into()); } - p.is_top_level = was_top_level; return Ok(Block { statements, has_implicit_return: true, @@ -61,7 +54,6 @@ pub fn parse_block(p: &mut Parser) -> Result { } _ => { if statement.require_semicolon(is_statement_like_expression) { - p.is_top_level = was_top_level; return Err(p .error("Expected semicolon or '}' after statement".to_string()) .into()); diff --git a/crates/parser/parse_declaration.rs b/crates/parser/parse_declaration.rs index 9e5176ed38fa2d414c15e048774d8012b62a0f49..80f140076bb711e3fdf03716bbccd416096a0c35 100644 --- a/crates/parser/parse_declaration.rs +++ b/crates/parser/parse_declaration.rs @@ -1,4 +1,4 @@ -use ast::{Decl, DocComment, Type}; +use ast::{DocComment, Type, VarDecl}; use miette::Result; use tokenizer::Token; @@ -11,7 +11,7 @@ pub fn parse_declaration( p: &mut Parser, is_pub: bool, comment: Option, -) -> Result { +) -> Result { let loc = p.loc(); let is_constant = match p.next() { Some(Token::Var) => false, @@ -38,7 +38,7 @@ pub fn parse_declaration( .into()) } }; - Ok(Decl { + Ok(VarDecl { name: name.clone(), data_type: return_type, init, diff --git a/crates/parser/parse_expr_basic.rs b/crates/parser/parse_expr_basic.rs index d1949c82fc65ad390e482f92707eb95b24415346..efcf48fb65ecc9b0f8c8512e682ddfe7aab3d96b 100644 --- a/crates/parser/parse_expr_basic.rs +++ b/crates/parser/parse_expr_basic.rs @@ -63,7 +63,7 @@ pub fn parse_expr_basic(p: &mut Parser) -> Result { } Some(Token::Return) => { - if p.is_top_level { + if p.allow_return { return Err(p .error("'return' cannot be used at top level".to_string()) .into()); diff --git a/crates/parser/parse_fn_declaration.rs b/crates/parser/parse_fn_declaration.rs index 32b4e3cff41688b8ee04d9c6e2cba54d01c64291..c1b576cec8709710e413f4f1ae9726d6ba80635f 100644 --- a/crates/parser/parse_fn_declaration.rs +++ b/crates/parser/parse_fn_declaration.rs @@ -1,8 +1,9 @@ -use ast::{Decl, DocComment, Expr, ExprData, Type}; +use ast::{DocComment, Expr, ExprData, FnDecl, Type}; use miette::Result; use tokenizer::Token; use crate::parse_block::parse_block; +use crate::parse_expr::parse_expr; use crate::parse_param::parse_param; use crate::parse_type::parse_type; use crate::Parser; @@ -12,7 +13,7 @@ pub fn parse_fn_declaration( p: &mut Parser, is_pub: bool, comment: Option, -) -> Result { +) -> Result { let loc = p.loc(); match p.next() { Some(Token::Fn) => (), @@ -57,9 +58,13 @@ pub fn parse_fn_declaration( break; } _ => { + let actual = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); return Err(p - .error("Expected ',' or ')' after identifier".to_string()) - .into()) + .error(format!( + "Expected ',' or ')' after identifier, got {}", + actual + )) + .into()); } }; } @@ -69,19 +74,6 @@ pub fn parse_fn_declaration( .into()) } } - - match p.peek() { - Some(Token::Comma) => p.next(), - Some(Token::ClosedParen) => { - _ = p.next(); - break; - } - _ => { - return Err(p - .error("Expected ',' or ')' after identifier".to_string()) - .into()) - } - }; } let return_type = match p.peek() { @@ -92,31 +84,39 @@ pub fn parse_fn_declaration( _ => Type::Infer, }; - if let Some(Token::OpenBrace) = p.peek() { - let block_loc = p.loc(); - let block = parse_block(p)?; - let function_body = ExprData::Fn { - name: name.clone(), - params, - return_type, - block, - }; - Ok(Decl { - name, - data_type: Type::Any, - init: Expr::new(function_body, block_loc, None), - is_constant: true, - is_pub, - comment, - loc: loc, - }) - } else { - let actually = p.peek().map_or("EOF".to_string(), |x| x.to_string()); - return Err(p - .error(format!( - "Expected a block to follow function declaration, but got {}", - actually, - )) - .into()); - } + let was_allow_return = p.allow_return; + p.allow_return = true; + + let body = match p.peek() { + Some(Token::OpenBrace) => { + let block_loc = p.loc(); + let block = parse_block(p)?; + Expr::new(ExprData::Block(block), block_loc, None) + } + Some(Token::BigRightArrow) => { + _ = p.next(); + parse_expr(p)? + } + _ => { + let actually = p.peek().map_or("EOF".to_string(), |x| format!("{:?}", x)); + return Err(p + .error(format!( + "Expected a block or '=>' to follow function declaration, but got {}", + actually, + )) + .into()); + } + }; + + p.allow_return = was_allow_return; + + Ok(FnDecl { + name, + return_type, + params, + body, + is_pub, + comment, + loc, + }) } diff --git a/crates/parser/parse_statement.rs b/crates/parser/parse_statement.rs index a135c1d4e6de8d3d17c0f0b3d7b03fa3f62c4859..537f9512571cf182854ce26e549ded5c65d729be 100644 --- a/crates/parser/parse_statement.rs +++ b/crates/parser/parse_statement.rs @@ -28,10 +28,12 @@ pub fn parse_statement(p: &mut Parser) -> Result { Some(Token::Pub) => { _ = p.next(); match p.peek() { - Some(Token::Var) | Some(Token::Let) => { - Ok(Statement::Decl(parse_declaration(p, true, m_doc_comment)?)) - } - Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration( + Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( + p, + true, + m_doc_comment, + )?)), + Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( p, true, m_doc_comment, @@ -40,10 +42,12 @@ pub fn parse_statement(p: &mut Parser) -> Result { None => Err(p.error("Unexpected end of file".to_string()).into()), } } - Some(Token::Var) | Some(Token::Let) => { - Ok(Statement::Decl(parse_declaration(p, false, m_doc_comment)?)) - } - Some(Token::Fn) => Ok(Statement::Decl(parse_fn_declaration( + Some(Token::Var) | Some(Token::Let) => Ok(Statement::VarDecl(parse_declaration( + p, + false, + m_doc_comment, + )?)), + Some(Token::Fn) => Ok(Statement::FnDecl(parse_fn_declaration( p, false, m_doc_comment, diff --git a/crates/parser/parser.rs b/crates/parser/parser.rs index 44f7cdd6f8750dee21ec88a0c746afc2b60f03ae..465ce180e9bdd2c388361825407ee0d14bedbf05 100644 --- a/crates/parser/parser.rs +++ b/crates/parser/parser.rs @@ -53,7 +53,7 @@ pub struct Parser { cursor: usize, /// Whether or not the parser is at the top level - pub is_top_level: bool, + pub allow_return: bool, comments: Vec, } @@ -63,7 +63,7 @@ impl Parser { Parser { tokens, cursor: 0, - is_top_level: true, + allow_return: false, comments: vec![], } } diff --git a/crates/tokenizer/tokenizer.rs b/crates/tokenizer/tokenizer.rs index aee421347f0e4a1a7a044043c4748acaadfe5360..06f2495729efddae8d1a4a3945f2805341d42201 100644 --- a/crates/tokenizer/tokenizer.rs +++ b/crates/tokenizer/tokenizer.rs @@ -5,6 +5,7 @@ use thiserror::Error; pub struct Loc {} impl Loc { + /// TODO(@paperdave): Remove this. every usage of this is incorrect. pub const EMPTY: Loc = Loc {}; pub fn new() -> Self { @@ -44,12 +45,14 @@ pub enum Token { AndAnd, // && Bang, // ! BangEqual, // != + BigRightArrow, // => Caret, // ^ ClosedBrace, // } ClosedBracket, // ] ClosedParen, // ) Colon, // : Comma, // , + Dot, // . Equal, // = EqualEqual, // == Gt, // > @@ -73,14 +76,13 @@ pub enum Token { Star, // * StarStar, // ** Tilde, // ~ - Dot, // . } impl Token { pub fn is_start_of_statement_like_expression(&self) -> bool { match &self { - Token::If | Token::OpenBrace => false, - _ => true, + Token::If | Token::OpenBrace => true, + _ => false, } } } @@ -110,17 +112,20 @@ impl std::fmt::Display for Token { Token::AndAnd => "&&".to_string(), Token::Bang => "!".to_string(), Token::BangEqual => "!=".to_string(), + Token::BigRightArrow => "=>".to_string(), Token::Caret => "^".to_string(), Token::ClosedBrace => "}".to_string(), Token::ClosedBracket => "]".to_string(), Token::ClosedParen => ")".to_string(), Token::Colon => ":".to_string(), Token::Comma => ",".to_string(), + Token::Dot => ".".to_string(), Token::Equal => "=".to_string(), Token::EqualEqual => "==".to_string(), Token::Gt => ">".to_string(), Token::GtEqual => ">=".to_string(), Token::GtGt => ">>".to_string(), + Token::GtGtGt => ">>>".to_string(), Token::Lt => "<".to_string(), Token::LtEqual => "<=".to_string(), Token::LtLt => "<<".to_string(), @@ -138,8 +143,6 @@ impl std::fmt::Display for Token { Token::Star => "*".to_string(), Token::StarStar => "**".to_string(), Token::Tilde => "~".to_string(), - Token::Dot => ".".to_string(), - Token::GtGtGt => ">>>".to_string(), }; write!(f, "{}", token) @@ -234,6 +237,10 @@ pub fn tokenize(content: String) -> Result> { chars.next(); tokens.push(Token::EqualEqual); } + Some((_, '>')) => { + chars.next(); + tokens.push(Token::BigRightArrow); + } _ => tokens.push(Token::Equal), }, ';' => tokens.push(Token::Semicolon), diff --git a/examples/basics/stupid.blight b/examples/basics/stupid.blight new file mode 100644 index 0000000000000000000000000000000000000000..4144819ae054ff63f524006f9d3c00e2e4793ba8 --- /dev/null +++ b/examples/basics/stupid.blight @@ -0,0 +1,2 @@ + +fn add(x: int, y: int) => x + y -- 2.54.0