diff --git a/run_examples.ps1 b/run_examples.ps1 new file mode 100644 index 0000000..194e183 --- /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 0000000..48c4616 --- /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 3c59f2f..fd7e658 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,5 +1,6 @@ use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::fmt; use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::Arc; @@ -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,27 +564,43 @@ 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. @@ -592,6 +615,34 @@ impl Scope { .insert(identifier, ty); } + /// Bind a variable from a `let` assignment pattern, tracking its span for unused-variable warnings. + /// + /// ## Panics + /// + /// The stack is empty. + pub fn bind_variable(&mut self, identifier: Identifier, ty: ResolvedType, span: Span) { + self.insert_variable(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 @@ -704,6 +755,42 @@ impl Scope { } } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum WarningName { + VariableUnused(Identifier), +} + +impl fmt::Display for WarningName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WarningName::VariableUnused(identifier) => write!(f, "Variable `{identifier}` is bound but never used. 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 { + fn variable_unused>(identifier: Identifier, span: S) -> Self { + Warning { + canonical_name: WarningName::VariableUnused(identifier), + span: span.into(), + } + } +} + +impl From for RichError { + fn from(value: Warning) -> Self { + RichError::new(Error::DeniedWarning(value.canonical_name), value.span) + } +} + /// Part of the abstract syntax tree that can be generated from a precursor in the parse tree. trait AbstractSyntaxTree: Sized { /// Component of the parse tree. @@ -714,41 +801,63 @@ 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); + match item { + Item::Function(Function::Main(expr)) => { + 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 +866,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 +878,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"); @@ -795,15 +907,16 @@ impl AbstractSyntaxTree for Function { for param in params.iter() { scope.insert_variable(param.identifier().clone(), param.ty().clone()); } - 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 +930,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 +957,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 assignment_span = *from.as_ref(); for (identifier, ty) in typed_variables { - scope.insert_variable(identifier, ty); + scope.bind_variable(identifier, ty, assignment_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 +997,60 @@ 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) + 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 +1059,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 +1073,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 +1094,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 +1124,14 @@ impl AbstractSyntaxTree for SingleExpression { return Err(Error::ExpressionTypeMismatch(ty.clone(), bound_ty.clone())) .with_span(from); } + scope.mark_variable_used(identifier); scope.insert_variable(identifier.clone(), ty.clone()); - SingleExpressionInner::Variable(identifier.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 +1141,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 +1161,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 +1180,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 +1198,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 +1210,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 +1273,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 +1300,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 +1316,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 +1327,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 +1345,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 +1374,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 +1388,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 +1410,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 +1432,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 +1459,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 +1484,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,9 +1526,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) + return Err(Error::FunctionNotFoldable(name.clone())).with_span(from); } else { - Ok(Self::ArrayFold(function, *size)) + Self::ArrayFold(function, *size) } } parse::CallName::Fold(name, bound) => { @@ -1348,9 +1541,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) + return Err(Error::FunctionNotFoldable(name.clone())).with_span(from); } else { - Ok(Self::Fold(function, *bound)) + Self::Fold(function, *bound) } } parse::CallName::ForWhile(name) => { @@ -1382,50 +1575,65 @@ 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((id_l, ty_l)) = from.left().pattern().as_typed_variable() { let ty_l = scope.resolve(ty_l).with_span(from)?; scope.insert_variable(id_l.clone(), ty_l); } - let ast_l = Expression::analyze(from.left().expression(), ty, scope).map(Arc::new)?; + 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); scope.pop_scope(); scope.push_scope(); if let Some((id_r, ty_r)) = from.right().pattern().as_typed_variable() { let ty_r = scope.resolve(ty_r).with_span(from)?; scope.insert_variable(id_r.clone(), ty_r); } - let ast_r = Expression::analyze(from.right().expression(), ty, scope).map(Arc::new)?; + 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); scope.pop_scope(); - Ok(Self { - scrutinee, - left: MatchArm { - pattern: from.left().pattern().clone(), - expression: ast_l, + 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(), }, - right: MatchArm { - pattern: from.right().pattern().clone(), - expression: ast_r, - }, - span: *from.as_ref(), - }) + all_warnings, + )) } } @@ -1435,16 +1643,18 @@ 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, - }); + 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 }; @@ -1480,14 +1690,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)), } } } @@ -1495,40 +1708,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 1ded6fd..7b614b1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,6 +11,7 @@ use chumsky::DefaultExpected; use itertools::Itertools; use simplicity::elements; +use crate::ast::WarningName; use crate::lexer::Token; use crate::parse::MatchPattern; use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName}; @@ -189,25 +190,25 @@ 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 last_newline_offset = 0; - - let slice = file.get(0..offset).unwrap_or_default(); +pub(crate) fn get_line_col(file: &str, offset: usize) -> (usize, usize) { + let mut line = 1; + let mut last_newline_offset = 0; - for (i, byte) in slice.bytes().enumerate() { - if byte == b'\n' { - line += 1; - last_newline_offset = i; - } - } + let slice = file.get(0..offset).unwrap_or_default(); - let col = (offset - last_newline_offset) + 1; - (line, col) + for (i, byte) in slice.bytes().enumerate() { + if byte == b'\n' { + line += 1; + last_newline_offset = i; } + } + let col = (offset - last_newline_offset) + 1; + (line, col) +} + +impl fmt::Display for RichError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.file { Some(ref file) if !file.is_empty() => { let (start_line, start_col) = get_line_col(file, self.span.start); @@ -438,6 +439,7 @@ pub enum Error { ModuleRedefined(ModuleName), ArgumentMissing(WitnessName), ArgumentTypeMismatch(WitnessName, ResolvedType, ResolvedType), + DeniedWarning(WarningName), } #[rustfmt::skip] @@ -587,6 +589,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}" + ), } } } diff --git a/src/lib.rs b/src/lib.rs index b4a1032..0e245d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ pub extern crate either; pub extern crate simplicity; pub use simplicity::elements; +use crate::ast::Warning; use crate::debug::DebugSymbols; use crate::error::{ErrorCollector, WithFile}; use crate::parse::ParseFromStrWithErrors; @@ -43,6 +44,7 @@ pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; pub struct TemplateProgram { simfony: ast::Program, file: Arc, + warnings: Vec, } impl TemplateProgram { @@ -51,16 +53,24 @@ impl TemplateProgram { /// ## Errors /// /// The string is not a valid SimplicityHL program. - pub fn new>>(s: Str) -> Result { + pub fn new>>(s: Str, deny_all_warnings: bool) -> Result { let file = s.into(); let mut error_handler = ErrorCollector::new(Arc::clone(&file)); 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))?; - Ok(Self { - simfony: ast_program, - file, - }) + let (ast_program, warnings) = + ast::Program::analyze(&program).with_file(Arc::clone(&file))?; + + if !warnings.is_empty() && deny_all_warnings { + error_handler.update(warnings.into_iter().map(|w| w.into())); + Err(ErrorCollector::to_string(&error_handler))? + } else { + Ok(Self { + simfony: ast_program, + file, + warnings, + }) + } } else { Err(ErrorCollector::to_string(&error_handler))? } @@ -76,6 +86,62 @@ 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::get_line_col; + 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 (start_line, start_col) = get_line_col(&self.file, warning.span.start); + let (end_line, end_col) = get_line_col(&self.file, warning.span.end); + let _ = writeln!(out, " --> {file_path}:{start_line}:{start_col}"); + + 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 _ = writeln!(out, "{:width$} |", " ", width = line_num_width); + let mut lines = self.file.lines().skip(start_line_index).peekable(); + let start_line_len = lines.peek().map_or(0, |l| l.len()); + + for (i, line_str) in lines.take(n_spanned_lines).enumerate() { + let line_num = start_line_index + i + 1; + let _ = writeln!(out, "{line_num:line_num_width$} | {line_str}"); + } + + 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 _ = write!(out, "{:width$} |", " ", width = line_num_width); + let _ = write!(out, "{:width$}", " ", width = underline_start); + let _ = writeln!( + out, + "{YELLOW_BOLD}{:^ Result { - TemplateProgram::new(s) + TemplateProgram::new(s, deny_all_warnings) .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } @@ -217,8 +284,9 @@ impl SatisfiedProgram { arguments: Arguments, witness_values: WitnessValues, include_debug_symbols: bool, + deny_all_warnings: bool, ) -> Result { - let compiled = CompiledProgram::new(s, arguments, include_debug_symbols)?; + let compiled = CompiledProgram::new(s, arguments, include_debug_symbols, deny_all_warnings)?; compiled.satisfy(witness_values) } @@ -321,7 +389,7 @@ pub(crate) mod tests { } pub fn template_text(program_text: Cow) -> Self { - let program = match TemplateProgram::new(program_text.as_ref()) { + let program = match TemplateProgram::new(program_text.as_ref(), false) { Ok(x) => x, Err(error) => panic!("{error}"), }; @@ -662,6 +730,7 @@ fn main() { Arguments::default(), WitnessValues::default(), false, + false, ) { Ok(_) => panic!("Accepted faulty program"), Err(error) => { @@ -711,6 +780,107 @@ fn main() { .assert_run_success(); } + mod warnings { + use crate::ast::WarningName; + use crate::TemplateProgram; + + fn warning_names(prog_text: &str) -> Vec { + TemplateProgram::new(prog_text, false) + .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::VariableUnused(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() + .filter_map(|w| match w { + WarningName::VariableUnused(id) => Some(id.as_inner()), + _ => None, + }) + .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, true).is_err(), + "Expected compilation to fail with --deny-warnings", + ); + } + } + #[cfg(feature = "serde")] mod regression { use super::TestCase; diff --git a/src/main.rs b/src/main.rs index 5cb7b57..a54d001 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use clap::{Arg, ArgAction, Command}; -use simplicityhl::{AbiMeta, CompiledProgram}; +use simplicityhl::{AbiMeta, TemplateProgram}; use std::{env, fmt}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] @@ -83,6 +83,12 @@ 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"), + ) }; let matches = command.get_matches(); @@ -113,7 +119,25 @@ 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 template = match TemplateProgram::new(prog_text, deny_warnings) { + Ok(t) => t, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; + let n_warnings = template.warnings().len(); + if n_warnings > 0 { + 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, + ); + } + let compiled = match template.instantiate(args_opt, include_debug_symbols) { Ok(program) => program, Err(e) => { eprintln!("{}", e); diff --git a/src/tracker.rs b/src/tracker.rs index 4a6f693..cd878aa 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -472,7 +472,7 @@ mod tests { #[test] fn test_debug_and_jet_tracing() { - let program = TemplateProgram::new(TEST_PROGRAM).unwrap(); + let program = TemplateProgram::new(TEST_PROGRAM, false).unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); @@ -541,7 +541,7 @@ mod tests { fn test_arith_jet_trace_regression() { let env = create_test_env(); - let program = TemplateProgram::new(TEST_ARITHMETIC_JETS).unwrap(); + let program = TemplateProgram::new(TEST_ARITHMETIC_JETS, false).unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); diff --git a/src/witness.rs b/src/witness.rs index 6d6ffef..eec4bf3 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -247,7 +247,7 @@ mod tests { WitnessName::from_str_unchecked("A"), Value::u16(42), )])); - match SatisfiedProgram::new(s, Arguments::default(), witness, false) { + match SatisfiedProgram::new(s, Arguments::default(), witness, false, false) { Ok(_) => panic!("Ill-typed witness assignment was falsely accepted"), Err(error) => assert_eq!( "Witness `A` was declared with type `u32` but its assigned value is of type `u16`", @@ -266,7 +266,7 @@ fn main() { assert!(jet::is_zero_32(f())); }"#; - match CompiledProgram::new(s, Arguments::default(), false) { + match CompiledProgram::new(s, Arguments::default(), false, false) { Ok(_) => panic!("Witness outside main was falsely accepted"), Err(error) => { assert!(error