diff --git a/.gitignore b/.gitignore index 4b40e9ab..f5961c43 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ node_modules/ # macOS .DS_Store + +.claude \ No newline at end of file diff --git a/examples/hash_loop.simf b/examples/hash_loop.simf index f554d894..3d18ec4d 100644 --- a/examples/hash_loop.simf +++ b/examples/hash_loop.simf @@ -1,5 +1,5 @@ // Add counter to streaming hash and finalize when the loop exists -fn hash_counter_8(ctx: Ctx8, unused: (), byte: u8) -> Either { +fn hash_counter_8(ctx: Ctx8, _unused: (), byte: u8) -> Either { let new_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(ctx, byte); match jet::all_8(byte) { true => Left(jet::sha_256_ctx_8_finalize(new_ctx)), @@ -8,7 +8,7 @@ fn hash_counter_8(ctx: Ctx8, unused: (), byte: u8) -> Either { } // Add counter to streaming hash and finalize when the loop exists -fn hash_counter_16(ctx: Ctx8, unused: (), bytes: u16) -> Either { +fn hash_counter_16(ctx: Ctx8, _unused: (), bytes: u16) -> Either { let new_ctx: Ctx8 = jet::sha_256_ctx_8_add_2(ctx, bytes); match jet::all_16(bytes) { true => Left(jet::sha_256_ctx_8_finalize(new_ctx)), diff --git a/run_examples.ps1 b/run_examples.ps1 new file mode 100644 index 00000000..194e1836 --- /dev/null +++ b/run_examples.ps1 @@ -0,0 +1,33 @@ +$ErrorActionPreference = "Stop" + +function Invoke-Cargo { + & cargo run -- @args + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} + +$examplesDir = Join-Path $PSScriptRoot "examples" +$simfFiles = Get-ChildItem -Path $examplesDir -Filter "*.simf" | Sort-Object Name + +foreach ($simf in $simfFiles) { + $base = $simf.BaseName + $argsFile = Join-Path $examplesDir "$base.args" + $witFiles = Get-ChildItem -Path $examplesDir -Filter "*.wit" | + Where-Object { $_.Name -like "$base.*" } | + Sort-Object Name + + $baseArgs = @($simf.FullName, "--deny-warnings") + if (Test-Path $argsFile) { + $baseArgs += "-a", $argsFile + } + + # Run without witness + Write-Host "`n=== $base (no witness) ===" -ForegroundColor Cyan + Invoke-Cargo @baseArgs + + # Run once per .wit file + foreach ($wit in $witFiles) { + Write-Host "`n=== $base + $($wit.Name) ===" -ForegroundColor Cyan + $witArgs = $baseArgs + @("-w", $wit.FullName) + Invoke-Cargo @witArgs + } +} diff --git a/run_examples.sh b/run_examples.sh new file mode 100644 index 00000000..48c46160 --- /dev/null +++ b/run_examples.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +examples_dir="$(dirname "$0")/examples" + +for simf in "$examples_dir"/*.simf; do + base=$(basename "$simf" .simf) + args_file="$examples_dir/$base.args" + + base_args=("$simf" "--deny-warnings") + if [ -f "$args_file" ]; then + base_args+=("-a" "$args_file") + fi + + # Run without witness + echo "" + echo "=== $base (no witness) ===" + cargo run -- "${base_args[@]}" + + # Run once per .wit file + for wit in "$examples_dir/$base".*.wit "$examples_dir/$base".wit; do + [ -f "$wit" ] || continue + echo "" + echo "=== $base + $(basename "$wit") ===" + cargo run -- "${base_args[@]}" -w "$wit" + done +done diff --git a/src/ast.rs b/src/ast.rs index 20dd2a70..17e00cd8 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,5 +1,5 @@ use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::Arc; @@ -18,6 +18,7 @@ use crate::types::{ AliasedType, ResolvedType, StructuralType, TypeConstructible, TypeDeconstructible, UIntType, }; use crate::value::{UIntValue, Value}; +use crate::warning::Warning; use crate::witness::{Parameters, WitnessTypes, WitnessValues}; use crate::{impl_eq_hash, parse}; @@ -523,6 +524,10 @@ impl TreeLike for ExprTree<'_> { #[derive(Clone, Debug, Eq, PartialEq, Default)] struct Scope { variables: Vec>, + /// Spans of variables bound via `let` assignment patterns, per scope level. + bound_spans: Vec>, + /// Variables that have been referenced (used) in each scope level. + used_variables: Vec>, aliases: HashMap, parameters: HashMap, witnesses: HashMap, @@ -540,6 +545,8 @@ impl Scope { /// Push a new scope onto the stack. pub fn push_scope(&mut self) { self.variables.push(HashMap::new()); + self.bound_spans.push(HashMap::new()); + self.used_variables.push(HashSet::new()); } /// Push the scope of the main function onto the stack. @@ -557,41 +564,93 @@ impl Scope { /// Pop the current scope from the stack. /// + /// Returns warnings for any variables that were bound but never used. + /// /// ## Panics /// /// The stack is empty. - pub fn pop_scope(&mut self) { + pub fn pop_scope(&mut self) -> Vec { self.variables.pop().expect("Stack is empty"); + let bound = self.bound_spans.pop().expect("Stack is empty"); + let used = self.used_variables.pop().expect("Stack is empty"); + let mut unused: Vec<(Identifier, Span)> = bound + .into_iter() + .filter(|(id, _)| !used.contains(id) && !id.as_inner().starts_with('_')) + .collect(); + unused.sort_by_key(|(_, span)| span.start); + unused + .into_iter() + .map(|(id, span)| Warning::variable_unused(id, span)) + .collect() } /// Pop the scope of the main function from the stack. /// + /// Returns warnings for any variables that were bound but never used. + /// /// ## Panics /// /// - The current scope is not inside the main function. /// - The current scope is not nested in the topmost scope. - pub fn pop_main_scope(&mut self) { + pub fn pop_main_scope(&mut self) -> Vec { assert!(self.is_main, "Current scope is not inside main function"); - self.pop_scope(); + let warnings = self.pop_scope(); self.is_main = false; assert!( self.is_topmost(), "Current scope is not nested in topmost scope" - ) + ); + warnings } - /// Push a variable onto the current stack. + /// Insert a variable into the current scope **without** tracking it for unused-variable warnings. + /// + /// Use this only when re-inserting a variable that is being referenced (i.e. consumed in an + /// expression), where the reference itself is the evidence of use. For new binding sites + /// (`let`, function parameters, match arm patterns) use [`bind_variable`] instead so that + /// unused-variable warnings are emitted correctly. + /// + /// [`bind_variable`]: Scope::bind_variable /// /// ## Panics /// /// The stack is empty. - pub fn insert_variable(&mut self, identifier: Identifier, ty: ResolvedType) { + pub fn insert_variable_usage(&mut self, identifier: Identifier, ty: ResolvedType) { self.variables .last_mut() .expect("Stack is empty") .insert(identifier, ty); } + /// Bind a variable from a new binding site (`let`, function parameter, match arm pattern), + /// tracking its span so an unused-variable warning can be emitted if it is never referenced. + /// + /// ## Panics + /// + /// The stack is empty. + pub fn bind_variable(&mut self, identifier: Identifier, ty: ResolvedType, span: Span) { + self.insert_variable_usage(identifier.clone(), ty); + self.bound_spans + .last_mut() + .expect("Stack is empty") + .insert(identifier, span); + } + + /// Mark a variable as used in the scope where it was bound. + pub fn mark_variable_used(&mut self, identifier: &Identifier) { + for (scope_vars, used) in self + .variables + .iter() + .zip(self.used_variables.iter_mut()) + .rev() + { + if scope_vars.contains_key(identifier) { + used.insert(identifier.clone()); + return; + } + } + } + /// Get the type of the variable. pub fn get_variable(&self, identifier: &Identifier) -> Option<&ResolvedType> { self.variables @@ -714,41 +773,60 @@ trait AbstractSyntaxTree: Sized { /// /// Check if the analyzed expression is of the expected type. /// Statements return no values so their expected type is always unit. - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result; + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError>; } impl Program { - pub fn analyze(from: &parse::Program) -> Result { + pub fn analyze(from: &parse::Program) -> Result<(Self, Vec), RichError> { let unit = ResolvedType::unit(); let mut scope = Scope::default(); - let items = from + let items: Vec<(Item, Vec)> = from .items() .iter() .map(|s| Item::analyze(s, &unit, &mut scope)) - .collect::, RichError>>()?; + .collect::>()?; debug_assert!(scope.is_topmost()); let (parameters, witness_types, call_tracker) = scope.destruct(); - let mut iter = items.into_iter().filter_map(|item| match item { - Item::Function(Function::Main(expr)) => Some(expr), - _ => None, - }); - let main = iter.next().ok_or(Error::MainRequired).with_span(from)?; - if iter.next().is_some() { - return Err(Error::FunctionRedefined(FunctionName::main())).with_span(from); + + let mut all_warnings: Vec = vec![]; + let mut main_expr = None; + let mut main_seen = false; + for (item, mut warnings) in items { + all_warnings.append(&mut warnings); + if let Item::Function(Function::Main(expr)) = item { + if main_seen { + return Err(Error::FunctionRedefined(FunctionName::main())).with_span(from); + } + main_seen = true; + main_expr = Some(expr); + } } - Ok(Self { - main, - parameters, - witness_types, - call_tracker: Arc::new(call_tracker), - }) + let main = main_expr.ok_or(Error::MainRequired).with_span(from)?; + + Ok(( + Self { + main, + parameters, + witness_types, + call_tracker: Arc::new(call_tracker), + }, + all_warnings, + )) } } impl AbstractSyntaxTree for Item { type From = parse::Item; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { assert!(ty.is_unit(), "Items cannot return anything"); assert!(scope.is_topmost(), "Items live in the topmost scope only"); @@ -757,12 +835,11 @@ impl AbstractSyntaxTree for Item { scope .insert_alias(alias.name().clone(), alias.ty().clone()) .with_span(alias)?; - Ok(Self::TypeAlias) - } - parse::Item::Function(function) => { - Function::analyze(function, ty, scope).map(Self::Function) + Ok((Self::TypeAlias, vec![])) } - parse::Item::Module => Ok(Self::Module), + parse::Item::Function(function) => Function::analyze(function, ty, scope) + .map(|(f, warnings)| (Self::Function(f), warnings)), + parse::Item::Module => Ok((Self::Module, vec![])), } } } @@ -770,7 +847,11 @@ impl AbstractSyntaxTree for Item { impl AbstractSyntaxTree for Function { type From = parse::Function; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { assert!(ty.is_unit(), "Function definitions cannot return anything"); assert!(scope.is_topmost(), "Items live in the topmost scope only"); @@ -792,18 +873,23 @@ impl AbstractSyntaxTree for Function { .transpose()? .unwrap_or_else(ResolvedType::unit); scope.push_scope(); - for param in params.iter() { - scope.insert_variable(param.identifier().clone(), param.ty().clone()); + for (param, parse_param) in params.iter().zip(from.params().iter()) { + scope.bind_variable( + param.identifier().clone(), + param.ty().clone(), + parse_param.span(), + ); } - let body = Expression::analyze(from.body(), &ret, scope).map(Arc::new)?; - scope.pop_scope(); + let (body_expr, mut warnings) = Expression::analyze(from.body(), &ret, scope)?; + let body = Arc::new(body_expr); + warnings.extend(scope.pop_scope()); debug_assert!(scope.is_topmost()); let function = CustomFunction { params, body }; scope .insert_function(from.name().clone(), function) .with_span(from)?; - return Ok(Self::Custom); + return Ok((Self::Custom, warnings)); } if !from.params().is_empty() { @@ -817,24 +903,26 @@ impl AbstractSyntaxTree for Function { } scope.push_main_scope(); - let body = Expression::analyze(from.body(), ty, scope)?; - scope.pop_main_scope(); - Ok(Self::Main(body)) + let (body, mut warnings) = Expression::analyze(from.body(), ty, scope)?; + warnings.extend(scope.pop_main_scope()); + Ok((Self::Main(body), warnings)) } } impl AbstractSyntaxTree for Statement { type From = parse::Statement; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { assert!(ty.is_unit(), "Statements cannot return anything"); match from { - parse::Statement::Assignment(assignment) => { - Assignment::analyze(assignment, ty, scope).map(Self::Assignment) - } - parse::Statement::Expression(expression) => { - Expression::analyze(expression, ty, scope).map(Self::Expression) - } + parse::Statement::Assignment(assignment) => Assignment::analyze(assignment, ty, scope) + .map(|(a, warnings)| (Self::Assignment(a), warnings)), + parse::Statement::Expression(expression) => Expression::analyze(expression, ty, scope) + .map(|(e, warnings)| (Self::Expression(e), warnings)), } } } @@ -842,24 +930,32 @@ impl AbstractSyntaxTree for Statement { impl AbstractSyntaxTree for Assignment { type From = parse::Assignment; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { assert!(ty.is_unit(), "Assignments cannot return anything"); // The assignment is a statement that returns nothing. // // However, the expression evaluated in the assignment does have a type, // namely the type specified in the assignment. let ty_expr = scope.resolve(from.ty()).with_span(from)?; - let expression = Expression::analyze(from.expression(), &ty_expr, scope)?; + let (expression, warnings) = Expression::analyze(from.expression(), &ty_expr, scope)?; let typed_variables = from.pattern().is_of_type(&ty_expr).with_span(from)?; + let pattern_span = from.pattern_span(); for (identifier, ty) in typed_variables { - scope.insert_variable(identifier, ty); + scope.bind_variable(identifier, ty, pattern_span); } - Ok(Self { - pattern: from.pattern().clone(), - expression, - span: *from.as_ref(), - }) + Ok(( + Self { + pattern: from.pattern().clone(), + expression, + span: *from.as_ref(), + }, + warnings, + )) } } @@ -874,47 +970,63 @@ impl Expression { /// The details depend on the current state of the SimplicityHL compiler. pub fn analyze_const(from: &parse::Expression, ty: &ResolvedType) -> Result { let mut empty_scope = Scope::default(); - Self::analyze(from, ty, &mut empty_scope) + // Constant expressions are literal values — no `let` bindings, so no + // unused-variable warnings can arise. Warnings are intentionally discarded here. + Self::analyze(from, ty, &mut empty_scope).map(|(e, _warnings)| e) } } impl AbstractSyntaxTree for Expression { type From = parse::Expression; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { match from.inner() { parse::ExpressionInner::Single(single) => { - let ast_single = SingleExpression::analyze(single, ty, scope)?; - Ok(Self { - ty: ty.clone(), - inner: ExpressionInner::Single(ast_single), - span: *from.as_ref(), - }) + let (ast_single, warnings) = SingleExpression::analyze(single, ty, scope)?; + Ok(( + Self { + ty: ty.clone(), + inner: ExpressionInner::Single(ast_single), + span: *from.as_ref(), + }, + warnings, + )) } parse::ExpressionInner::Block(statements, expression) => { scope.push_scope(); - let ast_statements = statements + let ast_statements_with_warnings = statements .iter() .map(|s| Statement::analyze(s, &ResolvedType::unit(), scope)) - .collect::, RichError>>()?; - let ast_expression = match expression { + .collect::)>, RichError>>()?; + let (ast_expression, expr_warnings) = match expression { Some(expression) => Expression::analyze(expression, ty, scope) - .map(Arc::new) - .map(Some), - None if ty.is_unit() => Ok(None), + .map(|(e, warnings)| (Some(Arc::new(e)), warnings)), + None if ty.is_unit() => Ok((None, vec![])), None => Err(Error::ExpressionTypeMismatch( ty.clone(), ResolvedType::unit(), )) .with_span(from), }?; - scope.pop_scope(); - - Ok(Self { - ty: ty.clone(), - inner: ExpressionInner::Block(ast_statements, ast_expression), - span: *from.as_ref(), - }) + let mut all_warnings: Vec = scope.pop_scope(); + + let (all_statements, stmt_warnings): (Vec<_>, Vec<_>) = + ast_statements_with_warnings.into_iter().unzip(); + all_warnings.extend(stmt_warnings.into_iter().flatten()); + all_warnings.extend(expr_warnings); + + Ok(( + Self { + ty: ty.clone(), + inner: ExpressionInner::Block(all_statements.into(), ast_expression), + span: *from.as_ref(), + }, + all_warnings, + )) } } } @@ -923,8 +1035,12 @@ impl AbstractSyntaxTree for Expression { impl AbstractSyntaxTree for SingleExpression { type From = parse::SingleExpression; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { - let inner = match from.inner() { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { + let (inner, warnings) = match from.inner() { parse::SingleExpressionInner::Boolean(bit) => { if !ty.is_boolean() { return Err(Error::ExpressionTypeMismatch( @@ -933,17 +1049,20 @@ impl AbstractSyntaxTree for SingleExpression { )) .with_span(from); } - SingleExpressionInner::Constant(Value::from(*bit)) + (SingleExpressionInner::Constant(Value::from(*bit)), vec![]) } parse::SingleExpressionInner::Decimal(decimal) => { let ty = ty .as_integer() .ok_or(Error::ExpressionUnexpectedType(ty.clone())) .with_span(from)?; - UIntValue::parse_decimal(decimal, ty) - .with_span(from) - .map(Value::from) - .map(SingleExpressionInner::Constant)? + ( + UIntValue::parse_decimal(decimal, ty) + .with_span(from) + .map(Value::from) + .map(SingleExpressionInner::Constant)?, + vec![], + ) } parse::SingleExpressionInner::Binary(bits) => { let ty = ty @@ -951,23 +1070,26 @@ impl AbstractSyntaxTree for SingleExpression { .ok_or(Error::ExpressionUnexpectedType(ty.clone())) .with_span(from)?; let value = UIntValue::parse_binary(bits, ty).with_span(from)?; - SingleExpressionInner::Constant(Value::from(value)) + (SingleExpressionInner::Constant(Value::from(value)), vec![]) } parse::SingleExpressionInner::Hexadecimal(bytes) => { let value = Value::parse_hexadecimal(bytes, ty).with_span(from)?; - SingleExpressionInner::Constant(value) + (SingleExpressionInner::Constant(value), vec![]) } parse::SingleExpressionInner::Witness(name) => { scope .insert_witness(name.clone(), ty.clone()) .with_span(from)?; - SingleExpressionInner::Witness(name.clone()) + (SingleExpressionInner::Witness(name.clone()), vec![]) } parse::SingleExpressionInner::Parameter(name) => { scope .insert_parameter(name.shallow_clone(), ty.clone()) .with_span(from)?; - SingleExpressionInner::Parameter(name.shallow_clone()) + ( + SingleExpressionInner::Parameter(name.shallow_clone()), + vec![], + ) } parse::SingleExpressionInner::Variable(identifier) => { let bound_ty = scope @@ -978,13 +1100,14 @@ impl AbstractSyntaxTree for SingleExpression { return Err(Error::ExpressionTypeMismatch(ty.clone(), bound_ty.clone())) .with_span(from); } - scope.insert_variable(identifier.clone(), ty.clone()); - SingleExpressionInner::Variable(identifier.clone()) + scope.mark_variable_used(identifier); + scope.insert_variable_usage(identifier.clone(), ty.clone()); + (SingleExpressionInner::Variable(identifier.clone()), vec![]) } parse::SingleExpressionInner::Expression(parse) => { - Expression::analyze(parse, ty, scope) - .map(Arc::new) - .map(SingleExpressionInner::Expression)? + Expression::analyze(parse, ty, scope).map(|(e, warnings)| { + (SingleExpressionInner::Expression(Arc::new(e)), warnings) + })? } parse::SingleExpressionInner::Tuple(tuple) => { let types = ty @@ -994,12 +1117,17 @@ impl AbstractSyntaxTree for SingleExpression { if tuple.len() != types.len() { return Err(Error::ExpressionUnexpectedType(ty.clone())).with_span(from); } - tuple + let results = tuple .iter() .zip(types.iter()) .map(|(el_parse, el_ty)| Expression::analyze(el_parse, el_ty, scope)) - .collect::, RichError>>() - .map(SingleExpressionInner::Tuple)? + .collect::)>, RichError>>()?; + let (all_expressions, warnings): (Vec<_>, Vec<_>) = results.into_iter().unzip(); + let all_warnings: Vec = warnings.into_iter().flatten().collect(); + ( + SingleExpressionInner::Tuple(all_expressions.into()), + all_warnings, + ) } parse::SingleExpressionInner::Array(array) => { let (el_ty, size) = ty @@ -1009,11 +1137,16 @@ impl AbstractSyntaxTree for SingleExpression { if array.len() != size { return Err(Error::ExpressionUnexpectedType(ty.clone())).with_span(from); } - array + let results = array .iter() .map(|el_parse| Expression::analyze(el_parse, el_ty, scope)) - .collect::, RichError>>() - .map(SingleExpressionInner::Array)? + .collect::)>, RichError>>()?; + let (all_expressions, warnings): (Vec<_>, Vec<_>) = results.into_iter().unzip(); + let all_warnings: Vec = warnings.into_iter().flatten().collect(); + ( + SingleExpressionInner::Array(all_expressions.into()), + all_warnings, + ) } parse::SingleExpressionInner::List(list) => { let (el_ty, bound) = ty @@ -1023,10 +1156,16 @@ impl AbstractSyntaxTree for SingleExpression { if bound.get() <= list.len() { return Err(Error::ExpressionUnexpectedType(ty.clone())).with_span(from); } - list.iter() + let results = list + .iter() .map(|e| Expression::analyze(e, el_ty, scope)) - .collect::, RichError>>() - .map(SingleExpressionInner::List)? + .collect::)>, RichError>>()?; + let (all_expressions, warnings): (Vec<_>, Vec<_>) = results.into_iter().unzip(); + let all_warnings: Vec = warnings.into_iter().flatten().collect(); + ( + SingleExpressionInner::List(all_expressions.into()), + all_warnings, + ) } parse::SingleExpressionInner::Either(either) => { let (ty_l, ty_r) = ty @@ -1035,13 +1174,11 @@ impl AbstractSyntaxTree for SingleExpression { .with_span(from)?; match either { Either::Left(parse_l) => Expression::analyze(parse_l, ty_l, scope) - .map(Arc::new) - .map(Either::Left), + .map(|(l, warnings)| (Either::Left(Arc::new(l)), warnings)), Either::Right(parse_r) => Expression::analyze(parse_r, ty_r, scope) - .map(Arc::new) - .map(Either::Right), + .map(|(r, warnings)| (Either::Right(Arc::new(r)), warnings)), } - .map(SingleExpressionInner::Either)? + .map(|(e, warnings)| (SingleExpressionInner::Either(e), warnings))? } parse::SingleExpressionInner::Option(maybe_parse) => { let ty = ty @@ -1049,33 +1186,37 @@ impl AbstractSyntaxTree for SingleExpression { .ok_or(Error::ExpressionUnexpectedType(ty.clone())) .with_span(from)?; match maybe_parse { - Some(parse) => { - Some(Expression::analyze(parse, ty, scope).map(Arc::new)).transpose() - } - None => Ok(None), + Some(parse) => Expression::analyze(parse, ty, scope) + .map(|(e, warnings)| (Some(Arc::new(e)), warnings)), + None => Ok((None, vec![])), } - .map(SingleExpressionInner::Option)? - } - parse::SingleExpressionInner::Call(call) => { - Call::analyze(call, ty, scope).map(SingleExpressionInner::Call)? - } - parse::SingleExpressionInner::Match(match_) => { - Match::analyze(match_, ty, scope).map(SingleExpressionInner::Match)? + .map(|(o, warnings)| (SingleExpressionInner::Option(o), warnings))? } + parse::SingleExpressionInner::Call(call) => Call::analyze(call, ty, scope) + .map(|(c, warnings)| (SingleExpressionInner::Call(c), warnings))?, + parse::SingleExpressionInner::Match(match_) => Match::analyze(match_, ty, scope) + .map(|(m, warnings)| (SingleExpressionInner::Match(m), warnings))?, }; - Ok(Self { - inner, - ty: ty.clone(), - span: *from.as_ref(), - }) + Ok(( + Self { + inner, + ty: ty.clone(), + span: *from.as_ref(), + }, + warnings, + )) } } impl AbstractSyntaxTree for Call { type From = parse::Call; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { fn check_argument_types( parse_args: &[parse::Expression], expected_tys: &[ResolvedType], @@ -1108,16 +1249,18 @@ impl AbstractSyntaxTree for Call { parse_args: &[parse::Expression], args_tys: &[ResolvedType], scope: &mut Scope, - ) -> Result, RichError> { - let args = parse_args + ) -> Result<(Arc<[Expression]>, Vec), RichError> { + let results = parse_args .iter() .zip(args_tys.iter()) .map(|(arg_parse, arg_ty)| Expression::analyze(arg_parse, arg_ty, scope)) - .collect::, RichError>>()?; - Ok(args) + .collect::)>, RichError>>()?; + let (all_args, warnings): (Vec<_>, Vec<_>) = results.into_iter().unzip(); + let all_warnings: Vec = warnings.into_iter().flatten().collect(); + Ok((all_args.into(), all_warnings)) } - let name = CallName::analyze(from, ty, scope)?; + let (name, mut all_warnings) = CallName::analyze(from, ty, scope)?; let args = match name.clone() { CallName::Jet(jet) => { let args_tys = crate::jet::source_type(jet) @@ -1133,12 +1276,15 @@ impl AbstractSyntaxTree for Call { .with_span(from)?; check_output_type(&out_ty, ty).with_span(from)?; scope.track_call(from, TrackedCallName::Jet); - analyze_arguments(from.args(), &args_tys, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); + args } CallName::UnwrapLeft(right_ty) => { let args_tys = [ResolvedType::either(ty.clone(), right_ty)]; check_argument_types(from.args(), &args_tys).with_span(from)?; - let args = analyze_arguments(from.args(), &args_tys, scope)?; + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); let [arg_ty] = args_tys; scope.track_call(from, TrackedCallName::UnwrapLeft(arg_ty)); args @@ -1146,7 +1292,8 @@ impl AbstractSyntaxTree for Call { CallName::UnwrapRight(left_ty) => { let args_tys = [ResolvedType::either(left_ty, ty.clone())]; check_argument_types(from.args(), &args_tys).with_span(from)?; - let args = analyze_arguments(from.args(), &args_tys, scope)?; + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); let [arg_ty] = args_tys; scope.track_call(from, TrackedCallName::UnwrapRight(arg_ty)); args @@ -1156,13 +1303,17 @@ impl AbstractSyntaxTree for Call { check_argument_types(from.args(), &args_tys).with_span(from)?; let out_ty = ResolvedType::boolean(); check_output_type(&out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_tys, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); + args } CallName::Unwrap => { let args_tys = [ResolvedType::option(ty.clone())]; check_argument_types(from.args(), &args_tys).with_span(from)?; scope.track_call(from, TrackedCallName::Unwrap); - analyze_arguments(from.args(), &args_tys, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); + args } CallName::Assert => { let args_tys = [ResolvedType::boolean()]; @@ -1170,19 +1321,24 @@ impl AbstractSyntaxTree for Call { let out_ty = ResolvedType::unit(); check_output_type(&out_ty, ty).with_span(from)?; scope.track_call(from, TrackedCallName::Assert); - analyze_arguments(from.args(), &args_tys, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); + args } CallName::Panic => { let args_tys = []; check_argument_types(from.args(), &args_tys).with_span(from)?; // panic! allows every output type because it will never return anything scope.track_call(from, TrackedCallName::Panic); - analyze_arguments(from.args(), &args_tys, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); + args } CallName::Debug => { let args_tys = [ty.clone()]; check_argument_types(from.args(), &args_tys).with_span(from)?; - let args = analyze_arguments(from.args(), &args_tys, scope)?; + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); let [arg_ty] = args_tys; scope.track_call(from, TrackedCallName::Debug(arg_ty)); args @@ -1194,7 +1350,9 @@ impl AbstractSyntaxTree for Call { let args_tys = [source]; check_argument_types(from.args(), &args_tys).with_span(from)?; - analyze_arguments(from.args(), &args_tys, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_tys, scope)?; + all_warnings.append(&mut warnings); + args } CallName::Custom(function) => { let args_ty = function @@ -1206,7 +1364,9 @@ impl AbstractSyntaxTree for Call { check_argument_types(from.args(), &args_ty).with_span(from)?; let out_ty = function.body().ty(); check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_ty, scope)?; + all_warnings.append(&mut warnings); + args } CallName::Fold(function, bound) => { // A list fold has the signature: @@ -1226,7 +1386,9 @@ impl AbstractSyntaxTree for Call { check_argument_types(from.args(), &args_ty).with_span(from)?; let out_ty = function.body().ty(); check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_ty, scope)?; + all_warnings.append(&mut warnings); + args } CallName::ArrayFold(function, size) => { // An array fold has the signature: @@ -1246,7 +1408,9 @@ impl AbstractSyntaxTree for Call { check_argument_types(from.args(), &args_ty).with_span(from)?; let out_ty = function.body().ty(); check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_ty, scope)?; + all_warnings.append(&mut warnings); + args } CallName::ForWhile(function, _bit_width) => { // A for-while loop has the signature: @@ -1271,15 +1435,20 @@ impl AbstractSyntaxTree for Call { check_argument_types(from.args(), &args_ty).with_span(from)?; let out_ty = function.body().ty(); check_output_type(out_ty, ty).with_span(from)?; - analyze_arguments(from.args(), &args_ty, scope)? + let (args, mut warnings) = analyze_arguments(from.args(), &args_ty, scope)?; + all_warnings.append(&mut warnings); + args } }; - Ok(Self { - name, - args, - span: *from.as_ref(), - }) + Ok(( + Self { + name, + args, + span: *from.as_ref(), + }, + all_warnings, + )) } } @@ -1291,38 +1460,38 @@ impl AbstractSyntaxTree for CallName { from: &Self::From, _ty: &ResolvedType, scope: &mut Scope, - ) -> Result { - match from.name() { + ) -> Result<(Self, Vec), RichError> { + let name = match from.name() { parse::CallName::Jet(name) => match Elements::from_str(name.as_inner()) { Ok(Elements::CheckSigVerify | Elements::Verify) | Err(_) => { - Err(Error::JetDoesNotExist(name.clone())).with_span(from) + return Err(Error::JetDoesNotExist(name.clone())).with_span(from); } - Ok(jet) => Ok(Self::Jet(jet)), + Ok(jet) => Self::Jet(jet), }, parse::CallName::UnwrapLeft(right_ty) => scope .resolve(right_ty) .map(Self::UnwrapLeft) - .with_span(from), + .with_span(from)?, parse::CallName::UnwrapRight(left_ty) => scope .resolve(left_ty) .map(Self::UnwrapRight) - .with_span(from), + .with_span(from)?, parse::CallName::IsNone(some_ty) => { - scope.resolve(some_ty).map(Self::IsNone).with_span(from) + scope.resolve(some_ty).map(Self::IsNone).with_span(from)? } - parse::CallName::Unwrap => Ok(Self::Unwrap), - parse::CallName::Assert => Ok(Self::Assert), - parse::CallName::Panic => Ok(Self::Panic), - parse::CallName::Debug => Ok(Self::Debug), + parse::CallName::Unwrap => Self::Unwrap, + parse::CallName::Assert => Self::Assert, + parse::CallName::Panic => Self::Panic, + parse::CallName::Debug => Self::Debug, parse::CallName::TypeCast(target) => { - scope.resolve(target).map(Self::TypeCast).with_span(from) + scope.resolve(target).map(Self::TypeCast).with_span(from)? } parse::CallName::Custom(name) => scope .get_function(name) .cloned() .map(Self::Custom) .ok_or(Error::FunctionUndefined(name.clone())) - .with_span(from), + .with_span(from)?, parse::CallName::ArrayFold(name, size) => { let function = scope .get_function(name) @@ -1333,10 +1502,9 @@ impl AbstractSyntaxTree for CallName { // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() { - Err(Error::FunctionNotFoldable(name.clone())).with_span(from) - } else { - Ok(Self::ArrayFold(function, *size)) + return Err(Error::FunctionNotFoldable(name.clone())).with_span(from); } + Self::ArrayFold(function, *size) } parse::CallName::Fold(name, bound) => { let function = scope @@ -1348,10 +1516,9 @@ impl AbstractSyntaxTree for CallName { // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() { - Err(Error::FunctionNotFoldable(name.clone())).with_span(from) - } else { - Ok(Self::Fold(function, *bound)) + return Err(Error::FunctionNotFoldable(name.clone())).with_span(from); } + Self::Fold(function, *bound) } parse::CallName::ForWhile(name) => { let function = scope @@ -1382,56 +1549,73 @@ impl AbstractSyntaxTree for CallName { | UIntType::U4 | UIntType::U8 | UIntType::U16), - ) => Ok(Self::ForWhile(function, int_ty.bit_width())), - _ => Err(Error::FunctionNotLoopable(name.clone())).with_span(from), + ) => Self::ForWhile(function, int_ty.bit_width()), + _ => return Err(Error::FunctionNotLoopable(name.clone())).with_span(from), } } - } + }; + Ok((name, vec![])) } } impl AbstractSyntaxTree for Match { type From = parse::Match; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { let scrutinee_ty = from.scrutinee_type(); let scrutinee_ty = scope.resolve(&scrutinee_ty).with_span(from)?; - let scrutinee = - Expression::analyze(from.scrutinee(), &scrutinee_ty, scope).map(Arc::new)?; + let (scrutinee_expr, mut all_warnings) = + Expression::analyze(from.scrutinee(), &scrutinee_ty, scope)?; + let scrutinee = Arc::new(scrutinee_expr); scope.push_scope(); if let Some((pat_l, ty_l)) = from.left().pattern().as_typed_pattern() { let ty_l = scope.resolve(ty_l).with_span(from)?; let typed_variables = pat_l.is_of_type(&ty_l).with_span(from)?; + let span = from.left().pattern_span().unwrap_or(*from.as_ref()); for (identifier, ty) in typed_variables { - scope.insert_variable(identifier, ty); + scope.bind_variable(identifier, ty, span); } } - let ast_l = Expression::analyze(from.left().expression(), ty, scope).map(Arc::new)?; - scope.pop_scope(); + let (ast_l_expr, mut warnings_l) = + Expression::analyze(from.left().expression(), ty, scope)?; + let ast_l = Arc::new(ast_l_expr); + all_warnings.append(&mut warnings_l); + all_warnings.extend(scope.pop_scope()); scope.push_scope(); if let Some((pat_r, ty_r)) = from.right().pattern().as_typed_pattern() { let ty_r = scope.resolve(ty_r).with_span(from)?; let typed_variables = pat_r.is_of_type(&ty_r).with_span(from)?; + let span = from.right().pattern_span().unwrap_or(*from.as_ref()); for (identifier, ty) in typed_variables { - scope.insert_variable(identifier, ty); + scope.bind_variable(identifier, ty, span); } } - let ast_r = Expression::analyze(from.right().expression(), ty, scope).map(Arc::new)?; - scope.pop_scope(); - - Ok(Self { - scrutinee, - left: MatchArm { - pattern: from.left().pattern().clone(), - expression: ast_l, - }, - right: MatchArm { - pattern: from.right().pattern().clone(), - expression: ast_r, + let (ast_r_expr, mut warnings_r) = + Expression::analyze(from.right().expression(), ty, scope)?; + let ast_r = Arc::new(ast_r_expr); + all_warnings.append(&mut warnings_r); + all_warnings.extend(scope.pop_scope()); + + Ok(( + Self { + scrutinee, + left: MatchArm { + pattern: from.left().pattern().clone(), + expression: ast_l, + }, + right: MatchArm { + pattern: from.right().pattern().clone(), + expression: ast_r, + }, + span: *from.as_ref(), }, - span: *from.as_ref(), - }) + all_warnings, + )) } } @@ -1441,16 +1625,23 @@ fn analyze_named_module( ) -> Result, RichError> { let unit = ResolvedType::unit(); let mut scope = Scope::default(); - let items = from + let items: Vec<(ModuleItem, Vec)> = from .items() .iter() .map(|s| ModuleItem::analyze(s, &unit, &mut scope)) - .collect::, RichError>>()?; + .collect::>()?; debug_assert!(scope.is_topmost()); - let mut iter = items.into_iter().filter_map(|item| match item { - ModuleItem::Module(module) if module.name == name => Some(module), - _ => None, - }); + // Module assignments only allow constant expressions — there are no `let` + // bindings, so no unused-variable warnings can arise. Warnings are + // intentionally discarded here. In future however, there may be + // other warnings added. Named modules are not really used at the moment + // and may be substituted for something else in future. + let mut iter = items + .into_iter() + .filter_map(|(item, _warnings)| match item { + ModuleItem::Module(module) if module.name == name => Some(module), + _ => None, + }); let Some(witness_module) = iter.next() else { return Ok(HashMap::new()); // "not present" is equivalent to empty }; @@ -1486,14 +1677,17 @@ impl crate::witness::Arguments { impl AbstractSyntaxTree for ModuleItem { type From = parse::ModuleItem; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { assert!(ty.is_unit(), "Items cannot return anything"); assert!(scope.is_topmost(), "Items live in the topmost scope only"); match from { - parse::ModuleItem::Ignored => Ok(Self::Ignored), - parse::ModuleItem::Module(witness_module) => { - Module::analyze(witness_module, ty, scope).map(Self::Module) - } + parse::ModuleItem::Ignored => Ok((Self::Ignored, vec![])), + parse::ModuleItem::Module(witness_module) => Module::analyze(witness_module, ty, scope) + .map(|(m, warnings)| (Self::Module(m), warnings)), } } } @@ -1501,40 +1695,57 @@ impl AbstractSyntaxTree for ModuleItem { impl AbstractSyntaxTree for Module { type From = parse::Module; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { assert!(ty.is_unit(), "Modules cannot return anything"); assert!(scope.is_topmost(), "Modules live in the topmost scope only"); - let assignments = from + let results = from .assignments() .iter() .map(|s| ModuleAssignment::analyze(s, ty, scope)) - .collect::, RichError>>()?; + .collect::)>, RichError>>()?; debug_assert!(scope.is_topmost()); - Ok(Self { - name: from.name().shallow_clone(), - span: *from.as_ref(), - assignments, - }) + let (all_assignments, warnings): (Vec<_>, Vec<_>) = results.into_iter().unzip(); + let all_warnings: Vec = warnings.into_iter().flatten().collect(); + + Ok(( + Self { + name: from.name().shallow_clone(), + span: *from.as_ref(), + assignments: all_assignments.into(), + }, + all_warnings, + )) } } impl AbstractSyntaxTree for ModuleAssignment { type From = parse::ModuleAssignment; - fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { + fn analyze( + from: &Self::From, + ty: &ResolvedType, + scope: &mut Scope, + ) -> Result<(Self, Vec), RichError> { assert!(ty.is_unit(), "Assignments cannot return anything"); let ty_expr = scope.resolve(from.ty()).with_span(from)?; - let expression = Expression::analyze(from.expression(), &ty_expr, scope)?; + let (expression, warnings) = Expression::analyze(from.expression(), &ty_expr, scope)?; let value = Value::from_const_expr(&expression) .ok_or(Error::ExpressionUnexpectedType(ty_expr.clone())) .with_span(from.expression())?; - Ok(Self { - name: from.name().clone(), - value, - span: *from.as_ref(), - }) + Ok(( + Self { + name: from.name().clone(), + value, + span: *from.as_ref(), + }, + warnings, + )) } } diff --git a/src/error.rs b/src/error.rs index 08320af8..f4b94471 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,7 @@ use crate::lexer::Token; use crate::parse::MatchPattern; use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName}; use crate::types::{ResolvedType, UIntType}; +use crate::warning::WarningName; /// Area that an object spans inside a file. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -131,28 +132,43 @@ impl WithFile for Result { } } +/// Helper trait to update `Result` with the file path for display. +pub trait WithFilePath { + /// Attach the file path (e.g. `"src/main.simf"`) to the error for `–– >` display. + fn with_file_path>>(self, path: P) -> Result; +} + +impl WithFilePath for Result { + fn with_file_path>>(self, path: P) -> Result { + self.map_err(|e| e.with_file_path(path.into())) + } +} + /// An error enriched with context. /// /// Records _what_ happened and _where_. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RichError { /// The error that occurred. - error: Error, + error: Box, /// Area that the error spans inside the file. span: Span, - /// File in which the error occurred. + /// File contents in which the error occurred. /// - /// Required to print pretty errors. + /// Required to render the source-code excerpt. file: Option>, + /// File path (e.g. `"src/main.simf"`) shown on the ` --> ` line. + file_path: Option>, } impl RichError { /// Create a new error with context. pub fn new(error: Error, span: Span) -> RichError { RichError { - error, + error: Box::new(error), span, file: None, + file_path: None, } } @@ -161,9 +177,16 @@ impl RichError { /// Enable pretty errors. pub fn with_file(self, file: Arc) -> Self { Self { - error: self.error, - span: self.span, file: Some(file), + ..self + } + } + + /// Add the file path shown on the ` --> ` line (e.g. `"src/main.simf"`). + pub fn with_file_path(self, path: Arc) -> Self { + Self { + file_path: Some(path), + ..self } } @@ -171,9 +194,10 @@ impl RichError { /// a problem on the parsing side. pub fn parsing_error(reason: &str) -> Self { Self { - error: Error::CannotParse(reason.to_string()), + error: Box::new(Error::CannotParse(reason.to_string())), span: Span::new(0, 0), file: None, + file_path: None, } } @@ -190,60 +214,101 @@ impl RichError { } } -impl fmt::Display for RichError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fn get_line_col(file: &str, offset: usize) -> (usize, usize) { - let mut line = 1; - let mut col = 0; - - let slice = file.get(0..offset).unwrap_or_default(); - - for char in slice.chars() { - if char.is_newline() { - line += 1; - col = 0; - } else { - col += char.len_utf16(); - } - } +pub fn get_line_col(file: &str, offset: usize) -> (usize, usize) { + let mut line = 1; + let mut col = 0; - (line, col + 1) + let slice = file.get(0..offset).unwrap_or_default(); + + for char in slice.chars() { + if char.is_newline() { + line += 1; + col = 0; + } else { + col += char.len_utf16(); } + } - match self.file { - Some(ref file) if !file.is_empty() => { - let (start_line, start_col) = get_line_col(file, self.span.start); - let (end_line, end_col) = get_line_col(file, self.span.end); + (line, col + 1) +} - let start_line_index = start_line - 1; +/// Pre-computed source-span metrics and rendered source lines. +/// +/// Centralises the line-number arithmetic and source-line iteration so that both +/// [`RichError`] display and warning formatting produce consistent output without +/// duplicating the computation. +pub(crate) struct SpanDisplay { + /// 1-based start line of the span. + pub start_line: usize, + /// 1-based start column of the span. + pub start_col: usize, + /// Width of the line-number column (digits in the last covered line number). + pub line_num_width: usize, + /// Number of spaces to indent before the underline carets. + pub underline_start: usize, + /// Number of underline caret characters (`^`). + pub underline_len: usize, + /// The blank-separator line and source lines, ready to write verbatim. + /// + /// Format: `" |\n1 | source line\n"` etc. + pub lines_block: String, +} - let n_spanned_lines = end_line - start_line_index; - let line_num_width = end_line.to_string().len(); +impl SpanDisplay { + pub fn new(file: &str, span: Span) -> Self { + use std::fmt::Write as _; - writeln!(f, "{:width$} |", " ", width = line_num_width)?; + let (start_line, start_col) = get_line_col(file, span.start); + let (end_line, end_col) = get_line_col(file, span.end); + let start_line_index = start_line - 1; + let n_spanned_lines = end_line - start_line_index; + let line_num_width = end_line.to_string().len(); - let mut lines = file.lines().skip(start_line_index).peekable(); - let start_line_len = lines.peek().map_or(0, |l| l.len()); + let mut lines = file.lines().skip(start_line_index).peekable(); + let start_line_len = lines.peek().map_or(0, |l| l.len()); - for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() { - let line_num = start_line_index + relative_line_index + 1; - writeln!(f, "{line_num:line_num_width$} | {line_str}")?; - } + let mut lines_block = String::new(); + let _ = writeln!(lines_block, "{:width$} |", " ", width = line_num_width); + for (i, line_str) in lines.take(n_spanned_lines).enumerate() { + let line_num = start_line_index + i + 1; + let _ = writeln!(lines_block, "{line_num:line_num_width$} | {line_str}"); + } - let is_multiline = end_line > start_line; + let is_multiline = end_line > start_line; + let (underline_start, underline_len) = if is_multiline { + (0, start_line_len) + } else { + (start_col, end_col - start_col) + }; - let (underline_start, underline_length) = match is_multiline { - true => (0, start_line_len), - false => (start_col, end_col - start_col), - }; - write!(f, "{:width$} |", " ", width = line_num_width)?; - write!(f, "{:width$}", " ", width = underline_start)?; - write!(f, "{:^ { + Self { + start_line, + start_col, + line_num_width, + underline_start, + underline_len, + lines_block, + } + } +} + +impl fmt::Display for RichError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b[1;31merror\x1b[0m: {}", self.error)?; + match self.file { + Some(ref file) if !file.is_empty() => { + let sd = SpanDisplay::new(file, self.span); + writeln!(f)?; + if let Some(ref path) = self.file_path { + writeln!(f, " --> {}:{}:{}", path, sd.start_line, sd.start_col)?; + } + write!(f, "{}", sd.lines_block)?; + write!(f, "{:width$} |", " ", width = sd.line_num_width)?; + write!(f, "{:width$}", " ", width = sd.underline_start)?; + write!(f, "{:^ Ok(()), } } } @@ -252,7 +317,7 @@ impl std::error::Error for RichError {} impl From for Error { fn from(error: RichError) -> Self { - error.error + *error.error } } @@ -268,7 +333,7 @@ where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { fn merge(self, other: Self) -> Self { - match (&self.error, &other.error) { + match (self.error.as_ref(), other.error.as_ref()) { (Error::Grammar(_), Error::Grammar(_)) => other, (Error::Grammar(_), _) => other, (_, Error::Grammar(_)) => self, @@ -304,13 +369,14 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_tokens, label: None, found: found_string, - }, + }), span, file: None, + file_path: None, } } } @@ -331,20 +397,21 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_strings, label: None, found: found_string, - }, + }), span, file: None, + file_path: None, } } fn label_with(&mut self, label: &'tokens str) { if let Error::Syntax { label: ref mut l, .. - } = &mut self.error + } = self.error.as_mut() { *l = Some(label.to_string()); } @@ -353,8 +420,10 @@ where #[derive(Debug, Clone, Hash)] pub struct ErrorCollector { - /// File in which the error occurred. + /// File contents in which the error occurred. file: Arc, + /// File path shown on the ` --> ` line (e.g. `"src/main.simf"`). + file_path: Option>, /// Collected errors. errors: Vec, @@ -364,15 +433,29 @@ impl ErrorCollector { pub fn new(file: Arc) -> Self { Self { file, + file_path: None, + errors: Vec::new(), + } + } + + /// Create a collector that also knows the file path for ` --> ` display. + pub fn new_with_path(file: Arc, file_path: Arc) -> Self { + Self { + file, + file_path: Some(file_path), errors: Vec::new(), } } /// Extend existing errors with slice of new errors. pub fn update(&mut self, errors: impl IntoIterator) { - let new_errors = errors - .into_iter() - .map(|err| err.with_file(Arc::clone(&self.file))); + let new_errors = errors.into_iter().map(|err| { + let err = err.with_file(Arc::clone(&self.file)); + match self.file_path { + Some(ref p) => err.with_file_path(Arc::clone(p)), + None => err, + } + }); self.errors.extend(new_errors); } @@ -441,6 +524,7 @@ pub enum Error { ModuleRedefined(ModuleName), ArgumentMissing(WitnessName), ArgumentTypeMismatch(WitnessName, ResolvedType, ResolvedType), + DeniedWarning(WarningName), } #[rustfmt::skip] @@ -594,6 +678,9 @@ impl fmt::Display for Error { f, "Parameter `{name}` was declared with type `{declared}` but its assigned argument is of type `{assigned}`" ), + Error::DeniedWarning(warning) => write!( + f, "Warning treated as error: {warning}" + ), } } } @@ -641,16 +728,32 @@ let x: u32 = Left( );"#; const EMPTY_FILE: &str = ""; + const RED_BOLD: &str = "\x1b[1;31m"; + const RESET: &str = "\x1b[0m"; + #[test] fn display_single_line() { let error = Error::ListBoundPow2(5) .with_span(Span::new(13, 19)) .with_file(Arc::from(FILE)); - let expected = r#" - | -1 | let a1: List = None; - | ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#; - assert_eq!(&expected[1..], &error.to_string()); + let msg = "Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"; + let expected = format!( + "{RED_BOLD}error{RESET}: {msg}\n |\n1 | let a1: List = None;\n | ^^^^^^ {msg}" + ); + assert_eq!(expected, error.to_string()); + } + + #[test] + fn display_single_line_with_path() { + let error = Error::ListBoundPow2(5) + .with_span(Span::new(13, 19)) + .with_file(Arc::from(FILE)) + .with_file_path(Arc::from("src/main.simf")); + let msg = "Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"; + let expected = format!( + "{RED_BOLD}error{RESET}: {msg}\n --> src/main.simf:1:14\n |\n1 | let a1: List = None;\n | ^^^^^^ {msg}" + ); + assert_eq!(expected, error.to_string()); } #[test] @@ -660,13 +763,11 @@ let x: u32 = Left( ) .with_span(Span::new(41, FILE.len())) .with_file(Arc::from(FILE)); - let expected = r#" - | -2 | let x: u32 = Left( -3 | Right(0) -4 | ); - | ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either, _>`"#; - assert_eq!(&expected[1..], &error.to_string()); + let msg = "Cannot parse: Expected value of type `u32`, got `Either, _>`"; + let expected = format!( + "{RED_BOLD}error{RESET}: {msg}\n |\n2 | let x: u32 = Left(\n3 | Right(0)\n4 | );\n | ^^^^^^^^^^^^^^^^^^ {msg}" + ); + assert_eq!(expected, error.to_string()); } #[test] @@ -674,26 +775,23 @@ let x: u32 = Left( let error = Error::CannotParse("This span covers the entire file".to_string()) .with_span(Span::from(FILE)) .with_file(Arc::from(FILE)); - let expected = r#" - | -1 | let a1: List = None; -2 | let x: u32 = Left( -3 | Right(0) -4 | ); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#; - assert_eq!(&expected[1..], &error.to_string()); + let msg = "Cannot parse: This span covers the entire file"; + let expected = format!( + "{RED_BOLD}error{RESET}: {msg}\n |\n1 | let a1: List = None;\n2 | let x: u32 = Left(\n3 | Right(0)\n4 | );\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ {msg}" + ); + assert_eq!(expected, error.to_string()); } #[test] fn display_no_file() { let error = Error::CannotParse("This error has no file".to_string()) .with_span(Span::from(EMPTY_FILE)); - let expected = "Cannot parse: This error has no file"; - assert_eq!(&expected, &error.to_string()); + let expected = format!("{RED_BOLD}error{RESET}: Cannot parse: This error has no file"); + assert_eq!(expected, error.to_string()); let error = Error::CannotParse("This error has no file".to_string()).with_span(Span::new(5, 10)); - assert_eq!(&expected, &error.to_string()); + assert_eq!(expected, error.to_string()); } #[test] @@ -701,8 +799,9 @@ let x: u32 = Left( let error = Error::CannotParse("This error has an empty file".to_string()) .with_span(Span::from(EMPTY_FILE)) .with_file(Arc::from(EMPTY_FILE)); - let expected = "Cannot parse: This error has an empty file"; - assert_eq!(&expected, &error.to_string()); + let expected = + format!("{RED_BOLD}error{RESET}: Cannot parse: This error has an empty file"); + assert_eq!(expected, error.to_string()); } #[test] @@ -711,13 +810,11 @@ let x: u32 = Left( let error = Error::CannotParse("number too large to fit in target type".to_string()) .with_span(Span::new(21, 26)) .with_file(Arc::from(file)); - - let expected = r#" - | -1 | /*😀*/ let a: u8 = 65536; - | ^^^^^ Cannot parse: number too large to fit in target type"#; - + let msg = "Cannot parse: number too large to fit in target type"; + let expected = format!( + "{RED_BOLD}error{RESET}: {msg}\n |\n1 | /*😀*/ let a: u8 = 65536;\n | ^^^^^ {msg}" + ); println!("{error}"); - assert_eq!(&expected[1..], &error.to_string()); + assert_eq!(expected, error.to_string()); } } diff --git a/src/lib.rs b/src/lib.rs index 58083c06..f6af1854 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ pub mod str; pub mod tracker; pub mod types; pub mod value; +pub mod warning; mod witness; use std::sync::Arc; @@ -30,10 +31,12 @@ pub extern crate simplicity; pub use simplicity::elements; use crate::debug::DebugSymbols; -use crate::error::{ErrorCollector, WithFile}; +pub use crate::error::get_line_col; +use crate::error::{ErrorCollector, WithFile, WithFilePath}; use crate::parse::ParseFromStrWithErrors; pub use crate::types::ResolvedType; pub use crate::value::Value; +pub use crate::warning::{WarnCategory, Warning}; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; /// The template of a SimplicityHL program. @@ -43,6 +46,8 @@ pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; pub struct TemplateProgram { simfony: ast::Program, file: Arc, + file_path: Arc, + warnings: Vec, } impl TemplateProgram { @@ -52,20 +57,90 @@ impl TemplateProgram { /// /// The string is not a valid SimplicityHL program. pub fn new>>(s: Str) -> Result { + Self::new_with_path(s, "") + } + + /// Parse the template of a SimplicityHL program, attaching a file path for error display. + /// + /// `path` is shown on the ` --> ` line in error and warning output (e.g. `"src/main.simf"`). + /// + /// ## Errors + /// + /// The string is not a valid SimplicityHL program. + pub fn new_with_path>, Path: Into>>( + s: Str, + path: Path, + ) -> Result { let file = s.into(); - let mut error_handler = ErrorCollector::new(Arc::clone(&file)); + let file_path: Arc = path.into(); + let mut error_handler = + ErrorCollector::new_with_path(Arc::clone(&file), Arc::clone(&file_path)); let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); if let Some(program) = parse_program { - let ast_program = ast::Program::analyze(&program).with_file(Arc::clone(&file))?; + let (ast_program, warnings) = ast::Program::analyze(&program) + .with_file(Arc::clone(&file)) + .with_file_path(Arc::clone(&file_path))?; Ok(Self { simfony: ast_program, file, + file_path, + warnings, }) } else { Err(ErrorCollector::to_string(&error_handler))? } } + /// Treat all warnings as hard errors. + /// + /// Returns `self` unchanged if there are no warnings. + /// + /// ## Errors + /// + /// The program produced one or more warnings. + pub fn deny_warnings(self) -> Result { + if self.warnings.is_empty() { + return Ok(self); + } + let mut error_handler = + ErrorCollector::new_with_path(Arc::clone(&self.file), Arc::clone(&self.file_path)); + error_handler.update(self.warnings.iter().cloned().map(|w| w.into())); + Err(ErrorCollector::to_string(&error_handler)) + } + + /// Treat warnings of the given category as hard errors. + /// + /// Returns `self` unchanged if no warnings of that category exist. + /// + /// ## Errors + /// + /// The program produced one or more warnings in the given category. + pub fn deny_warning(self, category: WarnCategory) -> Result { + let matching: Vec<_> = self + .warnings + .iter() + .filter(|w| w.canonical_name.category() == category) + .cloned() + .collect(); + if matching.is_empty() { + return Ok(self); + } + let mut error_handler = + ErrorCollector::new_with_path(Arc::clone(&self.file), Arc::clone(&self.file_path)); + error_handler.update(matching.into_iter().map(|w| w.into())); + Err(ErrorCollector::to_string(&error_handler)) + } + + /// Silence warnings of the given category. + /// + /// The matching warnings are removed from [`TemplateProgram::warnings`] and will + /// not appear in [`format_warnings`](Self::format_warnings) output. + pub fn allow_warning(mut self, category: WarnCategory) -> Self { + self.warnings + .retain(|w| w.canonical_name.category() != category); + self + } + /// Access the parameters of the program. pub fn parameters(&self) -> &Parameters { self.simfony.parameters() @@ -76,6 +151,42 @@ impl TemplateProgram { self.simfony.witness_types() } + /// Access any warnings produced during compilation. + pub fn warnings(&self) -> &[Warning] { + &self.warnings + } + + /// Format warnings for display in rustc style, with source location and yellow color. + pub fn format_warnings(&self, file_path: &str) -> String { + use crate::error::SpanDisplay; + use std::fmt::Write as _; + + const YELLOW_BOLD: &str = "\x1b[1;33m"; + const RESET: &str = "\x1b[0m"; + + let mut out = String::new(); + for warning in &self.warnings { + let message = warning.canonical_name.to_string(); + let _ = writeln!(out, "{YELLOW_BOLD}warning{RESET}: {message}"); + + if !self.file.is_empty() { + let sd = SpanDisplay::new(&self.file, warning.span); + let _ = writeln!(out, " --> {file_path}:{}:{}", sd.start_line, sd.start_col); + let _ = write!(out, "{}", sd.lines_block); + let _ = write!(out, "{:width$} |", " ", width = sd.line_num_width); + let _ = write!(out, "{:width$}", " ", width = sd.underline_start); + let _ = writeln!( + out, + "{YELLOW_BOLD}{:^, /// Commitment Merkle Root (CMR) of the program, hex encoded. cmr: String, + /// Compiler warnings produced during compilation. + warnings: Vec, } impl fmt::Display for Output { @@ -83,6 +93,26 @@ fn main() -> Result<(), Box> { .action(ArgAction::SetTrue) .help("Additional ABI .simf contract types"), ) + .arg( + Arg::new("deny_warnings") + .long("deny-warnings") + .action(ArgAction::SetTrue) + .help("Treat warnings as errors"), + ) + .arg( + Arg::new("deny_warning") + .long("deny-warning") + .value_name("CATEGORY") + .action(ArgAction::Append) + .help("Treat warnings of a specific category as errors (unused-variable)"), + ) + .arg( + Arg::new("allow_warning") + .long("allow-warning") + .value_name("CATEGORY") + .action(ArgAction::Append) + .help("Silence warnings of a specific category (unused-variable)"), + ) }; let matches = command.get_matches(); @@ -113,7 +143,81 @@ fn main() -> Result<(), Box> { simplicityhl::Arguments::default() }; - let compiled = match CompiledProgram::new(prog_text, args_opt, include_debug_symbols) { + let deny_warnings = matches.get_flag("deny_warnings"); + + let deny_categories: Vec = matches + .get_many::("deny_warning") + .unwrap_or_default() + .map(|s| parse_warn_category(s)) + .collect::>() + .unwrap_or_else(|e| { + eprintln!("\x1b[1;31merror\x1b[0m: {e}"); + std::process::exit(1); + }); + + let allow_categories: Vec = matches + .get_many::("allow_warning") + .unwrap_or_default() + .map(|s| parse_warn_category(s)) + .collect::>() + .unwrap_or_else(|e| { + eprintln!("\x1b[1;31merror\x1b[0m: {e}"); + std::process::exit(1); + }); + + let template = match TemplateProgram::new_with_path(prog_text.clone(), prog_file.as_str()) { + Ok(t) => t, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; + + let template = allow_categories + .iter() + .fold(template, |t, &cat| t.allow_warning(cat)); + + let template = deny_categories + .iter() + .try_fold(template, |t, &cat| t.deny_warning(cat)) + .unwrap_or_else(|e| { + eprintln!("{e}"); + std::process::exit(1); + }); + + let n_warnings = template.warnings().len(); + let warning_strings: Vec = template + .warnings() + .iter() + .map(|w| { + let (line, column) = get_line_col(&prog_text, w.span.start); + OutputWarning { + message: w.canonical_name.to_string(), + file: prog_file.clone(), + line, + column, + } + }) + .collect(); + if n_warnings > 0 { + if !output_json { + eprint!("{}", template.format_warnings(prog_file)); + let word = if n_warnings == 1 { + "warning" + } else { + "warnings" + }; + eprintln!( + "\x1b[1;33mwarning\x1b[0m: `{}` generated {} {}", + prog_file, n_warnings, word, + ); + } + if deny_warnings { + eprintln!("\x1b[1;31merror\x1b[0m: warnings treated as errors (--deny-warnings)"); + std::process::exit(1); + } + } + let compiled = match template.instantiate(args_opt, include_debug_symbols) { Ok(program) => program, Err(e) => { eprintln!("{}", e); @@ -165,6 +269,7 @@ fn main() -> Result<(), Box> { witness: witness_bytes.map(|bytes| Base64Display::new(&bytes, &STANDARD).to_string()), abi_meta: abi_opt, cmr: cmr_hex, + warnings: warning_strings, }; if output_json { @@ -180,3 +285,12 @@ fn main() -> Result<(), Box> { Ok(()) } + +fn parse_warn_category(s: &str) -> Result { + match s { + "unused-variable" => Ok(WarnCategory::UnusedVariable), + other => Err(format!( + "unknown warning category `{other}`; valid categories: unused-variable" + )), + } +} diff --git a/src/parse.rs b/src/parse.rs index a6eebd18..75da44db 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -103,6 +103,7 @@ impl_eq_hash!(Function; name, params, ret, body); pub struct FunctionParam { identifier: Identifier, ty: AliasedType, + span: Span, } impl FunctionParam { @@ -115,6 +116,11 @@ impl FunctionParam { pub fn ty(&self) -> &AliasedType { &self.ty } + + /// Access the span of this parameter declaration. + pub fn span(&self) -> Span { + self.span + } } /// A statement is a component of a block expression. @@ -130,6 +136,7 @@ pub enum Statement { #[derive(Clone, Debug)] pub struct Assignment { pattern: Pattern, + pattern_span: Span, ty: AliasedType, expression: Expression, span: Span, @@ -141,6 +148,11 @@ impl Assignment { &self.pattern } + /// Access the span of just the pattern (not the full assignment). + pub fn pattern_span(&self) -> Span { + self.pattern_span + } + /// Access the return type of assigned expression. pub fn ty(&self) -> &AliasedType { &self.ty @@ -410,6 +422,8 @@ impl_eq_hash!(Match; scrutinee, left, right); #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct MatchArm { pattern: MatchPattern, + /// Span of the pattern, if it binds a variable (`Left`, `Right`, `Some`). + pattern_span: Option, expression: Arc, } @@ -419,6 +433,11 @@ impl MatchArm { &self.pattern } + /// Access the span of the binding pattern, if present. + pub fn pattern_span(&self) -> Option { + self.pattern_span + } + /// Access the expression that is executed in the match arm. pub fn expression(&self) -> &Expression { &self.expression @@ -1222,7 +1241,11 @@ impl ChumskyParse for FunctionParam { identifier .then_ignore(just(Token::Colon)) .then(ty) - .map(|(identifier, ty)| Self { identifier, ty }) + .map_with(|(identifier, ty), e| Self { + identifier, + ty, + span: e.span(), + }) } } @@ -1251,13 +1274,14 @@ impl Assignment { E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, { just(Token::Let) - .ignore_then(Pattern::parser()) + .ignore_then(Pattern::parser().map_with(|pat, e| (pat, e.span()))) .then_ignore(parse_token_with_recovery(Token::Colon)) .then(AliasedType::parser()) .then_ignore(parse_token_with_recovery(Token::Eq)) .then(expr) - .map_with(|((pattern, ty), expression), e| Self { + .map_with(|(((pattern, pattern_span), ty), expression), e| Self { pattern, + pattern_span, ty, expression, span: e.span(), @@ -1668,26 +1692,39 @@ impl MatchArm { E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, { MatchPattern::parser() + .map_with(|pat, e| { + let has_binding = matches!( + pat, + MatchPattern::Left(..) | MatchPattern::Right(..) | MatchPattern::Some(..) + ); + let pattern_span = if has_binding { Some(e.span()) } else { None }; + (pat, pattern_span) + }) .then_ignore(just(Token::FatArrow)) .then(expr.map(Arc::new)) .then(just(Token::Comma).or_not()) - .validate(|((pattern, expression), comma), e, emitter| { - let is_block = matches!(expression.as_ref().inner, ExpressionInner::Block(_, _)); - - if !is_block && comma.is_none() { - emitter.emit( - Error::Grammar( - "Missing ',' after a match arm that isn't block expression".to_string(), - ) - .with_span(e.span()), - ); - } + .validate( + |(((pattern, pattern_span), expression), comma), e, emitter| { + let is_block = + matches!(expression.as_ref().inner, ExpressionInner::Block(_, _)); + + if !is_block && comma.is_none() { + emitter.emit( + Error::Grammar( + "Missing ',' after a match arm that isn't block expression" + .to_string(), + ) + .with_span(e.span()), + ); + } - Self { - pattern, - expression, - } - }) + Self { + pattern, + pattern_span, + expression, + } + }, + ) } } @@ -1763,6 +1800,7 @@ impl Match { let match_arm_fallback = MatchArm { expression: Arc::new(Expression::empty(Span::new(0, 0))), pattern: MatchPattern::False, + pattern_span: None, }; let (left, right) = ( @@ -2031,6 +2069,7 @@ impl crate::ArbitraryRec for Assignment { Ok(Self { pattern, + pattern_span: Span::DUMMY, ty, expression, span: Span::DUMMY, @@ -2160,10 +2199,12 @@ impl crate::ArbitraryRec for Match { scrutinee, left: MatchArm { pattern: pat_l, + pattern_span: None, expression: expr_l, }, right: MatchArm { pattern: pat_r, + pattern_span: None, expression: expr_r, }, span: Span::DUMMY, diff --git a/src/warning.rs b/src/warning.rs new file mode 100644 index 00000000..489431f5 --- /dev/null +++ b/src/warning.rs @@ -0,0 +1,61 @@ +use std::fmt; + +use crate::error::{Error, RichError, Span}; +use crate::str::Identifier; + +/// Category of a warning, used for per-class allow/deny control. +/// +/// Unlike [`WarningName`], which carries instance-specific data for display, +/// `WarnCategory` is a unit enum that identifies the *class* of warning so that +/// callers can write `template.deny_warning(WarnCategory::UnusedVariable)` without +/// needing a concrete identifier. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum WarnCategory { + /// A variable was bound but never used. + UnusedVariable, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum WarningName { + UnusedVariable(Identifier), +} + +impl WarningName { + /// Return the category this warning belongs to. + pub fn category(&self) -> WarnCategory { + match self { + WarningName::UnusedVariable(_) => WarnCategory::UnusedVariable, + } + } +} + +impl fmt::Display for WarningName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WarningName::UnusedVariable(identifier) => write!(f, "unused variable: `{identifier}`. Prefix the variable name with `_` to silence this warning."), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Warning { + /// Canonical name used for allowing and denying specific warnings. + pub canonical_name: WarningName, + /// Span in which this warning occured. + pub span: Span, +} + +impl Warning { + pub(crate) fn variable_unused>(identifier: Identifier, span: S) -> Self { + Warning { + canonical_name: WarningName::UnusedVariable(identifier), + span: span.into(), + } + } +} + +impl From for RichError { + fn from(value: Warning) -> Self { + RichError::new(Error::DeniedWarning(value.canonical_name), value.span) + } +} diff --git a/tests/warnings.rs b/tests/warnings.rs new file mode 100644 index 00000000..8c37830f --- /dev/null +++ b/tests/warnings.rs @@ -0,0 +1,272 @@ +use simplicityhl::warning::WarningName; +use simplicityhl::{TemplateProgram, WarnCategory}; + +fn warning_names(prog_text: &str) -> Vec { + TemplateProgram::new(prog_text) + .expect("Program should compile") + .warnings() + .iter() + .map(|w| w.canonical_name.clone()) + .collect() +} + +#[test] +fn unused_variable_warns() { + let prog = r#"fn main() { + let (carry, sum): (bool, u8) = jet::add_8(2, 3); + assert!(jet::eq_8(sum, 5)) +}"#; + let names = warning_names(prog); + assert_eq!(names.len(), 1); + assert!( + matches!(&names[0], WarningName::UnusedVariable(id) if id.as_inner() == "carry"), + "Expected VariableUnused(carry), got: {:?}", + names, + ); +} + +#[test] +fn used_variable_no_warning() { + // Both carry and sum are used in the tuple expression. + let prog = r#"fn main() { + let (carry, sum): (bool, u8) = jet::add_8(2, 3); + let _: (bool, u8) = (carry, sum); +}"#; + assert!(warning_names(prog).is_empty()); +} + +#[test] +fn underscore_prefix_silences_warning() { + let prog = r#"fn main() { + let (_carry, sum): (bool, u8) = jet::add_8(2, 3); + assert!(jet::eq_8(sum, 5)) +}"#; + assert!(warning_names(prog).is_empty()); +} + +#[test] +fn ignore_pattern_no_warning() { + let prog = r#"fn main() { + let (_, sum): (bool, u8) = jet::add_8(2, 3); + assert!(jet::eq_8(sum, 5)) +}"#; + assert!(warning_names(prog).is_empty()); +} + +#[test] +fn multiple_unused_variables_warn() { + let prog = r#"fn main() { + let x: u8 = 1; + let y: u8 = 2; + assert!(jet::eq_8(0, 0)) +}"#; + let names = warning_names(prog); + let unused: Vec<&str> = names + .iter() + .map(|w| match w { + WarningName::UnusedVariable(id) => id.as_inner(), + }) + .collect(); + assert_eq!( + unused.len(), + 2, + "Expected 2 unused-variable warnings, got: {unused:?}" + ); + // Warnings are emitted in source order (sorted by span start). + assert_eq!(unused, ["x", "y"]); +} + +#[test] +fn variable_used_in_nested_block_no_warning() { + let prog = r#"fn main() { + let x: u8 = 1; + let y: u8 = { + x + }; + assert!(jet::eq_8(y, 1)) +}"#; + assert!(warning_names(prog).is_empty()); +} + +#[test] +fn deny_unused_variable_is_error() { + let prog = r#"fn main() { + let (carry, sum): (bool, u8) = jet::add_8(2, 3); + assert!(jet::eq_8(sum, 5)) +}"#; + assert!( + TemplateProgram::new(prog).unwrap().deny_warnings().is_err(), + "Expected compilation to fail with --deny-warnings", + ); +} + +#[test] +fn unused_match_arm_binding_warns() { + let prog = r#"fn main() { + let val: Either = Left(42); + match val { + Left(x: u32) => assert!(jet::eq_32(0, 0)), + Right(_: bool) => assert!(jet::eq_32(0, 0)), + } +}"#; + let names = warning_names(prog); + assert_eq!(names.len(), 1); + assert!( + matches!(&names[0], WarningName::UnusedVariable(id) if id.as_inner() == "x"), + "Expected VariableUnused(x), got: {:?}", + names, + ); +} + +#[test] +fn used_match_arm_binding_no_warning() { + let prog = r#"fn main() { + let val: Either = Left(42); + match val { + Left(x: u32) => assert!(jet::eq_32(x, 42)), + Right(_: bool) => assert!(jet::eq_32(0, 0)), + } +}"#; + assert!(warning_names(prog).is_empty()); +} + +#[test] +fn underscore_prefix_silences_match_arm_warning() { + let prog = r#"fn main() { + let val: Either = Left(42); + match val { + Left(_x: u32) => assert!(jet::eq_32(0, 0)), + Right(_: bool) => assert!(jet::eq_32(0, 0)), + } +}"#; + assert!(warning_names(prog).is_empty()); +} + +#[test] +fn unused_function_param_warns() { + let prog = r#"fn always_zero(x: u32) -> u32 { + 0 +} + +fn main() { + assert!(jet::eq_32(always_zero(42), 0)) +}"#; + let names = warning_names(prog); + assert_eq!(names.len(), 1); + assert!( + matches!(&names[0], WarningName::UnusedVariable(id) if id.as_inner() == "x"), + "Expected VariableUnused(x), got: {:?}", + names, + ); +} + +#[test] +fn used_function_param_no_warning() { + let prog = r#"fn identity(x: u32) -> u32 { + x +} + +fn main() { + assert!(jet::eq_32(identity(42), 42)) +}"#; + assert!(warning_names(prog).is_empty()); +} + +#[test] +fn shadowed_outer_variable_unused_warns() { + // Outer `x` is never referenced; inner `x` shadows it and is used. + // Only the outer binding should warn. + let prog = r#"fn main() { + let x: u8 = 1; + let y: u8 = { + let x: u8 = 2; + x + }; + assert!(jet::eq_8(y, 2)) +}"#; + let names = warning_names(prog); + assert_eq!( + names.len(), + 1, + "Expected exactly one warning (outer x), got: {names:?}", + ); + assert!( + matches!(&names[0], WarningName::UnusedVariable(id) if id.as_inner() == "x"), + "Expected UnusedVariable(x) for the outer binding, got: {:?}", + names, + ); +} + +#[test] +fn shadowed_inner_variable_unused_warns() { + // Outer `x` is used as the RHS of the inner binding. + // Inner `x` is never referenced after being bound, so it should warn. + let prog = r#"fn main() { + let x: u8 = 1; + let y: u8 = { + let x: u8 = x; + 0 + }; + assert!(jet::eq_8(y, 0)) +}"#; + let names = warning_names(prog); + assert_eq!( + names.len(), + 1, + "Expected exactly one warning (inner x), got: {names:?}", + ); + assert!( + matches!(&names[0], WarningName::UnusedVariable(id) if id.as_inner() == "x"), + "Expected UnusedVariable(x) for the inner binding, got: {:?}", + names, + ); +} + +#[test] +fn outer_variable_used_after_inner_scope_no_warning() { + // `x` is bound in the outer scope, referenced after an inner block + // that binds a different name. Neither binding should warn. + let prog = r#"fn main() { + let x: u8 = 1; + let y: u8 = { + let z: u8 = 2; + z + }; + let (_, sum): (bool, u8) = jet::add_8(x, y); + assert!(jet::eq_8(sum, 3)) +}"#; + assert!( + warning_names(prog).is_empty(), + "Expected no warnings when all variables are used", + ); +} + +#[test] +fn deny_warning_by_category_is_error() { + let prog = r#"fn main() { + let (carry, sum): (bool, u8) = jet::add_8(2, 3); + assert!(jet::eq_8(sum, 5)) +}"#; + assert!( + TemplateProgram::new(prog) + .unwrap() + .deny_warning(WarnCategory::UnusedVariable) + .is_err(), + "deny_warning(UnusedVariable) should fail when there is an unused variable", + ); +} + +#[test] +fn allow_warning_by_category_suppresses_it() { + let prog = r#"fn main() { + let (carry, sum): (bool, u8) = jet::add_8(2, 3); + assert!(jet::eq_8(sum, 5)) +}"#; + let template = TemplateProgram::new(prog) + .unwrap() + .allow_warning(WarnCategory::UnusedVariable); + assert!( + template.warnings().is_empty(), + "allow_warning(UnusedVariable) should remove the warning", + ); +}