use std::path::Path;
use std::rc::Rc;
use crate::core::{
KernelForm, KernelType, Keyword, ModulePath, Namespace, PassingAspect, PassingDifference,
};
use crate::diagnostic::{Counted, Diagnostic, DiagnosticCode, Label, MissingName, Suggestion};
use crate::interpret::InterpretProblems;
use crate::language::Language;
use crate::modules::{HiddenSourceModule, LibraryTable, LocalModuleClash, SearchScope};
use crate::reader::Form;
use crate::source::Span;
pub enum MissingExportRemedy {
AddToExports,
UseADefiner,
NoHelp,
}
pub struct ExpandProblems;
impl ExpandProblems {
const QUOTE_SHORTHAND: char = '\'';
const SIMILAR_SPELLING_HELP: &'static str = "a name with a similar spelling exists";
const CODE_REPLACEMENTS: [(&'static str, &'static str, &'static str); 9] = [
("+", "number-add", "code"),
("-", "number-sub", "code"),
("<", "number-lt", "code"),
("<=", "number-le", "code"),
(">", "number-gt", "code"),
(">=", "number-ge", "code"),
("=", "value-eq", "code"),
("not", "bool-not", "code"),
("map", "map-list", "prelude/macros"),
];
pub fn malformed_type_template(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::BAD_TYPE_TEMPLATE,
"this type template has a malformed placeholder",
Label::new(span, "a placeholder is `{`, a number and `}`"),
)
.with_help("write a literal brace as `{{` or `}}`")
}
pub fn unknown_type_placeholder(name: &str, parameters: &[String], span: Span) -> Diagnostic {
let help = if parameters.is_empty() {
format!("add `({name})` to the template's `where`")
} else {
let names: Vec<String> = parameters
.iter()
.map(|parameter| format!("`{parameter}`"))
.collect();
format!("the type parameters are {}", names.join(", "))
};
Diagnostic::error(
DiagnosticCode::TYPE_PARAMETER_PLACEHOLDER,
format!("`{{{name}}}` names no type parameter"),
Label::new(span, "this template's `where` has no such type parameter"),
)
.with_help(help)
}
pub fn type_placeholder_in_type_template(definer: &str, name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::TYPE_PLACEHOLDER_IN_RUST_TYPE,
format!("a `{definer}` template cannot name `{name}`"),
Label::new(
span,
format!("`{definer}` templates name their parameters by number"),
),
)
.with_help("write the placeholder as `{0}`")
}
pub fn rust_type_clauses_need_one_parameter(
definer: &str,
name: &str,
parameters: usize,
span: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::RUST_TYPE_CLAUSES_NEED_ONE_PARAMETER,
format!("a `{definer}` with clauses takes one type parameter"),
Label::new(
span,
format!(
"this type has {}",
Counted::noun(parameters, "type parameter")
),
),
)
.with_help(format!(
"give the type one parameter, as in `({definer} ({name} item) ...)`"
))
}
pub fn rust_clauses_need_one_parameter(parameters: usize, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::RUST_CLAUSES_NEED_ONE_PARAMETER,
"a `rust` with clauses takes one type parameter",
Label::new(
span,
format!(
"this type binds {}",
Counted::noun(parameters, "type parameter")
),
),
)
.with_help("bind one parameter, as in `(where ((s source)) (fn ((ref s)) string))`")
}
pub fn structural_clause_needs_one_method(definer: &str, clause: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
"a `structural` clause needs one method that takes `(ref self)`",
Label::new(clause, "this clause fills the body of one method"),
)
.with_help(format!(
"declare one `(method name (fn ((ref self) ...) result))` in this `{definer}`"
))
}
pub fn structural_clause_on_derived_trait(
definer: &str,
name: &str,
path: &str,
clause: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{name}` binds `{path}`, which `derive` already writes"),
Label::new(
clause,
"every struct would get both the derive and this impl",
),
)
.with_help(format!(
"keep the clause off this `{definer}`, or bind a trait that `derive` does not write"
))
}
pub fn structural_clause_on_bound_path(
definer: &str,
name: &str,
path: &str,
existing: &str,
clause: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{name}` binds `{path}`, which `{existing}` already binds"),
Label::new(
clause,
"the two traits would infer two impls of one Rust trait",
),
)
.with_help(format!(
"keep the clause off this `{definer}`, or give it a Rust path that no other trait binds"
))
}
pub fn repeated_structural_clause(definer: &str, repeated: Span, first: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("this `{definer}` has a second `structural` clause"),
Label::new(repeated, "second clause"),
)
.with_label(Label::new(first, "first clause"))
.with_help("merge the two clauses into one")
}
pub fn second_structural_path(
definer: &str,
name: &str,
path: &str,
first: &str,
clause: Span,
first_clause: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{name}` binds `{path}`, which `{first}` already infers"),
Label::new(
clause,
"the two clauses would infer two impls of one Rust trait",
),
)
.with_label(Label::new(
first_clause,
format!("`{first}` infers `{path}` here"),
))
.with_help(format!(
"keep one of the two clauses, or give this `{definer}` another Rust path"
))
}
pub fn missing_structural_key(key: &str, clause: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{key}` is missing from this `structural` clause"),
Label::new(clause, format!("this clause has no `{key}`")),
)
.with_help(format!("add `{key}` and its template string"))
}
pub fn unknown_structural_key(written: &str, keys: &[String], span: Span) -> Diagnostic {
let listed: Vec<String> = keys.iter().map(|key| format!("`{key}`")).collect();
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{written}` is not a key of a `structural` clause"),
Label::new(span, "expected a key here"),
)
.with_help(format!("a `structural` clause takes {}", listed.join(", ")))
}
pub fn repeated_structural_key(key: &str, repeated: Span, first: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{key}` appears twice in this `structural` clause"),
Label::new(repeated, "repeated here"),
)
.with_label(Label::new(first, "first given here"))
.with_help(format!("remove one of the two `{key}` templates"))
}
pub fn structural_key_without_template(key: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{key}` has no template string after it"),
Label::new(span, "expected a string after this key"),
)
.with_help(format!(
"write the Rust text after the key, as in `{key} \"...\"`"
))
}
pub fn unfilled_structural_placeholder(
key: &str,
placeholder: &str,
accepted: &[String],
template: Span,
) -> Diagnostic {
let listed: Vec<String> = accepted
.iter()
.map(|accepted| format!("`{accepted}`"))
.collect();
Diagnostic::error(
DiagnosticCode::MALFORMED_STRUCTURAL_CLAUSE,
format!("`{key}` cannot fill `{placeholder}`"),
Label::new(template, format!("this template holds `{placeholder}`")),
)
.with_help(format!(
"`{key}` takes {}, and `{{{{` or `}}}}` for a literal brace",
listed.join(", ")
))
}
pub fn clause_passes_arguments_unlike_template(
difference: PassingDifference,
clause: Span,
template: Span,
) -> Diagnostic {
let (clause_label, template_label) = match difference {
PassingDifference::Argument(argument, aspect) => (
Self::passing_aspect_label(argument, aspect),
format!("unlike the template's `{{{argument}}}` here"),
),
PassingDifference::CallOnlyInClause => (
"this clause calls a function".to_string(),
"the template calls no function".to_string(),
),
PassingDifference::CallOnlyInTemplate => (
"this clause calls no function".to_string(),
"the template calls a function".to_string(),
),
};
Diagnostic::error(
DiagnosticCode::CLAUSE_PASSES_ARGUMENTS_UNLIKE_TEMPLATE,
"a `rust` clause must pass its arguments as its template does",
Label::new(clause, clause_label),
)
.with_label(Label::new(template, template_label))
.with_help(
"write each placeholder as the template writes it, with the same borrow, receiver, index or call",
)
}
fn passing_aspect_label(argument: usize, aspect: PassingAspect) -> String {
let placeholder = format!("`{{{argument}}}`");
match aspect {
PassingAspect::Use => format!("this clause differs in whether it uses {placeholder}"),
PassingAspect::Borrow => format!("this clause borrows {placeholder} differently"),
PassingAspect::Receiver => {
format!("this clause differs in whether {placeholder} receives a method call")
}
PassingAspect::Index => {
format!("this clause differs in whether it indexes {placeholder}")
}
PassingAspect::Dereference => {
format!("this clause differs in whether it dereferences {placeholder}")
}
PassingAspect::Call => format!("this clause differs in whether it calls {placeholder}"),
PassingAspect::Operand => {
format!("this clause differs in whether {placeholder} is an operator's operand")
}
}
}
pub fn malformed_template(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_TEMPLATE,
"this Rust template has a malformed placeholder",
Label::new(span, "a placeholder is `{`, a number and `}`"),
)
.with_help("write a literal brace as `{{` or `}}`")
}
pub fn borrowed_type_placeholder(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::BORROWED_TYPE_PLACEHOLDER,
"a type template cannot borrow a placeholder",
Label::new(span, "`{&...}` only makes sense for a value"),
)
.with_help("write the placeholder as `{0}`")
}
pub fn placeholder_without_parameter(
index: usize,
parameters: usize,
example: &ParameterExample<'_>,
span: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::PLACEHOLDER_WITHOUT_PARAMETER,
format!("`{{{index}}}` names no type parameter"),
Label::new(
span,
format!(
"this type has {}",
Counted::noun(parameters, "type parameter")
),
),
)
.with_help(format!(
"declare the type parameters on the name, as in `{}`",
example.definition()
))
}
pub fn unused_type_placeholder(index: usize, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::UNUSED_TYPE_PLACEHOLDER,
"the template never uses this type parameter",
Label::new(span, format!("its placeholder is `{{{index}}}`")),
)
.with_help(format!(
"use `{{{index}}}` in the template, or remove the parameter"
))
}
pub fn unbound(
name: &str,
namespace: Namespace,
span: Span,
similar: Option<Rc<str>>,
) -> Diagnostic {
Self::unbound_report(name, namespace, span, similar).with_missing_name(MissingName {
name: name.into(),
namespace,
})
}
fn unbound_report(
name: &str,
namespace: Namespace,
span: Span,
similar: Option<Rc<str>>,
) -> Diagnostic {
if namespace == Namespace::Type && name.starts_with(Self::QUOTE_SHORTHAND) {
return Diagnostic::error(
DiagnosticCode::UNBOUND_NAME,
format!("cannot find lifetime `{name}` in this scope"),
Label::new(span, "not found in this scope"),
)
.with_help("declare the lifetime in the enclosing `where` or type head");
}
let what = Self::describe(namespace);
let diagnostic = Diagnostic::error(
DiagnosticCode::UNBOUND_NAME,
format!("cannot find {what}`{name}` in this scope"),
Label::new(span, "not found in this scope"),
);
let diagnostic = match similar {
Some(similar) => diagnostic.with_suggestion(
Self::SIMILAR_SPELLING_HELP,
Suggestion::new(span, similar.to_string()),
),
None => diagnostic,
};
if namespace == Namespace::Value && name.starts_with(Self::QUOTE_SHORTHAND) {
diagnostic.with_help(format!(
"there is no quote shorthand; write `(quote {})`",
&name[1..]
))
} else {
diagnostic
}
}
pub fn bounded_lifetime(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::BOUNDED_TYPE_PARAMETER,
"a lifetime cannot require traits",
Label::new(span, "traits given here"),
)
.with_help("write the lifetime alone, as in `'a`")
}
pub fn ambiguous(name: &str, span: Span, definitions: Vec<Span>) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::AMBIGUOUS_NAME,
format!("`{name}` is ambiguous"),
Label::new(span, "this name could refer to more than one binding"),
);
definitions
.into_iter()
.fold(diagnostic, |diagnostic, definition| {
diagnostic.with_label(Label::new(definition, "one candidate is defined here"))
})
}
pub fn enum_rust_template(span: Span, label: &str, example: &str) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::ENUM_RUST_TEMPLATE,
"a `rust` template needs one variant with a part for each placeholder",
Label::new(span, label),
)
.with_help(format!(
"write one positional part for each placeholder, as in `{example}`"
))
}
pub fn duplicate(name: &str, span: Span, previous: Option<Span>) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::DUPLICATE_DEFINITION,
format!("`{name}` is defined more than once"),
Label::new(span, "defined again here"),
);
match previous {
Some(previous) => diagnostic.with_label(Label::new(previous, "first defined here")),
None => diagnostic.with_note(format!(
"`{name}` is already defined by the prelude or an import"
)),
}
}
pub fn set_aside(input: &SetAsideInput<'_>, error: &Diagnostic) -> Diagnostic {
let primary = error.primary();
let warning = Diagnostic::warning(
DiagnosticCode::INPUT_SET_ASIDE,
input.title(),
Label::new(primary.span(), primary.message()),
);
let labeled = error
.labels()
.skip(1)
.fold(warning, |warning, label| warning.with_label(label.clone()));
let noted = error.notes().iter().fold(
labeled.with_note(Self::error_line(error)),
|warning, note| warning.with_note(note.clone()),
);
let helped = Self::with_helps_of(noted, error).with_help(input.enter_again());
match error.missing_name() {
Some(missing) => helped.with_missing_name(missing.clone()),
None => helped,
}
}
pub fn dropped(input: &SetAsideInput<'_>, replacing: &str, span: Span) -> Diagnostic {
Diagnostic::warning(
DiagnosticCode::INPUT_SET_ASIDE,
input.title(),
Label::new(span, "set aside"),
)
.with_note(format!(
"`{replacing}` replaced the input that defined {}",
input.listed_names()
))
.with_help(input.enter_again())
}
pub fn set_aside_reason(replacing: &str, dropped: &str) -> String {
format!("`{replacing}` replaced the input that defined `{dropped}`")
}
pub fn set_aside_note(setter: &str, name: &str, reason: &str) -> String {
format!("`{setter}` set aside `{name}`: {reason}")
}
pub fn would_set_aside_note(setter: &str, name: &str, reason: &str) -> String {
format!("`{setter}` would set aside `{name}`: {reason}")
}
pub fn no_longer_defines_note(input: &str, name: &str, setter: &str) -> String {
format!("`{input}` no longer defines `{name}` after `{setter}`")
}
pub fn checks_no_longer(diagnostic: Diagnostic) -> Diagnostic {
diagnostic.with_help("enter a definition to set aside the inputs that no longer check")
}
fn error_line(error: &Diagnostic) -> String {
let code = error.code().expect("an error has a code");
format!("error[{}]: {}", code.as_str(), error.title())
}
fn with_helps_of(warning: Diagnostic, error: &Diagnostic) -> Diagnostic {
let helps = error.helps().iter().cloned();
helps.fold(warning, Diagnostic::with_help)
}
pub fn unknown_module(path: &ModulePath, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::UNKNOWN_MODULE,
format!("cannot find module `{path}`"),
Label::new(span, "no module with this path"),
)
.with_help(
"a module path names a file relative to the root directory, without its extension",
)
}
pub fn unreachable_module(path: &ModulePath, file: &Path, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::UNKNOWN_MODULE,
format!("cannot find module `{path}`"),
Label::new(span, "no module this file can use has this path"),
)
.with_note(format!(
"`{}` exists, under a directory a task cannot reach this way",
file.display()
))
.with_help(format!(
"a task uses a module of `{tasks}/` by its path from `{tasks}/`, and cannot use a module under `{tests}/`",
tasks = SearchScope::TASKS_DIRECTORY,
tests = SearchScope::TESTS_DIRECTORY,
))
}
pub fn local_module_beside_library(clash: &LocalModuleClash<'_>, span: Span) -> Diagnostic {
let (local, library) = (clash.local.display(), clash.library);
Diagnostic::error(
DiagnosticCode::LOCAL_MODULE_BESIDE_LIBRARY,
format!("`{local}` and library `{library}` share the module path `{library}`"),
Label::new(span, "this names both a local module and a library"),
)
.with_note(format!(
"library `{}` lists `{library}`, whose modules take the path `{library}`",
clash.lister
))
.with_help(format!("rename `{local}`"))
}
pub fn module_outside_checkout(
path: &ModulePath,
file: &std::path::Path,
span: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MODULE_OUTSIDE_CHECKOUT,
format!("module `{path}` resolves outside its library's checkout"),
Label::new(span, "this module's file links outside the checkout"),
)
.with_note(format!(
"`{}` is a symbolic link, and a library fetched from git may only read files inside its own checkout",
file.display()
))
.with_help("ask the library's authors to commit the module's file instead of a link")
}
pub fn internal_module(path: &ModulePath, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::INTERNAL_MODULE,
format!("`{path}` is internal to the prelude"),
Label::new(span, "only the prelude imports this module"),
)
.with_note("it holds helpers of the prelude's own forms")
.with_help("remove this `use`")
}
pub fn unlisted_library(library: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::UNLISTED_LIBRARY,
format!("this project does not list library `{library}`"),
Label::new(span, "a library that another library uses"),
)
.with_help(format!(
"add `{library}` to the `libraries` of `{}`",
Language::project_file_name()
))
}
pub fn library_hides_local_module(
written: &ModulePath,
local: &std::path::Path,
span: Span,
) -> Diagnostic {
let library = LibraryTable::head(written);
Diagnostic::warning(
DiagnosticCode::LIBRARY_HIDES_LOCAL_MODULE,
format!("library `{library}` hides `{}`", local.display()),
Label::new(span, format!("this loads library `{library}`")),
)
.with_note("a library's name takes precedence over a local module of the same name")
.with_help(format!(
"rename `{}` to reach it, or rename the library in `{}`",
local.display(),
Language::project_file_name()
))
}
pub fn test_module_hides_source_module(shadow: &HiddenSourceModule, span: Span) -> Diagnostic {
let hiding = shadow.hiding.display();
let hidden = shadow.hidden.display();
Diagnostic::warning(
DiagnosticCode::TEST_MODULE_HIDES_SOURCE_MODULE,
format!("`{hiding}` hides `{hidden}`"),
Label::new(span, format!("this loads `{hiding}`")),
)
.with_note(format!(
"a module of the {} directory takes precedence over a source module of the same path",
shadow.directory_name
))
.with_help(format!("rename `{hiding}` to reach `{hidden}`"))
}
pub fn file_takes_compiler_module(path: &ModulePath, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::FILE_TAKES_COMPILER_MODULE,
format!("this file's module path `{path}` belongs to the compiler"),
Label::new(span, "this file never loads"),
)
.with_note("`prelude`, `kernel`, `code` and every path under `prelude/` are the compiler's, so this file would never run")
.with_help("rename the file")
}
pub fn reserved_module(path: &ModulePath, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::RESERVED_MODULE_NAME,
format!("`{path}` cannot name a module"),
Label::new(span, "this name is reserved"),
)
.with_note(format!(
"`{}` at the root is the project file",
Language::project_file_name()
))
.with_help("rename the module's file, and this path with it")
}
pub fn missing_export(
path: &ModulePath,
name: &str,
span: Span,
similar: Option<Rc<str>>,
remedy: MissingExportRemedy,
) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::MISSING_EXPORT,
format!("module `{path}` does not export `{name}`"),
Label::new(span, "not exported"),
);
match (remedy, similar) {
(MissingExportRemedy::UseADefiner, _) => diagnostic
.with_note(format!("`{name}` is internal to the prelude"))
.with_help(
"let a prelude definer build and document the definition, \
as in `(def name \"text\" value)` or `(defn name (parameter) body)`",
),
(_, Some(similar)) => diagnostic.with_suggestion(
"an export with a similar name exists",
Suggestion::new(span, similar.to_string()),
),
(MissingExportRemedy::AddToExports, None) => {
diagnostic.with_help(format!("add `{name}` to the `export` list of `{path}`"))
}
(MissingExportRemedy::NoHelp, None) => diagnostic,
}
}
pub fn import_cycle(path: &ModulePath, span: Span, cycle: Vec<String>) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::IMPORT_CYCLE,
format!("importing `{path}` creates a cycle"),
Label::new(span, "this import closes the cycle"),
)
.with_note(format!(
"the modules import each other: {}",
cycle.join(" -> ")
))
.with_help("move the shared definitions into a module that both can import")
}
pub fn not_a_definition(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::NOT_A_DEFINITION,
"expected a definition at module level",
Label::new(span, "this is not a definition"),
)
.with_help("put expressions inside a function, such as `(def run (fn () ...))`")
}
pub fn evaluated_definition(span: Span) -> Diagnostic {
Diagnostic::note(
"the module defines this form, so it does not run",
Label::new(span, "a definition of the module"),
)
}
pub fn module_level_only(definer: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MODULE_LEVEL_ONLY,
format!("`{definer}` can only appear at module level"),
Label::new(span, "not allowed inside an expression"),
)
.with_help("use `local` to bind a name inside a body")
}
pub fn type_in_value_note(name: &str) -> String {
format!(
"`{name}` is a type; a type goes beside a name in a binding, as in \
`(local (year i64) 2025)`, or in a `type` signature"
)
}
pub fn local_outside_body(written_binding: &str, span: Span) -> Diagnostic {
let local = KernelForm::Local.name();
Diagnostic::error(
DiagnosticCode::LET_OUTSIDE_BODY,
format!("`{local}` can only appear directly in a body"),
Label::new(span, "not allowed here"),
)
.with_help(format!(
"wrap the code that uses it in `(let ({written_binding}) ...)`"
))
}
pub fn macro_outside_def(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MACRO_OUTSIDE_DEF,
"`macro` must be the value of a module-level `def`",
Label::new(span, "not allowed here"),
)
.with_help("write `(def name (macro (form) body ...))`")
}
pub fn empty_call(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::EMPTY_CALL,
"`()` is not an expression",
Label::new(span, "an empty list has nothing to call"),
)
.with_suggestion(
"the unit value is written `(do)`",
Suggestion::new(span, "(do)"),
)
}
pub fn form_as_value(kernel: KernelForm, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::FORM_AS_VALUE,
format!("`{}` is a form, not a value", kernel.name()),
Label::new(span, "used as a value here"),
)
.with_help(format!("write it as `{}`", kernel.shape()))
}
pub fn macro_as_value(name: &str, span: Span, function: Option<Rc<str>>) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::MACRO_AS_VALUE,
format!("`{name}` is a macro, not a value"),
Label::new(span, "used as a value here"),
);
match function {
Some(function) => diagnostic.with_suggestion(
format!("a macro can only be called; pass `{function}` instead"),
Suggestion::new(span, function.to_string()),
),
None => diagnostic.with_help("a macro can only be called; pass a function instead"),
}
}
pub fn wrong_namespace(name: &str, expected: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::WRONG_NAMESPACE,
format!("expected {expected}, found `{name}`"),
Label::new(span, format!("not {expected}")),
)
}
pub fn recur_outside_tail(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::RECUR_OUTSIDE_TAIL,
"`recur` is not in tail position",
Label::new(span, "this `recur` is not the last thing the function does"),
)
.with_help("make `recur` the last form of the function, or call the function by name")
}
pub fn recur_argument_count(expected: usize, found: usize, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::RECUR_ARGUMENT_COUNT,
format!(
"`recur` passes {}, but its function takes {expected}",
Counted::noun(found, "argument")
),
Label::new(span, format!("passes {}", Counted::noun(found, "argument"))),
)
.with_help(format!(
"write `recur` with {}, one for each parameter of the function",
Counted::noun(expected, "argument")
))
}
pub fn try_outside_fn(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::TRY_OUTSIDE_FN,
"`try` is not inside a function",
Label::new(span, "there is no function here for `try` to return from"),
)
.with_help("write `try` inside a `fn` body")
}
pub fn recur_outside_fn(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::RECUR_OUTSIDE_FN,
"`recur` is not inside a function",
Label::new(
span,
"there is no function here for `recur` to jump back to",
),
)
.with_help("write `recur` inside a `fn` body")
}
pub fn macro_result(found: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MACRO_RESULT_NOT_FORM,
format!("this macro returned {found} instead of code"),
Self::macro_call(span),
)
}
pub fn macro_call(span: Span) -> Label {
Label::at_macro_call(span, "in this macro call")
}
pub fn expansion_too_deep(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::EXPANSION_TOO_DEEP,
"macro expansion did not finish",
Label::new(span, "expanding this form keeps producing more macro calls"),
)
.with_help("check that every recursive macro has a case that stops")
}
pub fn expansion_too_large(name: &str, limit: usize, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::EXPANSION_TOO_LARGE,
"macro expansion did not finish",
Label::new(
span,
format!("expanding this call of `{name}` passes {limit} forms"),
),
)
.with_note(format!(
"the macro calls in one top-level form expand to at most {limit} forms in all"
))
.with_help(
"make each recursive macro stop, or split a large expansion across several top-level forms",
)
}
pub fn expansion_nests_too_deep(
span: Span,
limit: usize,
expansion: Option<&str>,
) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::EXPANSION_NESTS_TOO_DEEP,
"this expression nests too deeply",
Label::new(span, format!("this expression sits inside {limit} others")),
)
.with_note(format!("expressions nest at most {limit} deep"));
match expansion {
Some(name) => diagnostic.with_note(format!("in this expansion of `{name}`")),
None => diagnostic,
}
.with_help("move the inner expressions into a function of their own")
}
pub fn compile_time_cycle(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::COMPILE_TIME_CYCLE,
format!("`{name}` depends on itself at compile time"),
Label::new(span, "needed here while it is still being computed"),
)
}
pub fn not_a_type(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::NOT_A_TYPE,
"expected a type",
Label::new(span, "this is not a type"),
)
.with_help(
"a type is a name such as `i64`, or a form such as `(fn (i64) i64)` or `(ref str)`",
)
}
pub fn malformed_type(constructor: KernelType, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_TYPE,
format!("malformed `{}` type", constructor.name()),
Label::new(span, "this does not fit the shape of the type"),
)
.with_help(format!("write it as `{}`", constructor.shape()))
}
pub fn malformed_associated_value(name: Option<&str>, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MALFORMED_TYPE,
"malformed associated value",
Label::new(span, "this does not fit the shape of a value clause"),
)
.with_help(format!(
"write each value as `({} type)`",
name.unwrap_or("name")
))
}
pub fn parameters_on_name(kind: &str, replacement: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::TYPE_PARAMETERS_ON_NAME,
format!("`{kind}` names its type parameters on its name"),
Label::new(span, "this `where` names them in the body"),
)
.with_help(format!("write `{replacement}`"))
}
pub fn mixed_variant_payload(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MIXED_VARIANT_PAYLOAD,
"a variant's payload is either types or one `struct` body",
Label::new(span, "this mixes the two"),
)
.with_help(
"write the payload as types, as in `(circle f64)`, or as one struct body, as in `(labeled (struct (text string)))`",
)
}
pub fn not_a_name(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::NOT_A_NAME,
"expected a name",
Label::new(span, "this is not a name"),
)
}
pub fn extra_field_keyword_value(keyword: &str, span: Span) -> Diagnostic {
Self::field_keyword_arity(keyword, Label::new(span, "this value is extra"))
}
pub fn missing_field_keyword_value(keyword: &str, span: Span) -> Diagnostic {
Self::field_keyword_arity(keyword, Label::new(span, "this call has no value"))
}
fn field_keyword_arity(keyword: &str, label: Label) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::FIELD_KEYWORD_ARITY,
"a field keyword takes one value",
label,
)
.with_help(format!("write ({keyword} value)"))
}
pub fn label_without_value(head: &Form, fields: &[Rc<str>], span: Span) -> Diagnostic {
Self::partly_labeled_call(head, fields, Label::new(span, "this label has no value"))
}
pub fn value_without_label(head: &Form, fields: &[Rc<str>], span: Span) -> Diagnostic {
Self::partly_labeled_call(head, fields, Label::new(span, "this argument has no label"))
}
fn partly_labeled_call(head: &Form, fields: &[Rc<str>], label: Label) -> Diagnostic {
let marker = Keyword::MARKER;
let labeled_values: Vec<String> = fields
.iter()
.map(|field| format!("{marker}{field} value"))
.collect();
Diagnostic::error(
DiagnosticCode::PARTLY_LABELED_CALL,
format!("`{head}` takes a label on every argument or on none"),
label,
)
.with_help(format!(
"a keyword argument is always a label: write `{}`",
labeled_values.join(" ")
))
}
pub fn keyword_binder(span: Span) -> Diagnostic {
let marker = Keyword::MARKER;
Diagnostic::error(
DiagnosticCode::KEYWORD_BINDER,
format!("a name cannot start with `{marker}`"),
Label::new(span, "this is a field keyword"),
)
.with_help(format!(
"names starting with `{marker}` read struct fields; choose another name"
))
}
pub fn ignored_binding_read(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::IGNORED_BINDING_READ,
"`_` binds no value to read",
Label::new(span, "this reads a binding named `_`"),
)
.with_help("give the binding a name, and read that name here")
}
pub fn not_a_pattern(span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::NOT_A_PATTERN,
"this is not a pattern",
Label::new(span, "a pattern names a binding, a literal or a variant"),
)
.with_help("write a binding name, `_`, a literal, or `(variant pattern ...)`")
}
pub fn variant_as_value(name: &str, arity: usize, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::VARIANT_AS_VALUE,
format!("`{name}` is a variant constructor, not a value"),
Label::new(
span,
format!(
"this constructor takes {} when it is called",
Counted::noun(arity, "argument")
),
),
)
.with_help(format!("write `({name} value ...)`"))
}
pub fn signature_target(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::SIGNATURE_TARGET,
format!("`{name}` cannot have a type signature here"),
Label::new(span, "not a definition of this module"),
)
.with_help("a `type` signature must sit in the module whose `def` it describes")
}
pub fn derive_target(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::DERIVE_TARGET,
format!("`{name}` cannot have a `derive` here"),
Label::new(span, "not a struct of this module"),
)
.with_help("a `derive` must sit in the module whose `struct` it describes")
}
pub fn derive_trait(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::DERIVE_TRAIT,
format!("`{name}` is not a trait"),
Label::new(span, "a `derive` lists traits"),
)
.with_help(format!("list a trait's name, not `{name}`"))
}
pub fn missing_method(
method: &str,
trait_name: &str,
type_name: &str,
span: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::MISSING_METHOD,
format!("`{type_name}` is missing `{method}` from `{trait_name}`"),
Label::new(span, format!("this implementation is missing `{method}`")),
)
.with_help(format!("add the `{method}` method"))
}
pub fn missing_associated_type(
trait_name: &str,
associated: &str,
type_name: &str,
span: Span,
) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::ASSOCIATED_TYPE,
format!("`{type_name}` does not define the associated type `{associated}`"),
Label::new(
span,
format!("`{trait_name}` requires the associated type `{associated}`"),
),
)
.with_help(format!("add `(type {associated} ...)` to the impl"))
}
pub fn unknown_associated_type(trait_name: &str, associated: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::ASSOCIATED_TYPE,
format!("`{trait_name}` does not declare the associated type `{associated}`"),
Label::new(span, "not an associated type of this trait"),
)
.with_help("declare it in the trait, or remove it from the impl")
}
pub fn duplicate_associated_type(associated: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::ASSOCIATED_TYPE,
format!("the associated type `{associated}` is defined more than once"),
Label::new(span, "defined again here"),
)
.with_help("remove one of the definitions")
}
pub fn extra_method(method: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::EXTRA_METHOD,
format!("`{method}` is implemented more than once"),
Label::new(span, "defined again here"),
)
.with_help("remove one of the definitions")
}
pub fn undeclared_method(method: &str, trait_name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::UNDECLARED_METHOD,
format!("`{trait_name}` does not declare `{method}`"),
Label::new(span, format!("not a method of `{trait_name}`")),
)
.with_help("declare it in the trait, or remove it")
}
pub fn generic_implementor(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::GENERIC_IMPLEMENTOR,
"an `impl` cannot name only a type variable",
Label::new(span, format!("`{name}` is a type variable")),
)
.with_help("apply the variable to a type, such as `(vec a)`")
}
pub fn undefined_documented_term(
name: &str,
span: Span,
similar: Option<Rc<str>>,
) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::UNDEFINED_DOCUMENTED_TERM,
format!("this module documents `{name}` but does not define it"),
Label::new(span, "not defined in this module"),
);
match similar {
Some(similar) => diagnostic.with_suggestion(
Self::SIMILAR_SPELLING_HELP,
Suggestion::new(span, similar.to_string()),
),
None => diagnostic.with_help(format!(
"define `{name}` in this module, or move this `document` to the module that defines it"
)),
}
}
pub fn broken_definition(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::BROKEN_DEFINITION,
format!("`{name}` has errors, so macro code cannot use it"),
Label::new(span, "used by macro code here"),
)
}
pub fn program_only(name: &str, used: Span, defined: Span) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::PROGRAM_ONLY,
format!("`{name}` belongs to the program, so macro code cannot run it"),
Label::new(used, "macro code runs this"),
)
.with_label(Label::new(
defined,
format!("`{name}` is Rust code that runs in the program"),
))
.with_help(InterpretProblems::RETURN_AS_CODE_HELP);
Self::with_code_replacement(diagnostic, name)
}
fn with_code_replacement(diagnostic: Diagnostic, name: &str) -> Diagnostic {
match Self::code_replacement(name) {
Some((replacement, module)) => diagnostic.with_help(format!(
"in macro code, use `{replacement}` from `{module}` instead"
)),
None => diagnostic,
}
}
pub fn macro_program_only(name: &str, used: Span, defined: Span) -> Diagnostic {
let diagnostic = Diagnostic::error(
DiagnosticCode::PROGRAM_ONLY,
format!("`{name}` belongs to the program, so macro code cannot run it"),
Label::new(used, "macro code runs this"),
)
.with_label(Label::new(
defined,
format!("`{name}` expands to Rust code that runs in the program"),
))
.with_help(InterpretProblems::RETURN_AS_CODE_HELP);
Self::with_code_replacement(diagnostic, name)
}
fn code_replacement(name: &str) -> Option<(&'static str, &'static str)> {
Self::CODE_REPLACEMENTS
.iter()
.find(|(program, _, _)| *program == name)
.map(|(_, replacement, module)| (*replacement, *module))
}
pub fn broken_macro(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::BROKEN_MACRO,
format!("the macro `{name}` has errors, so this call cannot expand"),
Label::new(span, "this call needs the macro"),
)
}
pub fn not_at_compile_time(name: &str, span: Span) -> Diagnostic {
Diagnostic::error(
DiagnosticCode::NOT_AT_COMPILE_TIME,
format!("`{name}` is not available at compile time"),
Label::new(span, "used by macro code here"),
)
}
pub fn second_trait_path(name: &str, existing: &str, path: &str, span: Span) -> Diagnostic {
let title = if name == existing {
format!("`{name}` binds `{path}` again")
} else {
format!("`{name}` binds a Rust path that `{existing}` already binds")
};
let help = if name == existing {
format!("remove the second `{name}`, or give it a different Rust path")
} else {
format!("use `{existing}`, or give `{name}` a different Rust path")
};
Diagnostic::warning(
DiagnosticCode::SECOND_TRAIT_PATH,
title,
Label::new(span, format!("this also binds `{path}`")),
)
.with_note("two traits that bind one Rust path can disagree about its implementors")
.with_help(help)
}
fn describe(namespace: Namespace) -> &'static str {
match namespace {
Namespace::Value => "",
Namespace::Type => "type ",
}
}
}
pub struct SetAsideInput<'a> {
names: &'a [&'a str],
source: &'a str,
setter: &'a str,
}
impl<'a> SetAsideInput<'a> {
const LISTED_NAMES: usize = 3;
pub fn new(names: &'a [&'a str], source: &'a str, setter: &'a str) -> Self {
Self {
names,
source,
setter,
}
}
fn title(&self) -> String {
format!("set aside {} after `{}`", self.listed_names(), self.setter)
}
fn enter_again(&self) -> String {
let pronoun = if self.names.len() > 1 { "them" } else { "it" };
format!("enter {} again to use {pronoun}", self.listed_names())
}
fn listed_names(&self) -> String {
let quoted: Vec<String> = self.names.iter().map(|name| format!("`{name}`")).collect();
match quoted.as_slice() {
[] => format!("`{}`", self.source),
[only] => only.clone(),
shown if shown.len() <= Self::LISTED_NAMES => {
let (last, rest) = shown.split_last().expect("two names or more");
format!("{} and {last}", rest.join(", "))
}
more => format!(
"{} and {} more",
more[..Self::LISTED_NAMES].join(", "),
more.len() - Self::LISTED_NAMES
),
}
}
}
pub enum TemplateSpelling {
KernelString,
RustClause,
Elided,
}
pub struct ParameterExample<'e> {
pub spelling: TemplateSpelling,
pub definer: &'e str,
pub name: &'e str,
pub template: &'e str,
pub placeholders: usize,
}
impl ParameterExample<'_> {
fn definition(&self) -> String {
let head = format!("({} {})", self.name, self.parameter_names().join(" "));
let body = match self.spelling {
TemplateSpelling::KernelString => format!("\"{}\"", self.template),
TemplateSpelling::RustClause => format!("(rust \"{}\")", self.template),
TemplateSpelling::Elided => "...".to_string(),
};
format!("({} {head} {body})", self.definer)
}
fn parameter_names(&self) -> Vec<String> {
(0..self.placeholders)
.map(|position| char::from(b'a' + position as u8).to_string())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::{ExpandProblems, SetAsideInput};
use crate::core::Namespace;
use crate::diagnostic::{Diagnostic, DiagnosticCode, Label, MissingName};
use crate::source::{SourceId, Span};
struct SetAsideFixture;
impl SetAsideFixture {
fn title(names: &[&str]) -> String {
SetAsideInput::new(names, "<input 2>", "<input 4>").title()
}
}
#[test]
fn an_input_that_bound_no_name_is_named_by_its_source() {
assert_eq!(
SetAsideFixture::title(&[]),
"set aside `<input 2>` after `<input 4>`"
);
}
#[test]
fn the_title_lists_one_name() {
assert_eq!(
SetAsideFixture::title(&["karl"]),
"set aside `karl` after `<input 4>`"
);
}
#[test]
fn the_title_lists_three_names() {
assert_eq!(
SetAsideFixture::title(&["a", "b", "c"]),
"set aside `a`, `b` and `c` after `<input 4>`"
);
}
#[test]
fn the_title_counts_the_names_past_three() {
assert_eq!(
SetAsideFixture::title(&["a", "b", "c", "d", "e"]),
"set aside `a`, `b`, `c` and 2 more after `<input 4>`"
);
}
#[test]
fn a_set_aside_input_keeps_the_quoted_form_of_a_help() {
let span = Span::new(SourceId::new(0), 0..4);
let error = Diagnostic::error(
DiagnosticCode::UNBOUND_NAME,
"a title",
Label::new(span, "a label"),
)
.with_quoting_help("bind it: `{form}`", span);
let input = SetAsideInput::new(&["karl"], "<input 2>", "<input 4>");
let warning = ExpandProblems::set_aside(&input, &error);
assert_eq!(warning.helps()[0].quoted_form(), Some(span));
}
#[test]
fn an_unbound_name_carries_the_name_and_its_namespace() {
let span = Span::new(SourceId::new(0), 0..4);
let diagnostic = ExpandProblems::unbound("user", Namespace::Type, span, None);
assert_eq!(
diagnostic.missing_name(),
Some(&MissingName {
name: "user".into(),
namespace: Namespace::Type,
})
);
}
}