diff --git a/Cargo.lock b/Cargo.lock index 732d2eb..9e68667 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,15 @@ dependencies = [ "windows-targets", ] +[[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 = "1.3.2" @@ -258,7 +267,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.1", "windows-sys 0.59.0", ] @@ -724,6 +733,12 @@ dependencies = [ "similar", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -838,6 +853,36 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -914,6 +959,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" + [[package]] name = "parking_lot" version = "0.12.4" @@ -1014,7 +1065,7 @@ dependencies = [ "strum_macros", "thiserror", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.1", ] [[package]] @@ -1110,6 +1161,7 @@ dependencies = [ "tracing-tree", "tree-sitter", "tree-sitter-r", + "typing", ] [[package]] @@ -1352,6 +1404,27 @@ dependencies = [ "syn", ] +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "2.0.102" @@ -1374,6 +1447,26 @@ dependencies = [ "syn", ] +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.1", +] + [[package]] name = "thiserror" version = "2.0.12" @@ -1623,6 +1716,13 @@ dependencies = [ [[package]] name = "typing" version = "0.0.5" +dependencies = [ + "miette", + "tracing", + "tracing-subscriber", + "tree-sitter", + "tree-sitter-r", +] [[package]] name = "unicode-ident" @@ -1630,12 +1730,24 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-linebreak" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 803b817..c9c2502 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ default-members = ["crates/roughly"] version = "0.0.5" [workspace.dependencies] +typing = { path = "crates/typing" } + clap = { version = "4.5.40", features = ["derive"] } console = "0.15.11" ignore = "0.4.23" @@ -41,6 +43,9 @@ nu-ansi-term = "0.50.1" extendr-api = "0.8.0" extendr-engine = "0.8.0" +# typing +miette = { version = "7.6.0", features = ["fancy"] } + # dev-dependencies insta = "1.43.1" diff --git a/crates/roughly/Cargo.toml b/crates/roughly/Cargo.toml index d0e070e..eeed676 100644 --- a/crates/roughly/Cargo.toml +++ b/crates/roughly/Cargo.toml @@ -4,6 +4,8 @@ edition = "2024" version.workspace = true [dependencies] +typing.workspace = true + clap.workspace = true console.workspace = true ignore.workspace = true diff --git a/crates/roughly/src/cli.rs b/crates/roughly/src/cli.rs index 8161876..0439431 100644 --- a/crates/roughly/src/cli.rs +++ b/crates/roughly/src/cli.rs @@ -99,7 +99,11 @@ pub fn check( let mut n_files = 0; let mut n_errors = 0; for (paths, config) in paths_with_config { - let config = diagnostics::Config::from_config(config, experimental_features.unused); + let config = diagnostics::Config::from_config( + config, + experimental_features.unused, + experimental_features.typing, + ); for path in paths { n_files += 1; let old = match std::fs::read_to_string(&path) { @@ -461,21 +465,25 @@ pub fn ast(path: &Path) -> Result<(), DebugError> { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ExperimentalFeatures { pub range_formatting: bool, + pub typing: bool, pub unused: bool, } impl ExperimentalFeatures { pub fn parse(flags: &[impl AsRef]) -> Self { - let mut unused = false; let mut range_formatting = false; + let mut typing = false; + let mut unused = false; for flag in flags { match flag.as_ref() { "all" => { - unused = true; range_formatting = true; + typing = true; + unused = true; } "range_formatting" => range_formatting = true, + "typing" => typing = true, "unused" => unused = true, unknown => { warn(&format!("unknown experimental feature: {unknown}")); @@ -485,6 +493,7 @@ impl ExperimentalFeatures { Self { unused, + typing, range_formatting, } } diff --git a/crates/roughly/src/diagnostics.rs b/crates/roughly/src/diagnostics.rs index 443fd96..e67697d 100644 --- a/crates/roughly/src/diagnostics.rs +++ b/crates/roughly/src/diagnostics.rs @@ -1,5 +1,6 @@ mod fast; mod syntax; +mod typing; mod unused; use { @@ -15,15 +16,21 @@ use { #[derive(Debug, Clone, Copy)] pub struct Config { - case: Case, - experimental_unused: bool, + pub case: Case, + pub experimental_unused: bool, + pub experimental_typing: bool, } impl Config { - pub fn from_config(config: config::Config, experimental_unused: bool) -> Self { + pub fn from_config( + config: config::Config, + experimental_unused: bool, + experimental_typing: bool, + ) -> Self { Config { case: config.case, experimental_unused, + experimental_typing, } } } @@ -44,6 +51,10 @@ pub fn analyze(node: Node, rope: &Rope, config: Config, full: bool) -> Vec Vec { + vec![] +} diff --git a/crates/roughly/src/lib.rs b/crates/roughly/src/lib.rs index 16caa99..238df4f 100644 --- a/crates/roughly/src/lib.rs +++ b/crates/roughly/src/lib.rs @@ -1,5 +1,3 @@ - - #![feature(let_chains)] pub mod cli; diff --git a/crates/roughly/src/server.rs b/crates/roughly/src/server.rs index 9f6b6db..d0f8fc2 100644 --- a/crates/roughly/src/server.rs +++ b/crates/roughly/src/server.rs @@ -231,7 +231,11 @@ impl LanguageServer for ServerState { let diagnostics = diagnostics::analyze_full( tree.root_node(), &rope, - diagnostics::Config::from_config(self.config, self.experimental_features.unused), + diagnostics::Config::from_config( + self.config, + self.experimental_features.unused, + self.experimental_features.typing, + ), ); let symbols = index::index(tree.root_node(), &rope, false); @@ -338,7 +342,11 @@ impl LanguageServer for ServerState { let diagnostics = diagnostics::analyze_fast( tree.root_node(), rope, - diagnostics::Config::from_config(self.config, self.experimental_features.unused), + diagnostics::Config::from_config( + self.config, + self.experimental_features.unused, + self.experimental_features.typing, + ), ); // UPDATE SYMBOLS @@ -386,7 +394,11 @@ impl LanguageServer for ServerState { let diagnostics = diagnostics::analyze_full( root_node, rope, - diagnostics::Config::from_config(self.config, self.experimental_features.unused), + diagnostics::Config::from_config( + self.config, + self.experimental_features.unused, + self.experimental_features.typing, + ), ); if let Err(error) = self diff --git a/crates/typing/Cargo.toml b/crates/typing/Cargo.toml index 553156b..7bec5db 100644 --- a/crates/typing/Cargo.toml +++ b/crates/typing/Cargo.toml @@ -4,3 +4,8 @@ edition = "2024" version.workspace = true [dependencies] +miette.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +tree-sitter.workspace = true +tree-sitter-r.workspace = true diff --git a/crates/typing/src/lib.rs b/crates/typing/src/lib.rs new file mode 100644 index 0000000..f036942 --- /dev/null +++ b/crates/typing/src/lib.rs @@ -0,0 +1,182 @@ +use { + std::collections::HashMap, + tree_sitter::{Node, Parser, Range, Tree}, +}; + +pub fn new_parser() -> Parser { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_r::LANGUAGE.into()) + .expect("Error loading R parser"); + parser +} + +pub fn parse(parser: &mut Parser, text: impl AsRef<[u8]>, maybe_tree: Option<&Tree>) -> Tree { + parser.parse(text, maybe_tree).unwrap() +} + +#[derive(Debug)] +pub enum Type { + /// Explicit opt-out of type checking + Any, + /// Type could not be inferred + Unknown, + /// The null type + Null, + // Atomic scalar + Scalar(Atomic), + // Atomic vector + Vector(Atomic), + /// Homogeneous list of arbitrary length + List(Box), + /// Heterogeneous list of fixed length with named fields + Record(HashMap), + /// Heterogeneous list of fixed length with positional fields + Tuple(Vec), + /// Function type + Function { + params: Vec, + named_params: HashMap, + return_type: Box, + }, +} + +#[derive(Debug, Clone, Copy)] +pub enum Atomic { + Logical, + Integer, + Double, + Complex, + Character, + Raw, +} + +#[derive(Debug)] +pub struct Error { + pub message: String, + pub range: Range, +} + +pub fn check(node: Node) -> Result { + match node.kind() { + "float" => Ok(Type::Scalar(Atomic::Double)), + "integer" => Ok(Type::Scalar(Atomic::Integer)), + "complex" => Ok(Type::Scalar(Atomic::Complex)), + "string" => Ok(Type::Scalar(Atomic::Character)), + "false" | "true" => Ok(Type::Scalar(Atomic::Logical)), + "binary_operator" => { + let lhs = node.child_by_field_name("lhs").unwrap(); + let rhs = node.child_by_field_name("rhs").unwrap(); + let lhs_type = check(lhs)?; + let rhs_type = check(rhs)?; + + let operator = node.child_by_field_name("operator").unwrap(); + match operator.kind() { + "+" => match (lhs_type, rhs_type) { + (Type::Scalar(Atomic::Integer), Type::Scalar(Atomic::Integer)) => { + Ok(Type::Scalar(Atomic::Integer)) + } + (Type::Scalar(Atomic::Double), Type::Scalar(Atomic::Double)) => { + Ok(Type::Scalar(Atomic::Double)) + } + (Type::Scalar(Atomic::Character), Type::Scalar(Atomic::Character)) => { + Ok(Type::Scalar(Atomic::Character)) + } + (Type::Scalar(Atomic::Integer), Type::Scalar(Atomic::Double)) + | (Type::Scalar(Atomic::Double), Type::Scalar(Atomic::Integer)) => { + Ok(Type::Scalar(Atomic::Double)) + } + (Type::Scalar(Atomic::Complex), Type::Scalar(Atomic::Integer)) + | (Type::Scalar(Atomic::Integer), Type::Scalar(Atomic::Complex)) + | (Type::Scalar(Atomic::Complex), Type::Scalar(Atomic::Double)) + | (Type::Scalar(Atomic::Double), Type::Scalar(Atomic::Complex)) + | (Type::Scalar(Atomic::Complex), Type::Scalar(Atomic::Complex)) => { + Ok(Type::Scalar(Atomic::Complex)) + } + // todo: don't use wildecards here! + (a, b) => Err(Error { + message: format!("Cannot add types {a:?} and {b:?}"), + range: node.range(), + }), + }, + "<-" => { + // TODO: add type + Ok(lhs_type) + } + _ => Ok(Type::Unknown), + } + } + "program" | "block" => node + .children(&mut node.walk()) + .map(|child| check(child)) + .last() + .unwrap_or(Ok(Type::Unknown)), + _ => Ok(Type::Unknown), + } +} +#[cfg(test)] +mod tests { + use super::*; + + fn setup(source: &str) -> Result { + let mut parser = new_parser(); + let tree = parse(&mut parser, source, None); + let root = tree.root_node(); + check(root) + } + + #[test] + fn test_atomics() { + assert!(matches!( + setup("TRUE").unwrap(), + Type::Scalar(Atomic::Logical) + )); + assert!(matches!( + setup("FALSE").unwrap(), + Type::Scalar(Atomic::Logical) + )); + assert!(matches!( + setup("42L").unwrap(), + Type::Scalar(Atomic::Integer) + )); + assert!(matches!( + setup("3.14").unwrap(), + Type::Scalar(Atomic::Double) + )); + assert!(matches!( + setup("1+2i").unwrap(), + Type::Scalar(Atomic::Complex) + )); + assert!(matches!( + setup(r#""hello""#).unwrap(), + Type::Scalar(Atomic::Character) + )); + } + + #[test] + fn test_addition() { + assert!(matches!( + setup("1L + 2L").unwrap(), + Type::Scalar(Atomic::Integer) + )); + assert!(matches!( + setup("1.0 + 2.0").unwrap(), + Type::Scalar(Atomic::Double) + )); + assert!(matches!( + setup(r#""a" + "b""#).unwrap(), + Type::Scalar(Atomic::Character) + )); + } + + #[test] + fn test_add_incompatible_types() { + assert!(setup(r#"1 + "a""#).is_err()); + } + + #[test] + fn test_program_returns_last_type() { + let ty = setup(r#"1\n2.0\n"foo""#).unwrap(); + assert!(matches!(ty, Type::Scalar(Atomic::Character))); + } +} diff --git a/crates/typing/src/main.rs b/crates/typing/src/main.rs index e7a11a9..3d0bcc7 100644 --- a/crates/typing/src/main.rs +++ b/crates/typing/src/main.rs @@ -1,3 +1,18 @@ fn main() { - println!("Hello, world!"); + let mut parser = typing::new_parser(); + + let exprs = vec![r#"4 + 4"#, r#""foo" + 4"#, r#"fn(foo)"#]; + for expr in exprs { + let tree = typing::parse(&mut parser, expr, None); + let result = typing::check(tree.root_node()); + match result { + Ok(typ) => eprintln!("expr: {expr}\ntype: {typ:?}\n"), + Err(err) => eprintln!( + "expr: {expr}\nrange: {:?}\nerror: {}\n", + err.range, err.message + ), + } + } + + // TODO: use miette }