diff --git a/Cargo.toml b/Cargo.toml index eb36d621..5eceefb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ inference-ast = { path = "./core/ast", version = "0.0.1" } inference-type-checker = { path = "./core/type-checker", version = "0.0.1" } inference-cli = { path = "./core/cli", version = "0.0.1" } inference-wasm-to-v-translator = { path = "./core/wasm-to-v", version = "0.0.1" } +inference-semantic-analysis = { path = "./core/semantic-analysis", version = "0.0.1" } inference-wasm-codegen = { path = "./core/wasm-codegen", version = "0.0.1" } # IDE support crates diff --git a/core/inference/Cargo.toml b/core/inference/Cargo.toml index 31f2b3a2..9cf9c7b9 100644 --- a/core/inference/Cargo.toml +++ b/core/inference/Cargo.toml @@ -17,3 +17,4 @@ inference-ast.workspace = true inference-wasm-codegen.workspace = true inference-wasm-to-v-translator.workspace = true inference-type-checker.workspace = true +inference-semantic-analysis.workspace = true diff --git a/core/inference/src/lib.rs b/core/inference/src/lib.rs index 7b85e573..4f33f521 100644 --- a/core/inference/src/lib.rs +++ b/core/inference/src/lib.rs @@ -485,19 +485,14 @@ pub fn type_check(arena: Arena) -> anyhow::Result { /// Performs semantic analysis on the typed AST. /// -/// This function is currently a placeholder for future semantic analysis passes. -/// Planned analyses include: -/// - Dead code detection -/// - Unused variable warnings -/// - Unreachable code analysis -/// - Control flow validation -/// - Initialization checking +/// This function runs semantic checks that are beyond the scope of type checking, +/// such as detecting prohibited combined unary operators, and other +/// scope-bounded language constraints. /// -/// # Current Status -/// -/// **Work in Progress**: This phase is under active development and currently -/// returns `Ok(())` without performing any checks. Once implemented, it will -/// provide additional semantic guarantees beyond type correctness. +/// Diagnostics are reported with three severity levels: +/// - `Error` — semantic violations that must be fixed +/// - `Warning` — suspicious patterns that may indicate bugs +/// - `Info` — informational notes about code style or usage /// /// # Examples /// @@ -507,30 +502,20 @@ pub fn type_check(arena: Arena) -> anyhow::Result { /// let source = r#"fn main() { return 0; }"#; /// let arena = parse(source)?; /// let typed_context = type_check(arena)?; -/// -/// // Currently a no-op, but will perform semantic checks in the future /// analyze(&typed_context)?; /// # Ok::<(), anyhow::Error>(()) /// ``` /// /// # Errors /// -/// Currently always returns `Ok(())`. Future implementations will return errors -/// for semantic violations that are not type errors, such as: -/// - Use of uninitialized variables -/// - Unreachable code paths -/// - Dead code that should be removed -/// - Control flow violations (e.g., missing return statements) -/// - Infinite loops without break conditions -/// -/// # Parameters -/// -/// - `typed_context`: The typed AST context from [`type_check`] -pub fn analyze(_: &TypedContext) -> anyhow::Result<()> { - // todo!("Type analysis not yet implemented"); +/// Returns an error if any semantic diagnostic with `Error` severity is found. +pub fn analyze(ctx: &TypedContext) -> anyhow::Result<()> { + let result = inference_semantic_analysis::analyze(ctx); + if result.has_errors() { + return Err(anyhow::anyhow!("{}", result.format_errors())); + } Ok(()) } - /// Generates WebAssembly binary format from the typed AST. /// /// This function compiles the typed AST into WebAssembly bytecode using LLVM diff --git a/core/semantic-analysis/Cargo.toml b/core/semantic-analysis/Cargo.toml new file mode 100644 index 00000000..bb346248 --- /dev/null +++ b/core/semantic-analysis/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "inference-semantic-analysis" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } + +[dependencies] +inference-ast.workspace = true +inference-type-checker.workspace = true diff --git a/core/semantic-analysis/src/diagnostics.rs b/core/semantic-analysis/src/diagnostics.rs new file mode 100644 index 00000000..5ff328dd --- /dev/null +++ b/core/semantic-analysis/src/diagnostics.rs @@ -0,0 +1,70 @@ +use std::fmt; + +use inference_ast::nodes::Location; + +/// Severity level for semantic analysis diagnostics. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Severity { + Info, + Warning, + Error, +} + +impl fmt::Display for Severity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Severity::Info => write!(f, "info"), + Severity::Warning => write!(f, "warning"), + Severity::Error => write!(f, "error"), + } + } +} + +/// A diagnostic produced by semantic analysis. +#[derive(Debug, Clone)] +pub struct SemanticDiagnostic { + pub severity: Severity, + pub message: String, + pub location: Location, +} + +impl fmt::Display for SemanticDiagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: [{}] {}", self.location, self.severity, self.message) + } +} + +/// Collection of diagnostics from a semantic analysis pass. +#[derive(Debug, Default)] +pub struct SemanticResult { + pub diagnostics: Vec, +} + +impl SemanticResult { + /// Returns `true` if any diagnostic has `Error` severity. + #[must_use] + pub fn has_errors(&self) -> bool { + self.diagnostics + .iter() + .any(|d| d.severity == Severity::Error) + } + + /// Returns only the error-level diagnostics. + #[must_use] + pub fn errors(&self) -> Vec<&SemanticDiagnostic> { + self.diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .collect() + } + + /// Formats all diagnostics into a single error message string. + #[must_use] + pub fn format_errors(&self) -> String { + self.errors() + .iter() + .map(|d| d.to_string()) + .collect::>() + .join("\n") + } +} diff --git a/core/semantic-analysis/src/lib.rs b/core/semantic-analysis/src/lib.rs new file mode 100644 index 00000000..63009213 --- /dev/null +++ b/core/semantic-analysis/src/lib.rs @@ -0,0 +1,67 @@ +#![warn(clippy::pedantic)] +//! Semantic Analysis Crate +//! +//! Performs semantic checks on the typed AST that are beyond the scope of type checking. +//! These are scope-bounded checks for language-level constraints that are not type errors. +//! +//! ## Current Checks +//! +//! - **Combined unary operators**: Prohibits chained/unparenthesized unary operator +//! combinations such as `--x`, `-~x`, `!!x`, and parenthesized variants like `-(~x)`. +//! +//! ## Diagnostics +//! +//! The analysis produces diagnostics with three severity levels: +//! - `Error` — semantic violations that must be fixed +//! - `Warning` — suspicious patterns that may indicate bugs +//! - `Info` — informational notes about code style or usage + +pub mod diagnostics; + +use diagnostics::{SemanticDiagnostic, SemanticResult, Severity}; +use inference_ast::nodes::{AstNode, Expression, Literal}; +use inference_type_checker::typed_context::TypedContext; + +/// Runs all semantic analysis passes on the typed AST. +/// +/// Returns a [`SemanticResult`] containing any diagnostics found. +#[must_use] +pub fn analyze(ctx: &TypedContext) -> SemanticResult { + let mut result = SemanticResult::default(); + check_combined_unary_operators(ctx, &mut result); + result +} + +/// Checks for prohibited combined unary operators in expressions. +/// +/// Detects chained prefix unary operators like `--x`, `!!x`, `-~x`, +/// and parenthesized variants like `-(~x)`, `~(-x)`. +fn check_combined_unary_operators(ctx: &TypedContext, result: &mut SemanticResult) { + let prefix_unary_nodes = ctx.filter_nodes(|node| { + matches!(node, AstNode::Expression(Expression::PrefixUnary(_))) + }); + + for node in prefix_unary_nodes { + if let AstNode::Expression(Expression::PrefixUnary(ref prefix_expr)) = node { + if is_combined_unary(&prefix_expr.expression.borrow()) { + result.diagnostics.push(SemanticDiagnostic { + severity: Severity::Error, + message: "combined unary operators are prohibited".to_string(), + location: node.location(), + }); + } + } + } +} + +/// Returns `true` if the expression is itself a unary operator (directly or +/// through parentheses), or a negative numeric literal — indicating a +/// combined/chained unary operator usage. +fn is_combined_unary(expr: &Expression) -> bool { + match expr { + Expression::PrefixUnary(_) => true, + Expression::Parenthesized(inner) => is_combined_unary(&inner.expression.borrow()), + Expression::Literal(Literal::Number(num)) => num.value.starts_with('-'), + _ => false, + } +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 47e3b0a2..8d5d5a3a 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -15,6 +15,7 @@ inference-ast.workspace = true inference-wasm-codegen.workspace = true inference-type-checker.workspace = true inference.workspace = true +inference-semantic-analysis.workspace = true inf-wasmparser.workspace = true tree-sitter.workspace = true tree-sitter-inference.workspace = true diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 05d64024..3cd6e01f 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -4,6 +4,7 @@ mod ast; mod codegen; +mod semantic_analysis; mod type_checker; mod utils; diff --git a/tests/src/semantic_analysis/combined_unary.rs b/tests/src/semantic_analysis/combined_unary.rs new file mode 100644 index 00000000..44581d20 --- /dev/null +++ b/tests/src/semantic_analysis/combined_unary.rs @@ -0,0 +1,127 @@ +//! Tests for the semantic analysis pass that prohibits combined unary operators. +//! +//! These checks run after type checking and detect chained/combined prefix +//! unary operators such as `--x`, `!!x`, `-~x`, and parenthesized variants +//! like `-(~x)`. + +#[cfg(test)] +mod combined_unary_tests { + use crate::utils::build_ast; + use inference_semantic_analysis::diagnostics::Severity; + use inference_type_checker::TypeCheckerBuilder; + + /// Runs parse → type check → semantic analysis on the source, returning the + /// semantic result. + fn run_semantic_analysis( + source: &str, + ) -> inference_semantic_analysis::diagnostics::SemanticResult { + let arena = build_ast(source.to_string()); + let typed_context = TypeCheckerBuilder::build_typed_context(arena) + .expect("type checking should succeed before semantic analysis") + .typed_context(); + inference_semantic_analysis::analyze(&typed_context) + } + + // ── single unary operators should pass ────────────────────────────── + + #[test] + fn single_negate_succeeds() { + let result = run_semantic_analysis(r#"fn test(x: i32) -> i32 { return -(x); }"#); + assert!( + !result.has_errors(), + "Single negation should not produce errors" + ); + } + + #[test] + fn single_bitnot_succeeds() { + let result = run_semantic_analysis(r#"fn test(x: i32) -> i32 { return ~x; }"#); + assert!( + !result.has_errors(), + "Single bitwise NOT should not produce errors" + ); + } + + #[test] + fn single_logical_not_succeeds() { + let result = run_semantic_analysis(r#"fn test(x: bool) -> bool { return !x; }"#); + assert!( + !result.has_errors(), + "Single logical NOT should not produce errors" + ); + } + + #[test] + fn negate_parenthesized_arithmetic_succeeds() { + let result = + run_semantic_analysis(r#"fn test(a: i32, b: i32) -> i32 { return -(a + b); }"#); + assert!( + !result.has_errors(), + "Negation of parenthesized arithmetic should not produce errors" + ); + } + + // ── combined / chained unary operators should error ────────────────── + + #[test] + fn double_negate_is_prohibited() { + let result = run_semantic_analysis(r#"fn test(x: i32) -> i32 { return --(x); }"#); + assert!(result.has_errors(), "Double negation should be prohibited"); + let errors = result.errors(); + assert!( + errors + .iter() + .any(|d| d.message.contains("combined unary operators")), + "Error should mention combined unary operators" + ); + assert!( + errors.iter().all(|d| d.severity == Severity::Error), + "Combined unary diagnostics should be errors" + ); + } + + #[test] + fn double_negate_literal_is_prohibited() { + let result = run_semantic_analysis(r#"fn test() -> i32 { return --42; }"#); + assert!( + result.has_errors(), + "Double negation of literal should be prohibited" + ); + } + + #[test] + fn bitnot_combined_with_negate_is_prohibited() { + let result = run_semantic_analysis(r#"fn test(x: i32) -> i32 { return ~-(x); }"#); + assert!( + result.has_errors(), + "Combining BitNot and Neg should be prohibited" + ); + } + + #[test] + fn negate_combined_with_bitnot_is_prohibited() { + let result = run_semantic_analysis(r#"fn test(x: i32) -> i32 { return -(~x); }"#); + assert!( + result.has_errors(), + "Combining Neg and BitNot should be prohibited" + ); + } + + #[test] + fn bitnot_then_neg_literal_is_prohibited() { + let result = run_semantic_analysis(r#"fn test() -> i32 { return -~42; }"#); + assert!( + result.has_errors(), + "Combined unary operators on literal should be prohibited" + ); + } + + #[test] + fn double_logical_not_is_prohibited() { + let result = run_semantic_analysis(r#"fn test(x: bool) -> bool { return !!x; }"#); + assert!( + result.has_errors(), + "Double logical NOT should be prohibited" + ); + } +} diff --git a/tests/src/semantic_analysis/mod.rs b/tests/src/semantic_analysis/mod.rs new file mode 100644 index 00000000..f849f283 --- /dev/null +++ b/tests/src/semantic_analysis/mod.rs @@ -0,0 +1 @@ +mod combined_unary;