diff --git a/.gitignore b/.gitignore index ad67955..7e6aa69 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,10 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# Added by cargo + +/target + +/test/build \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d08efc2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,241 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mist" +version = "0.1.0" +dependencies = [ + "pest", + "pest_derive", + "serde", + "serde_json", +] + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3f1f1f9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mist" +version = "0.1.0" +edition = "2024" + +[dependencies] +pest = "2.8.6" +pest_derive = "2.8.6" +serde = { version = "1.0.228", features = ["serde_derive"] } +serde_json = "1.0.149" diff --git a/src/compiler.rs b/src/compiler.rs new file mode 100644 index 0000000..7091a9a --- /dev/null +++ b/src/compiler.rs @@ -0,0 +1,95 @@ +use std::{fs, path::PathBuf, process, time::Instant}; + +use crate::{gogen, parser}; + +use serde::Deserialize; + +#[derive(Deserialize)] +struct Config { + entry: String, + out_dir: String, +} + +pub fn build() { + let start = Instant::now(); + + // 1. find project root + let root = find_project_root().unwrap_or_else(|| { + panic!("error: could not find project root (mist.json)"); + }); + + println!("mistc build ({})", root.display()); + + // 2. load config + let config = load_config(&root); + + let entry_path = root.join(&config.entry); + let out_dir = root.join(&config.out_dir); + + println!(" → entry: {}", entry_path.display()); + + // 3. read entry file + let source = match fs::read_to_string(&entry_path) { + Ok(s) => s, + Err(e) => { + eprintln!("error: failed to read entry file\n {}", e); + process::exit(1); + } + }; + + println!(" → parsing..."); + + let ast = match parser::parse(&source) { + Ok(ast) => { + println!(" ✓ parsed {} items", ast.statements.len()); + ast + } + Err(e) => { + eprintln!("error: parse failed\n{}", e); + process::exit(1); + } + }; + + println!(" → generating Go code..."); + + let output = gogen::generate(&ast); + + // 4. ensure build dir + if let Err(e) = fs::create_dir_all(&out_dir) { + eprintln!("error: failed to create build dir\n {}", e); + process::exit(1); + } + + let out_file = out_dir.join("main.go"); + + if let Err(e) = fs::write(&out_file, output) { + eprintln!("error: failed to write output\n {}", e); + process::exit(1); + } + + let elapsed = start.elapsed(); + + println!(" ✓ built {}", out_file.display()); + println!("build finished in {:.2?}", elapsed); +} + +pub fn find_project_root() -> Option { + let mut dir = std::env::current_dir().ok()?; + + loop { + if dir.join("mist.json").exists() { + return Some(dir); + } + + if !dir.pop() { + return None; + } + } +} + +fn load_config(root: &std::path::Path) -> Config { + let content = + std::fs::read_to_string(root.join("mist.json")).expect("failed to read mist.json"); + + serde_json::from_str(&content).expect("invalid mist.json format") +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/gogen/mod.rs b/src/gogen/mod.rs new file mode 100644 index 0000000..4402e36 --- /dev/null +++ b/src/gogen/mod.rs @@ -0,0 +1,283 @@ +use crate::parser::ast::{ + BinOperator, Class, Expression, Function, Program, Statement, Struct, TopLevel, TypeExpr, +}; + +pub fn generate(program: &Program) -> String { + let mut out = String::new(); + + out.push_str("package main\n\n"); + + // imports + let mut imports = vec![]; + for item in &program.statements { + if let TopLevel::Import(i) = item { + imports.push(i.path.clone()); + } + } + + if !imports.is_empty() { + out.push_str("import (\n"); + for imp in imports { + out.push_str(&format!(" {}\n", imp)); + } + out.push_str(")\n\n"); + } + + // rest + for item in &program.statements { + match item { + TopLevel::Function(f) => out.push_str(&gen_function(f)), + TopLevel::Struct(s) => out.push_str(&gen_struct(s)), + TopLevel::Class(c) => out.push_str(&gen_class(c)), + TopLevel::Import(_) => {} + } + out.push('\n'); + } + + out +} + +fn gen_struct(s: &Struct) -> String { + let mut out = format!("type {} struct {{\n", s.name); + + for field in &s.fields { + out.push_str(&format!( + " {} {}\n", + capitalize(&field.name), + gen_type(&field.type_expr) + )); + } + + out.push_str("}\n"); + out +} + +fn gen_class(c: &Class) -> String { + let mut out = String::new(); + + // struct + out.push_str(&format!("type {} struct {{\n", c.name)); + for field in &c.fields { + out.push_str(&format!( + " {} {}\n", + capitalize(&field.name), + gen_type(&field.type_expr) + )); + } + out.push_str("}\n\n"); + + // methods + for method in &c.methods { + out.push_str(&gen_method(c, method)); + out.push('\n'); + } + + out +} + +fn gen_function(f: &Function) -> String { + let mut out = format!("func {}(", f.name); + + // params + for (i, p) in f.params.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push_str(&format!("{} {}", p.name, gen_type(&p.type_expr))); + } + + out.push(')'); + + // return + if let Some(ret) = &f.return_type { + out.push_str(&format!(" {}", gen_type(ret))); + } + + out.push_str(" {\n"); + + for stmt in &f.body { + out.push_str(&gen_statement(stmt)); + } + + out.push_str("}\n"); + out +} + +fn gen_method(class: &Class, f: &Function) -> String { + let mut out = format!("func (self *{}) {}(", class.name, f.name); + + for (i, p) in f.params.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push_str(&format!("{} {}", p.name, gen_type(&p.type_expr))); + } + + out.push(')'); + + if let Some(ret) = &f.return_type { + out.push_str(&format!(" {}", gen_type(ret))); + } + + out.push_str(" {\n"); + + for stmt in &f.body { + out.push_str(&gen_statement(stmt)); + } + + out.push_str("}\n"); + + out +} + +fn gen_statement(stmt: &Statement) -> String { + match stmt { + Statement::Let(s) => { + let mut out = format!(" {} := {}", s.name, gen_expr(&s.value)); + + out.push_str(";\n"); + out + } + + Statement::Return(r) => match &r.value { + Some(v) => format!(" return {};\n", gen_expr(v)), + None => " return;\n".to_string(), + }, + + Statement::Expression(e) => { + format!(" {};\n", gen_expr(e)) + } + + Statement::If(i) => { + let mut out = format!(" if {} {{\n", gen_expr(&i.condition)); + + for stmt in &i.body { + out.push_str(&gen_statement(stmt)); + } + + out.push_str(" }"); + + if let Some(else_body) = &i.else_body { + out.push_str(" else {\n"); + for stmt in else_body { + out.push_str(&gen_statement(stmt)); + } + out.push_str(" }"); + } + + out.push('\n'); + out + } + + Statement::For(f) => { + let mut out = format!( + " for _, {} := range {} {{\n", + f.var, + gen_expr(&f.iterator) + ); + + for stmt in &f.body { + out.push_str(&gen_statement(stmt)); + } + + out.push_str(" }\n"); + out + } + } +} + +fn gen_expr(expr: &Expression) -> String { + match expr { + Expression::Identifier(name, _) => name.clone(), + Expression::Integer(v, _) => v.to_string(), + Expression::Float(v, _) => v.to_string(), + Expression::StringLit(s, _) => format!("\"{}\"", s), + Expression::Bool(b, _) => b.to_string(), + + Expression::BinaryOp(b) => format!( + "{} {} {}", + gen_expr(&b.left), + op_to_str(&b.op), + gen_expr(&b.right) + ), + + Expression::Call(c) => { + let args = c.args.iter().map(gen_expr).collect::>().join(", "); + format!("{}({})", gen_expr(&c.callee), args) + } + + Expression::FieldAccess(f) => { + format!("{}.{}", gen_expr(&f.object), capitalize(&f.field)) + } + + Expression::StructInit(s) => { + let mut out = format!("{}{{", s.name); + + for (i, (name, val)) in s.fields.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push_str(&format!("{}: {}", capitalize(name), gen_expr(val))); + } + + out.push('}'); + out + } + + Expression::ArrayLiteral(arr) => { + let elems = arr + .elements + .iter() + .map(gen_expr) + .collect::>() + .join(", "); + format!("[]any{{{}}}", elems) // simple version + } + + _ => todo!(), + } +} + +fn gen_type(t: &TypeExpr) -> String { + match t { + TypeExpr::Named(n) => match n.as_str() { + "int" => "int".into(), + "float" => "float64".into(), + "string" => "string".into(), + _ => n.clone(), + }, + + TypeExpr::Array(inner) => { + format!("[]{}", gen_type(inner)) + } + + TypeExpr::Optional(inner) => { + format!("*{}", gen_type(inner)) // pointer for optional + } + } +} + +fn capitalize(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + Some(c) => c.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } +} + +fn op_to_str(op: &BinOperator) -> &'static str { + match op { + BinOperator::Add => "+", + BinOperator::Sub => "-", + BinOperator::Mul => "*", + BinOperator::Div => "/", + BinOperator::Eq => "==", + BinOperator::NotEq => "!=", + BinOperator::Lt => "<", + BinOperator::Gt => ">", + BinOperator::LtEq => "<=", + BinOperator::GtEq => ">=", + BinOperator::And => "&&", + BinOperator::Or => "||", + } +} diff --git a/src/gogen/sourcemap.rs b/src/gogen/sourcemap.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..56fd64a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,91 @@ +pub mod compiler; +pub mod errors; +pub mod gogen; +pub mod parser; + +use std::fs; +use std::path::PathBuf; +use std::process; + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() < 2 { + print_usage(); + process::exit(1); + } + + match args[1].as_str() { + "build" => { + if args.len() < 2 { + eprintln!("error: expected a file path\n usage: mist build"); + process::exit(1); + } + cmd_build(); + } + "check" => { + if args.len() < 3 { + eprintln!("error: expected a file path\n usage: mist check "); + process::exit(1); + } + cmd_check(&args[2]); + } + "version" | "--version" | "-v" => { + println!("mist {}", env!("CARGO_PKG_VERSION")); + } + "help" | "--help" | "-h" => { + print_usage(); + } + unknown => { + eprintln!("error: unknown command '{}'\n", unknown); + print_usage(); + process::exit(1); + } + } +} + +pub fn cmd_build() { + compiler::build(); +} + +fn cmd_check(path: &str) { + let source = read_ms_file(path); + match parser::parse(&source) { + Ok(_) => { + println!("ok"); + } + Err(e) => { + eprintln!("parse error:\n{}", e); + process::exit(1); + } + } +} + +fn read_ms_file(path: &str) -> String { + let pb = PathBuf::from(path); + + if !pb.exists() { + eprintln!("error: file '{}' not found", path); + process::exit(1); + } + + if pb.extension().and_then(|e| e.to_str()) != Some("ms") { + eprintln!("error: expected a .ms file, got '{}'", path); + process::exit(1); + } + + fs::read_to_string(&pb).unwrap_or_else(|e| { + eprintln!("error: could not read '{}': {}", path, e); + process::exit(1); + }) +} + +fn print_usage() { + println!("mist - the mist compiler"); + println!(); + println!("usage:"); + println!(" mist build compile the project in the current directory"); + println!(" mist check parse and validate without compiling"); + println!(" mist version print the compiler version"); + println!(" mist help print this message"); +} diff --git a/src/parser/ast.rs b/src/parser/ast.rs new file mode 100644 index 0000000..057fac0 --- /dev/null +++ b/src/parser/ast.rs @@ -0,0 +1,187 @@ +#[derive(Debug, Clone)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +#[derive(Debug, Clone)] +pub struct Program { + pub statements: Vec, +} + +#[derive(Debug, Clone)] +pub enum TopLevel { + Function(Function), + Struct(Struct), + Class(Class), + Import(Import), +} + +#[derive(Debug, Clone)] +pub struct Function { + pub name: String, + pub params: Vec, + pub return_type: Option, + pub body: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Param { + pub name: String, + pub type_expr: TypeExpr, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Struct { + pub name: String, + pub fields: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct StructField { + pub name: String, + pub type_expr: TypeExpr, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Class { + pub name: String, + pub fields: Vec, + pub methods: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Import { + pub path: String, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum Statement { + Let(LetStatement), + Return(ReturnStatement), + Expression(Expression), + If(IfStatement), + For(ForStatement), +} + +#[derive(Debug, Clone)] +pub struct LetStatement { + pub name: String, + pub type_expr: Option, + pub value: Expression, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct ReturnStatement { + pub value: Option, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct IfStatement { + pub condition: Expression, + pub body: Vec, + pub else_body: Option>, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct ForStatement { + pub var: String, + pub iterator: Expression, + pub body: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum Expression { + Identifier(String, Span), + Integer(i64, Span), + Float(f64, Span), + StringLit(String, Span), + Bool(bool, Span), + BinaryOp(Box), + UnaryOp(Box), + Call(Box), + FieldAccess(Box), + StructInit(Box), + ArrayLiteral(Box), +} + +#[derive(Debug, Clone)] +pub struct BinaryOp { + pub left: Expression, + pub op: BinOperator, + pub right: Expression, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum BinOperator { + Add, + Sub, + Mul, + Div, + Eq, + NotEq, + Lt, + Gt, + LtEq, + GtEq, + And, + Or, +} + +#[derive(Debug, Clone)] +pub struct UnaryOp { + pub op: UnaryOperator, + pub expr: Expression, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum UnaryOperator { + Neg, + Not, +} + +#[derive(Debug, Clone)] +pub struct CallExpr { + pub callee: Expression, + pub args: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct FieldAccess { + pub object: Expression, + pub field: String, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct StructInit { + pub name: String, + pub fields: Vec<(String, Expression)>, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum TypeExpr { + Named(String), + Array(Box), + Optional(Box), +} + +#[derive(Debug, Clone)] +pub struct ArrayLiteral { + pub elements: Vec, + pub span: Span, +} diff --git a/src/parser/grammar.pest b/src/parser/grammar.pest new file mode 100644 index 0000000..8a59ca2 --- /dev/null +++ b/src/parser/grammar.pest @@ -0,0 +1,163 @@ +WHITESPACE = _{ " " | "\t" | "\r" | "\n" } +COMMENT = _{ "//" ~ (!"\n" ~ ANY)* } + +program = { SOI ~ item* ~ EOI } +item = _{ import_decl | function_decl | struct_decl | class_decl } + +// primitives +identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } +integer = @{ ASCII_DIGIT+ } +float = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ } +boolean = { "true" | "false" } +string_lit = { "\"" ~ inner_str ~ "\"" } +inner_str = @{ (!"\"" ~ ANY)* } + +// ================= TYPE SCRIPT STYLE CHANGES ================= + +// import becomes TS-like (still identical structure) +import_decl = { "import" ~ string_lit ~ ";" } + +// struct stays, but feels like TS "type object shape" +struct_decl = { + "struct" ~ identifier ~ "{" ~ struct_field* ~ "}" +} + +// class already TS-like — just improved consistency +class_decl = { + "class" ~ identifier ~ "{" ~ class_member* ~ "}" +} + +class_member = _{ struct_field | function_decl } + +// function keyword changed feel: fn → function +function_decl = { + "function" ~ identifier ~ "(" ~ param_list? ~ ")" ~ ("->" ~ type_expr)? ~ block +} + +// struct fields: TS-style semicolon consistency (already good) +struct_field = { identifier ~ ":" ~ type_expr ~ ";" } + +// params: TS optional typing style feel (unchanged structure) +param_list = { param ~ ("," ~ param)* ~ ","? } +param = { identifier ~ ":" ~ type_expr } + +// ================= STATEMENTS ================= + +block = { "{" ~ statement* ~ "}" } + +statement = _{ + let_stmt + | return_stmt + | if_stmt + | for_stmt + | assign_stmt + | expression_stmt +} + +// expression statements: TS allows no semicolon in real life, +// but we keep your structure +expression_stmt = { expression ~ ";" } + +// assignment unchanged (JS/TS feel already) +assign_stmt = { identifier ~ "=" ~ expression ~ ";" } + +// let → const/let feel (still your rule, just TS naming) +let_stmt = { + "const" ~ identifier ~ (":" ~ type_expr)? ~ "=" ~ expression ~ ";" +} + +// return: TS-like (no change except feel consistency) +return_stmt = { "return" ~ expression? ~ ";" } + +// if: add TS-style parentheses (MAJOR feel upgrade, same structure) +if_stmt = { + "if" ~ "(" ~ expression ~ ")" ~ block ~ ("else" ~ block)? +} + +// for: TS-like "of" +for_stmt = { + "for" ~ identifier ~ "of" ~ expression ~ block +} + +// ================= TYPES (KEEP STRUCTURE, TS FEEL) ================= + +type_expr = { base_type ~ "?"? } +base_type = _{ array_type | identifier } +array_type = { "[" ~ type_expr ~ "]" } + +// ================= EXPRESSIONS ================= + +expression = { term ~ (bin_op ~ term)* } + +bin_op = _{ + eq + | neq + | lte + | gte + | lt + | gt + | and + | or + | add + | sub + | mul + | div +} + +add = { "+" } +sub = { "-" } +mul = { "*" } +div = { "/" } +eq = { "==" } +neq = { "!=" } +lte = { "<=" } +gte = { ">=" } +lt = { "<" } +gt = { ">" } +and = { "&&" } +or = { "||" } + +// ================= LITERALS ================= + +struct_literal = { + identifier ~ "{" ~ struct_init_list? ~ "}" +} + +struct_init_list = { + struct_init_field ~ ("," ~ struct_init_field)* ~ ","? +} + +struct_init_field = { + identifier ~ ":" ~ expression +} + +array_literal = { + "[" ~ (expression ~ ("," ~ expression)*)? ~ ","? ~ "]" +} + +// ================= CHAINING ================= + +term = { primary ~ (field_access | call_suffix)* } + +field_access = { "." ~ identifier } + +call_suffix = { + "(" ~ (expression ~ ("," ~ expression)*)? ~ ","? ~ ")" +} + +// ================= PRIMARY ================= + +primary = _{ + struct_literal + | array_literal + | float + | integer + | string_lit + | boolean + | self_kw + | null_kw + | identifier +} + +self_kw = { "this" } +null_kw = { "null" } diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 0000000..a8ad54b --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,363 @@ +use pest::Parser; +use pest::iterators::Pair; +use pest_derive::Parser; + +pub mod ast; +use ast::*; + +#[derive(Parser)] +#[grammar = "./src/parser/grammar.pest"] +pub struct MistParser; + +// convenience alias for pest errors +pub type ParseError = pest::error::Error; + +pub fn parse(source: &str) -> Result { + let pairs = MistParser::parse(Rule::program, source)?; + + let mut statements = vec![]; + + // pairs is an iterator over the top-level program pair + // we need to get its inner children + for pair in pairs { + match pair.as_rule() { + Rule::program => { + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::function_decl => { + statements.push(TopLevel::Function(parse_function(inner))) + } + Rule::struct_decl => statements.push(TopLevel::Struct(parse_struct(inner))), + Rule::class_decl => statements.push(TopLevel::Class(parse_class(inner))), + Rule::import_decl => statements.push(TopLevel::Import(parse_import(inner))), + Rule::EOI => {} + _ => {} + } + } + } + Rule::EOI => {} + _ => {} + } + } + + Ok(Program { statements }) +} + +fn span_of(pair: &Pair) -> Span { + let s = pair.as_span(); + Span { + start: s.start(), + end: s.end(), + } +} + +fn parse_function(pair: Pair) -> Function { + let span = span_of(&pair); + let mut inner = pair.into_inner(); + + let name = inner.next().unwrap().as_str().to_string(); + + let mut params = vec![]; + let mut return_type = None; + let mut body = vec![]; + + for part in inner { + match part.as_rule() { + Rule::param_list => params = parse_param_list(part), + Rule::type_expr => return_type = Some(parse_type_expr(part)), + Rule::block => body = parse_block(part), + _ => {} + } + } + + Function { + name, + params, + return_type, + body, + span, + } +} + +fn parse_param_list(pair: Pair) -> Vec { + pair.into_inner() + .map(|p| { + let span = span_of(&p); + let mut inner = p.into_inner(); + let name = inner.next().unwrap().as_str().to_string(); + let type_expr = parse_type_expr(inner.next().unwrap()); + Param { + name, + type_expr, + span, + } + }) + .collect() +} + +fn parse_struct(pair: Pair) -> Struct { + let span = span_of(&pair); + let mut inner = pair.into_inner(); + let name = inner.next().unwrap().as_str().to_string(); + let fields = inner.map(|f| parse_struct_field(f)).collect(); + Struct { name, fields, span } +} + +fn parse_struct_field(pair: Pair) -> StructField { + let span = span_of(&pair); + let mut inner = pair.into_inner(); + let name = inner.next().unwrap().as_str().to_string(); + let type_expr = parse_type_expr(inner.next().unwrap()); + StructField { + name, + type_expr, + span, + } +} + +fn parse_class(pair: Pair) -> Class { + let span = span_of(&pair); + let mut inner = pair.into_inner(); + let name = inner.next().unwrap().as_str().to_string(); + let mut fields = vec![]; + let mut methods = vec![]; + + for part in inner { + match part.as_rule() { + Rule::struct_field => fields.push(parse_struct_field(part)), + Rule::function_decl => methods.push(parse_function(part)), + _ => {} + } + } + + Class { + name, + fields, + methods, + span, + } +} + +fn parse_import(pair: Pair) -> Import { + let span = span_of(&pair); + let path = pair.into_inner().next().unwrap().as_str().to_string(); + Import { path, span } +} + +fn parse_block(pair: Pair) -> Vec { + pair.into_inner() + .filter_map(|p| parse_statement(p)) + .collect() +} + +fn parse_statement(pair: Pair) -> Option { + match pair.as_rule() { + Rule::let_stmt => Some(Statement::Let(parse_let(pair))), + Rule::return_stmt => Some(Statement::Return(parse_return(pair))), + Rule::if_stmt => Some(Statement::If(parse_if(pair))), + Rule::for_stmt => Some(Statement::For(parse_for(pair))), + Rule::expression_stmt => { + let expr = pair.into_inner().next().unwrap(); + Some(Statement::Expression(parse_expression(expr))) + } + Rule::expression => Some(Statement::Expression(parse_expression(pair))), + _ => None, + } +} + +fn parse_let(pair: Pair) -> LetStatement { + let span = span_of(&pair); + let mut inner = pair.into_inner(); + let name = inner.next().unwrap().as_str().to_string(); + + // peek ahead — next is either a type or an expression + let next = inner.next().unwrap(); + let (type_expr, value) = if next.as_rule() == Rule::type_expr { + ( + Some(parse_type_expr(next)), + parse_expression(inner.next().unwrap()), + ) + } else { + (None, parse_expression(next)) + }; + + LetStatement { + name, + type_expr, + value, + span, + } +} + +fn parse_return(pair: Pair) -> ReturnStatement { + let span = span_of(&pair); + let value = pair.into_inner().next().map(|p| parse_expression(p)); + ReturnStatement { value, span } +} + +fn parse_if(pair: Pair) -> IfStatement { + let span = span_of(&pair); + let mut inner = pair.into_inner(); + let condition = parse_expression(inner.next().unwrap()); + let body = parse_block(inner.next().unwrap()); + let else_body = inner.next().map(|p| parse_block(p)); + IfStatement { + condition, + body, + else_body, + span, + } +} + +fn parse_for(pair: Pair) -> ForStatement { + let span = span_of(&pair); + let mut inner = pair.into_inner(); + let var = inner.next().unwrap().as_str().to_string(); + let iterator = parse_expression(inner.next().unwrap()); + let body = parse_block(inner.next().unwrap()); + ForStatement { + var, + iterator, + body, + span, + } +} + +fn parse_expression(pair: Pair) -> Expression { + match pair.as_rule() { + Rule::expression => { + let mut inner = pair.into_inner(); + let mut expr = parse_term(inner.next().unwrap()); + + // consume pairs of (bin_op, term) + while let Some(op_pair) = inner.next() { + let right = parse_term(inner.next().unwrap()); + let span = span_of(&op_pair); + let op = match op_pair.as_rule() { + Rule::add => BinOperator::Add, + Rule::sub => BinOperator::Sub, + Rule::mul => BinOperator::Mul, + Rule::div => BinOperator::Div, + Rule::eq => BinOperator::Eq, + Rule::neq => BinOperator::NotEq, + Rule::lt => BinOperator::Lt, + Rule::gt => BinOperator::Gt, + Rule::lte => BinOperator::LtEq, + Rule::gte => BinOperator::GtEq, + Rule::and => BinOperator::And, + Rule::or => BinOperator::Or, + _ => unreachable!(), + }; + expr = Expression::BinaryOp(Box::new(BinaryOp { + left: expr, + op, + right, + span, + })); + } + + expr + } + _ => parse_term(pair), + } +} + +fn parse_term(pair: Pair) -> Expression { + let mut inner = pair.into_inner(); + let mut expr = parse_primary(inner.next().unwrap()); + + for part in inner { + let span = span_of(&part); + match part.as_rule() { + Rule::field_access => { + let field = part.into_inner().next().unwrap().as_str().to_string(); + expr = Expression::FieldAccess(Box::new(FieldAccess { + object: expr, + field, + span, + })); + } + Rule::call_suffix => { + let args = part.into_inner().map(|p| parse_expression(p)).collect(); + expr = Expression::Call(Box::new(CallExpr { + callee: expr, + args, + span, + })); + } + _ => {} + } + } + + expr +} + +fn parse_primary(pair: Pair) -> Expression { + let span = span_of(&pair); + + match pair.as_rule() { + Rule::struct_literal => { + let mut inner = pair.into_inner(); + let name = inner.next().unwrap().as_str().to_string(); + + inner = inner.next().unwrap().into_inner(); + + // println!("{inner:#?}"); + + let mut fields = vec![]; + for field in inner { + let mut f_inner = field.into_inner(); + let field_name = f_inner.next().unwrap().as_str().to_string(); + let value = parse_expression(f_inner.next().unwrap()); + + fields.push((field_name, value)); + } + + Expression::StructInit(Box::new(StructInit { name, fields, span })) + } + + Rule::array_literal => { + let elements = pair.into_inner().map(|p| parse_expression(p)).collect(); + + Expression::ArrayLiteral(Box::new(ArrayLiteral { elements, span })) + } + + Rule::integer => Expression::Integer(pair.as_str().parse().unwrap(), span), + Rule::float => Expression::Float(pair.as_str().parse().unwrap(), span), + + Rule::string_lit => { + Expression::StringLit(pair.into_inner().next().unwrap().as_str().to_string(), span) + } + + Rule::boolean => Expression::Bool(pair.as_str() == "true", span), + + Rule::self_kw => Expression::Identifier("self".to_string(), span), + Rule::null_kw => Expression::Identifier("null".to_string(), span), + Rule::identifier => Expression::Identifier(pair.as_str().to_string(), span), + + Rule::term => parse_term(pair), + + _ => unreachable!("unexpected primary rule: {:?}", pair.as_rule()), + } +} + +fn parse_type_expr(pair: Pair) -> TypeExpr { + let mut inner = pair.into_inner(); + let base = inner.next().unwrap(); + + let base_type = match base.as_rule() { + Rule::array_type => { + let inner_type = parse_type_expr(base.into_inner().next().unwrap()); + TypeExpr::Array(Box::new(inner_type)) + } + Rule::identifier => TypeExpr::Named(base.as_str().to_string()), + _ => unreachable!(), + }; + + // if a "?" suffix was present, wrap in Optional + if inner.next().is_some() { + TypeExpr::Optional(Box::new(base_type)) + } else { + base_type + } +} diff --git a/test/mist.json b/test/mist.json new file mode 100644 index 0000000..c639516 --- /dev/null +++ b/test/mist.json @@ -0,0 +1,4 @@ +{ + "entry": "src/main.ms", + "out_dir": "build" +} \ No newline at end of file diff --git a/test/src/main.ms b/test/src/main.ms new file mode 100644 index 0000000..ce922de --- /dev/null +++ b/test/src/main.ms @@ -0,0 +1,9 @@ +import "fmt"; + +function add(a: int, b: int) -> int { + return a + b; +} + +function main() { + fmt.Println(add(10, 10) / 2); +} \ No newline at end of file