Skip to content

Commit d1b52b0

Browse files
committed
Merge #378: Add multi-error support to is_consistent and print colored errors in main.rs
38a4531 feat: add errors with color and multierrors to is_consistent funcs (LesterEvSe) Pull request description: - Output errors to `stderr` with color support in `main.rs`. - Add multi-error support to `Arguments` and `Witness`. ACKs for top commit: KyrylR: ACK 38a4531; successfully ran local tests Tree-SHA512: a88de0c45fd79760cde9a5cfef55f72eca696b229687721d9de90e568bcfcf595116ffdb3f0e90219ec46d2a347a7104854d8f6adc710cdccd5bf18550101385
2 parents bf2fd43 + 38a4531 commit d1b52b0

5 files changed

Lines changed: 115 additions & 34 deletions

File tree

src/error.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::collections::hash_map::Entry;
22
use std::collections::HashMap;
3+
use std::ffi::OsStr;
34
use std::fmt;
4-
use std::io::{self, Write};
5+
use std::io::{self, IsTerminal, Write};
56
use std::ops::Range;
67
use std::path::PathBuf;
78
use std::sync::Arc;
@@ -380,6 +381,8 @@ where
380381
}
381382
}
382383

384+
/// Collects diagnostics emitted during a single compilation and renders
385+
/// them against the source files they refer to.
383386
#[derive(Debug, Clone, Default)]
384387
pub struct DiagnosticManager {
385388
diags: Vec<Diagnostic>,
@@ -520,6 +523,36 @@ impl<'a> Cache<usize> for RenderCache<'a> {
520523
}
521524
}
522525

