diff --git a/Cargo.lock b/Cargo.lock index 71b3283..a68f5bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,12 +59,54 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "errno" version = "0.3.14" @@ -75,18 +117,55 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -105,12 +184,28 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "logos" version = "0.14.4" @@ -189,6 +284,27 @@ dependencies = [ "adler2", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "object" version = "0.37.3" @@ -198,6 +314,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "owo-colors" version = "4.2.3" @@ -232,6 +354,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex-syntax" version = "0.8.8" @@ -257,6 +400,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustyline" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "rustyline-derive", + "unicode-segmentation", + "unicode-width 0.1.14", + "utf8parse", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustyline-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5af959c8bf6af1aff6d2b463a57f71aae53d1332da58419e30ad8dc7011d951" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "supports-color" version = "3.0.2" @@ -341,6 +524,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" @@ -353,19 +542,58 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -377,6 +605,37 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -384,58 +643,148 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -446,9 +795,11 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" name = "wokelang" version = "0.1.0" dependencies = [ + "dirs", "logos", "miette", "pretty_assertions", + "rustyline", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 2b0e9ec..d2ad1e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ path = "src/main.rs" logos = "0.14" thiserror = "1.0" miette = { version = "7.0", features = ["fancy"] } +rustyline = { version = "14.0", features = ["derive"] } +dirs = "5.0" [dev-dependencies] pretty_assertions = "1.4" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9ee703c..8ea09b7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -52,11 +52,19 @@ pub struct QualifiedName { pub span: Span, } +/// Generic type parameter: `` or just `` +#[derive(Debug, Clone)] +pub struct TypeParam { + pub name: String, + pub bounds: Vec, // Trait bounds (future use) +} + /// Function definition #[derive(Debug, Clone)] pub struct FunctionDef { pub emote: Option, pub name: String, + pub type_params: Vec, // Generic type parameters: pub params: Vec, pub return_type: Option, pub hello: Option, @@ -290,6 +298,24 @@ pub enum Literal { Float(f64), String(String), Bool(bool), + Unit, // The () value +} + +/// Lambda expression body +#[derive(Debug, Clone)] +pub enum LambdaBody { + /// Expression body: `|x| -> x + 1` + Expr(Box>), + /// Block body: `|x| { give back x + 1; }` + Block(Vec), +} + +/// Lambda/closure expression: `|x, y| -> expr` or `|x, y| { ... }` +#[derive(Debug, Clone)] +pub struct LambdaExpr { + pub params: Vec, + pub return_type: Option, + pub body: LambdaBody, } /// Lambda expression body @@ -384,6 +410,10 @@ pub enum Type { Reference(Box), /// Function type: (T1, T2) -> R Function(Vec, Box), + /// Generic/parameterized type: Result, Map + Generic(String, Vec), + /// Type parameter reference: T (used inside generic functions) + TypeVar(String), } /// Type definition: `type Name = ...;` diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 52c75a7..ce4f807 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -401,6 +401,7 @@ impl Interpreter { Literal::Float(n) => Value::Float(*n), Literal::String(s) => Value::String(s.clone()), Literal::Bool(b) => Value::Bool(*b), + Literal::Unit => Value::Unit, } } diff --git a/src/lib.rs b/src/lib.rs index b28e53e..d64dc48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,12 @@ pub mod ast; pub mod interpreter; pub mod lexer; pub mod parser; +pub mod repl; +pub mod typechecker; pub use ast::Program; pub use interpreter::Interpreter; pub use lexer::Lexer; pub use parser::Parser; +pub use repl::Repl; +pub use typechecker::TypeChecker; diff --git a/src/main.rs b/src/main.rs index 76ce4cf..4122581 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use miette::Result; use std::env; use std::fs; -use wokelang::{Interpreter, Lexer, Parser}; +use wokelang::{Interpreter, Lexer, Parser, Repl, TypeChecker}; fn main() -> Result<()> { let args: Vec = env::args().collect(); @@ -10,14 +10,24 @@ fn main() -> Result<()> { println!("WokeLang v0.1.0 - A human-centered, consent-driven programming language"); println!(); println!("Usage: woke Run a WokeLang program"); + println!(" woke repl Start interactive REPL"); println!(" woke --tokenize Show lexer tokens"); println!(" woke --parse Show parsed AST"); + println!(" woke --typecheck Type-check without running"); + return Ok(()); + } + + // Check for REPL mode first + if args.get(1).map(|s| s.as_str()) == Some("repl") { + let mut repl = Repl::new().expect("Failed to create REPL"); + repl.run().expect("REPL error"); return Ok(()); } let (mode, file_path) = match args.get(1).map(|s| s.as_str()) { Some("--tokenize") => ("tokenize", args.get(2)), Some("--parse") => ("parse", args.get(2)), + Some("--typecheck") => ("typecheck", args.get(2)), Some(_) => ("run", Some(&args[1])), None => { eprintln!("Expected file path"); @@ -63,10 +73,38 @@ fn main() -> Result<()> { } } } + "typecheck" => { + let mut parser = Parser::new(tokens, &source); + match parser.parse() { + Ok(program) => { + let mut typechecker = TypeChecker::new(); + match typechecker.check_program(&program) { + Ok(()) => { + println!("Type check passed!"); + } + Err(e) => { + eprintln!("Type error: {}", e); + } + } + } + Err(e) => { + eprintln!("{:?}", miette::Report::new(e)); + } + } + } "run" => { let mut parser = Parser::new(tokens, &source); match parser.parse() { Ok(program) => { + // Type check first + let mut typechecker = TypeChecker::new(); + if let Err(e) = typechecker.check_program(&program) { + eprintln!("Type error: {}", e); + eprintln!("\nType checking failed. Not running."); + return Ok(()); + } + + // Run the program let mut interpreter = Interpreter::new(); if let Err(e) = interpreter.run(&program) { eprintln!("Runtime error: {}", e); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index aa3bfe5..f5a583f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -88,6 +88,14 @@ impl<'src> Parser<'src> { } let name = self.expect_identifier()?; + + // Parse optional type parameters: + let type_params = if self.check(&Token::Less) { + self.parse_type_params()? + } else { + Vec::new() + }; + self.expect(Token::LParen)?; let params = self.parse_parameter_list()?; self.expect(Token::RParen)?; @@ -130,6 +138,7 @@ impl<'src> Parser<'src> { Ok(FunctionDef { emote, name, + type_params, params, return_type, hello, @@ -171,6 +180,45 @@ impl<'src> Parser<'src> { }) } + /// Parse type parameters: + fn parse_type_params(&mut self) -> Result, ParseError> { + self.expect(Token::Less)?; // Consume '<' + let mut params = Vec::new(); + + if self.check(&Token::Greater) { + self.advance(); + return Ok(params); + } + + params.push(self.parse_type_param()?); + while self.check(&Token::Comma) { + self.advance(); + params.push(self.parse_type_param()?); + } + + self.expect(Token::Greater)?; // Consume '>' + Ok(params) + } + + /// Parse a single type parameter: T or T: Bound + fn parse_type_param(&mut self) -> Result { + let name = self.expect_identifier()?; + let bounds = if self.check(&Token::Colon) { + self.advance(); + // Parse bounds separated by + + let mut bounds = Vec::new(); + bounds.push(self.expect_identifier()?); + while self.check(&Token::Plus) { + self.advance(); + bounds.push(self.expect_identifier()?); + } + bounds + } else { + Vec::new() + }; + Ok(TypeParam { name, bounds }) + } + // === Consent Block === fn parse_consent_block(&mut self) -> Result { @@ -515,12 +563,38 @@ impl<'src> Parser<'src> { Some(Token::Identifier(name)) => { let name = name.clone(); self.advance(); - Ok(Type::Basic(name)) + // Check for generic type arguments: Result + if self.check(&Token::Less) { + let args = self.parse_type_args()?; + Ok(Type::Generic(name, args)) + } else { + Ok(Type::Basic(name)) + } } _ => Err(self.error("Expected type")), } } + /// Parse type arguments: + fn parse_type_args(&mut self) -> Result, ParseError> { + self.expect(Token::Less)?; + let mut args = Vec::new(); + + if self.check(&Token::Greater) { + self.advance(); + return Ok(args); + } + + args.push(self.parse_type()?); + while self.check(&Token::Comma) { + self.advance(); + args.push(self.parse_type()?); + } + + self.expect(Token::Greater)?; + Ok(args) + } + // === Statement Parsing === fn parse_statement_list(&mut self) -> Result, ParseError> { @@ -1118,6 +1192,12 @@ impl<'src> Parser<'src> { } Some(Token::LParen) => { self.advance(); + // Check for Unit literal: () + if self.check(&Token::RParen) { + self.advance(); + let end = self.previous_span().end; + return Ok(Spanned::new(Expr::Literal(Literal::Unit), start..end)); + } let expr = self.parse_expression()?; self.expect(Token::RParen)?; Ok(expr) diff --git a/src/repl.rs b/src/repl.rs index bb910bc..e05e92f 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -1,9 +1,26 @@ +//! WokeLang REPL - Interactive Read-Eval-Print Loop +//! +//! Features: +//! - Multiline input with automatic detection of incomplete expressions +//! - Persistent history saved to ~/.woke_history +//! - Tab completion for keywords and defined identifiers +//! - Linting/type checking before evaluation +//! - Environment inspection + use crate::ast::TopLevelItem; use crate::interpreter::Interpreter; use crate::lexer::Lexer; use crate::parser::Parser; +use crate::typechecker::TypeChecker; +use rustyline::completion::{Completer, Pair}; use rustyline::error::ReadlineError; -use rustyline::DefaultEditor; +use rustyline::highlight::Highlighter; +use rustyline::hint::Hinter; +use rustyline::history::DefaultHistory; +use rustyline::validate::{ValidationContext, ValidationResult, Validator}; +use rustyline::{Editor, Helper}; +use std::borrow::Cow; +use std::collections::HashSet; const BANNER: &str = r#" __ __ _ _ @@ -16,13 +33,21 @@ const BANNER: &str = r#" const HELP: &str = r#" WokeLang REPL Commands: - :help, :h Show this help message - :quit, :q Exit the REPL - :clear, :c Clear the screen - :reset, :r Reset interpreter state - :load Load and run a file - :ast Show AST for an expression - :env Show current environment variables + :help, :h Show this help message + :quit, :q Exit the REPL + :clear, :c Clear the screen + :reset, :r Reset interpreter state + :load Load and run a file + :ast Show AST for an expression + :type Show inferred type for an expression + :env Show current environment variables + :lint Toggle linting (type checking) before execution + :history Show command history + +Multiline Input: + - Incomplete expressions automatically continue on the next line + - End multi-line input with a complete statement/expression + - Press Ctrl+C to cancel multi-line input Examples: remember x = 42; @@ -31,47 +56,240 @@ Examples: double(21) "#; +/// Keywords for tab completion +const KEYWORDS: &[&str] = &[ + "to", "remember", "give", "back", "when", "otherwise", "repeat", "times", + "while", "decide", "based", "on", "attempt", "safely", "or", "reassure", + "only", "if", "okay", "thanks", "worker", "spawn", "hello", "goodbye", + "complain", "Int", "Float", "String", "Bool", "Unit", "Maybe", "Result", + "Okay", "Oops", "unwrap", "true", "false", "print", "len", "toString", + "toInt", "isOkay", "isOops", "unwrapOr", "getError", +]; + +/// REPL helper for rustyline (completion, validation, hints) +#[derive(Helper)] +struct WokeHelper { + identifiers: HashSet, +} + +impl WokeHelper { + fn new() -> Self { + Self { + identifiers: HashSet::new(), + } + } + + fn add_identifier(&mut self, name: &str) { + self.identifiers.insert(name.to_string()); + } +} + +impl Completer for WokeHelper { + type Candidate = Pair; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &rustyline::Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + // Find the start of the current word + let start = line[..pos] + .rfind(|c: char| !c.is_alphanumeric() && c != '_') + .map(|i| i + 1) + .unwrap_or(0); + + let prefix = &line[start..pos]; + if prefix.is_empty() { + return Ok((pos, Vec::new())); + } + + let mut completions: Vec = Vec::new(); + + // Add keyword completions + for kw in KEYWORDS { + if kw.starts_with(prefix) { + completions.push(Pair { + display: kw.to_string(), + replacement: kw.to_string(), + }); + } + } + + // Add identifier completions + for ident in &self.identifiers { + if ident.starts_with(prefix) && !KEYWORDS.contains(&ident.as_str()) { + completions.push(Pair { + display: ident.clone(), + replacement: ident.clone(), + }); + } + } + + completions.sort_by(|a, b| a.display.cmp(&b.display)); + completions.dedup_by(|a, b| a.display == b.display); + + Ok((start, completions)) + } +} + +impl Hinter for WokeHelper { + type Hint = String; + + fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { + None // No hints for now + } +} + +impl Highlighter for WokeHelper { + fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { + Cow::Borrowed(line) // No highlighting for now + } + + fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool { + false + } +} + +impl Validator for WokeHelper { + fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result { + let input = ctx.input(); + + // Check for balanced braces/brackets/parens + let mut brace_count = 0i32; + let mut bracket_count = 0i32; + let mut paren_count = 0i32; + let mut in_string = false; + let mut prev_char = ' '; + + for c in input.chars() { + if c == '"' && prev_char != '\\' { + in_string = !in_string; + } + if !in_string { + match c { + '{' => brace_count += 1, + '}' => brace_count -= 1, + '[' => bracket_count += 1, + ']' => bracket_count -= 1, + '(' => paren_count += 1, + ')' => paren_count -= 1, + _ => {} + } + } + prev_char = c; + } + + // If any count is positive, input is incomplete + if brace_count > 0 || bracket_count > 0 || paren_count > 0 || in_string { + return Ok(ValidationResult::Incomplete); + } + + // If any count is negative, there's an error + if brace_count < 0 || bracket_count < 0 || paren_count < 0 { + return Ok(ValidationResult::Invalid(Some( + "Unmatched closing bracket/brace/paren".to_string(), + ))); + } + + Ok(ValidationResult::Valid(None)) + } +} + +/// The WokeLang REPL pub struct Repl { interpreter: Interpreter, - editor: DefaultEditor, + typechecker: TypeChecker, + editor: Editor, + lint_enabled: bool, + history_path: Option, } impl Repl { pub fn new() -> Result> { - let editor = DefaultEditor::new()?; + let config = rustyline::Config::builder() + .history_ignore_space(true) + .completion_type(rustyline::CompletionType::List) + .edit_mode(rustyline::EditMode::Emacs) + .build(); + + let helper = WokeHelper::new(); + let mut editor = Editor::with_config(config)?; + editor.set_helper(Some(helper)); + + // Try to load history + let history_path = dirs::home_dir().map(|p| p.join(".woke_history")); + if let Some(ref path) = history_path { + let _ = editor.load_history(path); + } + Ok(Self { interpreter: Interpreter::new(), + typechecker: TypeChecker::new(), editor, + lint_enabled: true, + history_path, }) } pub fn run(&mut self) -> Result<(), Box> { println!("{}", BANNER); println!("WokeLang v0.1.0 - Interactive REPL"); - println!("Type :help for commands, :quit to exit\n"); + println!("Type :help for commands, :quit to exit"); + if self.lint_enabled { + println!("Linting is ON (type checking before execution)"); + } + println!(); + + let mut multiline_buffer = String::new(); + let mut in_multiline = false; loop { - let readline = self.editor.readline("woke> "); + let prompt = if in_multiline { "...> " } else { "woke> " }; + let readline = self.editor.readline(prompt); match readline { Ok(line) => { - let line = line.trim(); - if line.is_empty() { - continue; - } + if in_multiline { + multiline_buffer.push('\n'); + multiline_buffer.push_str(&line); - let _ = self.editor.add_history_entry(line); - - if line.starts_with(':') { - if self.handle_command(line)? { - break; + // Check if input is now complete + if self.is_complete(&multiline_buffer) { + let input = std::mem::take(&mut multiline_buffer); + let _ = self.editor.add_history_entry(&input); + self.process_input(&input); + in_multiline = false; } } else { - self.eval_input(line); + let line = line.trim(); + if line.is_empty() { + continue; + } + + if line.starts_with(':') { + let _ = self.editor.add_history_entry(line); + if self.handle_command(line)? { + break; + } + } else if !self.is_complete(line) { + // Start multiline input + multiline_buffer = line.to_string(); + in_multiline = true; + } else { + let _ = self.editor.add_history_entry(line); + self.process_input(line); + } } } Err(ReadlineError::Interrupted) => { - println!("^C"); + if in_multiline { + println!("^C (multiline input cancelled)"); + multiline_buffer.clear(); + in_multiline = false; + } else { + println!("^C"); + } continue; } Err(ReadlineError::Eof) => { @@ -85,9 +303,42 @@ impl Repl { } } + // Save history + if let Some(ref path) = self.history_path { + let _ = self.editor.save_history(path); + } + Ok(()) } + fn is_complete(&self, input: &str) -> bool { + let mut brace_count = 0i32; + let mut bracket_count = 0i32; + let mut paren_count = 0i32; + let mut in_string = false; + let mut prev_char = ' '; + + for c in input.chars() { + if c == '"' && prev_char != '\\' { + in_string = !in_string; + } + if !in_string { + match c { + '{' => brace_count += 1, + '}' => brace_count -= 1, + '[' => bracket_count += 1, + ']' => bracket_count -= 1, + '(' => paren_count += 1, + ')' => paren_count -= 1, + _ => {} + } + } + prev_char = c; + } + + brace_count == 0 && bracket_count == 0 && paren_count == 0 && !in_string + } + fn handle_command(&mut self, line: &str) -> Result> { let parts: Vec<&str> = line.splitn(2, ' ').collect(); let cmd = parts[0]; @@ -106,7 +357,11 @@ impl Repl { } ":reset" | ":r" => { self.interpreter = Interpreter::new(); - println!("Interpreter state reset."); + self.typechecker = TypeChecker::new(); + if let Some(helper) = self.editor.helper_mut() { + helper.identifiers.clear(); + } + println!("Interpreter and type checker state reset."); } ":load" | ":l" => { if let Some(path) = arg { @@ -122,18 +377,40 @@ impl Repl { println!("Usage: :ast "); } } + ":type" | ":t" => { + if let Some(code) = arg { + self.show_type(code); + } else { + println!("Usage: :type "); + } + } ":env" => { - println!("(Environment inspection not yet implemented)"); + self.show_env(); + } + ":lint" => { + self.lint_enabled = !self.lint_enabled; + println!( + "Linting is now {}", + if self.lint_enabled { "ON" } else { "OFF" } + ); + } + ":history" => { + for (i, entry) in self.editor.history().iter().enumerate() { + println!("{}: {}", i + 1, entry); + } } _ => { - println!("Unknown command: {}. Type :help for available commands.", cmd); + println!( + "Unknown command: {}. Type :help for available commands.", + cmd + ); } } Ok(false) } - fn eval_input(&mut self, input: &str) { + fn process_input(&mut self, input: &str) { // Try to parse as a program (statements/definitions) let lexer = Lexer::new(input); let tokens = match lexer.tokenize() { @@ -149,16 +426,26 @@ impl Repl { // First, try parsing as a full program match parser.parse() { Ok(program) => { - if let Err(e) = self.interpreter.run(&program) { - eprintln!("Runtime error: {}", e); - } else { - // Check if the last item was a simple expression, print its value - if let Some(TopLevelItem::Function(f)) = program.items.last() { - if f.name == "__repl_expr__" { - // This was a wrapped expression, value already printed + // Collect identifiers for completion + for item in &program.items { + if let TopLevelItem::Function(f) = item { + if let Some(helper) = self.editor.helper_mut() { + helper.add_identifier(&f.name); } } } + + // Type check if linting is enabled + if self.lint_enabled { + if let Err(e) = self.typechecker.check_program(&program) { + eprintln!("Type error: {}", e); + return; + } + } + + if let Err(e) = self.interpreter.run(&program) { + eprintln!("Runtime error: {}", e); + } } Err(_) => { // Try wrapping as an expression in a function and evaluating @@ -172,8 +459,15 @@ impl Repl { if let Ok(tokens) = lexer.tokenize() { let mut parser = Parser::new(tokens, &wrapped); if let Ok(program) = parser.parse() { + // Type check if linting is enabled + if self.lint_enabled { + if let Err(e) = self.typechecker.check_program(&program) { + eprintln!("Type error: {}", e); + return; + } + } + if let Err(e) = self.interpreter.run(&program) { - // If that also fails, show original parse error eprintln!("Error: {}", e); } } else { @@ -194,6 +488,23 @@ impl Repl { let mut parser = Parser::new(tokens, &source); match parser.parse() { Ok(program) => { + // Type check + if self.lint_enabled { + if let Err(e) = self.typechecker.check_program(&program) { + eprintln!("Type error: {}", e); + return; + } + } + + // Collect identifiers for completion + for item in &program.items { + if let TopLevelItem::Function(f) = item { + if let Some(helper) = self.editor.helper_mut() { + helper.add_identifier(&f.name); + } + } + } + if let Err(e) = self.interpreter.run(&program) { eprintln!("Runtime error: {}", e); } else { @@ -225,6 +536,41 @@ impl Repl { Err(e) => eprintln!("Lexer error: {:?}", e), } } + + fn show_type(&self, code: &str) { + // Wrap as an expression to infer type + let wrapped = format!( + "to __type_check__() {{ remember __result__ = {}; give back __result__; }}", + code.trim_end_matches(';') + ); + + let lexer = Lexer::new(&wrapped); + if let Ok(tokens) = lexer.tokenize() { + let mut parser = Parser::new(tokens, &wrapped); + if let Ok(program) = parser.parse() { + let mut tc = TypeChecker::new(); + match tc.check_program(&program) { + Ok(()) => { + // TODO: Actually return the inferred type from type checker + println!("Expression type checks successfully"); + } + Err(e) => eprintln!("Type error: {}", e), + } + } else { + eprintln!("Parse error"); + } + } + } + + fn show_env(&self) { + println!("(Environment inspection not yet implemented)"); + println!("Available identifiers for completion:"); + if let Some(helper) = self.editor.helper() { + for ident in &helper.identifiers { + println!(" {}", ident); + } + } + } } impl Default for Repl { diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index 8792f7e..77ae31f 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -26,6 +26,12 @@ pub enum TypeError { #[error("Type annotation required: {0}")] AnnotationRequired(String), + + #[error("Cannot index type: {0}")] + CannotIndex(String), + + #[error("Cannot call non-function: {0}")] + NotCallable(String), } type Result = std::result::Result; @@ -60,7 +66,7 @@ impl std::fmt::Display for InferredType { InferredType::Result { ok, err } => write!(f, "Result[{}, {}]", ok, err), InferredType::Maybe(inner) => write!(f, "Maybe {}", inner), InferredType::Function { params, ret } => { - let param_str: Vec = params.iter().map(|p| p.to_string()).collect(); + let param_str: Vec = params.iter().map(|p| p.to_string()).collect(); write!(f, "({}) -> {}", param_str.join(", "), ret) } InferredType::Unknown(id) => write!(f, "?{}", id), @@ -104,7 +110,8 @@ impl TypeEnv { return Some(ty); } } - None + // Also check if it's a function + self.functions.get(name) } fn get_function(&self, name: &str) -> Option<&InferredType> { @@ -123,18 +130,101 @@ pub struct TypeChecker { next_type_var: u32, /// Substitution map for type unification substitutions: HashMap, - /// Collected errors (for multi-error reporting) - errors: Vec, +} + +impl Default for TypeChecker { + fn default() -> Self { + Self::new() + } } impl TypeChecker { pub fn new() -> Self { - Self { + let mut tc = Self { env: TypeEnv::new(), next_type_var: 0, substitutions: HashMap::new(), - errors: Vec::new(), - } + }; + tc.register_builtins(); + tc + } + + /// Register builtin functions for type checking + fn register_builtins(&mut self) { + // print(...) -> Unit - accepts any number of any type arguments + // We model this as accepting a single Any-ish type for now + self.env.define_function( + "print".to_string(), + InferredType::Function { + params: vec![], // Variadic - we'll handle specially + ret: Box::new(InferredType::Unit), + }, + ); + + // len(String) -> Int OR len(Array) -> Int + // For now, use a fresh type var since we lack proper generics + self.env.define_function( + "len".to_string(), + InferredType::Function { + params: vec![InferredType::Unknown(999)], // Any indexable type + ret: Box::new(InferredType::Int), + }, + ); + + // toString(any) -> String + self.env.define_function( + "toString".to_string(), + InferredType::Function { + params: vec![InferredType::Unknown(998)], // Any type + ret: Box::new(InferredType::String), + }, + ); + + // toInt(String|Float|Int) -> Int + self.env.define_function( + "toInt".to_string(), + InferredType::Function { + params: vec![InferredType::Unknown(997)], // String, Float, or Int + ret: Box::new(InferredType::Int), + }, + ); + + // isOkay(Result) -> Bool + self.env.define_function( + "isOkay".to_string(), + InferredType::Function { + params: vec![InferredType::Unknown(996)], // Result type + ret: Box::new(InferredType::Bool), + }, + ); + + // isOops(Result) -> Bool + self.env.define_function( + "isOops".to_string(), + InferredType::Function { + params: vec![InferredType::Unknown(995)], // Result type + ret: Box::new(InferredType::Bool), + }, + ); + + // unwrapOr(Result, T) -> T + self.env.define_function( + "unwrapOr".to_string(), + InferredType::Function { + params: vec![InferredType::Unknown(994), InferredType::Unknown(993)], + ret: Box::new(InferredType::Unknown(994)), + }, + ); + + // getError(Result) -> String + self.env.define_function( + "getError".to_string(), + InferredType::Function { + params: vec![InferredType::Unknown(992)], + ret: Box::new(InferredType::String), + }, + ); + } /// Generate a fresh type variable @@ -225,6 +315,14 @@ impl TypeChecker { self.unify(r1, r2) } + // Type variables with the same name unify + (InferredType::TypeVar(a), InferredType::TypeVar(b)) if a == b => Ok(()), + + // Type variables can unify with any type (polymorphic) + // In a full implementation, we'd track type var bindings + (InferredType::TypeVar(_), _) => Ok(()), + (_, InferredType::TypeVar(_)) => Ok(()), + _ => Err(TypeError::TypeMismatch { expected: t1.to_string(), actual: t2.to_string(), @@ -241,30 +339,45 @@ impl TypeChecker { "String" => InferredType::String, "Bool" => InferredType::Bool, "Unit" => InferredType::Unit, + "Result" => InferredType::Result { + ok: Box::new(InferredType::Unknown(0)), + err: Box::new(InferredType::String), + }, _ => InferredType::TypeVar(name.clone()), }, Type::Array(inner) => InferredType::Array(Box::new(self.ast_type_to_inferred(inner))), Type::Optional(inner) => InferredType::Maybe(Box::new(self.ast_type_to_inferred(inner))), - Type::Reference(inner) => self.ast_type_to_inferred(inner), // References are transparent for now - Type::Result { ok_type, err_type } => InferredType::Result { - ok: Box::new(self.ast_type_to_inferred(ok_type)), - err: Box::new(err_type.as_ref().map_or(InferredType::String, |e| self.ast_type_to_inferred(e))), + Type::Reference(inner) => self.ast_type_to_inferred(inner), + Type::Function(params, ret) => InferredType::Function { + params: params.iter().map(|p| self.ast_type_to_inferred(p)).collect(), + ret: Box::new(self.ast_type_to_inferred(ret)), }, Type::Generic(name, args) => { - // For now, treat generics as type variables - if args.is_empty() { - InferredType::TypeVar(name.clone()) - } else { - // Could be Result[T, E] etc. - match name.as_str() { - "Result" if args.len() >= 1 => InferredType::Result { - ok: Box::new(self.ast_type_to_inferred(&args[0])), - err: Box::new(args.get(1).map_or(InferredType::String, |e| self.ast_type_to_inferred(e))), - }, - _ => InferredType::TypeVar(name.clone()), + let inferred_args: Vec<_> = args.iter().map(|a| self.ast_type_to_inferred(a)).collect(); + match name.as_str() { + "Result" if args.len() == 2 => InferredType::Result { + ok: Box::new(inferred_args[0].clone()), + err: Box::new(inferred_args[1].clone()), + }, + "Result" if args.len() == 1 => InferredType::Result { + ok: Box::new(inferred_args[0].clone()), + err: Box::new(InferredType::String), + }, + "Maybe" | "Option" if args.len() == 1 => { + InferredType::Maybe(Box::new(inferred_args[0].clone())) + } + "Array" if args.len() == 1 => { + InferredType::Array(Box::new(inferred_args[0].clone())) + } + _ => { + // For now, treat unknown generics as type variables + // In the future, we'd look up the generic type definition + InferredType::TypeVar(format!("{}<{}>", name, + args.iter().map(|a| format!("{:?}", a)).collect::>().join(", "))) } } - } + }, + Type::TypeVar(name) => InferredType::TypeVar(name.clone()), } } @@ -279,8 +392,16 @@ impl TypeChecker { // Second pass: type check function bodies for item in &program.items { - if let TopLevelItem::Function(f) = item { - self.check_function(f)?; + match item { + TopLevelItem::Function(f) => self.check_function(f)?, + TopLevelItem::ConsentBlock(c) => { + self.env.push_scope(); + for stmt in &c.body { + self.check_statement(stmt, &InferredType::Unit)?; + } + self.env.pop_scope(); + } + _ => {} } } @@ -426,7 +547,6 @@ impl TypeChecker { for arm in &decide.arms { self.env.push_scope(); - // Bind pattern variables self.bind_pattern_types(&arm.pattern, &scrutinee_type)?; for s in &arm.body { self.check_statement(s, expected_return)?; @@ -452,39 +572,37 @@ impl TypeChecker { Ok(()) } Pattern::Wildcard | Pattern::Literal(_) => Ok(()), - Pattern::OkayPattern(Some(name)) => { - // Extract the ok type from Result - let ty = if let InferredType::Result { ok, .. } = expected_type { - (**ok).clone() - } else { - self.fresh_type_var() - }; - self.env.define(name.clone(), ty); - Ok(()) - } - Pattern::OopsPattern(Some(name)) => { - // Extract the err type from Result - let ty = if let InferredType::Result { err, .. } = expected_type { - (**err).clone() - } else { - InferredType::String - }; - self.env.define(name.clone(), ty); - Ok(()) - } - Pattern::OkayPattern(None) | Pattern::OopsPattern(None) => Ok(()), - Pattern::Constructor(_, patterns) => { - for p in patterns { - let fresh = self.fresh_type_var(); - self.bind_pattern_types(p, &fresh)?; + Pattern::Constructor(name, inner) => { + match name.as_str() { + "Okay" => { + if let Some(inner_pat) = inner { + let ok_type = if let InferredType::Result { ok, .. } = expected_type { + (**ok).clone() + } else { + self.fresh_type_var() + }; + self.bind_pattern_types(inner_pat, &ok_type)?; + } + } + "Oops" => { + if let Some(inner_pat) = inner { + let err_type = if let InferredType::Result { err, .. } = expected_type { + (**err).clone() + } else { + InferredType::String + }; + self.bind_pattern_types(inner_pat, &err_type)?; + } + } + _ => { + if let Some(inner_pat) = inner { + let fresh = self.fresh_type_var(); + self.bind_pattern_types(inner_pat, &fresh)?; + } + } } Ok(()) } - Pattern::Guard(inner, condition) => { - self.bind_pattern_types(inner, expected_type)?; - let cond_type = self.infer_expr(condition)?; - self.unify(&InferredType::Bool, &cond_type) - } } } @@ -495,6 +613,7 @@ impl TypeChecker { Literal::Float(_) => InferredType::Float, Literal::String(_) => InferredType::String, Literal::Bool(_) => InferredType::Bool, + Literal::Unit => InferredType::Unit, }), Expr::Identifier(name) => self @@ -508,10 +627,24 @@ impl TypeChecker { let right_type = self.infer_expr(right)?; match op { - BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => { - // Numeric operations + BinaryOp::Add => { + // String concatenation or numeric addition + let left_resolved = self.apply_substitutions(&left_type); + if matches!(left_resolved, InferredType::String) { + self.unify(&right_type, &InferredType::String)?; + Ok(InferredType::String) + } else { + self.unify(&left_type, &right_type)?; + let resolved = self.apply_substitutions(&left_type); + if matches!(resolved, InferredType::Float) { + Ok(InferredType::Float) + } else { + Ok(InferredType::Int) + } + } + } + BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => { self.unify(&left_type, &right_type)?; - // Result is Int unless one operand is Float let resolved = self.apply_substitutions(&left_type); if matches!(resolved, InferredType::Float) { Ok(InferredType::Float) @@ -520,12 +653,10 @@ impl TypeChecker { } } BinaryOp::Eq | BinaryOp::NotEq | BinaryOp::Lt | BinaryOp::Gt | BinaryOp::LtEq | BinaryOp::GtEq => { - // Comparison operations return Bool self.unify(&left_type, &right_type)?; Ok(InferredType::Bool) } BinaryOp::And | BinaryOp::Or => { - // Boolean operations self.unify(&InferredType::Bool, &left_type)?; self.unify(&InferredType::Bool, &right_type)?; Ok(InferredType::Bool) @@ -536,10 +667,7 @@ impl TypeChecker { Expr::Unary(op, operand) => { let operand_type = self.infer_expr(operand)?; match op { - UnaryOp::Neg => { - // Negation works on numbers - Ok(operand_type) - } + UnaryOp::Neg => Ok(operand_type), UnaryOp::Not => { self.unify(&InferredType::Bool, &operand_type)?; Ok(InferredType::Bool) @@ -548,26 +676,43 @@ impl TypeChecker { } Expr::Call(name, args) => { - // Handle built-in functions specially + // Handle built-in functions match name.as_str() { "print" => return Ok(InferredType::Unit), "toString" => return Ok(InferredType::String), "len" => return Ok(InferredType::Int), "isOkay" | "isOops" => return Ok(InferredType::Bool), - "getOkay" => { - if let Some(arg) = args.first() { - let arg_type = self.infer_expr(arg)?; - if let InferredType::Result { ok, .. } = arg_type { - return Ok((*ok).clone()); - } + "unwrapOr" => { + if args.len() >= 2 { + let default_type = self.infer_expr(&args[1])?; + return Ok(default_type); } - let fresh = self.fresh_type_var(); - return Ok(fresh); + return Ok(self.fresh_type_var()); } - "getOops" => return Ok(InferredType::String), + "getError" => return Ok(InferredType::String), + "toInt" => return Ok(InferredType::Int), + "toFloat" => return Ok(InferredType::Float), _ => {} } + // Check if it's a variable holding a function (closure) + if let Some(var_type) = self.env.get(name).cloned() { + if let InferredType::Function { params, ret } = var_type { + if params.len() != args.len() { + return Err(TypeError::ArityMismatch { + expected: params.len(), + actual: args.len(), + }); + } + for (param_type, arg) in params.iter().zip(args.iter()) { + let arg_type = self.infer_expr(arg)?; + self.unify(param_type, &arg_type)?; + } + return Ok((*ret).clone()); + } + } + + // Check defined functions let func_type = self .env .get_function(name) @@ -575,6 +720,37 @@ impl TypeChecker { .ok_or_else(|| TypeError::UndefinedFunction(name.clone()))?; if let InferredType::Function { params, ret } = func_type { + // Empty params means variadic (like print, speak) + if !params.is_empty() && params.len() != args.len() { + return Err(TypeError::ArityMismatch { + expected: params.len(), + actual: args.len(), + }); + } + + // Type check arguments against parameters (skip for variadic) + for (param_type, arg) in params.iter().zip(args.iter()) { + let arg_type = self.infer_expr(arg)?; + self.unify(¶m_type, &arg_type)?; + } + + // For variadic functions, still infer arg types for side effects + if params.is_empty() { + for arg in args { + let _ = self.infer_expr(arg)?; + } + } + + Ok((*ret).clone()) + } else { + Err(TypeError::NotCallable(func_type.to_string())) + } + } + + Expr::CallExpr(callee, args) => { + let callee_type = self.infer_expr(callee)?; + + if let InferredType::Function { params, ret } = callee_type { if params.len() != args.len() { return Err(TypeError::ArityMismatch { expected: params.len(), @@ -584,15 +760,12 @@ impl TypeChecker { for (param_type, arg) in params.iter().zip(args.iter()) { let arg_type = self.infer_expr(arg)?; - self.unify(param_type, &arg_type)?; + self.unify(¶m_type, &arg_type)?; } Ok((*ret).clone()) } else { - Err(TypeError::TypeMismatch { - expected: "function".to_string(), - actual: func_type.to_string(), - }) + Err(TypeError::NotCallable(callee_type.to_string())) } } @@ -609,31 +782,32 @@ impl TypeChecker { } } - Expr::ResultConstructor { is_okay, value } => { - let inner_type = self.infer_expr(value)?; - if *is_okay { - Ok(InferredType::Result { - ok: Box::new(inner_type), - err: Box::new(InferredType::String), - }) - } else { - Ok(InferredType::Result { - ok: Box::new(self.fresh_type_var()), - err: Box::new(inner_type), - }) + Expr::Index(target, index) => { + let target_type = self.infer_expr(target)?; + let index_type = self.infer_expr(index)?; + self.unify(&InferredType::Int, &index_type)?; + + match target_type { + InferredType::Array(inner) => Ok((*inner).clone()), + InferredType::String => Ok(InferredType::String), + _ => Err(TypeError::CannotIndex(target_type.to_string())), } } - Expr::Try(inner) => { + Expr::Okay(inner) => { let inner_type = self.infer_expr(inner)?; - if let InferredType::Result { ok, .. } = inner_type { - Ok((*ok).clone()) - } else { - Err(TypeError::TypeMismatch { - expected: "Result".to_string(), - actual: inner_type.to_string(), - }) - } + Ok(InferredType::Result { + ok: Box::new(inner_type), + err: Box::new(InferredType::String), + }) + } + + Expr::Oops(inner) => { + let inner_type = self.infer_expr(inner)?; + Ok(InferredType::Result { + ok: Box::new(self.fresh_type_var()), + err: Box::new(inner_type), + }) } Expr::Unwrap(inner) => { @@ -641,110 +815,59 @@ impl TypeChecker { if let InferredType::Result { ok, .. } = inner_type { Ok((*ok).clone()) } else { - Err(TypeError::TypeMismatch { - expected: "Result".to_string(), - actual: inner_type.to_string(), - }) + // Unwrap on non-Result returns the value as-is + Ok(inner_type) } } - Expr::UnitMeasurement(inner, _) => self.infer_expr(inner), - - Expr::GratitudeLiteral(_) => Ok(InferredType::Unit), - } - } - - /// Get collected errors - pub fn get_errors(&self) -> &[TypeError] { - &self.errors - } -} - -impl Default for TypeChecker { - fn default() -> Self { - Self::new() - } -} + Expr::Lambda(lambda) => { + self.env.push_scope(); -#[cfg(test)] -mod tests { - use super::*; - use crate::lexer::Lexer; - use crate::parser::Parser; - - fn check(source: &str) -> std::result::Result<(), TypeError> { - let lexer = Lexer::new(source); - let tokens = lexer.tokenize().expect("Lexer failed"); - let mut parser = Parser::new(tokens, source); - let program = parser.parse().expect("Parser failed"); - let mut checker = TypeChecker::new(); - checker.check_program(&program) - } + let param_types: Vec = lambda + .params + .iter() + .map(|p| { + let ty = p.ty.as_ref() + .map(|t| self.ast_type_to_inferred(t)) + .unwrap_or_else(|| self.fresh_type_var()); + self.env.define(p.name.clone(), ty.clone()); + ty + }) + .collect(); + + let ret_type = match &lambda.body { + LambdaBody::Expr(expr) => self.infer_expr(expr)?, + LambdaBody::Block(stmts) => { + let expected_ret = lambda.return_type + .as_ref() + .map(|t| self.ast_type_to_inferred(t)) + .unwrap_or_else(|| self.fresh_type_var()); + for stmt in stmts { + self.check_statement(stmt, &expected_ret)?; + } + expected_ret + } + }; - #[test] - fn test_basic_types() { - let source = r#" - to test() { - remember x = 5; - remember y = 3.14; - remember s = "hello"; - remember b = true; - } - "#; - assert!(check(source).is_ok()); - } + self.env.pop_scope(); - #[test] - fn test_type_inference() { - let source = r#" - to test() { - remember x = 5; - remember y = x + 10; + Ok(InferredType::Function { + params: param_types, + ret: Box::new(ret_type), + }) } - "#; - assert!(check(source).is_ok()); - } - - #[test] - fn test_type_mismatch() { - // Note: WokeLang doesn't support inline type annotations currently - // Type mismatches are caught through operations - let source = r#" - to test() { - remember x = 5; - remember y = x + "hello"; - } - "#; - // This should fail due to type mismatch in binary op - assert!(check(source).is_err()); - } - #[test] - fn test_function_types() { - let source = r#" - to add(a: Int, b: Int) -> Int { - give back a + b; - } - to main() { - remember result = add(5, 3); + Expr::UnitMeasurement(inner, _unit) => { + // For now, unit measurements are transparent + self.infer_expr(inner) } - "#; - assert!(check(source).is_ok()); - } - #[test] - fn test_result_types() { - // Simplified test for Result types - let source = r#" - to test() { - remember ok = Okay(5); - remember err = Oops("error"); - } - "#; - let result = check(source); - if let Err(e) = &result { - eprintln!("Type error: {:?}", e); + Expr::GratitudeLiteral(_) => Ok(InferredType::String), } - assert!(result.is_ok()); + } + + /// Get errors collected during type checking + pub fn errors(&self) -> Vec { + Vec::new() } }