diff --git a/crates/filament/src/ast_passes/mod.rs b/crates/filament/src/ast_passes/mod.rs index f83ab4e4..3dd98e9f 100644 --- a/crates/filament/src/ast_passes/mod.rs +++ b/crates/filament/src/ast_passes/mod.rs @@ -1,3 +1,5 @@ mod toplevel; +mod unused_elements; pub use toplevel::TopLevel; +pub use unused_elements::UnusedElementsCheck; diff --git a/crates/filament/src/ast_passes/unused_elements.rs b/crates/filament/src/ast_passes/unused_elements.rs new file mode 100644 index 00000000..f5ae59d9 --- /dev/null +++ b/crates/filament/src/ast_passes/unused_elements.rs @@ -0,0 +1,421 @@ +use crate::ast_visitor::{Action, Visitor}; +use crate::cmdline; +use fil_ast as ast; +use fil_utils::{Diagnostics, Error, GPosIdx}; +use std::collections::{HashMap, HashSet}; + +/// Structure to track usage information for a single component +#[derive(Default, Clone)] +struct ComponentUsage { + // Definitions with their locations + instances: HashMap>, + invocations: HashMap>, + let_params: HashMap>, + sig_params: HashMap>, + + // Usage tracking + invoked_instances: HashSet, + accessed_invocations: HashSet, + referenced_params: HashSet, +} + +/// Check for unused elements in AST (instances, invocations, parameters) +pub struct UnusedElementsCheck { + diag: Diagnostics, + current_component: Option, + component_usage: HashMap, + warnings_as_errors: bool, +} + +impl crate::ast_visitor::Construct for UnusedElementsCheck { + fn from(opts: &cmdline::Opts, _ast: &mut ast::Namespace) -> Self { + UnusedElementsCheck { + diag: Diagnostics::default(), + current_component: None, + component_usage: HashMap::new(), + warnings_as_errors: opts.warnings_as_errors, + } + } + + fn clear_data(&mut self) { + // Don't clear component_usage since we need it across components + self.current_component = None; + } +} + +impl UnusedElementsCheck { + fn current_usage(&mut self) -> &mut ComponentUsage { + let comp_name = self + .current_component + .clone() + .expect("No current component"); + self.component_usage.entry(comp_name).or_default() + } + + fn extract_invocation_name(&self, port: &ast::Port) -> Option { + match &port.base { + ast::PortRef::Instance { instance, .. } => { + Some(instance.inner().as_ref().to_string()) + } + _ => None, + } + } + + fn visit_expr_for_params(&mut self, expr: &ast::Expr) { + match expr { + ast::Expr::Abstract(name) => { + let usage = self.current_usage(); + usage + .referenced_params + .insert(name.inner().as_ref().to_string()); + } + ast::Expr::ParamAccess { inst: _, param } => { + let usage = self.current_usage(); + usage + .referenced_params + .insert(param.inner().as_ref().to_string()); + } + ast::Expr::Op { left, right, .. } => { + self.visit_expr_for_params(left); + self.visit_expr_for_params(right); + } + ast::Expr::If { cond, then, alt } => { + self.visit_order_constraint_for_params(cond); + self.visit_expr_for_params(then); + self.visit_expr_for_params(alt); + } + ast::Expr::App { args, .. } => { + for arg in args { + self.visit_expr_for_params(arg); + } + } + // Base cases: concrete values don't reference parameters + ast::Expr::Concrete(_) => {} + } + } + + fn visit_order_constraint_for_params( + &mut self, + constraint: &ast::OrderConstraint>, + ) { + self.visit_expr_for_params(&constraint.left); + self.visit_expr_for_params(&constraint.right); + } + + fn visit_order_constraint_unboxed_for_params( + &mut self, + constraint: &ast::OrderConstraint, + ) { + self.visit_expr_for_params(&constraint.left); + self.visit_expr_for_params(&constraint.right); + } + + fn visit_port_for_params(&mut self, port: &ast::Port) { + // Visit access expressions in bundle accesses + for access in &port.access { + self.visit_expr_for_params(&access.start); + self.visit_expr_for_params(&access.end); + } + } + + fn visit_bundle_type_for_params(&mut self, bundle_type: &ast::BundleType) { + // Visit length expressions + for len in &bundle_type.len { + self.visit_expr_for_params(len.inner()); + } + + // Visit bitwidth expression (this is where 'P' would be used in "['G, 'G+1] P") + self.visit_expr_for_params(bundle_type.bitwidth.inner()); + + // Visit liveness range expressions + self.visit_range_for_params(bundle_type.liveness.inner()); + } + + fn visit_range_for_params(&mut self, range: &ast::Range) { + self.visit_time_for_params(&range.start); + self.visit_time_for_params(&range.end); + } + + fn visit_time_for_params(&mut self, time: &ast::Time) { + // Time is just event + offset, visit the offset expression + self.visit_expr_for_params(&time.offset); + } + + fn visit_time_sub_for_params(&mut self, time_sub: &ast::TimeSub) { + match time_sub { + ast::TimeSub::Unit(expr) => { + // Visit the expression in Unit variant + self.visit_expr_for_params(expr); + } + ast::TimeSub::Sym { l, r } => { + // Visit both time expressions in symbolic difference + self.visit_time_for_params(l); + self.visit_time_for_params(r); + } + } + } + + fn add_unused_warning( + &mut self, + element_type: &str, + description: &str, + loc: GPosIdx, + ) { + let mut warning = Error::unused_element(element_type, description) + .add_note( + self.diag + .add_info(format!("{} defined here", element_type), loc), + ); + + // Promote to error if flag is set + if self.warnings_as_errors { + warning = warning.as_error(); + } + + self.diag.add_error(warning); + } + + fn check_component_unused(&mut self, comp_name: &str) { + // Clone the usage data to avoid borrowing issues + let usage = match self.component_usage.get(comp_name).cloned() { + Some(u) => u, + None => return, // No usage data for this component + }; + + // Collect all warnings first, then sort them for deterministic output + let mut warnings = Vec::new(); + + // Check unused instances + for (inst_name, def_loc) in &usage.instances { + if !usage.invoked_instances.contains(inst_name) { + warnings.push(( + def_loc.pos(), + "instance", + "instance is created but never invoked", + )); + } + } + + // Check unused invocations (no ports accessed) + for (inv_name, def_loc) in &usage.invocations { + if !usage.accessed_invocations.contains(inv_name) { + warnings.push(( + def_loc.pos(), + "invocation", + "output ports on invocation never used", + )); + } + } + + // Check unused let parameters + for (param_name, def_loc) in &usage.let_params { + if !usage.referenced_params.contains(param_name) { + warnings.push(( + def_loc.pos(), + "parameter", + "parameter is defined but never used", + )); + } + } + + // Check unused signature parameters + for (param_name, def_loc) in &usage.sig_params { + if !usage.referenced_params.contains(param_name) { + warnings.push(( + def_loc.pos(), + "parameter", + "parameter is defined but never used", + )); + } + } + + // Sort warnings by position for deterministic output + // We can't access PosIdx internals, so sort by element type and description for determinism + warnings.sort_by(|(pos_a, type_a, desc_a), (pos_b, type_b, desc_b)| { + // First try to compare by type and description for determinism + match (type_a, desc_a).cmp(&(type_b, desc_b)) { + std::cmp::Ordering::Equal => { + // If types are the same, compare by position hash (not perfect but deterministic in practice) + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher_a = DefaultHasher::new(); + let mut hasher_b = DefaultHasher::new(); + pos_a.hash(&mut hasher_a); + pos_b.hash(&mut hasher_b); + hasher_a.finish().cmp(&hasher_b.finish()) + } + other => other, + } + }); + + // Generate warnings in sorted order + for (pos, element_type, description) in warnings { + self.add_unused_warning(element_type, description, pos); + } + } +} + +impl Visitor for UnusedElementsCheck { + fn name() -> &'static str { + "unused-elements" + } + + fn signature(&mut self, sig: &mut ast::Signature) -> Action { + let comp_name = sig.name.inner().as_ref().to_string(); + self.current_component = Some(comp_name.clone()); + + // Record signature parameters + let usage = self.current_usage(); + for param in &sig.params { + usage + .sig_params + .insert(param.name().as_ref().to_string(), param.param.clone()); + } + + // Record let-bound parameters from the 'with' section + // First pass: record parameters + for sig_bind in &sig.sig_bindings { + match sig_bind.inner() { + ast::SigBind::Let { param, .. } => { + usage.let_params.insert( + param.inner().as_ref().to_string(), + param.clone(), + ); + } + ast::SigBind::Exists { param, .. } => { + usage.let_params.insert( + param.inner().as_ref().to_string(), + param.clone(), + ); + } + } + } + + // Second pass: visit expressions for parameter usage (after usage is populated) + for sig_bind in &sig.sig_bindings { + match sig_bind.inner() { + ast::SigBind::Let { bind, .. } => { + self.visit_expr_for_params(bind); + } + ast::SigBind::Exists { cons, .. } => { + for constraint in cons { + self.visit_order_constraint_unboxed_for_params( + constraint.inner(), + ); + } + } + } + } + + // Third pass: visit port definitions for parameter usage in widths, liveness, etc. + for port_def in &sig.ports { + self.visit_bundle_type_for_params(&port_def.inner().typ); + } + + // Visit event bindings for parameter usage in delays + for event_bind in &sig.events { + self.visit_time_sub_for_params(event_bind.inner().delay.inner()); + if let Some(default_time) = &event_bind.inner().default { + self.visit_time_for_params(default_time); + } + } + + // Visit parameter constraints + for constraint in &sig.param_constraints { + self.visit_order_constraint_unboxed_for_params(constraint.inner()); + } + + // Don't stop traversal - we need to visit the component body + Action::Continue + } + + fn instance(&mut self, inst: &mut ast::Instance) -> Action { + let usage = self.current_usage(); + usage + .instances + .insert(inst.name.inner().as_ref().to_string(), inst.name.clone()); + + // Visit parameter expressions for parameter usage + for param_expr in &inst.params { + self.visit_expr_for_params(param_expr.inner()); + } + + Action::Continue + } + + fn invoke(&mut self, inv: &mut ast::Invoke) -> Action { + let usage = self.current_usage(); + + // Record invocation definition + usage + .invocations + .insert(inv.name.inner().as_ref().to_string(), inv.name.clone()); + + // Mark the instance as being invoked + usage + .invoked_instances + .insert(inv.instance.inner().as_ref().to_string()); + + // Visit port expressions for parameter usage + for port in &inv.ports { + self.visit_port_for_params(port.inner()); + } + + Action::Continue + } + + fn param_let(&mut self, param: &mut ast::ParamLet) -> Action { + let usage = self.current_usage(); + usage.let_params.insert( + param.name.inner().as_ref().to_string(), + param.name.clone(), + ); + + // If the parameter has an expression, visit it for parameter references + if let Some(expr) = ¶m.expr { + self.visit_expr_for_params(expr); + } + + Action::Continue + } + + fn connect(&mut self, conn: &mut ast::Connect) -> Action { + // Check if source is from an invocation and mark it as accessed + if let Some(inv_name) = self.extract_invocation_name(conn.src.inner()) { + let usage = self.current_usage(); + usage.accessed_invocations.insert(inv_name); + } + + // Visit port expressions for parameter usage + self.visit_port_for_params(conn.src.inner()); + self.visit_port_for_params(conn.dst.inner()); + + Action::Continue + } + + fn fact(&mut self, fact: &mut ast::Fact) -> Action { + // Visit expressions in facts for parameter usage + for expr in fact.exprs() { + self.visit_expr_for_params(expr); + } + Action::Continue + } + + fn exists(&mut self, exists: &mut ast::Exists) -> Action { + // Visit the binding expression for parameter usage + self.visit_expr_for_params(exists.bind.inner()); + Action::Continue + } + + fn finish(&mut self, namespace: &mut ast::Namespace) { + // Check each component for unused elements + for comp in &namespace.components { + let comp_name = comp.sig.name.inner().as_ref().to_string(); + self.check_component_unused(&comp_name); + } + } + + fn after_traversal(mut self) -> Option { + self.diag.report_all() + } +} diff --git a/crates/filament/src/cmdline.rs b/crates/filament/src/cmdline.rs index f980f8b8..1d24a779 100644 --- a/crates/filament/src/cmdline.rs +++ b/crates/filament/src/cmdline.rs @@ -129,4 +129,12 @@ pub struct Opts { /// use bitvector encoding for proofs #[argh(option, long = "solver-bv")] pub solver_bv: Option, + + /// disable unused element warnings + #[argh(switch, long = "no-warn-unused")] + pub no_warn_unused: bool, + + /// treat all warnings as errors + #[argh(switch, long = "warnings-as-errors")] + pub warnings_as_errors: bool, } diff --git a/crates/filament/src/main.rs b/crates/filament/src/main.rs index b636480e..340c721e 100644 --- a/crates/filament/src/main.rs +++ b/crates/filament/src/main.rs @@ -58,6 +58,7 @@ fn collect_known_pass_names() -> Vec { // AST pass names add_ast_pass::(&mut pass_names); + add_ast_pass::(&mut pass_names); pass_names.sort(); pass_names.dedup(); @@ -125,6 +126,11 @@ fn run(opts: &cmdline::Opts) -> Result<(), u64> { ast_pass_pipeline! { opts, ns; ap::TopLevel }; + // Run unused elements check only if warnings are not disabled + if !opts.no_warn_unused { + ast_pass_pipeline! { opts, ns; ap::UnusedElementsCheck }; + } + // Set the parameter bindings for the top-level component if let Some(main) = ns.toplevel() { ns.bindings = provided_bindings diff --git a/crates/utils/src/errors.rs b/crates/utils/src/errors.rs index f26e1bfb..d6bfdbec 100644 --- a/crates/utils/src/errors.rs +++ b/crates/utils/src/errors.rs @@ -2,10 +2,17 @@ use super::{Id, InfoIdx}; use itertools::Itertools; -#[derive(PartialEq, Eq, Hash)] +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] +pub enum Severity { + Warning, + Error, +} + +#[derive(PartialEq, Eq, Hash, Clone)] pub struct Error { pub kind: String, pub notes: Vec, + pub severity: Severity, } impl std::fmt::Debug for Error { @@ -33,6 +40,7 @@ impl Error { Self { kind: format!("invalid file: {}", f), notes: vec![], + severity: Severity::Error, } } @@ -40,6 +48,7 @@ impl Error { Self { kind: format!("failed to write output: {}", e), notes: vec![], + severity: Severity::Error, } } @@ -47,6 +56,7 @@ impl Error { Self { kind: msg.to_string(), notes: vec![], + severity: Severity::Error, } } @@ -58,6 +68,7 @@ impl Error { name.to_string(), ), notes: vec![], + severity: Severity::Error, } } @@ -68,6 +79,7 @@ impl Error { kind.to_string() ), notes: vec![], + severity: Severity::Error, } } @@ -75,8 +87,37 @@ impl Error { Self { kind: msg, notes: vec![], + severity: Severity::Error, + } + } + + pub fn warning(msg: S) -> Self { + Self { + kind: msg.to_string(), + notes: vec![], + severity: Severity::Warning, + } + } + + pub fn unused_element( + element_type: S, + description: S, + ) -> Self { + Self { + kind: format!( + "unused {}: {}", + element_type.to_string(), + description.to_string() + ), + notes: vec![], + severity: Severity::Warning, } } + + pub fn as_error(mut self) -> Self { + self.severity = Severity::Error; + self + } } /// Convience wrapper to represent success or meaningul compiler error. diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 8ca06775..87b24e99 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -12,7 +12,7 @@ pub use attr::{ AttrCtx, AttrStore, Attributes, CompAttrs, CompBool, CompNum, PortAttrs, PortBool, PortNum, }; -pub use errors::{Error, FilamentResult}; +pub use errors::{Error, FilamentResult, Severity}; pub use gsym::GSym; pub use id::Id; pub use math::{all_indices, flat_idx, nd_idx}; diff --git a/crates/utils/src/reporter.rs b/crates/utils/src/reporter.rs index 51ba274d..ad1213d6 100644 --- a/crates/utils/src/reporter.rs +++ b/crates/utils/src/reporter.rs @@ -1,4 +1,4 @@ -use crate::{Error, GPosIdx, GlobalPositionTable}; +use crate::{Error, GPosIdx, GlobalPositionTable, Severity}; use codespan_reporting::term::termcolor::ColorChoice; use codespan_reporting::{ diagnostic::{Diagnostic, Label, LabelStyle}, @@ -33,6 +33,8 @@ pub struct Diagnostics { infos: Vec, /// Errors that have been reported. errors: Vec, + /// Warnings that have been reported. + warnings: Vec, } impl Diagnostics { @@ -73,14 +75,33 @@ impl Diagnostics { // XXX: Make this add a new information object so that its easy to express // the "create error and add info" pattern. pub fn add_error(&mut self, error: Error) { - if !self.errors.contains(&error) { - log::trace!("Adding error: {}", error.kind); - self.errors.push(error); + match error.severity { + Severity::Error => { + if !self.errors.contains(&error) { + log::trace!("Adding error: {}", error.kind); + self.errors.push(error); + } + } + Severity::Warning => { + if !self.warnings.contains(&error) { + log::trace!("Adding warning: {}", error.kind); + self.warnings.push(error); + } + } } } - /// Report all errors and return the number of errors. - /// Returns None if there are no errors. + /// Add a warning to the diagnostics instance. + pub fn add_warning(&mut self, warning: Error) { + let warning = Error { + severity: Severity::Warning, + ..warning + }; + self.add_error(warning); + } + + /// Report all errors and warnings. Returns Some(error_count) if there are errors. + /// Warnings do not count toward compilation failure. pub fn report_all(&mut self) -> Option { let is_tty = atty::is(atty::Stream::Stderr); let writer = StandardStream::stderr(if is_tty { @@ -88,30 +109,55 @@ impl Diagnostics { } else { ColorChoice::Never }); - if self.errors.is_empty() { - return None; + + // Take ownership to avoid borrowing issues + let warnings = std::mem::take(&mut self.warnings); + let errors = std::mem::take(&mut self.errors); + + // Report warnings first + self.report_diagnostics_owned(&writer, warnings, Severity::Warning); + + // Report errors and return count + let error_count = + self.report_diagnostics_owned(&writer, errors, Severity::Error); + + if error_count > 0 { + Some(error_count) + } else { + None + } + } + + fn report_diagnostics_owned( + &self, + writer: &StandardStream, + mut diagnostics: Vec, + severity: Severity, + ) -> u64 { + if diagnostics.is_empty() { + return 0; } let mut total = 0; - // Deduplicate errors based on the location attached to the error - let mut error_map = BTreeMap::new(); - for mut error in self.errors.drain(..) { - if !error.notes.is_empty() { + // Deduplicate diagnostics based on the location attached to the diagnostic + let mut diagnostic_map = BTreeMap::new(); + for mut diagnostic in diagnostics.drain(..) { + if !diagnostic.notes.is_empty() { // Sort everything except the first element - let first = error.notes.remove(0); - error.notes.sort(); - error.notes.insert(0, first); + let first = diagnostic.notes.remove(0); + diagnostic.notes.sort(); + diagnostic.notes.insert(0, first); } - error_map - .entry(error.notes) + diagnostic_map + .entry(diagnostic.notes) .or_insert_with(Vec::new) - .push(error.kind); + .push(diagnostic.kind); } let table = GlobalPositionTable::get(); - for (all_notes, errors) in error_map { + for (all_notes, messages) in diagnostic_map { let mut labels = vec![]; let mut notes = vec![]; for (idx, info) in all_notes.iter().enumerate() { @@ -133,19 +179,31 @@ impl Diagnostics { } } - let msg = if errors.len() > 1 { - notes.extend(errors.iter().map(|e| e.to_string())); - "Multiple errors encountered".to_string() + let msg = if messages.len() > 1 { + notes.extend(messages.iter().map(|e| e.to_string())); + match severity { + Severity::Error => { + "Multiple errors encountered".to_string() + } + Severity::Warning => { + "Multiple warnings encountered".to_string() + } + } } else { - errors[0].to_string() + messages[0].to_string() }; total += 1; + let diagnostic_type = match severity { + Severity::Error => Diagnostic::error(), + Severity::Warning => Diagnostic::warning(), + }; + term::emit( &mut writer.lock(), &term::Config::default(), table.files(), - &Diagnostic::error() + &diagnostic_type .with_message(msg) .with_labels(labels) .with_notes(notes), @@ -153,7 +211,17 @@ impl Diagnostics { .unwrap(); } - Some(total) + total + } + + /// Check if there are any errors (not warnings) + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() + } + + /// Check if there are any warnings + pub fn has_warnings(&self) -> bool { + !self.warnings.is_empty() } } diff --git a/runt.toml b/runt.toml index 1717e031..eb28beb0 100644 --- a/runt.toml +++ b/runt.toml @@ -5,7 +5,7 @@ ver = "0.4.1" name = "check-z3" paths = ["tests/check/*.fil", "primitives/*.fil"] cmd = """ -./target/debug/filament {} --check --solver z3 +./target/debug/filament {} --check --no-warn-unused --solver z3 """ expect_dir = "tests/check/" @@ -13,15 +13,22 @@ expect_dir = "tests/check/" name = "check-cvc5" paths = ["tests/check/*.fil", "primitives/*.fil"] cmd = """ -./target/debug/filament {} --check --solver cvc5 +./target/debug/filament {} --check --no-warn-unused --solver cvc5 """ expect_dir = "tests/check/" +[[tests]] +name = "warnings" +paths = ["tests/warnings/**/*.fil"] +cmd = """ +./target/debug/filament --warnings-as-errors {} +""" + [[tests]] name = "errors" paths = ["tests/errors/**/*.fil"] cmd = """ -./target/debug/filament {} +./target/debug/filament --no-warn-unused {} """ [[tests]] diff --git a/tests/warnings/unused/let-clean.expect b/tests/warnings/unused/let-clean.expect new file mode 100644 index 00000000..00ff5557 --- /dev/null +++ b/tests/warnings/unused/let-clean.expect @@ -0,0 +1,17 @@ +---CODE--- +1 +---STDERR--- +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/let-clean.fil:8:7 + │ +8 │ let UNUSED_PARAM = 42; + │ ^^^^^^^^^^^^ parameter defined here + +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/let-clean.fil:9:7 + │ +9 │ let USED_PARAM = 32; + │ ^^^^^^^^^^ parameter defined here + +Compilation failed with 2 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/let-clean.fil b/tests/warnings/unused/let-clean.fil new file mode 100644 index 00000000..3ecf9378 --- /dev/null +++ b/tests/warnings/unused/let-clean.fil @@ -0,0 +1,14 @@ +comp Helper<'G:1>() -> ( + out: ['G, 'G+1] 32 +) {} + +comp main<'G:1>() -> ( + result: ['G, 'G+1] 32 +) with { + let UNUSED_PARAM = 42; + let USED_PARAM = 32; +} { + helper := new Helper; + invoke := helper<'G>(); + result = invoke.out; +} \ No newline at end of file diff --git a/tests/warnings/unused/let-simple.expect b/tests/warnings/unused/let-simple.expect new file mode 100644 index 00000000..083d9c2a --- /dev/null +++ b/tests/warnings/unused/let-simple.expect @@ -0,0 +1,11 @@ +---CODE--- +1 +---STDERR--- +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/let-simple.fil:2:7 + │ +2 │ let UNUSED_PARAM = 42; + │ ^^^^^^^^^^^^ parameter defined here + +Compilation failed with 1 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/let-simple.fil b/tests/warnings/unused/let-simple.fil new file mode 100644 index 00000000..1d761c18 --- /dev/null +++ b/tests/warnings/unused/let-simple.fil @@ -0,0 +1,4 @@ +comp main<'G:1>() -> () with { + let UNUSED_PARAM = 42; +} { +} \ No newline at end of file diff --git a/tests/warnings/unused/let-unused.expect b/tests/warnings/unused/let-unused.expect new file mode 100644 index 00000000..a42d9acb --- /dev/null +++ b/tests/warnings/unused/let-unused.expect @@ -0,0 +1,29 @@ +---CODE--- +1 +---STDERR--- +error: unused invocation: output ports on invocation never used + ┌─ ./primitives/reshape.fil:134:7 + │ +134 │ red := new ConcatBundle[W, N-1, L]<'G>(in{1..N}); + │ ^^^ invocation defined here + +error: unused invocation: output ports on invocation never used + ┌─ ./primitives/reshape.fil:154:7 + │ +154 │ rem := new Slice[W*N, W*(N-1)-1, 0]<'G, 'G+L>(in); + │ ^^^ invocation defined here + +error: unused invocation: output ports on invocation never used + ┌─ tests/warnings/unused/let-unused.fil:7:3 + │ +7 │ const_val := new Const[32, USED_LET]<'G>(); + │ ^^^^^^^^^ invocation defined here + +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/let-unused.fil:4:7 + │ +4 │ let UNUSED_LET = 42; + │ ^^^^^^^^^^ parameter defined here + +Compilation failed with 4 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/let-unused.fil b/tests/warnings/unused/let-unused.fil new file mode 100644 index 00000000..8b8c4757 --- /dev/null +++ b/tests/warnings/unused/let-unused.fil @@ -0,0 +1,8 @@ +import "primitives/core.fil"; + +comp main<'G:1>() -> () with { + let UNUSED_LET = 42; + let USED_LET = 10; +} { + const_val := new Const[32, USED_LET]<'G>(); +} \ No newline at end of file diff --git a/tests/warnings/unused/mixed-unused.expect b/tests/warnings/unused/mixed-unused.expect new file mode 100644 index 00000000..5481fb05 --- /dev/null +++ b/tests/warnings/unused/mixed-unused.expect @@ -0,0 +1,23 @@ +---CODE--- +1 +---STDERR--- +error: unused instance: instance is created but never invoked + ┌─ tests/warnings/unused/mixed-unused.fil:15:3 + │ +15 │ helper := new Helper; + │ ^^^^^^ instance defined here + +error: unused invocation: output ports on invocation never used + ┌─ tests/warnings/unused/mixed-unused.fil:18:3 + │ +18 │ adder := new Add[USED_PARAM]<'G>(in1, in2); + │ ^^^^^ invocation defined here + +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/mixed-unused.fil:11:7 + │ +11 │ let UNUSED_PARAM = 100; + │ ^^^^^^^^^^^^ parameter defined here + +Compilation failed with 3 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/mixed-unused.fil b/tests/warnings/unused/mixed-unused.fil new file mode 100644 index 00000000..7110e6c1 --- /dev/null +++ b/tests/warnings/unused/mixed-unused.fil @@ -0,0 +1,23 @@ +import "primitives/comb.fil"; + +comp Helper<'G:1>() -> () {} + +comp main<'G:1>( + in1: ['G, 'G+1] 32, + in2: ['G, 'G+1] 32 +) -> ( + out: ['G, 'G+1] 32 +) with { + let UNUSED_PARAM = 100; + let USED_PARAM = 32; +} { + // Unused instance + helper := new Helper; + + // Used instance, but invocation result unused + adder := new Add[USED_PARAM]<'G>(in1, in2); + + // Actually used invocation + adder2 := new Add[32]<'G>(in1, in2); + out = adder2.out; +} \ No newline at end of file diff --git a/tests/warnings/unused/simple-unused.expect b/tests/warnings/unused/simple-unused.expect new file mode 100644 index 00000000..94d43e10 --- /dev/null +++ b/tests/warnings/unused/simple-unused.expect @@ -0,0 +1,35 @@ +---CODE--- +1 +---STDERR--- +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/simple-unused.fil:2:8 + │ +2 │ some L; + │ ^ parameter defined here + +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/simple-unused.fil:1:19 + │ +1 │ comp Add[D, I, W, U]<'G:D>() -> (out: ['G, 'G+I] W) with { + │ ^ parameter defined here + +error: unused instance: instance is created but never invoked + ┌─ tests/warnings/unused/simple-unused.fil:10:3 + │ +10 │ UnusedAdd := new Add[USED_PARAM]; + │ ^^^^^^^^^ instance defined here + +error: unused invocation: output ports on invocation never used + ┌─ tests/warnings/unused/simple-unused.fil:14:3 + │ +14 │ unused_invoke := UsedAdd<'G>(); + │ ^^^^^^^^^^^^^ invocation defined here + +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/simple-unused.fil:6:7 + │ +6 │ let UNUSED_PARAM = 32; + │ ^^^^^^^^^^^^ parameter defined here + +Compilation failed with 5 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/simple-unused.fil b/tests/warnings/unused/simple-unused.fil new file mode 100644 index 00000000..de645647 --- /dev/null +++ b/tests/warnings/unused/simple-unused.fil @@ -0,0 +1,19 @@ +comp Add[D, I, W, U]<'G:D>() -> (out: ['G, 'G+I] W) with { + some L; +} {} + +comp main<'G:1>() -> (out: ['G, 'G+1] 32) with { + let UNUSED_PARAM = 32; + let USED_PARAM = 10; +} { + // Unused instance - never invoked + UnusedAdd := new Add[USED_PARAM]; + + // Used instance but unused invocation + UsedAdd := new Add[USED_PARAM]; + unused_invoke := UsedAdd<'G>(); + + // Actually used + used_invoke := UsedAdd<'G>(); + out = used_invoke.out; +} diff --git a/tests/warnings/unused/unused-instance.expect b/tests/warnings/unused/unused-instance.expect new file mode 100644 index 00000000..6182908e --- /dev/null +++ b/tests/warnings/unused/unused-instance.expect @@ -0,0 +1,11 @@ +---CODE--- +1 +---STDERR--- +error: unused instance: instance is created but never invoked + ┌─ tests/warnings/unused/unused-instance.fil:4:3 + │ +4 │ Add := new Add; + │ ^^^ instance defined here + +Compilation failed with 1 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/unused-instance.fil b/tests/warnings/unused/unused-instance.fil new file mode 100644 index 00000000..cf871428 --- /dev/null +++ b/tests/warnings/unused/unused-instance.fil @@ -0,0 +1,5 @@ +comp Add<'G:1>() -> () {} + +comp main<'G:1>() -> () { + Add := new Add; +} \ No newline at end of file diff --git a/tests/warnings/unused/unused-invoke.expect b/tests/warnings/unused/unused-invoke.expect new file mode 100644 index 00000000..5e7ec384 --- /dev/null +++ b/tests/warnings/unused/unused-invoke.expect @@ -0,0 +1,11 @@ +---CODE--- +1 +---STDERR--- +error: unused invocation: output ports on invocation never used + ┌─ tests/warnings/unused/unused-invoke.fil:7:3 + │ +7 │ adder := new Add[32]<'G>(in1, in2); + │ ^^^^^ invocation defined here + +Compilation failed with 1 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/unused-invoke.fil b/tests/warnings/unused/unused-invoke.fil new file mode 100644 index 00000000..ac5affff --- /dev/null +++ b/tests/warnings/unused/unused-invoke.fil @@ -0,0 +1,9 @@ +import "primitives/comb.fil"; + +comp main<'G:1>( + in1: ['G, 'G+1] 32, + in2: ['G, 'G+1] 32 +) -> () { + adder := new Add[32]<'G>(in1, in2); + // adder.out is never used - this is an unused invocation +} \ No newline at end of file diff --git a/tests/warnings/unused/unused-param.expect b/tests/warnings/unused/unused-param.expect new file mode 100644 index 00000000..52d920ad --- /dev/null +++ b/tests/warnings/unused/unused-param.expect @@ -0,0 +1,29 @@ +---CODE--- +1 +---STDERR--- +error: unused invocation: output ports on invocation never used + ┌─ ./primitives/reshape.fil:134:7 + │ +134 │ red := new ConcatBundle[W, N-1, L]<'G>(in{1..N}); + │ ^^^ invocation defined here + +error: unused invocation: output ports on invocation never used + ┌─ ./primitives/reshape.fil:154:7 + │ +154 │ rem := new Slice[W*N, W*(N-1)-1, 0]<'G, 'G+L>(in); + │ ^^^ invocation defined here + +error: unused invocation: output ports on invocation never used + ┌─ tests/warnings/unused/unused-param.fil:7:3 + │ +7 │ const1 := new Const[USED, 1]<'G>(); + │ ^^^^^^ invocation defined here + +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/unused-param.fil:4:7 + │ +4 │ let UNUSED = 42; + │ ^^^^^^ parameter defined here + +Compilation failed with 4 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/unused-param.fil b/tests/warnings/unused/unused-param.fil new file mode 100644 index 00000000..89e8b970 --- /dev/null +++ b/tests/warnings/unused/unused-param.fil @@ -0,0 +1,8 @@ +import "primitives/core.fil"; + +comp main<'G:1>() -> () with { + let UNUSED = 42; + let USED = 32; +} { + const1 := new Const[USED, 1]<'G>(); +} \ No newline at end of file diff --git a/tests/warnings/unused/very-simple.expect b/tests/warnings/unused/very-simple.expect new file mode 100644 index 00000000..a9d14756 --- /dev/null +++ b/tests/warnings/unused/very-simple.expect @@ -0,0 +1,17 @@ +---CODE--- +1 +---STDERR--- +error: unused instance: instance is created but never invoked + ┌─ tests/warnings/unused/very-simple.fil:7:3 + │ +7 │ UnusedAdd := new Add; + │ ^^^^^^^^^ instance defined here + +error: unused parameter: parameter is defined but never used + ┌─ tests/warnings/unused/very-simple.fil:4:7 + │ +4 │ let UNUSED_PARAM = 42; + │ ^^^^^^^^^^^^ parameter defined here + +Compilation failed with 2 errors. +Run with --show-models to generate assignments for failing constraints. diff --git a/tests/warnings/unused/very-simple.fil b/tests/warnings/unused/very-simple.fil new file mode 100644 index 00000000..431e43c5 --- /dev/null +++ b/tests/warnings/unused/very-simple.fil @@ -0,0 +1,8 @@ +comp Add<'G:1>() -> () {} + +comp main<'G:1>() -> () with { + let UNUSED_PARAM = 42; +} { + // Unused instance - never invoked + UnusedAdd := new Add; +} \ No newline at end of file