diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 0627edb390..3d129a48eb 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -730,6 +730,16 @@ pub fn build( Ok(problems.exit_code()) } BuildAndRun => { + if problems.fatally_errored { + problems.print_to_stdout(total_time); + println!( + ".\n\nCannot run program due to fatal error…\n\n\x1B[36m{}\x1B[39m", + "─".repeat(80) + ); + + // Return a nonzero exit code due to falta problem + return Ok(problems.exit_code()); + } if problems.errors > 0 || problems.warnings > 0 { problems.print_to_stdout(total_time); println!( diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs index e5b8583a96..da8330420b 100644 --- a/crates/compiler/can/src/expr.rs +++ b/crates/compiler/can/src/expr.rs @@ -735,28 +735,54 @@ pub fn canonicalize_expr<'a>( ast::Expr::Str(literal) => flatten_str_literal(env, var_store, scope, literal), - ast::Expr::IngestedFile(file_path, type_ann) => { - let mut file = File::open(file_path).expect("file should exist due to earlier check. In the rare case the file got deleted, we should create a can error here too."); - let mut bytes = vec![]; - match file.read_to_end(&mut bytes) { - Ok(_) => ( - Expr::IngestedFile( - bytes, - annotation::canonicalize_annotation( - env, - scope, - &type_ann.value, - region, - var_store, - &VecMap::default(), - annotation::AnnotationFor::Value, + ast::Expr::IngestedFile(file_path, type_ann) => match File::open(file_path) { + Ok(mut file) => { + let mut bytes = vec![]; + match file.read_to_end(&mut bytes) { + Ok(_) => ( + Expr::IngestedFile( + bytes, + annotation::canonicalize_annotation( + env, + scope, + &type_ann.value, + region, + var_store, + &VecMap::default(), + annotation::AnnotationFor::Value, + ), ), + Output::default(), ), - Output::default(), - ), - Err(e) => todo!("failed to load file emit can error: {}", e), + Err(e) => { + env.problems.push(Problem::FileProblem { + filename: file_path.to_path_buf(), + error: e.kind(), + }); + + // This will not manifest as a real runtime error and is just returned to have a value here. + // The pushed FileProblem will be fatal to compilation. + ( + Expr::RuntimeError(roc_problem::can::RuntimeError::NoImplementation), + Output::default(), + ) + } + } } - } + Err(e) => { + env.problems.push(Problem::FileProblem { + filename: file_path.to_path_buf(), + error: e.kind(), + }); + + // This will not manifest as a real runtime error and is just returned to have a value here. + // The pushed FileProblem will be fatal to compilation. + ( + Expr::RuntimeError(roc_problem::can::RuntimeError::NoImplementation), + Output::default(), + ) + } + }, ast::Expr::SingleQuote(string) => { let mut it = string.chars().peekable(); diff --git a/crates/compiler/load_internal/src/file.rs b/crates/compiler/load_internal/src/file.rs index 9ec2b21af6..b0e71d319f 100644 --- a/crates/compiler/load_internal/src/file.rs +++ b/crates/compiler/load_internal/src/file.rs @@ -51,7 +51,7 @@ use roc_parse::module::module_defs; use roc_parse::parser::{FileError, Parser, SourceError, SyntaxError}; use roc_problem::Severity; use roc_region::all::{LineInfo, Loc, Region}; -use roc_reporting::report::{Annotation, Palette, RenderTarget}; +use roc_reporting::report::{to_file_problem_report_string, Palette, RenderTarget}; use roc_solve::module::{extract_module_owned_implementations, Solved, SolvedModule}; use roc_solve_problem::TypeError; use roc_target::TargetInfo; @@ -1791,7 +1791,7 @@ fn state_thread_step<'a>( Ok(ControlFlow::Break(LoadResult::Monomorphized(monomorphized))) } Msg::FailedToReadFile { filename, error } => { - let buf = to_file_problem_report(&filename, error); + let buf = to_file_problem_report_string(&filename, error); Err(LoadingProblem::FormattedReport(buf)) } @@ -1939,7 +1939,9 @@ pub fn report_loading_problem( ) } LoadingProblem::FormattedReport(report) => report, - LoadingProblem::FileProblem { filename, error } => to_file_problem_report(&filename, error), + LoadingProblem::FileProblem { filename, error } => { + to_file_problem_report_string(&filename, error) + } err => todo!("Loading error: {:?}", err), } } @@ -5688,15 +5690,19 @@ fn value_def_from_imports<'a>( let value = match entry.value { Module(_, _) => None, Package(_, _, _) => None, - IngestedFile(file_name, typed_ident) => { - let file_path = if let StrLiteral::PlainLine(filename) = file_name { - let file_path = header_path.to_path_buf().with_file_name(filename); + IngestedFile(ingested_path, typed_ident) => { + let file_path = if let StrLiteral::PlainLine(ingested_path) = ingested_path { + let mut file_path = header_path.to_path_buf(); + // Remove the header file name and push the new path. + file_path.pop(); + file_path.push(ingested_path); + match fs::metadata(&file_path) { Ok(md) => { - if !md.is_file() { - // TODO: is there a better loading problem to return when not a file. + if md.is_dir() { return Err(LoadingProblem::FileProblem { filename: file_path, + // TODO: change to IsADirectory once that is stable. error: io::ErrorKind::InvalidInput, }); } @@ -6551,87 +6557,6 @@ fn run_task<'a>( Ok(()) } -fn to_file_problem_report(filename: &Path, error: io::ErrorKind) -> String { - use roc_reporting::report::{Report, RocDocAllocator, DEFAULT_PALETTE}; - use ven_pretty::DocAllocator; - - let src_lines: Vec<&str> = Vec::new(); - - let mut module_ids = ModuleIds::default(); - - let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); - - let interns = Interns::default(); - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); - - let report = match error { - io::ErrorKind::NotFound => { - let doc = alloc.stack([ - alloc.reflow(r"I am looking for this file, but it's not there:"), - alloc - .parser_suggestion(filename.to_str().unwrap()) - .indent(4), - alloc.concat([ - alloc.reflow(r"Is the file supposed to be there? "), - alloc.reflow("Maybe there is a typo in the file name?"), - ]), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "FILE NOT FOUND".to_string(), - severity: Severity::RuntimeError, - } - } - io::ErrorKind::PermissionDenied => { - let doc = alloc.stack([ - alloc.reflow(r"I don't have the required permissions to read this file:"), - alloc - .parser_suggestion(filename.to_str().unwrap()) - .indent(4), - alloc - .concat([alloc.reflow(r"Is it the right file? Maybe change its permissions?")]), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "FILE PERMISSION DENIED".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let error = std::io::Error::from(error); - let formatted = format!("{}", error); - let doc = alloc.stack([ - alloc.reflow(r"I tried to read this file:"), - alloc - .text(filename.to_str().unwrap()) - .annotate(Annotation::Error) - .indent(4), - alloc.reflow(r"But ran into:"), - alloc.text(formatted).annotate(Annotation::Error).indent(4), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "FILE PROBLEM".to_string(), - severity: Severity::RuntimeError, - } - } - }; - - let mut buf = String::new(); - let palette = DEFAULT_PALETTE; - report.render_color_terminal(&mut buf, &alloc, &palette); - - buf -} - fn to_import_cycle_report( module_ids: ModuleIds, all_ident_ids: IdentIdsByModule, diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index a49117d3c9..11914256c1 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -1,3 +1,6 @@ +use std::io; +use std::path::PathBuf; + use roc_collections::all::MutSet; use roc_module::called_via::BinOp; use roc_module::ident::{Ident, Lowercase, ModuleName, TagName}; @@ -204,11 +207,15 @@ pub enum Problem { OverAppliedCrash { region: Region, }, + FileProblem { + filename: PathBuf, + error: io::ErrorKind, + }, } impl Problem { pub fn severity(&self) -> Severity { - use Severity::{RuntimeError, Warning}; + use Severity::{Fatal, RuntimeError, Warning}; match self { Problem::UnusedDef(_, _) => Warning, @@ -269,6 +276,7 @@ impl Problem { Problem::UnappliedCrash { .. } => RuntimeError, Problem::OverAppliedCrash { .. } => RuntimeError, Problem::DefsOnlyUsedInRecursion(_, _) => Warning, + Problem::FileProblem { .. } => Fatal, } } @@ -414,6 +422,7 @@ impl Problem { | Problem::RuntimeError(RuntimeError::VoidValue) | Problem::RuntimeError(RuntimeError::ExposedButNotDefined(_)) | Problem::RuntimeError(RuntimeError::NoImplementationNamed { .. }) + | Problem::FileProblem { .. } | Problem::ExposedButNotDefined(_) => None, } } diff --git a/crates/compiler/problem/src/lib.rs b/crates/compiler/problem/src/lib.rs index 1e1de043e5..ce5ca0d9d4 100644 --- a/crates/compiler/problem/src/lib.rs +++ b/crates/compiler/problem/src/lib.rs @@ -6,6 +6,10 @@ pub mod can; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Severity { + /// This should stop compilation in all cases. + /// Due to delayed loading of ingested files, this is wanted behaviour over a runtime error. + Fatal, + /// This will cause a runtime error if some code get srun /// (e.g. type mismatch, naming error) RuntimeError, diff --git a/crates/repl_eval/src/gen.rs b/crates/repl_eval/src/gen.rs index a55c7844c8..d7cbf3d06f 100644 --- a/crates/repl_eval/src/gen.rs +++ b/crates/repl_eval/src/gen.rs @@ -136,7 +136,7 @@ pub fn compile_to_mono<'a, 'i, I: Iterator>( Severity::Warning => { warnings.push(buf); } - Severity::RuntimeError => { + Severity::Fatal | Severity::RuntimeError => { errors.push(buf); } } @@ -154,7 +154,7 @@ pub fn compile_to_mono<'a, 'i, I: Iterator>( Severity::Warning => { warnings.push(buf); } - Severity::RuntimeError => { + Severity::Fatal | Severity::RuntimeError => { errors.push(buf); } } diff --git a/crates/reporting/src/cli.rs b/crates/reporting/src/cli.rs index c188df3ba7..22bd527da0 100644 --- a/crates/reporting/src/cli.rs +++ b/crates/reporting/src/cli.rs @@ -7,6 +7,7 @@ use roc_solve_problem::TypeError; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct Problems { + pub fatally_errored: bool, pub errors: usize, pub warnings: usize, } @@ -65,6 +66,7 @@ pub fn report_problems( // never need to re-allocate either the warnings or the errors vec! let mut warnings = Vec::with_capacity(total_problems); let mut errors = Vec::with_capacity(total_problems); + let mut fatally_errored = false; for (home, (module_path, src)) in sources.iter() { let mut src_lines: Vec<&str> = Vec::new(); @@ -92,6 +94,10 @@ pub fn report_problems( RuntimeError => { errors.push(buf); } + Fatal => { + fatally_errored = true; + errors.push(buf); + } } } @@ -111,6 +117,10 @@ pub fn report_problems( RuntimeError => { errors.push(buf); } + Fatal => { + fatally_errored = true; + errors.push(buf); + } } } } @@ -144,6 +154,7 @@ pub fn report_problems( } Problems { + fatally_errored, errors: errors.len(), warnings: warnings.len(), } diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index 7468e1d93e..eebd8ef77d 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -12,7 +12,7 @@ use roc_types::types::AliasKind; use std::path::PathBuf; use crate::error::r#type::suggest; -use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder}; +use crate::report::{to_file_problem_report, Annotation, Report, RocDocAllocator, RocDocBuilder}; use ven_pretty::DocAllocator; const SYNTAX_PROBLEM: &str = "SYNTAX PROBLEM"; @@ -1093,6 +1093,11 @@ pub fn can_problem<'b>( ]); title = "OVERAPPLIED CRASH".to_string(); } + Problem::FileProblem { filename, error } => { + let report = to_file_problem_report(&alloc, &filename, error); + doc = report.doc; + title = report.title; + } }; Report { diff --git a/crates/reporting/src/report.rs b/crates/reporting/src/report.rs index a6b34858c4..543c4c6ab3 100644 --- a/crates/reporting/src/report.rs +++ b/crates/reporting/src/report.rs @@ -1,10 +1,10 @@ use roc_module::ident::Ident; use roc_module::ident::{Lowercase, ModuleName, TagName, Uppercase}; -use roc_module::symbol::{Interns, ModuleId, PQModuleName, PackageQualified, Symbol}; +use roc_module::symbol::{Interns, ModuleId, ModuleIds, PQModuleName, PackageQualified, Symbol}; use roc_problem::Severity; use roc_region::all::LineColumnRegion; -use std::fmt; use std::path::{Path, PathBuf}; +use std::{fmt, io}; use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated}; pub use crate::error::canonicalize::can_problem; @@ -1077,3 +1077,88 @@ where Ok(()) } } + +pub fn to_file_problem_report_string(filename: &Path, error: io::ErrorKind) -> String { + let src_lines: Vec<&str> = Vec::new(); + + let mut module_ids = ModuleIds::default(); + + let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); + + let interns = Interns::default(); + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); + + let mut buf = String::new(); + let palette = DEFAULT_PALETTE; + let report = to_file_problem_report(&alloc, filename, error); + report.render_color_terminal(&mut buf, &alloc, &palette); + + buf +} + +pub fn to_file_problem_report<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: &Path, + error: io::ErrorKind, +) -> Report<'b> { + let filename: String = filename.to_str().unwrap().to_string(); + match error { + io::ErrorKind::NotFound => { + let doc = alloc.stack([ + alloc.reflow(r"I am looking for this file, but it's not there:"), + alloc + .string(filename) + .annotate(Annotation::ParserSuggestion) + .indent(4), + alloc.concat([ + alloc.reflow(r"Is the file supposed to be there? "), + alloc.reflow("Maybe there is a typo in the file name?"), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE NOT FOUND".to_string(), + severity: Severity::Fatal, + } + } + io::ErrorKind::PermissionDenied => { + let doc = alloc.stack([ + alloc.reflow(r"I don't have the required permissions to read this file:"), + alloc + .string(filename) + .annotate(Annotation::ParserSuggestion) + .indent(4), + alloc + .concat([alloc.reflow(r"Is it the right file? Maybe change its permissions?")]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE PERMISSION DENIED".to_string(), + severity: Severity::Fatal, + } + } + _ => { + let error = std::io::Error::from(error); + let formatted = format!("{}", error); + let doc = alloc.stack([ + alloc.reflow(r"I tried to read this file:"), + alloc.string(filename).annotate(Annotation::Error).indent(4), + alloc.reflow(r"But ran into:"), + alloc.text(formatted).annotate(Annotation::Error).indent(4), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE PROBLEM".to_string(), + severity: Severity::Fatal, + } + } + } +}