Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions core/inference/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 13 additions & 28 deletions core/inference/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,19 +485,14 @@ pub fn type_check(arena: Arena) -> anyhow::Result<TypedContext> {

/// 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
///
Expand All @@ -507,30 +502,20 @@ pub fn type_check(arena: Arena) -> anyhow::Result<TypedContext> {
/// 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
Expand Down
11 changes: 11 additions & 0 deletions core/semantic-analysis/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions core/semantic-analysis/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -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<SemanticDiagnostic>,
}

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::<Vec<_>>()
.join("\n")
}
}
67 changes: 67 additions & 0 deletions core/semantic-analysis/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
1 change: 1 addition & 0 deletions tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

mod ast;
mod codegen;
mod semantic_analysis;
mod type_checker;
mod utils;

Expand Down
127 changes: 127 additions & 0 deletions tests/src/semantic_analysis/combined_unary.rs
Original file line number Diff line number Diff line change
@@ -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"
);
}
}
1 change: 1 addition & 0 deletions tests/src/semantic_analysis/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod combined_unary;