diff --git a/Cargo.lock b/Cargo.lock index 35a08219c052f..88043fd21d7fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,21 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "binary-serialize-derive" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d615dce242f5c93ae0af95f380ff2d72f2944158dc2fa81f5420bff9e70254" +dependencies = [ + "syn-helpers", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -241,6 +256,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -408,6 +433,26 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "derive-debug-extras" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e7a68c77a933db6b8907154fbd45f15f6ccc11f796b00b04354b8899d9d131" +dependencies = [ + "syn-helpers", +] + +[[package]] +name = "derive-enum-from-into" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc2a1b7c0031fb651e9bc1fa4255da82747c187b9ac1dc36b3783d71fadd9d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -424,6 +469,12 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "either_n" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c91ae510829160d5cfb19eb4ae7b6e01d44b767ca8f727c6cee936e53cc9ae5" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -448,6 +499,37 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "enum-variants-strings" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208ec1cfed58007d641f74552a523a405cd374417ec65ba01fb89ab2796054a1" +dependencies = [ + "enum-variants-strings-derive", +] + +[[package]] +name = "enum-variants-strings-derive" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acea45431925e008a911e3fded23d302c9eb81493e7b9cae0c5aa29a9342a5a" +dependencies = [ + "either_n", + "proc-macro2", + "quote", + "string-cases", + "syn 1.0.109", +] + +[[package]] +name = "erased-serde" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.1" @@ -469,6 +551,31 @@ dependencies = [ "libc", ] +[[package]] +name = "ezno-checker" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e5c8dfbd90b19744169af6bc83e3b7b8dd0e359d8e39db9c959cdde8378aba" +dependencies = [ + "bimap", + "binary-serialize-derive", + "derive-debug-extras", + "derive-enum-from-into", + "either", + "enum-variants-strings", + "erased-serde", + "indexmap", + "iterator-endiate", + "levenshtein", + "map_vec", + "once_cell", + "ordered-float", + "path-absolutize", + "serde", + "source-map", + "temporary-annex", +] + [[package]] name = "flate2" version = "1.0.26" @@ -674,6 +781,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +[[package]] +name = "iterator-endiate" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab947031a0a0cb37f982ef2a0ab3bacfd3de5ed97dd5c7e98bcc92bba357112" + [[package]] name = "itertools" version = "0.10.5" @@ -724,6 +837,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + [[package]] name = "libc" version = "0.2.144" @@ -774,6 +893,15 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +[[package]] +name = "map_vec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e9839b06bd5129fa636a93cb53601ffae739b8ff115841374d110cafb02cad" +dependencies = [ + "serde", +] + [[package]] name = "memchr" version = "2.5.0" @@ -949,6 +1077,15 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "ordered-float" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc2dbde8f8a79f2102cc474ceb0ad68e3b80b85289ea62389b60e66777e4213" +dependencies = [ + "num-traits", +] + [[package]] name = "owo-colors" version = "3.5.0" @@ -1014,6 +1151,7 @@ name = "oxc_cli" version = "0.0.0" dependencies = [ "clap", + "codespan-reporting", "ignore", "jemallocator", "miette", @@ -1025,6 +1163,7 @@ dependencies = [ "oxc_parser", "oxc_semantic", "oxc_span", + "oxc_type_synthesis", "rayon", "rustc-hash", ] @@ -1260,6 +1399,20 @@ dependencies = [ "url", ] +[[package]] +name = "oxc_type_synthesis" +version = "0.0.0" +dependencies = [ + "ezno-checker", + "miette", + "oxc_allocator", + "oxc_ast", + "oxc_parser", + "oxc_span", + "oxc_syntax", + "serde_json", +] + [[package]] name = "oxc_wasm" version = "0.0.0" @@ -1283,6 +1436,24 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "path-absolutize" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e" +dependencies = [ + "once_cell", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1685,6 +1856,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +[[package]] +name = "source-map" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedd4de16a29859eebdc14368c1fc2f5227ba58e1fdaba748072bf80ed9c5a35" +dependencies = [ + "serde", +] + [[package]] name = "spin" version = "0.5.2" @@ -1710,6 +1890,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string-cases" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31d23461f9e0fbe756cf9d5a36be93740fe12c8b094409a5f78f0f912ee2b6f" + [[package]] name = "strsim" version = "0.10.0" @@ -1766,6 +1952,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-helpers" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872f211f0299a39fff932355b2e304ef0e95a486a8147244fa8d477c1b0acfce" +dependencies = [ + "either_n", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "temporary-annex" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9c16d33fb2759286102fa235fd7029a8de2c2961165b2942c9cac81607a044" + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index ce2008d0207a6..76dfa8aba47c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,15 +24,16 @@ oxc_formatter = { version = "0.0.6", path = "crates/oxc_formatter" } oxc_semantic = { version = "0.0.6", path = "crates/oxc_semantic" } # publish = false -oxc_span = { version = "0.0.0", path = "crates/oxc_span" } -oxc_macros = { version = "0.0.0", path = "crates/oxc_macros" } -oxc_linter = { version = "0.0.0", path = "crates/oxc_linter" } -oxc_minifier = { version = "0.0.0", path = "crates/oxc_minifier" } -oxc_hir = { version = "0.0.0", path = "crates/oxc_hir" } -oxc_ast_lower = { version = "0.0.0", path = "crates/oxc_ast_lower" } -oxc_syntax = { version = "0.0.0", path = "crates/oxc_syntax" } -oxc_index = { version = "0.0.0", path = "crates/oxc_index" } -oxc_semantic2 = { version = "0.0.0", path = "crates/oxc_semantic2" } +oxc_span = { version = "0.0.0", path = "crates/oxc_span" } +oxc_macros = { version = "0.0.0", path = "crates/oxc_macros" } +oxc_linter = { version = "0.0.0", path = "crates/oxc_linter" } +oxc_type_synthesis = { version = "0.0.0", path = "crates/oxc_type_synthesis" } +oxc_minifier = { version = "0.0.0", path = "crates/oxc_minifier" } +oxc_hir = { version = "0.0.0", path = "crates/oxc_hir" } +oxc_ast_lower = { version = "0.0.0", path = "crates/oxc_ast_lower" } +oxc_syntax = { version = "0.0.0", path = "crates/oxc_syntax" } +oxc_index = { version = "0.0.0", path = "crates/oxc_index" } +oxc_semantic2 = { version = "0.0.0", path = "crates/oxc_semantic2" } oxc_tasks_common = { path = "tasks/common" } diff --git a/crates/oxc_cli/Cargo.toml b/crates/oxc_cli/Cargo.toml index e46d55e30bf76..3d9c96bbf5452 100644 --- a/crates/oxc_cli/Cargo.toml +++ b/crates/oxc_cli/Cargo.toml @@ -17,12 +17,16 @@ jemallocator = { workspace = true } mimalloc = { workspace = true } [dependencies] -oxc_diagnostics = { workspace = true } -oxc_allocator = { workspace = true } -oxc_parser = { workspace = true } -oxc_semantic = { workspace = true } -oxc_linter = { workspace = true } -oxc_span = { workspace = true } +oxc_diagnostics = { workspace = true } +oxc_allocator = { workspace = true } +oxc_parser = { workspace = true } +oxc_semantic = { workspace = true } +oxc_linter = { workspace = true } +oxc_type_synthesis = { workspace = true } +oxc_span = { workspace = true } + +# TODO temp, for type check output, replace with Miette +codespan-reporting = "0.11.1" clap = { workspace = true } rayon = { workspace = true } diff --git a/crates/oxc_cli/src/lib.rs b/crates/oxc_cli/src/lib.rs index e2a13803b284c..d6f72ce5e1990 100644 --- a/crates/oxc_cli/src/lib.rs +++ b/crates/oxc_cli/src/lib.rs @@ -1,14 +1,16 @@ // mod git; mod lint; mod result; +mod type_check; mod walk; use clap::{Arg, Command}; -use crate::lint::lint_command; +use crate::{lint::lint_command, type_check::type_check_command}; pub use crate::{ lint::{LintOptions, LintRunner}, result::CliRunResult, + type_check::{TypeCheckOptions, TypeCheckRunner}, walk::Walk, }; @@ -21,6 +23,7 @@ pub fn command() -> Command { .subcommand_required(true) .arg_required_else_help(true) .subcommand(lint_command()) + .subcommand(type_check_command()) .arg( Arg::new("threads") .long("threads") diff --git a/crates/oxc_cli/src/lint/command.rs b/crates/oxc_cli/src/lint/command.rs index ef071897369fc..7649148a5100e 100644 --- a/crates/oxc_cli/src/lint/command.rs +++ b/crates/oxc_cli/src/lint/command.rs @@ -2,7 +2,6 @@ use clap::{builder::ValueParser, Arg, ArgAction, Command}; pub fn lint_command() -> Command { Command::new("lint") - .alias("check") .about("Lint this repository.") .arg_required_else_help(true) .after_help( diff --git a/crates/oxc_cli/src/main.rs b/crates/oxc_cli/src/main.rs index 456090a5f9298..f3673ab27b3e2 100644 --- a/crates/oxc_cli/src/main.rs +++ b/crates/oxc_cli/src/main.rs @@ -8,7 +8,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; -use oxc_cli::{command, CliRunResult, LintOptions, LintRunner}; +use oxc_cli::{command, CliRunResult, LintOptions, LintRunner, TypeCheckOptions, TypeCheckRunner}; fn main() -> CliRunResult { let matches = command().get_matches(); @@ -32,6 +32,10 @@ fn main() -> CliRunResult { LintRunner::new(options).run() } + "check" => { + let options = TypeCheckOptions::from(matches); + TypeCheckRunner::new(options).run() + } _ => CliRunResult::None, } } diff --git a/crates/oxc_cli/src/result.rs b/crates/oxc_cli/src/result.rs index 24e9ca1670102..bc2823ef11947 100644 --- a/crates/oxc_cli/src/result.rs +++ b/crates/oxc_cli/src/result.rs @@ -17,6 +17,10 @@ pub enum CliRunResult { number_of_diagnostics: usize, max_warnings_exceeded: bool, }, + TypeCheckResult { + duration: std::time::Duration, + number_of_diagnostics: usize, + }, } impl Termination for CliRunResult { @@ -54,6 +58,19 @@ impl Termination for CliRunResult { println!("Found no errors."); ExitCode::from(0) } + Self::TypeCheckResult { duration, number_of_diagnostics } => { + let ms = duration.as_millis(); + println!("Finished in {ms}ms."); + + if number_of_diagnostics > 0 { + println!("Found {number_of_diagnostics} errors."); + return ExitCode::from(1); + } + + // TODO + // println!("Found no errors."); + ExitCode::from(0) + } } } } diff --git a/crates/oxc_cli/src/type_check/mod.rs b/crates/oxc_cli/src/type_check/mod.rs new file mode 100644 index 0000000000000..0358dfe408adb --- /dev/null +++ b/crates/oxc_cli/src/type_check/mod.rs @@ -0,0 +1,316 @@ +use std::path::{Path, PathBuf}; + +use clap::{Arg, ArgMatches, Command}; +use oxc_allocator::Allocator; +use oxc_parser::Parser; +use oxc_span::SourceType; +use oxc_type_synthesis::synthesize_program; + +use crate::CliRunResult; + +const PRELUDE: &str = " +type StringOrNumber = string | number; + +interface Operators { + Add(a: T, b: U): (T extends string ? string : U extends string ? string: number) & Ezno.ConstantFunction<'add'>; + + Mul(a: number, b: number): number & Ezno.ConstantFunction<'mul'>; + + StrictEqual(a: any, b: any): boolean & Ezno.ConstantFunction<'equal'>; +} + +interface Math { + sin(x: number): number & Ezno.ConstantFunction<'sin'>; +} + +interface string { + toUppercase(): string & Ezno.ConstantFunction<'uppercase'> +} + +interface Console { + log(msg: any): void; +} + +declare var Math: Math; +declare var console: Console; +"; + +/// TODO temp +#[derive(Debug)] +#[allow(clippy::struct_excessive_bools)] +pub struct TypeCheckOptions { + pub path: PathBuf, +} + +#[allow(clippy::fallible_impl_from)] +impl<'a> From<&'a ArgMatches> for TypeCheckOptions { + fn from(matches: &'a ArgMatches) -> Self { + Self { path: PathBuf::from(matches.get_one::("path").unwrap()) } + } +} + +pub fn type_check_command() -> Command { + Command::new("check") + .about( + "NOTE: Experimental / work in progress. Check source code for type errors using Ezno", + ) + .arg(Arg::new("path").value_name("PATH").num_args(1..).help("File to type check")) +} + +pub struct TypeCheckRunner { + options: TypeCheckOptions, +} + +impl TypeCheckRunner { + pub fn new(options: TypeCheckOptions) -> Self { + Self { options } + } + + /// # Panics + pub fn run(&self) -> CliRunResult { + let now = std::time::Instant::now(); + + let path = Path::new(&self.options.path); + let source_text = PRELUDE.to_owned() + + &std::fs::read_to_string(path) + .unwrap_or_else(|_| panic!("{} not found", self.options.path.display())); + let allocator = Allocator::default(); + let source_type = SourceType::from_path(path).unwrap(); + + let ret = Parser::new(&allocator, &source_text, source_type).parse(); + + if ret.errors.is_empty() { + let (diagnostics, _events, _types) = + synthesize_program(&ret.program, |_: &std::path::Path| None); + + let duration = now.elapsed(); + + // if args.iter().any(|arg| arg == "--types") { + // eprintln!("Types:"); + // for item in types { + // eprintln!("\t{:?}", item); + // } + // } + // if args.iter().any(|arg| arg == "--events") { + // eprintln!("Events:"); + // for item in events { + // eprintln!("\t{:?}", item); + // } + // } + + // TODO + let number_of_diagnostics = 0; + type_check_output::print_diagnostics_container( + diagnostics, + self.options.path.display().to_string(), + source_text.clone(), + ); + CliRunResult::TypeCheckResult { duration, number_of_diagnostics } + } else { + let duration = now.elapsed(); + let number_of_diagnostics = ret.errors.len(); + for error in ret.errors { + let error = error.with_source_code(source_text.clone()); + println!("{error:?}"); + } + CliRunResult::TypeCheckResult { duration, number_of_diagnostics } + } + } +} + +mod type_check_output { + use std::iter; + + use codespan_reporting::{ + diagnostic::{Diagnostic, Label}, + files::SimpleFile, + term::{ + termcolor::{ColorChoice, StandardStream}, + Config, + }, + }; + use oxc_type_synthesis::{ + Diagnostic as TypeCheckDiagnostic, DiagnosticsContainer, ErrorWarningInfo, + }; + + #[allow(clippy::items_after_statements)] + pub(super) fn print_diagnostics_container( + error_handler: DiagnosticsContainer, + path: String, + content: String, + ) { + let files = SimpleFile::new(path, content); + // let mut file_id_to_source_id = HashMap::::new(); + + // Handling adding filename-file id mappings + // for source_id in error_handler.sources() { + // let (filename, file_content) = source_id.get_file().unwrap(); + // let name = + // filename.strip_prefix(env::current_dir().unwrap()).unwrap_or(&filename).to_owned(); + // let file_id = files.add(name.display().to_string(), file_content); + // file_id_to_source_id.insert(source_id, file_id); + // } + + for item in error_handler.into_iter().rev() { + // TODO tidy this up: + let (diagnostic, info) = match item { + ErrorWarningInfo::Error(error) => (Diagnostic::error(), error), + ErrorWarningInfo::Warning(warning) => (Diagnostic::warning(), warning), + ErrorWarningInfo::Info(info) => (Diagnostic::note(), info), + ErrorWarningInfo::Data(_) => { + continue; + } + }; + + let diagnostic = checker_diagnostic_to_code_span_diagnostic(diagnostic, info); + + emit(&files, &diagnostic); + + #[cfg(target_arch = "wasm")] + fn emit<'a, F: codespan_reporting::files::Files<'a>>( + files: &F, + diagnostic: &Diagnostic, + ) { + todo!("buffer then print") + } + + #[cfg(not(target_arch = "wasm"))] + fn emit<'a, F: codespan_reporting::files::Files<'a>>( + files: &'a F, + diagnostic: &Diagnostic, + ) { + let writer = StandardStream::stderr(ColorChoice::Always); + + // TODO lines in diagnostic could be different + codespan_reporting::term::emit( + &mut writer.lock(), + &Config::default(), + files, + diagnostic, + ) + .unwrap(); + } + } + } + + fn checker_diagnostic_to_code_span_diagnostic( + diagnostic: Diagnostic<()>, + information: TypeCheckDiagnostic, + // source_map: &HashMap, + ) -> Diagnostic<()> { + match information { + TypeCheckDiagnostic::Global(message) => diagnostic.with_message(message), + TypeCheckDiagnostic::Position { reason: message, pos } => { + diagnostic.with_labels(vec![Label::primary((), pos).with_message(message)]) + } + TypeCheckDiagnostic::PositionWithAdditionLabels { reason, pos, labels } => { + let (labels, notes) = + labels.into_iter().partition::, _>(|(_, value)| value.is_some()); + + diagnostic + .with_labels( + iter::once(Label::primary((), pos).with_message(reason)) + .chain(labels.into_iter().map(|(message, pos)| { + let pos = pos.unwrap(); + Label::secondary((), pos).with_message(message) + })) + .collect(), + ) + .with_notes(notes.into_iter().map(|(message, _)| message).collect()) + } + } + } +} + +// struct MietteEznoDiagnostic { +// diagnostic: EznoDiagnostic, +// severity: miette::Severity, +// // TODO temp +// source: &'static str, +// } + +// impl std::fmt::Debug for MietteEznoDiagnostic { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.debug_struct("MietteEznoDiagnostic").field("diagnostic", &"..").finish() +// } +// } + +// impl std::fmt::Display for MietteEznoDiagnostic { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.write_str(self.diagnostic.reason()) +// } +// } +// impl std::error::Error for MietteEznoDiagnostic {} + +// impl miette::Diagnostic for MietteEznoDiagnostic { +// fn code<'a>(&'a self) -> Option> { +// None +// } + +// fn severity(&self) -> Option { +// Some(self.severity) +// } + +// fn help<'a>(&'a self) -> Option> { +// None +// } + +// fn url<'a>(&'a self) -> Option> { +// None +// } + +// fn source_code(&self) -> Option<&dyn miette::SourceCode> { +// match self.diagnostic { +// EznoDiagnostic::Global(_) => None, +// EznoDiagnostic::Position { reason: _, ref pos } +// | EznoDiagnostic::PositionWithAdditionLabels { reason: _, labels: _, ref pos } => { +// // TODO temp +// None +// } +// } +// } + +// fn labels(&self) -> Option + '_>> { +// match self.diagnostic { +// // TODO temp +// EznoDiagnostic::Global(ref reason) => { +// Some(Box::new(iter::once(miette::LabeledSpan::new(Some(reason.clone()), 0, 0)))) +// } +// EznoDiagnostic::Position { ref reason, ref pos } => { +// Some(Box::new(iter::once(miette::LabeledSpan::new( +// Some(reason.clone()), +// pos.start as usize, +// pos.end as usize - pos.start as usize, +// )))) +// } +// EznoDiagnostic::PositionWithAdditionLabels { ref reason, ref labels, ref pos } => { +// Some(Box::new( +// iter::once(miette::LabeledSpan::new( +// Some(reason.clone()), +// pos.start as usize, +// pos.end as usize - pos.start as usize, +// )) +// .chain(labels.iter().map(|(label, pos)| { +// if let Some(pos) = pos { +// miette::LabeledSpan::new( +// Some(label.clone()), +// pos.start as usize, +// pos.end as usize - pos.start as usize, +// ) +// } else { +// miette::LabeledSpan::new(Some(label.clone()), 0, 0) +// } +// })), +// )) +// } +// } +// } + +// fn related<'a>(&'a self) -> Option + 'a>> { +// None +// } + +// fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> { +// None +// } +// } diff --git a/crates/oxc_type_synthesis/Cargo.toml b/crates/oxc_type_synthesis/Cargo.toml new file mode 100644 index 0000000000000..0633a237b61e7 --- /dev/null +++ b/crates/oxc_type_synthesis/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "oxc_type_synthesis" +version = "0.0.0" +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +oxc_parser = { workspace = true } +oxc_ast = { workspace = true } +oxc_span = { workspace = true } +oxc_syntax = { workspace = true } +oxc_allocator = { workspace = true } +serde_json = { workspace = true } + +ezno-checker = { version = "0.0.2" } + +[dev_dependencies] +miette = { workspace = true, features = ["fancy-no-backtrace"] } diff --git a/crates/oxc_type_synthesis/examples/check.rs b/crates/oxc_type_synthesis/examples/check.rs new file mode 100644 index 0000000000000..847c038e75a19 --- /dev/null +++ b/crates/oxc_type_synthesis/examples/check.rs @@ -0,0 +1,76 @@ +use std::{env, path::Path}; + +use oxc_allocator::Allocator; +use oxc_parser::Parser; +use oxc_span::SourceType; +use oxc_type_synthesis::synthesize_program; + +const PRELUDE: &str = " +type StringOrNumber = string | number; + +interface Operators { + Add(a: T, b: U): (T extends string ? string : U extends string ? string: number) & Ezno.ConstantFunction<'add'>; + + Mul(a: number, b: number): number & Ezno.ConstantFunction<'mul'>; + + StrictEqual(a: any, b: any): boolean & Ezno.ConstantFunction<'equal'>; +} + +interface Math { + sin(x: number): number & Ezno.ConstantFunction<'sin'>; +} + +interface string { + toUppercase(): string & Ezno.ConstantFunction<'uppercase'> +} + +interface Console { + log(msg: any): void; +} + +declare var Math: Math; +declare var console: Console; +"; + +fn main() { + let name = env::args().nth(1).unwrap_or_else(|| "examples/demo.ts".to_string()); + let path = Path::new(&name); + let source_text = PRELUDE.to_owned() + + &std::fs::read_to_string(path).unwrap_or_else(|_| panic!("{name} not found")); + let allocator = Allocator::default(); + let source_type = SourceType::from_path(path).unwrap(); + let ret = Parser::new(&allocator, &source_text, source_type).parse(); + + if ret.errors.is_empty() { + println!("Program parsed"); + // println!("{}", serde_json::to_string_pretty(&ret.program).unwrap()); + + let (diagnostics, events, types) = + synthesize_program(&ret.program, |_: &std::path::Path| None); + + let args: Vec<_> = env::args().collect(); + + if args.iter().any(|arg| arg == "--types") { + eprintln!("Types:"); + for item in types { + eprintln!("\t{item:?}"); + } + } + if args.iter().any(|arg| arg == "--events") { + eprintln!("Events:"); + for item in events { + eprintln!("\t{item:?}"); + } + } + + eprintln!("Diagnostics:"); + for diag in diagnostics.into_iter() { + eprintln!("\t{}", diag.get_diagnostic().unwrap().reason()); + } + } else { + for error in ret.errors { + let error = error.with_source_code(source_text.clone()); + println!("{error:?}"); + } + } +} diff --git a/crates/oxc_type_synthesis/examples/demo.ts b/crates/oxc_type_synthesis/examples/demo.ts new file mode 100644 index 0000000000000..e7d796ce46b1e --- /dev/null +++ b/crates/oxc_type_synthesis/examples/demo.ts @@ -0,0 +1,41 @@ +const a: 2 = 4; + +const b: 3 = 5 + 2; + +let c = 5; +c = 3; +let d: 2 = c; + +interface Car { + model: string, + power: number, + weight: number +} + +const car1: Car = { model: "Koenigsegg One:1", power: 1360, weight: 1360 } + +console.lag("log not lag") + +const weight: string = car1["we" + "ight"] + +if (car1.power === car1.weight) { + console.log("always here") +} + +function assertType(t: T): void; + +function getPerson(name: string) { + return { name } +} + +assertType<{name: "not ben" }>(getPerson("Ben")); + +function throwValue(value) { + throw value +} + +try { + throwValue("my error") +} catch (e) { + assertType<"different error">(e) +} diff --git a/crates/oxc_type_synthesis/src/expressions.rs b/crates/oxc_type_synthesis/src/expressions.rs new file mode 100644 index 0000000000000..a9a119cddb95e --- /dev/null +++ b/crates/oxc_type_synthesis/src/expressions.rs @@ -0,0 +1,264 @@ +use ezno_checker::{ + self, structures::functions::SynthesizedArgument, CheckingData, Environment, FSResolver, TypeId, +}; +use oxc_ast::ast; +use oxc_span::GetSpan; + +use super::property_key_to_type; +use crate::{oxc_span_to_source_map_span, types::synthesize_type_annotation}; + +pub(crate) fn synthesize_expression( + expr: &ast::Expression, + environment: &mut Environment, + checking_data: &mut CheckingData, +) -> TypeId { + match expr { + ast::Expression::BooleanLiteral(boolean) => { + checking_data.types.new_constant_type(ezno_checker::Constant::Boolean(boolean.value)) + } + ast::Expression::NullLiteral(_) => TypeId::NULL_TYPE, + ast::Expression::BigintLiteral(_) => todo!(), + ast::Expression::RegExpLiteral(_) => todo!(), + ast::Expression::NumberLiteral(number) => checking_data.types.new_constant_type( + number + .value + .try_into() + .map(ezno_checker::Constant::Number) + .unwrap_or(ezno_checker::Constant::NaN), + ), + ast::Expression::StringLiteral(string) => { + // TODO could be better here :) + checking_data + .types + .new_constant_type(ezno_checker::Constant::String(string.value.as_str().to_owned())) + } + ast::Expression::TemplateLiteral(_) => todo!(), + ast::Expression::Identifier(identifier) => { + let result = environment.get_variable_or_error( + &identifier.name, + &oxc_span_to_source_map_span(identifier.span), + checking_data, + ); + + match result { + Ok(ok) => ok.1, + Err(err) => err, + } + } + ast::Expression::MetaProperty(_) => todo!(), + ast::Expression::Super(_) => todo!(), + ast::Expression::ArrayExpression(_) => todo!(), + ast::Expression::AssignmentExpression(assignment) => { + synthesize_assignment(assignment, environment, checking_data) + } + ast::Expression::AwaitExpression(_) => todo!(), + ast::Expression::BinaryExpression(bin_expr) => synthesize_binary_expression( + &bin_expr.left, + bin_expr.operator, + &bin_expr.right, + checking_data, + environment, + ), + ast::Expression::CallExpression(expr) => { + let parent = synthesize_expression(&expr.callee, environment, checking_data); + + let type_arguments = expr.type_parameters.as_ref().map(|tp| { + tp.params + .iter() + .map(|ta| { + ( + oxc_span_to_source_map_span(ta.span()), + synthesize_type_annotation(ta, environment, checking_data), + ) + }) + .collect() + }); + + let arguments: Vec = expr + .arguments + .iter() + .map(|arg| match arg { + ast::Argument::SpreadElement(_) => todo!(), + ast::Argument::Expression(expr) => { + let ty = synthesize_expression(expr, environment, checking_data); + SynthesizedArgument::NonSpread { + ty, + position: oxc_span_to_source_map_span(GetSpan::span(expr)), + } + } + }) + .collect(); + + // TODO + let this_argument = None; + + ezno_checker::call_type_handle_errors( + parent, + arguments, + this_argument, + type_arguments, + environment, + checking_data, + ezno_checker::events::CalledWithNew::None, + oxc_span_to_source_map_span(expr.span), + ) + } + ast::Expression::ChainExpression(_) => todo!(), + ast::Expression::ClassExpression(_) => todo!(), + ast::Expression::ConditionalExpression(_) => todo!(), + ast::Expression::FunctionExpression(_) => todo!(), + ast::Expression::ImportExpression(_) => todo!(), + ast::Expression::LogicalExpression(_) => todo!(), + ast::Expression::MemberExpression(expr) => match &**expr { + ast::MemberExpression::ComputedMemberExpression(comp) => { + let property = synthesize_expression(&comp.expression, environment, checking_data); + let parent = synthesize_expression(&comp.object, environment, checking_data); + environment.get_property_handle_errors( + parent, + property, + checking_data, + oxc_span_to_source_map_span(comp.span), + ) + } + ast::MemberExpression::StaticMemberExpression(expr) => { + let parent = synthesize_expression(&expr.object, environment, checking_data); + let property = checking_data.types.new_constant_type( + ezno_checker::Constant::String(expr.property.name.as_str().to_owned()), + ); + + environment.get_property_handle_errors( + parent, + property, + checking_data, + oxc_span_to_source_map_span(expr.span), + ) + } + ast::MemberExpression::PrivateFieldExpression(_) => todo!(), + }, + ast::Expression::NewExpression(_) => todo!(), + ast::Expression::ObjectExpression(object) => { + synthesize_object(object, environment, checking_data) + } + ast::Expression::ParenthesizedExpression(inner) => { + synthesize_expression(&inner.expression, environment, checking_data) + } + ast::Expression::SequenceExpression(_) => todo!(), + ast::Expression::TaggedTemplateExpression(_) => todo!(), + ast::Expression::ThisExpression(_) => todo!(), + ast::Expression::UnaryExpression(_) => todo!(), + ast::Expression::UpdateExpression(_update_expr) => { + todo!("update_expr") + } + ast::Expression::YieldExpression(_) => todo!(), + ast::Expression::PrivateInExpression(_) => todo!(), + ast::Expression::JSXElement(_) => todo!(), + ast::Expression::JSXFragment(_) => todo!(), + ast::Expression::TSAsExpression(_) => todo!(), + ast::Expression::TSSatisfiesExpression(_) => todo!(), + ast::Expression::TSTypeAssertion(_) => todo!(), + ast::Expression::TSNonNullExpression(_) => todo!(), + ast::Expression::TSInstantiationExpression(_) => todo!(), + ast::Expression::ArrowExpression(_) => todo!(), + } +} + +// TODO others need to be built into helper methods in the checker +fn synthesize_assignment( + assignment: &ast::AssignmentExpression, + environment: &mut Environment, + checking_data: &mut CheckingData, +) -> TypeId { + match &assignment.left { + ast::AssignmentTarget::SimpleAssignmentTarget(simple) => match simple { + ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(identifier) => { + let rhs = synthesize_expression(&assignment.right, environment, checking_data); + environment.assign_variable_handle_errors( + identifier.name.as_str(), + oxc_span_to_source_map_span(assignment.span), + rhs, + checking_data, + ) + } + ast::SimpleAssignmentTarget::MemberAssignmentTarget(_) => todo!(), + ast::SimpleAssignmentTarget::TSAsExpression(_) => todo!(), + ast::SimpleAssignmentTarget::TSSatisfiesExpression(_) => todo!(), + ast::SimpleAssignmentTarget::TSNonNullExpression(_) => todo!(), + ast::SimpleAssignmentTarget::TSTypeAssertion(_) => todo!(), + }, + ast::AssignmentTarget::AssignmentTargetPattern(_) => todo!(), + } +} + +/// TODO this logic needs to be moved to ezno-checker and +/// abstracted to use a builder pattern, which can be reused for array literals +pub(crate) fn synthesize_object( + object: &ast::ObjectExpression, + environment: &mut Environment, + checking_data: &mut CheckingData, +) -> TypeId { + let ty = environment.new_object(&mut checking_data.types, None); + for property in object.properties.iter() { + match property { + ast::ObjectPropertyKind::ObjectProperty(property) => { + let key_ty = property_key_to_type(&property.key, environment, checking_data); + + let value_ty = synthesize_expression(&property.value, environment, checking_data); + environment.register_property_on_object(ty, key_ty, value_ty); + } + ast::ObjectPropertyKind::SpreadProperty(_) => todo!(), + } + } + ty +} + +fn synthesize_binary_expression( + lhs: &ast::Expression, + operator: oxc_syntax::operator::BinaryOperator, + rhs: &ast::Expression, + checking_data: &mut CheckingData, + environment: &mut Environment, +) -> TypeId { + let lhs_ty = synthesize_expression(lhs, environment, checking_data); + let rhs_ty = synthesize_expression(rhs, environment, checking_data); + use oxc_syntax::operator::BinaryOperator; + + let op = match operator { + BinaryOperator::Equality => { + todo!() + } + BinaryOperator::Inequality => todo!(), + BinaryOperator::StrictEquality => { + ezno_checker::structures::operators::BinaryOperator::RelationOperator( + ezno_checker::structures::operators::RelationOperator::Equal, + ) + } + BinaryOperator::StrictInequality => todo!(), + BinaryOperator::LessThan => todo!(), + BinaryOperator::LessEqualThan => todo!(), + BinaryOperator::GreaterThan => todo!(), + BinaryOperator::GreaterEqualThan => todo!(), + BinaryOperator::ShiftLeft => todo!(), + BinaryOperator::ShiftRight => todo!(), + BinaryOperator::ShiftRightZeroFill => todo!(), + BinaryOperator::Addition => ezno_checker::structures::operators::BinaryOperator::Add, + BinaryOperator::Subtraction => todo!(), + BinaryOperator::Multiplication => { + ezno_checker::structures::operators::BinaryOperator::Multiply + } + BinaryOperator::Division => todo!(), + BinaryOperator::Remainder => todo!(), + BinaryOperator::BitwiseOR => todo!(), + BinaryOperator::BitwiseXOR => todo!(), + BinaryOperator::BitwiseAnd => todo!(), + BinaryOperator::In => todo!(), + BinaryOperator::Instanceof => todo!(), + BinaryOperator::Exponential => todo!(), + }; + ezno_checker::evaluate_binary_operator_handle_errors( + op, + (lhs_ty, oxc_span_to_source_map_span(GetSpan::span(lhs))), + (rhs_ty, oxc_span_to_source_map_span(GetSpan::span(rhs))), + environment, + checking_data, + ) +} diff --git a/crates/oxc_type_synthesis/src/functions.rs b/crates/oxc_type_synthesis/src/functions.rs new file mode 100644 index 0000000000000..04817f9d6d6f8 --- /dev/null +++ b/crates/oxc_type_synthesis/src/functions.rs @@ -0,0 +1,149 @@ +use ezno_checker::{ + context::VariableRegisterBehavior, + structures::parameters::{SynthesizedParameter, SynthesizedParameters}, + GenericFunctionTypeParameters, SynthesizableFunction, TypeId, +}; +use oxc_ast::ast; + +use crate::{ + oxc_span_to_source_map_span, + statements_and_declarations::{register_variable, synthesize_statements}, + types::synthesize_type_annotation, +}; + +pub(crate) struct OxcFunction<'a, 'b>(pub &'a ast::Function<'b>); + +impl<'a, 'b> SynthesizableFunction for OxcFunction<'a, 'b> { + fn is_declare(&self) -> bool { + self.0.is_ts_declare_function() + } + + fn parameters( + &self, + environment: &mut ezno_checker::Environment, + checking_data: &mut ezno_checker::CheckingData, + ) -> SynthesizedParameters { + synthesize_parameters(&self.0.params, environment, checking_data) + } + + fn body( + &self, + environment: &mut ezno_checker::Environment, + checking_data: &mut ezno_checker::CheckingData, + ) { + let body = self.0.body.as_ref().expect("trying to synthesize declare function"); + synthesize_statements(&body.statements, environment, checking_data); + } + + fn type_parameters( + &self, + environment: &mut ezno_checker::Environment, + checking_data: &mut ezno_checker::CheckingData, + ) -> GenericFunctionTypeParameters { + let type_parameters = self.0.type_parameters.as_deref(); + synthesize_type_parameters(type_parameters, environment, checking_data) + } + + fn return_type( + &self, + environment: &mut ezno_checker::Environment, + checking_data: &mut ezno_checker::CheckingData, + ) -> Option { + self.0 + .return_type + .as_ref() + .map(|ta| synthesize_type_annotation(&ta.type_annotation, environment, checking_data)) + } +} + +pub(crate) fn synthesize_type_parameters( + type_parameters: Option<&oxc_ast::ast::TSTypeParameterDeclaration>, + environment: &mut ezno_checker::Environment, + checking_data: &mut ezno_checker::CheckingData, +) -> GenericFunctionTypeParameters { + match type_parameters { + Some(params) => { + GenericFunctionTypeParameters::TypedParameters( + params + .params + .iter() + .map(|ta| { + // TODO effects in a map :/ + let constraint_type = ta + .constraint + .as_ref() + .map(|ta| synthesize_type_annotation(ta, environment, checking_data)); + let default_type = ta + .default + .as_ref() + .map(|ta| synthesize_type_annotation(ta, environment, checking_data)); + environment.new_explicit_type_parameter( + ta.name.name.as_str(), + constraint_type, + default_type, + &mut checking_data.types, + ) + }) + .collect(), + ) + } + None => GenericFunctionTypeParameters::None, + } +} + +pub(crate) fn synthesize_parameters( + params: &ast::FormalParameters, + environment: &mut ezno_checker::Environment, + checking_data: &mut ezno_checker::CheckingData, +) -> SynthesizedParameters { + let (mut parameters, optional_parameters, rest_parameter) = (Vec::new(), Vec::new(), None); + + for param in params.items.iter() { + let base = param + .pattern + .type_annotation + .as_ref() + .map(|ta| synthesize_type_annotation(&ta.type_annotation, environment, checking_data)) + .unwrap_or(TypeId::ANY_TYPE); + + let param_type = register_variable( + ¶m.pattern.kind, + ¶m.span, + environment, + checking_data, + VariableRegisterBehavior::FunctionParameter { base }, + ); + + match ¶m.pattern.kind { + p @ ast::BindingPatternKind::BindingIdentifier(_) + | p @ ast::BindingPatternKind::ObjectPattern(_) + | p @ ast::BindingPatternKind::ArrayPattern(_) => { + parameters.push(SynthesizedParameter { + name: param_to_string(p), + ty: param_type, + position: oxc_span_to_source_map_span(param.span), + }); + } + ast::BindingPatternKind::AssignmentPattern(_) => todo!(), + // ast::BindingPatternKind::RestElement(element) => { + // rest_parameter = Some(SynthesizedRestParameter { + // name: param_to_string(&element.argument.kind), + // item_type: param_type, + // position: oxc_span_to_source_map_span(element.span), + // }) + // } + } + } + + SynthesizedParameters { parameters, optional_parameters, rest_parameter } +} + +fn param_to_string(binding: &ast::BindingPatternKind) -> String { + match binding { + ast::BindingPatternKind::BindingIdentifier(ident) => ident.name.as_str().to_owned(), + ast::BindingPatternKind::ObjectPattern(_) => todo!(), + ast::BindingPatternKind::ArrayPattern(_) => todo!(), + // ast::BindingPatternKind::RestElement(_) => todo!(), + ast::BindingPatternKind::AssignmentPattern(_) => todo!(), + } +} diff --git a/crates/oxc_type_synthesis/src/interfaces.rs b/crates/oxc_type_synthesis/src/interfaces.rs new file mode 100644 index 0000000000000..dbfd9033c4d8a --- /dev/null +++ b/crates/oxc_type_synthesis/src/interfaces.rs @@ -0,0 +1,148 @@ +use ezno_checker::{CheckingData, Environment, TypeId}; +use oxc_ast::ast; + +use crate::{ + oxc_span_to_source_map_span, property_key_to_type, + statements_and_declarations::synthesize_statement, types::synthesize_type_annotation, +}; + +pub(crate) fn synthesize_interface( + interface: &ast::TSInterfaceDeclaration, + interface_id: TypeId, + environment: &mut Environment, + checking_data: &mut CheckingData, +) { + synthesize_signatures(&interface.body.body, environment, checking_data, interface_id); +} + +pub(crate) fn synthesize_signatures( + signatures: &[ast::TSSignature], + environment: &mut Environment, + checking_data: &mut CheckingData, + onto: TypeId, +) { + for declaration in signatures.iter() { + match declaration { + ast::TSSignature::TSIndexSignature(_) => todo!(), + ast::TSSignature::TSPropertySignature(property) => { + let key_ty = property_key_to_type(&property.key, environment, checking_data); + let value_ty = synthesize_type_annotation( + &property.type_annotation.as_ref().unwrap().type_annotation, + environment, + checking_data, + ); + environment.register_property(onto, key_ty, value_ty); + } + ast::TSSignature::TSCallSignatureDeclaration(_) => todo!(), + ast::TSSignature::TSConstructSignatureDeclaration(_) => todo!(), + ast::TSSignature::TSMethodSignature(method) => { + // TODO reuse more functions + let key_ty = property_key_to_type(&method.key, environment, checking_data); + + let ((type_parameters, parameters, returned, constant_fn), stuff, _) = environment + .new_lexical_environment_fold_into_parent( + ezno_checker::Scope::FunctionReference {}, + checking_data, + |environment, checking_data| { + let type_parameters = crate::functions::synthesize_type_parameters( + method.type_parameters.as_deref(), + environment, + checking_data, + ); + + let parameters = crate::functions::synthesize_parameters( + &method.params, + environment, + checking_data, + ); + + let (returned, constant_fn) = + if let Some(ta) = method.return_type.as_ref() { + let mut ta = &ta.type_annotation; + let mut constant_fn = None; + + // TODO temp + if let ast::TSType::TSIntersectionType(intersection) = ta { + if let ast::TSType::TSTypeReference(type_ref) = + intersection.types.last().unwrap() + { + if let ( + ast::TSTypeName::QualifiedName(ref qual), + Some(tp), + ) = (&type_ref.type_name, &type_ref.type_parameters) + { + if let ast::TSTypeName::IdentifierName( + ref parent_name, + ) = qual.left + { + if parent_name.name == "Ezno" { + // *remove* the right annotation + // TODO discards middle ones + ta = intersection.types.first().unwrap(); + + match qual.right.name.as_str() { + "Performs" => compute_internal_events( + tp, + environment, + checking_data, + ), + "ConstantFunction" => { + if let Some( + ast::TSType::TSLiteralType(lit), + ) = tp.params.first() + { + if let ast::TSLiteral::StringLiteral(string) = &lit.literal { + constant_fn = Some(string.value.as_str().to_owned()); + } + } + } + unknown => panic!("Ezno.{}", unknown), + } + } + } + } + } + } + + (synthesize_type_annotation(ta, environment, checking_data), constant_fn) + } else { + (TypeId::UNDEFINED_TYPE, None) + }; + (type_parameters, parameters, returned, constant_fn) + }, + ); + + let func_ty = checking_data.types.new_function_type_reference( + type_parameters, + parameters, + returned, + oxc_span_to_source_map_span(method.span), + stuff.unwrap().0, + constant_fn, + ); + + environment.register_property(onto, key_ty, func_ty); + } + } + } +} + +fn compute_internal_events( + tp: &ast::TSTypeParameterInstantiation, + environment: &mut Environment, + checking_data: &mut CheckingData, +) { + if let Some(ast::TSType::TSLiteralType(lit)) = tp.params.first() { + if let ast::TSLiteral::StringLiteral(string) = &lit.literal { + let source = string.value.as_str().to_owned(); + + let allocator = oxc_allocator::Allocator::default(); + let ret = oxc_parser::Parser::new(&allocator, &source, oxc_span::SourceType::default()) + .parse(); + + for statement in ret.program.body.iter() { + synthesize_statement(statement, environment, checking_data) + } + } + } +} diff --git a/crates/oxc_type_synthesis/src/lib.rs b/crates/oxc_type_synthesis/src/lib.rs new file mode 100644 index 0000000000000..fa3b453ca489d --- /dev/null +++ b/crates/oxc_type_synthesis/src/lib.rs @@ -0,0 +1,66 @@ +#![allow(clippy::all, clippy::restriction, clippy::pedantic, clippy::nursery)] + +use ezno_checker::{ + events::Event, CheckingData, Environment, FSResolver, Root, Scope, Span as SourceMapSpan, + TypeId, +}; +pub use ezno_checker::{ + Diagnostic, DiagnosticsContainer, ErrorWarningInfo, SourceId as EznoSourceId, Span as EznoSpan, +}; +use oxc_ast::ast; +use oxc_span::Span; +use statements_and_declarations::synthesize_statements; + +mod expressions; +mod functions; +mod interfaces; +mod statements_and_declarations; +mod types; + +pub fn synthesize_program( + program: &ast::Program, + resolver: T, +) -> (DiagnosticsContainer, Vec, Vec<(TypeId, ezno_checker::Type)>) { + let default_settings = Default::default(); + let mut checking_data = CheckingData::new(default_settings, &resolver); + + let mut root = Root::new_with_primitive_references_and_ezno_magic(); + + let (_, stuff, _) = root.new_lexical_environment_fold_into_parent( + Scope::Block {}, + &mut checking_data, + |environment, checking_data| { + synthesize_statements(&program.body, environment, checking_data) + }, + ); + + (checking_data.diagnostics_container, stuff.unwrap().0, checking_data.types.into_vec_temp()) +} + +fn oxc_span_to_source_map_span(span: Span) -> SourceMapSpan { + SourceMapSpan { + start: span.start, + end: span.end, + // TODO!! + source_id: ezno_checker::SourceId::NULL, + } +} + +fn property_key_to_type( + key: &ast::PropertyKey, + environment: &mut Environment, + checking_data: &mut CheckingData, +) -> TypeId { + match key { + ast::PropertyKey::Identifier(ident) => checking_data + .types + .new_constant_type(ezno_checker::Constant::String(ident.name.as_str().to_string())), + ast::PropertyKey::PrivateIdentifier(_) => todo!(), + ast::PropertyKey::Expression(expr) => { + let key = expressions::synthesize_expression(expr, environment, checking_data); + + // TODO make key into key + key + } + } +} diff --git a/crates/oxc_type_synthesis/src/statements_and_declarations.rs b/crates/oxc_type_synthesis/src/statements_and_declarations.rs new file mode 100644 index 0000000000000..aa2ae0082b269 --- /dev/null +++ b/crates/oxc_type_synthesis/src/statements_and_declarations.rs @@ -0,0 +1,371 @@ +use std::{borrow::Cow, collections::HashMap}; + +use ezno_checker::{ + self, context::VariableId, structures::variables::VariableMutability, CheckingData, + Environment, FSResolver, TypeId, +}; +use oxc_ast::{ + self, + ast::{self, Statement}, +}; + +use super::{oxc_span_to_source_map_span, types::synthesize_type_annotation}; +use crate::{ + expressions::{self, synthesize_expression}, + functions::OxcFunction, +}; + +/// See `checking.md`s Hoisting section in docs for details +pub(crate) fn hoist_statements( + statements: &[ast::Statement], + environment: &mut Environment, + checking_data: &mut CheckingData, +) { + // TODO temp? + let mut idx_to_types = HashMap::new(); + + // First stage + for (idx, statement) in statements.iter().enumerate() { + if let Statement::Declaration(declaration) = statement { + match declaration { + ast::Declaration::VariableDeclaration(_) + | ast::Declaration::FunctionDeclaration(_) => {} + ast::Declaration::ClassDeclaration(_) => todo!(), + ast::Declaration::TSTypeAliasDeclaration(alias) => { + if alias.type_parameters.is_some() { + todo!() + } + + // TODO eager and so won't work with hoisting + let to = synthesize_type_annotation( + &alias.type_annotation, + environment, + checking_data, + ); + + environment.new_alias(&alias.id.name.as_str(), to, &mut checking_data.types); + } + ast::Declaration::TSInterfaceDeclaration(interface) => { + let ty = environment.new_interface( + &interface.id.name.as_str(), + oxc_span_to_source_map_span(interface.span), + &mut checking_data.types, + ); + idx_to_types.insert(idx, ty); + } + ast::Declaration::TSEnumDeclaration(_) => todo!(), + ast::Declaration::TSModuleDeclaration(_) => todo!(), + ast::Declaration::TSImportEqualsDeclaration(_) => todo!(), + } + } + } + + // Second stage + for (idx, statement) in statements.iter().enumerate() { + match statement { + Statement::ModuleDeclaration(_) => todo!(), + Statement::Declaration(declaration) => match declaration { + ast::Declaration::VariableDeclaration(declaration) => { + let is_declare = declaration.modifiers.contains(ast::ModifierKind::Declare); + let is_const = matches!(declaration.kind, ast::VariableDeclarationKind::Const); + + for declaration in declaration.declarations.iter() { + let ty = declaration + .id + .type_annotation + .as_ref() + .map(|ta| { + synthesize_type_annotation( + &ta.type_annotation, + environment, + checking_data, + ) + }) + .unwrap_or(TypeId::ANY_TYPE); + + // TODO save ty + let behavior = if is_declare { + ezno_checker::context::VariableRegisterBehavior::Declare { base: ty } + } else { + ezno_checker::context::VariableRegisterBehavior::Register { + mutability: if is_const { + VariableMutability::Constant + } else { + VariableMutability::Mutable { reassignment_constraint: ty } + }, + } + }; + register_variable( + &declaration.id.kind, + &declaration.span, + environment, + checking_data, + behavior, + ); + } + } + ast::Declaration::FunctionDeclaration(func) => { + // TODO unsynthesized function? ... + let behavior = ezno_checker::context::VariableRegisterBehavior::Register { + // TODO + mutability: + ezno_checker::structures::variables::VariableMutability::Constant, + }; + environment + .register_variable( + func.id.as_ref().unwrap().name.as_str(), + VariableId(oxc_span_to_source_map_span(func.span)), + behavior, + &mut checking_data.types, + ) + .unwrap(); + } + ast::Declaration::ClassDeclaration(_) => todo!(), + ast::Declaration::TSTypeAliasDeclaration(_) => {} + ast::Declaration::TSInterfaceDeclaration(interface) => { + let ty = idx_to_types.remove(&idx).unwrap(); + crate::interfaces::synthesize_interface( + interface, + ty, + environment, + checking_data, + ) + } + ast::Declaration::TSEnumDeclaration(_) => todo!(), + ast::Declaration::TSModuleDeclaration(_) => todo!(), + ast::Declaration::TSImportEqualsDeclaration(_) => todo!(), + }, + _ => {} + } + } + + // Third stage + for statement in statements { + if let Statement::Declaration(declaration) = statement { + match declaration { + ast::Declaration::FunctionDeclaration(func) => { + let returned = + environment.new_function_context(checking_data, &OxcFunction(&func)); + + environment.attach_to_existing_function( + func.id.as_ref().unwrap().name.as_str(), + returned, + &mut checking_data.types, + ezno_checker::context::FunctionId(oxc_span_to_source_map_span(func.span)), + ); + } + _ => {} + } + } + } +} + +/// TODO different modes for parameters +/// +/// Returns the type for reasons +pub(crate) fn register_variable( + pattern: &ast::BindingPatternKind, + span: &oxc_span::Span, + environment: &mut Environment, + checking_data: &mut CheckingData, + behaviour: ezno_checker::context::VariableRegisterBehavior, +) -> TypeId { + match &pattern { + ast::BindingPatternKind::BindingIdentifier(ident) => environment + .register_variable( + ident.name.as_str(), + VariableId(oxc_span_to_source_map_span(span.clone())), + behaviour, + &mut checking_data.types, + ) + .unwrap(), + ast::BindingPatternKind::ObjectPattern(_) => todo!(), + ast::BindingPatternKind::ArrayPattern(_) => todo!(), + ast::BindingPatternKind::AssignmentPattern(_) => todo!(), + } +} + +pub(crate) fn synthesize_statement( + statement: &ast::Statement, + environment: &mut Environment, + checking_data: &mut CheckingData, +) { + match statement { + ast::Statement::BlockStatement(block) => { + synthesize_statements(&block.body, environment, checking_data); + } + ast::Statement::BreakStatement(_) => todo!(), + ast::Statement::ContinueStatement(_) => todo!(), + ast::Statement::DebuggerStatement(_) => todo!(), + ast::Statement::DoWhileStatement(_) => todo!(), + ast::Statement::EmptyStatement(_) => {} + ast::Statement::ExpressionStatement(expr) => { + expressions::synthesize_expression(&expr.expression, environment, checking_data); + } + ast::Statement::ForInStatement(_) => todo!(), + ast::Statement::ForOfStatement(_) => todo!(), + ast::Statement::ForStatement(_) => todo!(), + ast::Statement::IfStatement(if_stmt) => { + synthesize_if_statement(if_stmt, environment, checking_data) + } + ast::Statement::LabeledStatement(_) => todo!(), + ast::Statement::ReturnStatement(ret_stmt) => { + if let Some(ref value) = ret_stmt.argument { + let returned = + expressions::synthesize_expression(value, environment, checking_data); + environment.return_value(returned) + } else { + environment.return_value(TypeId::UNDEFINED_TYPE) + } + } + ast::Statement::SwitchStatement(_) => todo!(), + ast::Statement::ThrowStatement(throw_stmt) => { + let thrown = expressions::synthesize_expression( + &throw_stmt.argument, + environment, + checking_data, + ); + environment.throw_value(thrown) + } + ast::Statement::TryStatement(stmt) => { + synthesize_try_statement(stmt, environment, checking_data) + } + ast::Statement::WhileStatement(_) => todo!(), + ast::Statement::WithStatement(_) => todo!(), + ast::Statement::ModuleDeclaration(_) => todo!(), + ast::Statement::Declaration(declaration) => { + if !matches!(declaration, ast::Declaration::FunctionDeclaration(..)) { + synthesize_declaration(declaration, environment, checking_data) + } + } + } +} + +// TODO full type narrowing behavior +fn synthesize_if_statement( + if_stmt: &ast::IfStatement, + environment: &mut Environment, + checking_data: &mut CheckingData, +) { + let condition = synthesize_expression(&if_stmt.test, environment, checking_data); + + if let ezno_checker::TruthyFalsy::Decidable(value) = + environment.is_type_truthy_falsy(condition, &checking_data.types) + { + checking_data + .raise_decidable_result_error(oxc_span_to_source_map_span(if_stmt.span), value); + + if value { + synthesize_statement(&if_stmt.consequent, environment, checking_data); + return; + } + } else { + synthesize_statement(&if_stmt.consequent, environment, checking_data); + } + + if let Some(ref alternative) = if_stmt.alternate { + synthesize_statement(alternative, environment, checking_data) + } +} + +pub(crate) fn synthesize_statements( + statements: &[oxc_ast::ast::Statement], + environment: &mut Environment, + checking_data: &mut ezno_checker::CheckingData, +) { + // TODO union this into one function + hoist_statements(statements, environment, checking_data); + + for statement in statements.iter() { + synthesize_statement(statement, environment, checking_data); + } +} + +// TODO some of this logic should be moved to the checker crate +fn synthesize_try_statement( + stmt: &ast::TryStatement, + environment: &mut Environment, + checking_data: &mut CheckingData, +) { + let throw_type: TypeId = + environment.new_try_context(checking_data, |environment, checking_data| { + synthesize_statements(&stmt.block.body, environment, checking_data); + }); + + if let Some(ref handler) = stmt.handler { + // TODO catch when never + environment.new_lexical_environment_fold_into_parent( + ezno_checker::Scope::Block {}, + checking_data, + |environment, checking_data| { + if let Some(ref clause) = handler.param { + // TODO clause.type_annotation + register_variable( + &clause.kind, + // TODO clause has no span + &handler.span, + environment, + checking_data, + ezno_checker::context::VariableRegisterBehavior::CatchVariable { + ty: throw_type, + }, + ); + } + synthesize_statements(&handler.body.body, environment, checking_data); + }, + ); + } + // TODO finally +} + +pub(crate) fn synthesize_declaration( + declaration: &ast::Declaration, + environment: &mut Environment, + checking_data: &mut CheckingData, +) { + match declaration { + ast::Declaration::VariableDeclaration(variable_declaration) => { + if variable_declaration.modifiers.contains(ast::ModifierKind::Declare) { + return; + } + + for declaration in variable_declaration.declarations.iter() { + // TODO get from existing!!!! + let var_ty_and_pos = declaration.id.type_annotation.as_ref().map(|ta| { + ( + synthesize_type_annotation(&ta.type_annotation, environment, checking_data), + ta.span, + ) + }); + + // TODO temp + let value = declaration.init.as_ref().unwrap(); + let value_ty = + expressions::synthesize_expression(value, environment, checking_data); + + if let Some((var_ty, ta_pos)) = var_ty_and_pos { + // TODO temp + let ta_span = oxc_span_to_source_map_span(ta_pos); + let value_span = oxc_span_to_source_map_span(oxc_span::GetSpan::span(value)); + + ezno_checker::check_variable_initialization( + (var_ty, Cow::Owned(ta_span)), + (value_ty, Cow::Owned(value_span)), + environment, + checking_data, + ); + } + + let id = VariableId(oxc_span_to_source_map_span(declaration.span)); + environment.set_variable_declaration_value(id, value_ty) + } + } + ast::Declaration::FunctionDeclaration(_) => unreachable!("should be hoisted..."), + ast::Declaration::ClassDeclaration(_) => todo!(), + ast::Declaration::TSTypeAliasDeclaration(_) => {} + ast::Declaration::TSInterfaceDeclaration(_) => {} + ast::Declaration::TSEnumDeclaration(_) => todo!(), + ast::Declaration::TSModuleDeclaration(_) => todo!(), + ast::Declaration::TSImportEqualsDeclaration(_) => todo!(), + } +} diff --git a/crates/oxc_type_synthesis/src/types.rs b/crates/oxc_type_synthesis/src/types.rs new file mode 100644 index 0000000000000..d35849036e9bf --- /dev/null +++ b/crates/oxc_type_synthesis/src/types.rs @@ -0,0 +1,123 @@ +use ezno_checker::{CheckingData, Environment, FSResolver, TypeId}; +use oxc_ast::ast; + +use crate::oxc_span_to_source_map_span; + +pub(crate) fn synthesize_type_annotation( + ta: &ast::TSType, + environment: &mut Environment, + checking_data: &mut CheckingData, +) -> TypeId { + match ta { + ast::TSType::TSAnyKeyword(_) => TypeId::ANY_TYPE, + ast::TSType::TSBigIntKeyword(_) => todo!(), + ast::TSType::TSBooleanKeyword(_) => TypeId::BOOLEAN_TYPE, + ast::TSType::TSNeverKeyword(_) => TypeId::NEVER_TYPE, + ast::TSType::TSNullKeyword(_) => TypeId::NULL_TYPE, + ast::TSType::TSNumberKeyword(_) => TypeId::NUMBER_TYPE, + ast::TSType::TSObjectKeyword(_) => TypeId::OBJECT_TYPE, + ast::TSType::TSStringKeyword(_) => TypeId::STRING_TYPE, + ast::TSType::TSSymbolKeyword(_) => todo!(), + ast::TSType::TSThisKeyword(_) => todo!(), + ast::TSType::TSUndefinedKeyword(_) => TypeId::UNDEFINED_TYPE, + ast::TSType::TSUnknownKeyword(_) => todo!(), + ast::TSType::TSVoidKeyword(_) => TypeId::UNDEFINED_TYPE, + ast::TSType::TSArrayType(_) => todo!(), + ast::TSType::TSConditionalType(condition) => { + let check_type = + synthesize_type_annotation(&condition.check_type, environment, checking_data); + let extends = + synthesize_type_annotation(&condition.extends_type, environment, checking_data); + let lhs = synthesize_type_annotation(&condition.true_type, environment, checking_data); + let rhs = synthesize_type_annotation(&condition.false_type, environment, checking_data); + + checking_data.types.new_conditional_extends_type(check_type, extends, lhs, rhs) + } + ast::TSType::TSConstructorType(_) => todo!(), + ast::TSType::TSFunctionType(_) => todo!(), + ast::TSType::TSImportType(_) => todo!(), + ast::TSType::TSIndexedAccessType(_) => todo!(), + ast::TSType::TSInferType(_) => todo!(), + ast::TSType::TSUnionType(r#union) => { + // Borrow checker doesn't like :( + // r#union + // .types + // .iter() + // .map(|ta| synthesize_type_annotation(ta, environment, checking_data)) + // .reduce(|lhs, rhs| checking_data.types.new_or_type(lhs, rhs)) + // .unwrap(), + let mut iter = r#union.types.iter(); + let mut acc = + synthesize_type_annotation(iter.next().unwrap(), environment, checking_data); + for next in iter { + let rhs = synthesize_type_annotation(next, environment, checking_data); + acc = checking_data.types.new_or_type(acc, rhs); + } + acc + } + ast::TSType::TSIntersectionType(intersection) => { + let mut iter = intersection.types.iter(); + let mut acc = + synthesize_type_annotation(iter.next().unwrap(), environment, checking_data); + for next in iter { + let rhs = synthesize_type_annotation(next, environment, checking_data); + acc = checking_data.types.new_and_type(acc, rhs); + } + acc + } + ast::TSType::TSLiteralType(lit) => match &lit.literal { + ast::TSLiteral::NullLiteral(_) => ezno_checker::TypeId::NULL_TYPE, + ast::TSLiteral::BooleanLiteral(bool) => { + checking_data.types.new_constant_type(ezno_checker::Constant::Boolean(bool.value)) + } + ast::TSLiteral::NumberLiteral(number) => checking_data.types.new_constant_type( + number + .value + .try_into() + .map(ezno_checker::Constant::Number) + .unwrap_or(ezno_checker::Constant::NaN), + ), + ast::TSLiteral::StringLiteral(string) => checking_data.types.new_constant_type( + ezno_checker::Constant::String(string.value.as_str().to_owned()), + ), + ast::TSLiteral::BigintLiteral(_) => todo!(), + ast::TSLiteral::RegExpLiteral(_) => todo!(), + ast::TSLiteral::TemplateLiteral(_) => todo!(), + ast::TSLiteral::UnaryExpression(_) => todo!(), + }, + ast::TSType::TSMappedType(_) => todo!(), + ast::TSType::TSQualifiedName(_name) => todo!(), + ast::TSType::TSTemplateLiteralType(_) => todo!(), + ast::TSType::TSTupleType(_) => todo!(), + ast::TSType::TSTypeLiteral(anom_interface) => { + let ty = checking_data.types.new_anonymous_interface_ty(); + crate::interfaces::synthesize_signatures( + &anom_interface.members, + environment, + checking_data, + ty, + ); + ty + } + ast::TSType::TSTypeOperatorType(_) => todo!(), + ast::TSType::TSTypePredicate(_) => todo!(), + ast::TSType::TSTypeQuery(_) => todo!(), + ast::TSType::TSTypeReference(reference) => { + if let Some(ref _args) = reference.type_parameters { + todo!() + } + let tn = &reference.type_name; + match tn { + ast::TSTypeName::IdentifierName(name) => environment + .get_type_by_name_handle_errors( + &name.name, + oxc_span_to_source_map_span(name.span), + checking_data, + ), + ast::TSTypeName::QualifiedName(_) => todo!(), + } + } + ast::TSType::JSDocNullableType(_) => todo!(), + ast::TSType::JSDocUnknownType(_) => todo!(), + } +}