526+
/// Pure color-decision logic.
527+
fn decide_color(clicolor_force: Option<&OsStr>, no_color: Option<&OsStr>, is_tty: bool) -> bool {
528+
let zero = OsStr::new("0");
529+
530+
if let Some(v) = clicolor_force {
531+
if !v.is_empty() && v != zero {
532+
return true;
533+
}
534+
}
535+
if let Some(v) = no_color {
536+
if !v.is_empty() {
537+
return false;
538+
}
539+
}
540+
is_tty
541+
}
542+
543+
/// Whether the given stream should be rendered with ANSI color.
544+
///
545+
/// Precedence: `CLICOLOR_FORCE` (non-empty, non-`"0"`) then force on;
546+
/// `NO_COLOR` (non-empty, per <https://no-color.org>) then force off;
547+
/// otherwise follow the stream's TTY status.
548+
pub fn should_color<S: IsTerminal>(stream: &S) -> bool {
549+
decide_color(
550+
std::env::var_os("CLICOLOR_FORCE").as_deref(),
551+
std::env::var_os("NO_COLOR").as_deref(),
552+
stream.is_terminal(),
553+
)
554+
}
555+
523556
fn render_one(
524557
diag: &Diagnostic,
525558
cache: &mut RenderCache,
@@ -824,6 +857,9 @@ pub enum Error {
824857
WitnessReused {
825858
name: WitnessName,
826859
},
860+
WitnessMissing {
861+
name: WitnessName,
862+
},
827863
WitnessTypeMismatch {
828864
name: WitnessName,
829865
declared: ResolvedType,
@@ -1075,6 +1111,10 @@ impl fmt::Display for Error {
10751111
f,
10761112
"Witness `{name}` has been used before somewhere in the program"
10771113
),
1114+
Error::WitnessMissing { name } => write!(
1115+
f,
1116+
"Missing witness for `{name}`"
1117+
),
10781118
Error::WitnessTypeMismatch { name, declared, assigned } => write!(
10791119
f,
10801120
"Witness `{name}` was declared with type `{declared}` but its assigned value is of type `{assigned}`"
@@ -1189,6 +1229,7 @@ mod render_tests {
11891229
use crate::source::CanonSourceFile;
11901230
use crate::test_utils::TempWorkspace;
11911231

1232+
use std::ffi::OsStr;
11921233
use std::sync::Arc;
11931234

11941235
const CONTENT: &str = "let a1: List<u32, 5> = None;\nlet x: u32 = Left(\n Right(0)\n);";
@@ -1247,6 +1288,16 @@ mod render_tests {
12471288
Span::new(MAIN_MODULE, range)
12481289
}
12491290

1291+
#[test]
1292+
fn clicolor_force_zero_does_not_force() {
1293+
// Regression case: CLICOLOR_FORCE=0 with non-TTY stderr must not emit escapes.
1294+
assert!(!decide_color(Some(OsStr::new("0")), None, false));
1295+
assert!(decide_color(Some(OsStr::new("1")), None, false));
1296+
assert!(!decide_color(None, Some(OsStr::new("1")), true));
1297+
assert!(decide_color(None, None, true));
1298+
assert!(!decide_color(None, None, false));
1299+
}
1300+
12501301
#[test]
12511302
fn golden_full_diagnostic() {
12521303
let mut fixture = Fixture::new(CONTENT);

src/lib.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,14 @@ impl TemplateProgram {
202202
arguments: Arguments,
203203
include_debug_symbols: bool,
204204
) -> Result<CompiledProgram, String> {
205-
arguments
206-
.is_consistent(self.simfony.parameters())
207-
.map_err(|error| error.to_string())?;
205+
// This function returns Result<_, String> and its neighbors do not carry a
206+
// DiagnosticManager, so we mint a local one to collect all witness mismatches,
207+
// then render it to a message on failure.
208+
let mut diagnostics = DiagnosticManager::new();
209+
arguments.is_consistent(self.simfony.parameters(), &mut diagnostics);
210+
if diagnostics.has_errors() {
211+
return Err(diagnostics.to_string());
212+
}
208213

209214
let commit = self.simfony.compile(
210215
arguments,
@@ -352,9 +357,14 @@ impl CompiledProgram {
352357
witness_values: WitnessValues,
353358
env: Option<&ElementsEnv<Arc<elements::Transaction>>>,
354359
) -> Result<SatisfiedProgram, String> {
355-
witness_values
356-
.is_consistent(&self.witness_types)
357-
.map_err(|e| e.to_string())?;
360+
// This function returns Result<_, String> and its neighbors do not carry a
361+
// DiagnosticManager, so we mint a local one to collect all witness mismatches,
362+
// then render it to a message on failure.
363+
let mut diagnostics = DiagnosticManager::new();
364+
witness_values.is_consistent(&self.witness_types, &mut diagnostics);
365+
if diagnostics.has_errors() {
366+
return Err(diagnostics.to_string());
367+
}
358368

359369
let mut simplicity_redeem = named::populate_witnesses(&self.simplicity, witness_values)?;
360370
if let Some(env) = env {

src/main.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ use base64::engine::general_purpose::STANDARD;
33
use clap::{Arg, ArgAction, Command};
44

55
use simplicityhl::ast::ElementsJetHinter;
6+
use simplicityhl::error::should_color;
67
use simplicityhl::version::SimcDirective;
78
use simplicityhl::{
89
resolution::DependencyMapBuilder, source::CanonPath, source::CanonSourceFile, AbiMeta,
910
TemplateProgram,
1011
};
1112
use simplicityhl::{UnstableFeature, UnstableFeatures};
1213
use std::path::Path;
13-
use std::{env, fmt};
14+
use std::{env, fmt, io};
1415

1516
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1617
/// The compilation output.
@@ -203,10 +204,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
203204
Box::new(ElementsJetHinter::new()),
204205
) {
205206
Ok(program) => program,
206-
Err(e) => {
207-
// `Display` is message-only; render for source snippets with
208-
// file and line pointers, as the single-file path does.
209-
eprintln!("{}", e.render_to_string());
207+
Err(diags) => {
208+
let stderr = io::stderr();
209+
let with_color = should_color(&stderr);
210+
let mut lock = stderr.lock();
211+
212+
diags
213+
.render(with_color, &mut lock)
214+
.expect("writing to stderr");
210215
std::process::exit(1);
211216
}
212217
};
@@ -222,7 +227,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
222227
let compiled = match template.instantiate(args_opt, include_debug_symbols) {
223228
Ok(program) => program,
224229
Err(e) => {
225-
eprintln!("{}", e);
230+
eprintln!("{e}");
226231
std::process::exit(1);
227232
}
228233
};
@@ -250,7 +255,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
250255

251256
let (program_bytes, witness_bytes) = match witness_opt {
252257
Some(witness) => {
253-
let satisfied = compiled.satisfy(witness)?;
258+
let satisfied = match compiled.satisfy(witness) {
259+
Ok(s) => s,
260+
Err(e) => {
261+
eprintln!("{e}");
262+
std::process::exit(1);
263+
}
264+
};
254265
let (program_bytes, witness_bytes) = satisfied.redeem().to_vec_with_witness();
255266
(program_bytes, Some(witness_bytes))
256267
}

src/named.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,12 @@ pub fn populate_witnesses(
210210
) -> Result<simplicity::Value, Self::Error> {
211211
match self.values.get(witness) {
212212
Some(val) => Ok(simplicity::Value::from(StructuralValue::from(val))),
213-
None => Err(format!("missing witness for {witness}")),
213+
// Presence is validated by `WitnessValues::is_consistent` before this
214+
// runs; reaching this arm means the caller skipped validation.
215+
None => Err(format!(
216+
"internal error: witness `{witness}` missing; \
217+
call WitnessValues::is_consistent before populate_witnesses"
218+
)),
214219
}
215220
}
216221

src/witness.rs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::HashMap;
22
use std::fmt;
33
use std::sync::Arc;
44

5-
use crate::error::{Diagnostic, Error, WithSpan};
5+
use crate::error::{Diagnostic, DiagnosticManager, Error, WithSpan};
66
use crate::parse::ParseFromStr;
77
use crate::str::WitnessName;
88
use crate::types::{AliasedType, ResolvedType};
@@ -110,23 +110,25 @@ impl WitnessValues {
110110
/// There may be witnesses that are referenced in the program that are not assigned a value
111111
/// in the witness map. These witnesses may lie on pruned branches that will not be part of the
112112
/// finalized Simplicity program. However, before the finalization, we cannot know which
113-
/// witnesses will be pruned and which won't be pruned. This check skips unassigned witnesses.
114-
pub fn is_consistent(&self, witness_types: &WitnessTypes) -> Result<(), Error> {
115-
for name in self.0.keys() {
116-
let Some(declared_ty) = witness_types.get(name) else {
113+
/// witnesses will be pruned and which won't be pruned.
114+
pub fn is_consistent(&self, witness_types: &WitnessTypes, diagnostics: &mut DiagnosticManager) {
115+
for (name, declared_ty) in witness_types.iter() {
116+
let Some(value) = self.get(name) else {
117+
diagnostics.push(Diagnostic::global(Error::WitnessMissing {
118+
name: name.shallow_clone(),
119+
}));
117120
continue;
118121
};
119-
let assigned_ty = self.0[name].ty();
122+
123+
let assigned_ty = value.ty();
120124
if assigned_ty != declared_ty {
121-
return Err(Error::WitnessTypeMismatch {
125+
diagnostics.push(Diagnostic::global(Error::WitnessTypeMismatch {
122126
name: name.clone(),
123127
declared: declared_ty.clone(),
124128
assigned: assigned_ty.clone(),
125-
});
129+
}));
126130
}
127131
}
128-
129-
Ok(())
130132
}
131133
}
132134

@@ -234,21 +236,23 @@ impl Arguments {
234236
/// 2. The type of each parameter must match the type of its argument.
235237
///
236238
/// Arguments without a corresponding parameter are ignored.
237-
pub fn is_consistent(&self, parameters: &Parameters) -> Result<(), Error> {
239+
pub fn is_consistent(&self, parameters: &Parameters, diagnostics: &mut DiagnosticManager) {
238240
for (name, parameter_ty) in parameters.iter() {
239-
let argument = self.get(name).ok_or_else(|| Error::ArgumentMissing {
240-
name: name.shallow_clone(),
241-
})?;
241+
let Some(argument) = self.get(name) else {
242+
diagnostics.push(Diagnostic::global(Error::ArgumentMissing {
243+
name: name.shallow_clone(),
244+
}));
245+
continue;
246+
};
247+
242248
if !argument.is_of_type(parameter_ty) {
243-
return Err(Error::ArgumentTypeMismatch {
249+
diagnostics.push(Diagnostic::global(Error::ArgumentTypeMismatch {
244250
name: name.clone(),
245251
declared: parameter_ty.clone(),
246252
assigned: argument.ty().clone(),
247-
});
253+
}));
248254
}
249255
}
250-
251-
Ok(())
252256
}
253257
}
254258

@@ -313,7 +317,7 @@ mod tests {
313317
) {
314318
Ok(_) => panic!("Ill-typed witness assignment was falsely accepted"),
315319
Err(error) => assert_eq!(
316-
"Witness `A` was declared with type `u32` but its assigned value is of type `u16`",
320+
"Witness `A` was declared with type `u32` but its assigned value is of type `u16`\n",
317321
error
318322
),
319323
}

0 commit comments

Comments
 (0)