use indexmap::IndexMap;
use nova_ast::item::{Attribute, ExternItem, Import, ImportKind, TypeDef};
use nova_ast::{File, Function, Item, Type};
use nova_diagnostics::{suggest, Diagnostic, Edit, FileId, Fix, Span};
use rustc_hash::FxHashMap;
pub mod index;
pub use index::{Index, LocalFlags, Occurrence, Role, Target};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DefId(pub u32);
macro_rules! builtins {
($($(#[$doc:meta])* $variant:ident),+ $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Builtin {
$($(#[$doc])* $variant,)+
}
impl Builtin {
pub const ALL: [Builtin; builtins!(@count $($variant)+)] = [
$(Builtin::$variant),+
];
}
};
(@count $($v:ident)+) => {
[$(builtins!(@one $v)),+].len()
};
(@one $v:ident) => { () };
}
builtins! {
Println,
Print,
EPrint,
EPrintln,
Panic,
StrCmp,
StrHash,
CharToInt,
StrLenChars,
StrChars,
StrFromChars,
StrJoin,
StrIndexOf,
JsonQuote,
StrToUpper,
StrToLower,
FloatFixed,
StrToFloat,
TestSelector,
TaskSpawn,
TaskIsDone,
TaskRelease,
TaskDrive,
TaskOutput,
TaskYieldFuture,
TaskSleepFutureNanos,
TaskJoinFuture,
TaskTimeoutFuture,
FsReadToString,
FsWriteString,
FsTakeString,
FsLastErrorMessage,
FsTempDir,
FsExists,
FsCreateDir,
FsCreateDirAll,
FsRemoveFile,
FsRemoveDirAll,
FsReadDir,
FsTakeStringArray,
FsKind,
FsRead,
FsTakeBytes,
FsWrite,
FileOpen,
FileClose,
FileRead,
FileWrite,
FileFlush,
IoStdinRead,
IoStdoutWrite,
IoStderrWrite,
IoStdoutFlush,
IoStderrFlush,
NetConnect,
NetClose,
NetRead,
NetWrite,
NetReadTimeout,
NetListen,
NetLocalPort,
NetAccept,
BytesLen,
BytesFromString,
BytesIsUtf8,
BytesToStringUnchecked,
BytesAt,
BytesSlice,
BytesConcat,
BytesToInts,
BytesFromInts,
BytesEq,
IntHashSeed,
HttpParseRequest,
CryptoHash,
CryptoRandomBytes,
CryptoRandomInt,
TimeNowNanos,
TimeNowEpochNanos,
LogConfigLevel,
LogConfigToStderr,
LogSetConfig,
ShutdownRequested,
ProcessArgCount,
ProcessArg,
ProcessExit,
}
impl Builtin {
pub fn name(self) -> &'static str {
match self {
Builtin::Println => "println",
Builtin::Print => "print",
Builtin::EPrint => "eprint",
Builtin::EPrintln => "eprintln",
Builtin::Panic => "panic",
Builtin::StrCmp => "str_cmp",
Builtin::StrHash => "str_hash",
Builtin::CharToInt => "char_to_int",
Builtin::StrLenChars => "str_len_chars",
Builtin::StrChars => "str_chars",
Builtin::StrFromChars => "str_from_chars",
Builtin::StrJoin => "str_join",
Builtin::StrIndexOf => "str_index_of",
Builtin::JsonQuote => "json_quote",
Builtin::StrToUpper => "str_to_upper",
Builtin::StrToLower => "str_to_lower",
Builtin::FloatFixed => "float_fixed",
Builtin::StrToFloat => "str_to_float",
Builtin::TestSelector => "test_selector",
Builtin::TaskSpawn => "task_spawn",
Builtin::TaskIsDone => "task_is_done",
Builtin::TaskRelease => "task_release",
Builtin::TaskDrive => "task_drive",
Builtin::TaskOutput => "task_output",
Builtin::TaskYieldFuture => "task_yield_future",
Builtin::TaskSleepFutureNanos => "task_sleep_future_nanos",
Builtin::TaskJoinFuture => "task_join_future",
Builtin::TaskTimeoutFuture => "task_timeout_future",
Builtin::FsReadToString => "fs_read_to_string",
Builtin::FsWriteString => "fs_write_string",
Builtin::FsTakeString => "fs_take_string",
Builtin::FsLastErrorMessage => "fs_last_error_message",
Builtin::FsTempDir => "fs_temp_dir",
Builtin::FsExists => "fs_exists",
Builtin::FsCreateDir => "fs_create_dir",
Builtin::FsCreateDirAll => "fs_create_dir_all",
Builtin::FsRemoveFile => "fs_remove_file",
Builtin::FsRemoveDirAll => "fs_remove_dir_all",
Builtin::FsReadDir => "fs_read_dir",
Builtin::FsTakeStringArray => "fs_take_string_array",
Builtin::FsKind => "fs_kind",
Builtin::FsRead => "fs_read",
Builtin::FsTakeBytes => "fs_take_bytes",
Builtin::FsWrite => "fs_write",
Builtin::FileOpen => "file_open",
Builtin::FileClose => "file_close",
Builtin::FileRead => "file_read",
Builtin::FileWrite => "file_write",
Builtin::FileFlush => "file_flush",
Builtin::IoStdinRead => "io_stdin_read",
Builtin::IoStdoutWrite => "io_stdout_write",
Builtin::IoStderrWrite => "io_stderr_write",
Builtin::IoStdoutFlush => "io_stdout_flush",
Builtin::IoStderrFlush => "io_stderr_flush",
Builtin::NetConnect => "net_connect",
Builtin::NetClose => "net_close",
Builtin::NetRead => "net_read",
Builtin::NetWrite => "net_write",
Builtin::NetReadTimeout => "net_read_timeout",
Builtin::NetListen => "net_listen",
Builtin::NetLocalPort => "net_local_port",
Builtin::NetAccept => "net_accept",
Builtin::BytesLen => "bytes_len",
Builtin::BytesFromString => "bytes_from_string_intrinsic",
Builtin::BytesIsUtf8 => "bytes_is_utf8",
Builtin::BytesToStringUnchecked => "bytes_to_string_unchecked",
Builtin::BytesAt => "bytes_at",
Builtin::BytesSlice => "bytes_slice",
Builtin::BytesConcat => "bytes_concat",
Builtin::BytesToInts => "bytes_to_ints",
Builtin::BytesFromInts => "bytes_from_ints_intrinsic",
Builtin::BytesEq => "bytes_eq",
Builtin::IntHashSeed => "int_hash_seed",
Builtin::HttpParseRequest => "http_parse_request",
Builtin::CryptoHash => "crypto_hash",
Builtin::CryptoRandomBytes => "crypto_random_bytes",
Builtin::CryptoRandomInt => "crypto_random_int",
Builtin::TimeNowNanos => "time_now_nanos",
Builtin::TimeNowEpochNanos => "time_now_epoch_nanos",
Builtin::LogConfigLevel => "log_config_level",
Builtin::LogConfigToStderr => "log_config_to_stderr",
Builtin::LogSetConfig => "log_set_config",
Builtin::ShutdownRequested => "shutdown_requested",
Builtin::ProcessArgCount => "process_arg_count",
Builtin::ProcessArg => "process_arg",
Builtin::ProcessExit => "process_exit",
}
}
pub const GLOBAL: [Builtin; 5] = [
Builtin::Println,
Builtin::Print,
Builtin::EPrint,
Builtin::EPrintln,
Builtin::Panic,
];
pub const STD_ONLY: [Builtin; 81] = [
Builtin::StrCmp,
Builtin::StrHash,
Builtin::CharToInt,
Builtin::StrLenChars,
Builtin::StrChars,
Builtin::StrFromChars,
Builtin::StrJoin,
Builtin::StrIndexOf,
Builtin::JsonQuote,
Builtin::StrToUpper,
Builtin::StrToLower,
Builtin::FloatFixed,
Builtin::StrToFloat,
Builtin::TestSelector,
Builtin::TaskSpawn,
Builtin::TaskIsDone,
Builtin::TaskRelease,
Builtin::TaskDrive,
Builtin::TaskOutput,
Builtin::TaskYieldFuture,
Builtin::TaskSleepFutureNanos,
Builtin::TaskJoinFuture,
Builtin::TaskTimeoutFuture,
Builtin::FsReadToString,
Builtin::FsWriteString,
Builtin::FsTakeString,
Builtin::FsLastErrorMessage,
Builtin::FsTempDir,
Builtin::FsExists,
Builtin::FsCreateDir,
Builtin::FsCreateDirAll,
Builtin::FsRemoveFile,
Builtin::FsRemoveDirAll,
Builtin::FsReadDir,
Builtin::FsTakeStringArray,
Builtin::FsKind,
Builtin::FsRead,
Builtin::FsTakeBytes,
Builtin::FsWrite,
Builtin::FileOpen,
Builtin::FileClose,
Builtin::FileRead,
Builtin::FileWrite,
Builtin::FileFlush,
Builtin::IoStdinRead,
Builtin::IoStdoutWrite,
Builtin::IoStderrWrite,
Builtin::IoStdoutFlush,
Builtin::IoStderrFlush,
Builtin::NetConnect,
Builtin::NetClose,
Builtin::NetRead,
Builtin::NetWrite,
Builtin::NetReadTimeout,
Builtin::NetListen,
Builtin::NetLocalPort,
Builtin::NetAccept,
Builtin::BytesLen,
Builtin::BytesFromString,
Builtin::BytesIsUtf8,
Builtin::BytesToStringUnchecked,
Builtin::BytesAt,
Builtin::BytesSlice,
Builtin::BytesConcat,
Builtin::BytesToInts,
Builtin::BytesFromInts,
Builtin::BytesEq,
Builtin::IntHashSeed,
Builtin::HttpParseRequest,
Builtin::CryptoHash,
Builtin::CryptoRandomBytes,
Builtin::CryptoRandomInt,
Builtin::TimeNowNanos,
Builtin::TimeNowEpochNanos,
Builtin::LogConfigLevel,
Builtin::LogConfigToStderr,
Builtin::LogSetConfig,
Builtin::ShutdownRequested,
Builtin::ProcessArgCount,
Builtin::ProcessArg,
Builtin::ProcessExit,
];
}
pub const RESERVED_TYPE_NAMES: [&str; 7] =
["Int", "Float", "Bool", "Char", "String", "Future", "Bytes"];
#[derive(Debug, Clone)]
pub enum DefKind {
Fn { item_index: usize },
Sum {
item_index: usize,
variants: Vec<VariantDef>,
},
Record { item_index: usize },
Const { item_index: usize },
Trait { item_index: usize },
Method {
item_index: usize,
method_index: usize,
owner: MethodOwner,
},
ExternFn { item_index: usize, fn_index: usize },
AssocType { trait_def: DefId },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodOwner {
Impl,
TraitDefault,
}
#[derive(Debug, Clone)]
pub struct VariantDef {
pub name: String,
pub span: Span,
pub arity: usize,
}
#[derive(Debug, Clone)]
pub struct Def {
pub name: String,
pub span: Span,
pub kind: DefKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Res {
Def(DefId),
Variant(DefId, usize),
Builtin(Builtin),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ModuleId(pub u32);
#[derive(Debug, Default)]
struct ModuleScope {
name: String,
values: FxHashMap<String, Res>,
types: FxHashMap<String, DefId>,
traits: FxHashMap<String, DefId>,
}
#[derive(Debug, Default)]
pub struct Definitions {
defs: Vec<Def>,
modules: Vec<ModuleScope>,
item_module: Vec<u32>,
exports: Vec<Exports>,
importable: Vec<Vec<(String, ModuleId)>>,
packages: Vec<Option<u32>>,
std_start: usize,
}
impl Definitions {
pub fn defs(&self) -> &[Def] {
&self.defs
}
pub fn def(&self, id: DefId) -> &Def {
&self.defs[id.0 as usize]
}
pub fn module_of(&self, item_index: usize) -> ModuleId {
ModuleId(self.item_module.get(item_index).copied().unwrap_or(0))
}
pub fn module_count(&self) -> usize {
self.modules.len()
}
pub fn module_name(&self, module: ModuleId) -> &str {
self.modules
.get(module.0 as usize)
.map(|m| m.name.as_str())
.unwrap_or("")
}
pub fn resolve_value(&self, module: ModuleId, name: &str) -> Option<Res> {
self.modules
.get(module.0 as usize)
.and_then(|m| m.values.get(name).copied())
}
pub fn resolve_type(&self, module: ModuleId, name: &str) -> Option<DefId> {
self.modules
.get(module.0 as usize)
.and_then(|m| m.types.get(name).copied())
}
pub fn resolve_trait(&self, module: ModuleId, name: &str) -> Option<DefId> {
self.modules
.get(module.0 as usize)
.and_then(|m| m.traits.get(name).copied())
}
pub fn methods(&self) -> impl Iterator<Item = (DefId, usize, usize, MethodOwner)> + '_ {
self.defs
.iter()
.enumerate()
.filter_map(|(i, d)| match d.kind {
DefKind::Method {
item_index,
method_index,
owner,
} => Some((DefId(i as u32), item_index, method_index, owner)),
_ => None,
})
}
pub fn functions(&self) -> impl Iterator<Item = (DefId, usize)> + '_ {
self.defs
.iter()
.enumerate()
.filter_map(|(i, d)| match d.kind {
DefKind::Fn { item_index } => Some((DefId(i as u32), item_index)),
_ => None,
})
}
pub fn extern_functions(&self) -> impl Iterator<Item = (DefId, usize, usize)> + '_ {
self.defs
.iter()
.enumerate()
.filter_map(|(i, d)| match d.kind {
DefKind::ExternFn {
item_index,
fn_index,
} => Some((DefId(i as u32), item_index, fn_index)),
_ => None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeEntry {
Value(Res),
Type(DefId),
Trait(DefId),
}
impl Definitions {
pub fn names_in_scope(&self, module: ModuleId) -> Vec<(String, ScopeEntry)> {
let Some(scope) = self.modules.get(module.0 as usize) else {
return Vec::new();
};
let mut out: Vec<(String, ScopeEntry)> = Vec::new();
out.extend(
scope
.values
.iter()
.map(|(n, r)| (n.clone(), ScopeEntry::Value(*r))),
);
out.extend(
scope
.types
.iter()
.map(|(n, d)| (n.clone(), ScopeEntry::Type(*d))),
);
out.extend(
scope
.traits
.iter()
.map(|(n, d)| (n.clone(), ScopeEntry::Trait(*d))),
);
let rank = |e: &ScopeEntry| match e {
ScopeEntry::Value(_) => 0,
ScopeEntry::Type(_) => 1,
ScopeEntry::Trait(_) => 2,
};
out.sort_by(|a, b| a.0.cmp(&b.0).then(rank(&a.1).cmp(&rank(&b.1))));
out
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Exported {
pub value: Option<Res>,
pub ty: Option<DefId>,
pub trait_def: Option<DefId>,
}
impl Definitions {
pub fn importable(&self, module: ModuleId) -> &[(String, ModuleId)] {
self.importable
.get(module.0 as usize)
.map_or(&[], Vec::as_slice)
}
pub fn exported(&self, module: ModuleId, name: &str) -> Exported {
let Some(e) = self.exports.get(module.0 as usize) else {
return Exported::default();
};
Exported {
value: e.values.get(name).copied(),
ty: e.types.get(name).copied(),
trait_def: e.traits.get(name).copied(),
}
}
pub fn same_package(&self, a: ModuleId, b: ModuleId) -> bool {
self.packages.get(a.0 as usize) == self.packages.get(b.0 as usize)
}
pub fn bound_outside_std(&self, module: ModuleId, name: &str) -> Exported {
let Some(scope) = self.modules.get(module.0 as usize) else {
return Exported::default();
};
let std = &self.exports[self.std_start.min(self.exports.len())..];
Exported {
value: scope
.values
.get(name)
.copied()
.filter(|r| !std.iter().any(|e| e.values.get(name) == Some(r))),
ty: scope
.types
.get(name)
.copied()
.filter(|d| !std.iter().any(|e| e.types.get(name) == Some(d))),
trait_def: scope
.traits
.get(name)
.copied()
.filter(|d| !std.iter().any(|e| e.traits.get(name) == Some(d))),
}
}
pub fn is_std_module(&self, module: ModuleId) -> bool {
let m = module.0 as usize;
m >= self.std_start && m < self.modules.len()
}
pub fn owned(&self, module: ModuleId) -> bool {
!self.is_std_module(module)
&& matches!(self.packages.get(module.0 as usize), Some(None | Some(0)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportTarget {
Module(usize),
Reported,
}
pub struct ModuleSource<'a> {
pub name: String,
pub file: &'a File,
pub imports: std::collections::HashMap<String, ImportTarget>,
pub text: Option<&'a str>,
pub package: Option<u32>,
pub importable: Vec<(String, usize)>,
}
impl<'a> ModuleSource<'a> {
pub fn new(name: impl Into<String>, file: &'a File) -> Self {
ModuleSource {
name: name.into(),
file,
imports: std::collections::HashMap::new(),
text: None,
package: None,
importable: Vec::new(),
}
}
}
pub fn name_imports(modules: &mut [ModuleSource]) {
let names: Vec<String> = modules.iter().map(|m| m.name.clone()).collect();
for (i, module) in modules.iter_mut().enumerate() {
for item in &module.file.items {
let Item::Import(import) = &item.value else {
continue;
};
let Some(first) = import.path.value.segments.first() else {
continue;
};
if let Some(index) = names.iter().position(|n| *n == first.value) {
module
.imports
.insert(first.value.clone(), ImportTarget::Module(index));
}
}
module.importable = names
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(j, n)| (n.clone(), j))
.collect();
}
}
#[derive(Debug, Default)]
struct Exports {
values: FxHashMap<String, Res>,
types: FxHashMap<String, DefId>,
traits: FxHashMap<String, DefId>,
}
fn push_def(defs: &mut Vec<Def>, def: Def) -> DefId {
let id = DefId(defs.len() as u32);
defs.push(def);
id
}
#[derive(Debug, Clone)]
pub struct TestFn {
pub def_id: DefId,
pub name: String,
pub should_panic: bool,
}
#[derive(Debug)]
pub struct ResolveResult {
pub definitions: Definitions,
pub file: File,
pub diagnostics: Vec<Diagnostic>,
pub tests: Vec<TestFn>,
}
pub struct ProgramResolution {
pub file: File,
pub definitions: Definitions,
pub diagnostics: Vec<Diagnostic>,
pub tests: Vec<TestFn>,
pub imports: Vec<Occurrence>,
}
pub fn resolve(file: &File) -> ResolveResult {
let mut sources = [ModuleSource::new("main", file)];
name_imports(&mut sources);
let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
let prog = resolve_program(&sources, &std_files, None);
ResolveResult {
definitions: prog.definitions,
file: prog.file,
diagnostics: prog.diagnostics,
tests: prog.tests,
}
}
pub fn resolve_program(
modules: &[ModuleSource],
std_files: &[FileId],
extra_std: Option<((&str, &str), FileId)>,
) -> ProgramResolution {
let mut definitions = Definitions::default();
let mut diagnostics = Vec::new();
let mut merged = Vec::new();
let mut tests = Vec::new();
let mut imports: Vec<Occurrence> = Vec::new();
let std_entries: Vec<(&str, &str)> = STD_MODULES
.iter()
.copied()
.chain(extra_std.map(|(entry, _)| entry))
.collect();
let std_file_ids: Vec<FileId> = std_files
.iter()
.copied()
.chain(extra_std.map(|(_, file_id)| file_id))
.collect();
let std_modules: Vec<Option<File>> = std_entries
.iter()
.zip(std_file_ids.iter())
.map(|(&(_, src), &file_id)| {
let (ast, diags) = std_module(src, file_id);
diagnostics.extend(diags);
ast
})
.collect();
let user_count = modules.len();
let all: Vec<ModuleSource> = modules
.iter()
.map(|m| ModuleSource {
name: m.name.clone(),
file: m.file,
imports: m.imports.clone(),
text: m.text,
package: m.package,
importable: m.importable.clone(),
})
.chain(
std_entries
.iter()
.zip(std_modules.iter())
.filter_map(|(&(name, _), file)| {
file.as_ref()
.map(|file| ModuleSource::new(name.to_string(), file))
}),
)
.collect();
let std_start = user_count;
let is_std_module = |mid: usize| mid >= std_start;
let mut exports: Vec<Exports> = Vec::new();
for (mid, m) in all.iter().enumerate() {
let mut scope = ModuleScope {
name: m.name.clone(),
..Default::default()
};
for b in Builtin::GLOBAL {
scope.values.insert(b.name().to_string(), Res::Builtin(b));
}
if is_std_module(mid) {
for b in Builtin::STD_ONLY {
scope.values.insert(b.name().to_string(), Res::Builtin(b));
}
}
definitions.modules.push(scope);
exports.push(Exports::default());
}
let owned = |mid: usize| mid < std_start && matches!(all[mid].package, None | Some(0));
for (mid, m) in all.iter().enumerate() {
let mut first_value: IndexMap<String, Span> = IndexMap::new();
let mut first_type: IndexMap<String, Span> = IndexMap::new();
let before = diagnostics.len();
for item in &m.file.items {
let item_index = merged.len();
definitions.item_module.push(mid as u32);
collect_item(
&mut definitions.defs,
&mut definitions.modules[mid],
&mut exports[mid],
&mut first_value,
&mut first_type,
&mut diagnostics,
&mut tests,
item_index,
&item.value,
);
merged.push(item.clone());
}
if !owned(mid) {
diagnostics[before..]
.iter_mut()
.for_each(|d| d.fixes.clear());
}
}
let mut public_fixes: Vec<(usize, usize, String)> = Vec::new();
for (mid, m) in all.iter().enumerate() {
let before = diagnostics.len();
for item in &m.file.items {
if let Item::Import(imp) = &item.value {
resolve_import(
&mut definitions,
&exports,
&all,
mid,
imp,
&mut diagnostics,
&mut imports,
&mut public_fixes,
);
}
}
if !owned(mid) {
diagnostics[before..]
.iter_mut()
.for_each(|d| d.fixes.clear());
}
}
for (at, target, name) in public_fixes {
let clash = all.iter().enumerate().any(|(g, m)| {
g != target && would_receive(m, target, &name) && binds(&definitions.modules[g], &name)
});
if clash {
diagnostics[at].fixes.clear();
}
}
for (mid, std_exports) in exports.iter().enumerate().skip(std_start) {
import_std_module(&mut definitions, std_exports, mid);
}
definitions.importable = all
.iter()
.map(|m| {
m.importable
.iter()
.map(|(n, i)| (n.clone(), ModuleId(*i as u32)))
.collect()
})
.collect();
definitions.packages = all.iter().map(|m| m.package).collect();
definitions.std_start = std_start;
definitions.exports = exports;
ProgramResolution {
file: File { items: merged },
definitions,
diagnostics,
tests,
imports,
}
}
pub const STD_MODULES: [(&str, &str); 16] = [
("$std.core", nova_std::CORE),
("$std.bytes", nova_std::BYTES),
("$std.io", nova_std::IO),
("$std.fs", nova_std::FS),
("$std.collections", nova_std::COLLECTIONS),
("$std.strings", nova_std::STRINGS),
("$std.fmt", nova_std::FMT),
("$std.task", nova_std::TASK),
("$std.sync", nova_std::SYNC),
("$std.net", nova_std::NET),
("$std.time", nova_std::TIME),
("$std.json", nova_std::JSON),
("$std.log", nova_std::LOG),
("$std.http", nova_std::HTTP),
("$std.crypto", nova_std::CRYPTO),
("$std.process", nova_std::PROCESS),
];
pub const STD_TEST_MODULE: (&str, &str) = ("$std.test", nova_std::TEST);
fn std_module(src: &str, file_id: FileId) -> (Option<File>, Vec<Diagnostic>) {
let (tokens, lex_errors) = nova_lexer::lex(src, file_id);
let mut diags: Vec<Diagnostic> = lex_errors
.iter()
.map(|e| Diagnostic::error("L0001", e.to_string()).with_primary_label(e.span(), "here"))
.collect();
let (ast, parse_errors) = nova_parser::parse(&tokens, file_id);
diags.extend(
parse_errors.iter().map(|e| {
Diagnostic::error("P0001", e.to_string()).with_primary_label(e.span(), "here")
}),
);
(ast, diags)
}
fn import_std_module(definitions: &mut Definitions, std_exports: &Exports, std_mid: usize) {
let values: Vec<(String, Res)> = std_exports
.values
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
let types: Vec<(String, DefId)> = std_exports
.types
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
let traits: Vec<(String, DefId)> = std_exports
.traits
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
for mid in 0..definitions.modules.len() {
if mid == std_mid {
continue;
}
let scope = &mut definitions.modules[mid];
for (n, r) in &values {
scope.values.entry(n.clone()).or_insert(*r);
}
for (n, id) in &types {
scope.types.entry(n.clone()).or_insert(*id);
}
for (n, id) in &traits {
scope.traits.entry(n.clone()).or_insert(*id);
}
}
}
#[allow(clippy::too_many_arguments)]
fn collect_item(
defs: &mut Vec<Def>,
scope: &mut ModuleScope,
exp: &mut Exports,
first_value: &mut IndexMap<String, Span>,
first_type: &mut IndexMap<String, Span>,
diagnostics: &mut Vec<Diagnostic>,
tests: &mut Vec<TestFn>,
item_index: usize,
item: &Item,
) {
match item {
Item::Function(f) => {
let name = f.name.value.clone();
let span = f.name.span;
let id = push_def(
defs,
Def {
name: name.clone(),
span,
kind: DefKind::Fn { item_index },
},
);
insert_value(
scope,
exp,
first_value,
diagnostics,
name,
span,
Res::Def(id),
is_pub(f.vis),
);
validate_test_function(f, id, diagnostics, tests);
}
Item::Type(t) => {
validate_attrs_reject_test(&t.attrs, "a type declaration", diagnostics);
match &t.def {
TypeDef::Sum(variants) => {
let name = t.name.value.clone();
let span = t.name.span;
if reject_reserved_type_name(diagnostics, &name, span) {
return;
}
let pubv = is_pub(t.vis);
let variant_defs: Vec<VariantDef> = variants
.iter()
.map(|v| VariantDef {
name: v.name.value.clone(),
span: v.name.span,
arity: v.fields.len(),
})
.collect();
let id = push_def(
defs,
Def {
name: name.clone(),
span,
kind: DefKind::Sum {
item_index,
variants: variant_defs,
},
},
);
insert_type(scope, exp, first_type, diagnostics, name, span, id, pubv);
for (vi, v) in variants.iter().enumerate() {
insert_value(
scope,
exp,
first_value,
diagnostics,
v.name.value.clone(),
v.name.span,
Res::Variant(id, vi),
pubv,
);
}
}
TypeDef::Alias(_) => {
diagnostics.push(unsupported(
t.name.span,
"type aliases are not supported yet in the Phase 1 compiler",
));
}
}
}
Item::Const(c) => {
validate_attrs_reject_test(&c.attrs, "a const", diagnostics);
let name = c.name.value.clone();
let span = c.name.span;
let id = push_def(
defs,
Def {
name: name.clone(),
span,
kind: DefKind::Const { item_index },
},
);
insert_value(
scope,
exp,
first_value,
diagnostics,
name,
span,
Res::Def(id),
is_pub(c.vis),
);
}
Item::Record(r) => {
validate_attrs_reject_test(&r.attrs, "a record", diagnostics);
let name = r.name.value.clone();
let span = r.name.span;
if reject_reserved_type_name(diagnostics, &name, span) {
return;
}
let id = push_def(
defs,
Def {
name: name.clone(),
span,
kind: DefKind::Record { item_index },
},
);
insert_type(
scope,
exp,
first_type,
diagnostics,
name,
span,
id,
is_pub(r.vis),
);
}
Item::Trait(t) => {
validate_attrs_reject_test(&t.attrs, "a trait", diagnostics);
let name = t.name.value.clone();
let span = t.name.span;
let pubv = is_pub(t.vis);
let id = push_def(
defs,
Def {
name: name.clone(),
span,
kind: DefKind::Trait { item_index },
},
);
if scope.traits.contains_key(&name) {
diagnostics.push(
Diagnostic::error("E0002", format!("duplicate trait `{name}`"))
.with_primary_label(span, "redefined here"),
);
} else {
scope.traits.insert(name.clone(), id);
if pubv {
exp.traits.insert(name, id);
}
}
for (method_index, ti) in t.items.iter().enumerate() {
match ti {
nova_ast::item::TraitItem::Provided(f) => {
push_def(
defs,
Def {
name: format!("{}::{}$default", t.name.value, f.name.value),
span: f.name.span,
kind: DefKind::Method {
item_index,
method_index,
owner: MethodOwner::TraitDefault,
},
},
);
}
nova_ast::item::TraitItem::AssocType { name: at_name, .. } => {
push_def(
defs,
Def {
name: at_name.value.clone(),
span: at_name.span,
kind: DefKind::AssocType { trait_def: id },
},
);
}
nova_ast::item::TraitItem::Required(_) => {}
}
}
}
Item::Impl(i) => {
validate_attrs_reject_test(&i.attrs, "an impl block", diagnostics);
let self_name = type_full_name(&i.ty.value);
for (method_index, f) in i.functions.iter().enumerate() {
let mangled = match &i.trait_ {
Some(tr) => format!("{}.{}.{}", self_name, path_tail(&tr.value), f.name.value),
None => format!("{}.{}", self_name, f.name.value),
};
push_def(
defs,
Def {
name: mangled,
span: f.name.span,
kind: DefKind::Method {
item_index,
method_index,
owner: MethodOwner::Impl,
},
},
);
}
}
Item::Extern(block) => {
validate_attrs_reject_test(&block.attrs, "an extern block", diagnostics);
for (fn_index, ext_item) in block.items.iter().enumerate() {
let ExternItem::Fn(sig) = ext_item;
let name = sig.name.value.clone();
let span = sig.name.span;
let id = push_def(
defs,
Def {
name: name.clone(),
span,
kind: DefKind::ExternFn {
item_index,
fn_index,
},
},
);
insert_value(
scope,
exp,
first_value,
diagnostics,
name,
span,
Res::Def(id),
false,
);
}
}
Item::Import(imp) => {
validate_attrs_reject_test(&imp.attrs, "an import", diagnostics);
}
Item::Module(m) => {
validate_attrs_reject_test(&m.attrs, "a module declaration", diagnostics);
}
}
}
fn reject_reserved_type_name(diagnostics: &mut Vec<Diagnostic>, name: &str, span: Span) -> bool {
if !RESERVED_TYPE_NAMES.contains(&name) {
return false;
}
diagnostics.push(
Diagnostic::error(
"E0089",
format!(
"`{name}` is already a built-in type; a type declared under \
this name could never be referred to in a type annotation, \
since `{name}` in a type position never resolves to a \
declaration under this name"
),
)
.with_primary_label(span, "redeclares a built-in type name"),
);
true
}
const KNOWN_ATTRIBUTES: &[&str] = &["test"];
const KNOWN_TEST_ARGS: &[&str] = &["should_panic"];
const TEST_ASYNC_NOTE: &str = "an `async fn` returns a future, and a `@test` function's result is \
discarded rather than awaited, so an `async` test's body would never run; \
write the test as a synchronous one that drives the future itself, e.g. \
`@test fn t() { assert_eq(block_on(f()), 42) }` โ `block_on` runs `f()`'s \
future to completion and produces its value";
fn unknown_attribute(attr: &Attribute) -> Diagnostic {
Diagnostic::error(
"E0082",
format!(
"unknown attribute `@{}`; known attributes are: {}",
attr.name.value,
KNOWN_ATTRIBUTES.join(", "),
),
)
.with_primary_label(attr.name.span, "unknown attribute")
}
fn validate_attrs_reject_test(attrs: &[Attribute], kind: &str, diagnostics: &mut Vec<Diagnostic>) {
for attr in attrs {
if attr.name.value == "test" {
diagnostics.push(
Diagnostic::error(
"E0083",
format!("`@test` can only be applied to a function, not {kind}"),
)
.with_primary_label(attr.name.span, "not a function"),
);
} else if !KNOWN_ATTRIBUTES.contains(&attr.name.value.as_str()) {
diagnostics.push(unknown_attribute(attr));
}
}
}
fn validate_test_function(
f: &Function,
def_id: DefId,
diagnostics: &mut Vec<Diagnostic>,
tests: &mut Vec<TestFn>,
) {
let mut collected = false;
for attr in &f.attrs {
if attr.name.value != "test" {
let mut d = unknown_attribute(attr);
if let Some(fix) = suggest::change_to(&attr.name, KNOWN_ATTRIBUTES.iter().copied()) {
d = d.with_fix(fix);
}
diagnostics.push(d);
continue;
}
let mut well_formed = true;
let mut should_panic = false;
for arg in &attr.args {
if arg.value == "should_panic" {
should_panic = true;
} else {
let mut d = Diagnostic::error(
"E0085",
format!(
"unknown `@test` argument `{}`; the accepted arguments are: {}",
arg.value,
KNOWN_TEST_ARGS.join(", "),
),
)
.with_primary_label(arg.span, "unknown argument");
if let Some(fix) = suggest::change_to(arg, KNOWN_TEST_ARGS.iter().copied()) {
d = d.with_fix(fix);
}
diagnostics.push(d);
well_formed = false;
}
}
let violations = test_shape_violations(f);
if !violations.is_empty() {
let names: Vec<&str> = violations.iter().map(|(name, _)| *name).collect();
let mut diag = Diagnostic::error(
"E0084",
format!("a `@test` function may not have {}", names.join(", ")),
)
.with_primary_label(f.name.span, "invalid test signature");
for (name, span) in &violations {
if let Some(span) = span {
diag = diag.with_secondary_label(*span, format!("{name} here"));
}
}
if f.is_async {
diag = diag.with_note(TEST_ASYNC_NOTE);
}
diagnostics.push(diag);
well_formed = false;
}
if well_formed && !collected {
tests.push(TestFn {
def_id,
name: f.name.value.clone(),
should_panic,
});
collected = true;
}
}
}
fn test_shape_violations(f: &Function) -> Vec<(&'static str, Option<Span>)> {
let mut violations = Vec::new();
if f.is_async {
violations.push(("an `async` body", None));
}
if let (Some(first), Some(last)) = (f.params.first(), f.params.last()) {
violations.push(("parameters", Some(first.name.span.merge(last.ty.span))));
}
if let (Some(first), Some(last)) = (f.generics.first(), f.generics.last()) {
violations.push((
"generic parameters",
Some(first.name.span.merge(last.name.span)),
));
}
if let Some(rt) = &f.return_ty {
if !matches!(&rt.value, Type::Tuple(items) if items.is_empty()) {
violations.push(("a non-Unit return type", Some(rt.span)));
}
}
violations
}
#[allow(clippy::too_many_arguments)]
fn resolve_import(
definitions: &mut Definitions,
exports: &[Exports],
modules: &[ModuleSource],
mid: usize,
imp: &Import,
diagnostics: &mut Vec<Diagnostic>,
occurrences: &mut Vec<Occurrence>,
public_fixes: &mut Vec<(usize, usize, String)>,
) {
let span = imp.path.span;
if imp.path.value.segments.len() > 1 {
diagnostics.push(unsupported(
span,
"qualified or nested import paths (`a::b`) are not supported yet; \
import a top-level module by its name",
));
return;
}
let target_name = imp
.path
.value
.segments
.last()
.map(|s| s.value.as_str())
.unwrap_or("");
let target = match modules[mid].imports.get(target_name) {
Some(ImportTarget::Module(target)) => *target,
Some(ImportTarget::Reported) => return,
None => {
diagnostics.push(
Diagnostic::error("E0001", format!("cannot find module `{target_name}`"))
.with_primary_label(span, "no such module"),
);
return;
}
};
if target == mid {
diagnostics.push(
Diagnostic::error("E0001", format!("module `{target_name}` imports itself"))
.with_primary_label(span, "self-import"),
);
return;
}
if let Some(first) = imp.path.value.segments.first() {
occurrences.push(Occurrence {
span: first.span,
role: Role::Use,
target: Target::Module(ModuleId(target as u32)),
shorthand: false,
});
}
match &imp.kind {
ImportKind::Simple => {
let names: Vec<(String, Res)> = exports[target]
.values
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
for (n, r) in names {
bind_value(&mut definitions.modules[mid], diagnostics, n, span, r);
}
let tys: Vec<(String, DefId)> = exports[target]
.types
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
for (n, id) in tys {
bind_type(&mut definitions.modules[mid], diagnostics, n, span, id);
}
let trs: Vec<(String, DefId)> = exports[target]
.traits
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
for (n, id) in trs {
bind_trait(&mut definitions.modules[mid], diagnostics, n, span, id);
}
}
ImportKind::List(names) => {
for n in names {
let name = &n.value;
let mut found = false;
if let Some(r) = exports[target].values.get(name).copied() {
bind_value(
&mut definitions.modules[mid],
diagnostics,
name.clone(),
n.span,
r,
);
let named = match r {
Res::Def(id) => Target::Def(id),
Res::Variant(sum, i) => Target::Variant(sum, i as u32),
Res::Builtin(b) => Target::Builtin(b),
};
occurrences.push(Occurrence {
span: n.span,
role: Role::Use,
target: named,
shorthand: false,
});
found = true;
}
if let Some(id) = exports[target].types.get(name).copied() {
bind_type(
&mut definitions.modules[mid],
diagnostics,
name.clone(),
n.span,
id,
);
occurrences.push(Occurrence {
span: n.span,
role: Role::Use,
target: Target::Def(id),
shorthand: false,
});
found = true;
}
if let Some(id) = exports[target].traits.get(name).copied() {
bind_trait(
&mut definitions.modules[mid],
diagnostics,
name.clone(),
n.span,
id,
);
occurrences.push(Occurrence {
span: n.span,
role: Role::Use,
target: Target::Def(id),
shorthand: false,
});
found = true;
}
if !found {
let mut diag = Diagnostic::error(
"E0001",
format!("`{name}` is not a public item of module `{target_name}`"),
)
.with_primary_label(n.span, "not found or not `pub`");
let same_package = modules[mid].package == modules[target].package;
if let Some(fix) =
make_public(&modules[target], name, target_name, same_package)
{
diag = diag.with_fix(fix);
public_fixes.push((diagnostics.len(), target, name.clone()));
}
diagnostics.push(diag);
}
}
}
ImportKind::Alias(_) => {
diagnostics.push(unsupported(
span,
"`import ... as` aliases are not supported yet",
));
}
}
}
fn is_pub(vis: nova_ast::item::Visibility) -> bool {
matches!(vis, nova_ast::item::Visibility::Pub)
}
fn make_public(module: &ModuleSource, name: &str, import: &str, same_package: bool) -> Option<Fix> {
if !same_package {
return None;
}
let text = module.text?;
let mut edits = Vec::new();
for item in &module.file.items {
let (vis, item_name) = match &item.value {
Item::Function(f) => (f.vis, &f.name),
Item::Record(r) => (r.vis, &r.name),
Item::Type(t) => (t.vis, &t.name),
Item::Trait(t) => (t.vis, &t.name),
Item::Const(c) => (c.vis, &c.name),
_ => continue,
};
if item_name.value != name || is_pub(vis) {
continue;
}
let at = nova_diagnostics::lines::keyword_start(text, item_name.span.start as usize)?;
edits.push(Edit::insert(at as u32, item_name.span.file, "pub "));
}
(!edits.is_empty()).then(|| Fix::new(format!("make `{name}` public in `{import}`"), edits))
}
fn would_receive(module: &ModuleSource, target: usize, name: &str) -> bool {
module.file.items.iter().any(|item| {
let Item::Import(imp) = &item.value else {
return false;
};
let [first] = imp.path.value.segments.as_slice() else {
return false;
};
if module.imports.get(&first.value) != Some(&ImportTarget::Module(target)) {
return false;
}
match &imp.kind {
ImportKind::Simple => true,
ImportKind::List(names) => names.iter().any(|n| n.value == name),
_ => false,
}
})
}
fn binds(scope: &ModuleScope, name: &str) -> bool {
scope.values.contains_key(name)
|| scope.types.contains_key(name)
|| scope.traits.contains_key(name)
}
#[allow(clippy::too_many_arguments)]
fn insert_value(
scope: &mut ModuleScope,
exp: &mut Exports,
first: &mut IndexMap<String, Span>,
diagnostics: &mut Vec<Diagnostic>,
name: String,
span: Span,
res: Res,
is_pub: bool,
) {
if scope.values.contains_key(&name) {
let mut diag = Diagnostic::error("E0002", format!("duplicate definition of `{name}`"))
.with_primary_label(span, "redefined here");
if let Some(prev) = first.get(&name) {
diag = diag.with_secondary_label(*prev, "first defined here");
} else {
diag = diag.with_note(format!("`{name}` is a compiler builtin"));
}
diagnostics.push(diag);
return;
}
first.insert(name.clone(), span);
if is_pub {
exp.values.insert(name.clone(), res);
}
scope.values.insert(name, res);
}
#[allow(clippy::too_many_arguments)]
fn insert_type(
scope: &mut ModuleScope,
exp: &mut Exports,
first: &mut IndexMap<String, Span>,
diagnostics: &mut Vec<Diagnostic>,
name: String,
span: Span,
id: DefId,
is_pub: bool,
) {
if scope.types.contains_key(&name) {
let mut diag = Diagnostic::error("E0002", format!("duplicate definition of type `{name}`"))
.with_primary_label(span, "redefined here");
if let Some(prev) = first.get(&name) {
diag = diag.with_secondary_label(*prev, "first defined here");
}
diagnostics.push(diag);
return;
}
first.insert(name.clone(), span);
if is_pub {
exp.types.insert(name.clone(), id);
}
scope.types.insert(name, id);
}
fn bind_value(
scope: &mut ModuleScope,
diagnostics: &mut Vec<Diagnostic>,
name: String,
span: Span,
res: Res,
) {
if scope.values.contains_key(&name) {
diagnostics.push(
Diagnostic::error(
"E0002",
format!("`{name}` is already defined or imported in this module"),
)
.with_primary_label(span, "conflicting import"),
);
return;
}
scope.values.insert(name, res);
}
fn bind_type(
scope: &mut ModuleScope,
diagnostics: &mut Vec<Diagnostic>,
name: String,
span: Span,
id: DefId,
) {
if scope.types.contains_key(&name) {
diagnostics.push(
Diagnostic::error(
"E0002",
format!("type `{name}` is already defined or imported in this module"),
)
.with_primary_label(span, "conflicting import"),
);
return;
}
scope.types.insert(name, id);
}
fn bind_trait(
scope: &mut ModuleScope,
diagnostics: &mut Vec<Diagnostic>,
name: String,
span: Span,
id: DefId,
) {
if scope.traits.contains_key(&name) {
diagnostics.push(
Diagnostic::error(
"E0002",
format!("trait `{name}` is already defined or imported in this module"),
)
.with_primary_label(span, "conflicting import"),
);
return;
}
scope.traits.insert(name, id);
}
fn unsupported(span: Span, msg: &str) -> Diagnostic {
Diagnostic::error("E0900", msg).with_primary_label(span, "not supported yet")
}
fn type_full_name(ty: &nova_ast::Type) -> String {
match ty {
nova_ast::Type::Path { path, args } => {
let head = path_tail(path);
if args.is_empty() {
head
} else {
let parts: Vec<String> = args.iter().map(|a| type_full_name(&a.value)).collect();
format!("{head}_{}", parts.join("_"))
}
}
nova_ast::Type::Array(elem) => format!("Arr_{}", type_full_name(&elem.value)),
_ => "anon".to_string(),
}
}
fn path_tail(path: &nova_ast::Path) -> String {
path.segments
.last()
.map(|s| s.value.clone())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use nova_diagnostics::FileId;
use nova_lexer::lex;
use nova_parser::parse;
fn resolve_src(src: &str) -> ResolveResult {
let file_id = FileId::DUMMY;
let (tokens, lex_errors) = lex(src, file_id);
assert!(lex_errors.is_empty(), "lex errors: {lex_errors:?}");
let (ast, parse_errors) = parse(&tokens, file_id);
assert!(parse_errors.is_empty(), "parse errors: {parse_errors:?}");
resolve(&ast.expect("no AST produced"))
}
fn parse_file(src: &str) -> nova_ast::File {
let (tokens, _) = lex(src, FileId::DUMMY);
let (ast, errs) = parse(&tokens, FileId::DUMMY);
assert!(errs.is_empty(), "parse errors: {errs:?}");
ast.expect("no AST")
}
fn resolve_two(main_src: &str, lib_src: &str) -> ProgramResolution {
let main = parse_file(main_src);
let lib = parse_file(lib_src);
let mut sources = [
ModuleSource::new("main", &main),
ModuleSource::new("lib", &lib),
];
name_imports(&mut sources);
let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
resolve_program(&sources, &std_files, None)
}
fn error_codes(diags: &[Diagnostic]) -> Vec<&str> {
diags.iter().map(|d| d.code.as_str()).collect()
}
#[test]
fn an_import_table_can_name_a_module_by_any_label() {
let main = parse_file("import geom::{area}\nfn main() { let a = area() }\n");
let lib = parse_file("pub fn area() -> Int { 1 }\n");
let mut sources = [
ModuleSource::new("main", &main),
ModuleSource::new("geom", &lib),
];
sources[1].name = "a label no import names".to_string();
sources[0]
.imports
.insert("geom".to_string(), ImportTarget::Module(1));
let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
let p = resolve_program(&sources, &std_files, None);
assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
assert!(matches!(
p.definitions.resolve_value(ModuleId(0), "area"),
Some(Res::Def(_))
));
}
#[test]
fn a_reported_import_binds_nothing_and_says_nothing() {
let main = parse_file("import geom\nfn main() {}\n");
let mut sources = [ModuleSource::new("main", &main)];
sources[0]
.imports
.insert("geom".to_string(), ImportTarget::Reported);
let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
let p = resolve_program(&sources, &std_files, None);
assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
}
#[test]
fn an_import_missing_from_the_table_cannot_find_its_module() {
let main = parse_file("import geom\nfn main() {}\n");
let lib = parse_file("pub fn area() -> Int { 1 }\n");
let sources = [
ModuleSource::new("main", &main),
ModuleSource::new("geom", &lib),
];
let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
let p = resolve_program(&sources, &std_files, None);
assert_eq!(error_codes(&p.diagnostics), ["E0001"]);
assert!(p.diagnostics[0]
.message
.contains("cannot find module `geom`"));
}
#[test]
fn import_binds_public_names_across_modules() {
let p = resolve_two(
"import lib::{add}\nfn main() { let x = add(1, 2) }\n",
"pub fn add(a: Int, b: Int) -> Int { a + b }\nfn secret() -> Int { 0 }\n",
);
assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
assert!(matches!(
p.definitions.resolve_value(ModuleId(0), "add"),
Some(Res::Def(_))
));
}
#[test]
fn importing_a_private_item_is_rejected() {
let p = resolve_two(
"import lib::{secret}\nfn main() { }\n",
"pub fn add() -> Int { 0 }\nfn secret() -> Int { 0 }\n",
);
assert!(
error_codes(&p.diagnostics).contains(&"E0001"),
"{:?}",
p.diagnostics
);
}
#[test]
fn dangling_import_reports_missing_module() {
let p = resolve_two(
"import nope::{x}\nfn main() { }\n",
"pub fn add() -> Int { 0 }\n",
);
assert!(
error_codes(&p.diagnostics).contains(&"E0001"),
"{:?}",
p.diagnostics
);
}
#[test]
fn private_item_is_not_visible_to_other_modules() {
let p = resolve_two(
"import lib\nfn main() { }\n",
"pub fn add() -> Int { 0 }\nfn secret() -> Int { 0 }\n",
);
assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
assert!(p.definitions.resolve_value(ModuleId(0), "add").is_some());
assert!(p.definitions.resolve_value(ModuleId(0), "secret").is_none());
}
#[test]
fn same_name_in_two_modules_does_not_collide() {
let p = resolve_two(
"fn helper() -> Int { 1 }\nfn main() { }\n",
"pub fn helper() -> Int { 2 }\n",
);
assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
}
#[test]
fn qualified_import_path_is_rejected() {
let p = resolve_two(
"import lib::extra\nfn main() { }\n",
"pub fn add() -> Int { 0 }\n",
);
assert!(
error_codes(&p.diagnostics).contains(&"E0900"),
"{:?}",
p.diagnostics
);
}
#[test]
fn std_core_option_result_are_in_scope() {
let r = resolve_src("fn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
assert!(r.definitions.resolve_type(ModuleId(0), "Option").is_some());
assert!(r.definitions.resolve_type(ModuleId(0), "Result").is_some());
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "Some"),
Some(Res::Variant(_, 0))
));
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "None"),
Some(Res::Variant(_, 1))
));
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "Ok"),
Some(Res::Variant(_, 0))
));
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "Err"),
Some(Res::Variant(_, 1))
));
}
#[test]
fn extern_fn_resolves_as_callable_extern_def() {
let r = resolve_src("extern \"C\" { fn sqrt(x: Float) -> Float }\nfn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
let Some(Res::Def(id)) = r.definitions.resolve_value(ModuleId(0), "sqrt") else {
panic!("sqrt should resolve to a Def");
};
assert!(matches!(
r.definitions.def(id).kind,
DefKind::ExternFn { .. }
));
}
#[test]
fn user_type_shadows_std_core() {
let r = resolve_src("type Option<T> = | Present(T) | Absent\nfn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
let opt = r
.definitions
.resolve_type(ModuleId(0), "Option")
.expect("Option resolves");
assert!(matches!(
r.definitions.def(opt).kind,
DefKind::Sum { ref variants, .. } if variants.len() == 2 && variants[0].name == "Present"
));
}
#[test]
fn user_trait_shadows_std_core() {
let r = resolve_src("trait Display { fn only_here(self) -> Int }\nfn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
let tr = r
.definitions
.resolve_trait(ModuleId(0), "Display")
.expect("Display resolves");
assert!(matches!(
r.definitions.def(tr).kind,
DefKind::Trait { item_index } if r.definitions.module_of(item_index) == ModuleId(0)
));
}
#[test]
fn associated_type_is_not_in_the_type_namespace() {
let r = resolve_src("trait It { type Item }\nfn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
assert_eq!(r.definitions.resolve_type(ModuleId(0), "Item"), None);
}
#[test]
fn associated_type_is_not_in_the_value_namespace() {
let r = resolve_src("trait It { type Item }\nfn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
assert_eq!(r.definitions.resolve_value(ModuleId(0), "Item"), None);
}
#[test]
fn user_fn_named_str_cmp_is_not_a_reserved_word() {
let r = resolve_src("fn str_cmp(a: Int, b: Int) -> Int { a + b }\nfn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "str_cmp"),
Some(Res::Def(_))
));
}
#[test]
fn no_std_only_builtin_is_a_reserved_word() {
for b in Builtin::STD_ONLY {
let name = b.name();
let src = format!("fn {name}(a: Int, b: Int) -> Int {{ a + b }}\nfn main() {{ }}\n");
let r = resolve_src(&src);
assert!(r.diagnostics.is_empty(), "`{name}`: {:?}", r.diagnostics);
assert!(
matches!(
r.definitions.resolve_value(ModuleId(0), name),
Some(Res::Def(_))
),
"`{name}` should resolve to the user's own definition"
);
}
}
#[test]
fn std_only_builtins_are_visible_inside_std_modules() {
let r = resolve_src("fn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
for mid in 1..=STD_MODULES.len() {
for b in Builtin::STD_ONLY {
assert!(
matches!(
r.definitions.resolve_value(ModuleId(mid as u32), b.name()),
Some(Res::Builtin(seen)) if seen == b
),
"`{}` must resolve to its builtin inside std module {mid}",
b.name()
);
}
}
}
#[test]
fn the_net_builtins_are_std_only() {
for b in [
Builtin::NetConnect,
Builtin::NetClose,
Builtin::NetRead,
Builtin::NetWrite,
Builtin::NetReadTimeout,
Builtin::NetListen,
Builtin::NetLocalPort,
Builtin::NetAccept,
] {
assert!(
Builtin::STD_ONLY.contains(&b),
"{b:?} must be invisible to user code"
);
}
}
#[test]
fn user_fn_named_panic_is_a_reserved_word() {
let r = resolve_src("fn panic(msg: String) { }\nfn main() { }\n");
let codes: Vec<&str> = r.diagnostics.iter().map(|d| d.code.as_str()).collect();
assert!(codes.contains(&"E0002"), "{:?}", r.diagnostics);
assert!(
r.diagnostics
.iter()
.any(|d| d.notes.iter().any(|n| n.contains("compiler builtin"))),
"expected the `panic` is a compiler builtin note: {:?}",
r.diagnostics
);
}
#[test]
fn std_core_traits_are_visible_from_a_user_module() {
let r = resolve_src("fn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
for name in ["Display", "Debug", "Eq", "Ord", "Clone", "Default"] {
assert!(
r.definitions.resolve_trait(ModuleId(0), name).is_some(),
"expected std/core's `{name}` trait to resolve from module 0"
);
}
}
#[test]
fn std_collections_module_is_compiled_and_visible() {
let r = resolve_src("fn main() { }");
assert!(
r.definitions.resolve_type(ModuleId(0), "Vec").is_some(),
"Vec should be visible from a user module"
);
}
#[test]
fn importing_a_std_core_name_from_a_module_is_allowed() {
let p = resolve_two(
"import lib\nfn main() { }\n",
"pub type Status = | Ok | Fail\n",
);
assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
assert!(p.definitions.resolve_value(ModuleId(0), "Ok").is_some());
}
#[test]
fn collects_functions_and_sum_types() {
let r = resolve_src(
"type Shape = | Circle(Int) | Empty\n\
fn area(s: Shape) -> Int { 0 }\n\
fn main() { }\n",
);
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "area"),
Some(Res::Def(_))
));
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "Circle"),
Some(Res::Variant(_, 0))
));
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "Empty"),
Some(Res::Variant(_, 1))
));
assert!(r.definitions.resolve_type(ModuleId(0), "Shape").is_some());
assert!(matches!(
r.definitions.resolve_value(ModuleId(0), "println"),
Some(Res::Builtin(Builtin::Println))
));
}
#[test]
fn collects_records() {
let r = resolve_src("record Point { x: Float, y: Float }\nfn main() { }\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
let id = r
.definitions
.resolve_type(ModuleId(0), "Point")
.expect("Point resolves");
assert!(matches!(r.definitions.def(id).kind, DefKind::Record { .. }));
}
#[test]
fn collects_traits_and_impl_methods() {
let r = resolve_src(
"record P { v: Int }\n\
trait Show { fn name(self) -> String\n fn shout(self) -> String { self.name() } }\n\
impl Show for P { fn name(self) -> String { \"p\" } }\n\
impl P { fn get(self) -> Int { self.v } }\n\
fn main() { }\n",
);
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
assert!(r.definitions.resolve_trait(ModuleId(0), "Show").is_some());
let methods = r
.definitions
.methods()
.filter(|&(_, item_index, _, _)| r.definitions.module_of(item_index) == ModuleId(0))
.count();
assert_eq!(methods, 3, "expected this module's own 3 method defs");
}
#[test]
fn duplicate_function_reports_e0002() {
let r = resolve_src("fn f() { }\nfn f() { }\n");
assert_eq!(r.diagnostics.len(), 1);
assert_eq!(r.diagnostics[0].code, "E0002");
}
#[test]
fn shadowing_builtin_reports_e0002() {
let r = resolve_src("fn println() { }\n");
assert_eq!(r.diagnostics.len(), 1);
assert_eq!(r.diagnostics[0].code, "E0002");
}
#[test]
fn names_in_scope_lists_items_imports_builtins_variants_and_std() {
let r = resolve_two(
"import lib::{area}\nrecord Point { x: Int }\nfn main() {}\n",
"pub fn area() -> Int { 1 }\n",
);
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
let names = r.definitions.names_in_scope(ModuleId(0));
let has = |name: &str, kind: fn(&ScopeEntry) -> bool| {
names.iter().any(|(n, e)| n == name && kind(e))
};
assert!(has("main", |e| matches!(e, ScopeEntry::Value(Res::Def(_)))));
assert!(has("Point", |e| matches!(e, ScopeEntry::Type(_))));
assert!(has("area", |e| matches!(e, ScopeEntry::Value(Res::Def(_)))));
assert!(has("println", |e| matches!(
e,
ScopeEntry::Value(Res::Builtin(_))
)));
assert!(has("Some", |e| matches!(
e,
ScopeEntry::Value(Res::Variant(_, _))
)));
assert!(has("Vec", |e| matches!(e, ScopeEntry::Type(_))));
assert!(has("Display", |e| matches!(e, ScopeEntry::Trait(_))));
let mut sorted = names.clone();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
assert_eq!(
names.iter().map(|n| &n.0).collect::<Vec<_>>(),
sorted.iter().map(|n| &n.0).collect::<Vec<_>>(),
"sorted by name"
);
}
#[test]
fn a_std_name_the_module_shadows_is_listed_once_as_its_own() {
let r = resolve_two("record Vec { n: Int }\nfn main() {}\n", "pub fn f() {}\n");
assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
let vecs: Vec<DefId> = r
.definitions
.names_in_scope(ModuleId(0))
.into_iter()
.filter_map(|(n, e)| match e {
ScopeEntry::Type(id) if n == "Vec" => Some(id),
_ => None,
})
.collect();
assert_eq!(vecs.len(), 1, "{vecs:?}");
let DefKind::Record { item_index } = r.definitions.def(vecs[0]).kind else {
panic!("not a record");
};
assert_eq!(r.definitions.module_of(item_index), ModuleId(0));
}
fn occurrence_at(prog: &ProgramResolution, src: &str, needle: &str) -> Vec<(Role, Target)> {
let start = src.find(needle).unwrap() as u32;
prog.imports
.iter()
.filter(|o| o.span.start == start && o.span.end == start + needle.len() as u32)
.map(|o| (o.role, o.target))
.collect()
}
#[test]
fn an_imports_module_is_a_use_of_it() {
let main = "import lib\nfn main() { let a = area() }\n";
let prog = resolve_two(main, "pub fn area() -> Int { 1 }\n");
assert_eq!(
occurrence_at(&prog, main, "lib"),
vec![(Role::Use, Target::Module(ModuleId(1)))]
);
}
#[test]
fn a_list_imports_module_is_its_first_segment() {
let main = "import lib::{area}\nfn main() { let a = area() }\n";
let prog = resolve_two(main, "pub fn area() -> Int { 1 }\n");
assert_eq!(
occurrence_at(&prog, main, "lib"),
vec![(Role::Use, Target::Module(ModuleId(1)))]
);
}
#[test]
fn a_listed_name_is_a_use_of_each_namespace_it_binds() {
let main = "import lib::{area, Figure, Paint, Round}\nfn main() {}\n";
let lib = "pub fn area() -> Int { 1 }\n\
pub type Figure = | Round(Int) | Square(Int)\n\
pub trait Paint { fn paint(self) -> String }\n";
let prog = resolve_two(main, lib);
let id = |name: &str| {
DefId(
prog.definitions
.defs()
.iter()
.position(|d| d.name == name)
.unwrap() as u32,
)
};
assert_eq!(
occurrence_at(&prog, main, "area"),
vec![(Role::Use, Target::Def(id("area")))]
);
assert_eq!(
occurrence_at(&prog, main, "Figure"),
vec![(Role::Use, Target::Def(id("Figure")))]
);
assert_eq!(
occurrence_at(&prog, main, "Paint"),
vec![(Role::Use, Target::Def(id("Paint")))]
);
assert_eq!(
occurrence_at(&prog, main, "Round"),
vec![(Role::Use, Target::Variant(id("Figure"), 0))]
);
}
#[test]
fn an_unresolved_import_records_nothing() {
let main = "import nowhere\nimport lib::{missing}\nfn main() {}\n";
let prog = resolve_two(main, "pub fn area() -> Int { 1 }\n");
assert!(occurrence_at(&prog, main, "nowhere").is_empty());
assert!(occurrence_at(&prog, main, "missing").is_empty());
assert_eq!(occurrence_at(&prog, main, "lib").len(), 1);
}
#[test]
fn name_imports_makes_every_other_module_importable() {
let p = resolve_two("fn main() {}\n", "pub fn area() -> Int { 1 }\n");
let d = &p.definitions;
assert_eq!(
d.importable(ModuleId(0)),
[("lib".to_string(), ModuleId(1))]
);
assert_eq!(
d.importable(ModuleId(1)),
[("main".to_string(), ModuleId(0))]
);
assert!(d.same_package(ModuleId(0), ModuleId(1)));
assert!(d.owned(ModuleId(0)) && d.owned(ModuleId(1)) && !d.owned(ModuleId(2)));
}
#[test]
fn exported_names_each_namespace_a_public_name_has() {
let p = resolve_two(
"fn main() {}\n",
"pub record Area { x: Int }\npub fn area() -> Int { 1 }\nfn hidden() -> Int { 0 }\n",
);
let d = &p.definitions;
let area = d.exported(ModuleId(1), "area");
assert!(matches!(area.value, Some(Res::Def(_))), "{area:?}");
assert!(area.ty.is_none() && area.trait_def.is_none(), "{area:?}");
assert!(d.exported(ModuleId(1), "Area").ty.is_some());
assert_eq!(d.exported(ModuleId(1), "hidden"), Exported::default());
}
#[test]
fn a_name_bound_through_std_is_not_bound_outside_it() {
let p = resolve_two(
"record Option { x: Int }\nfn main() {}\n",
"pub fn area() -> Int { 1 }\n",
);
let d = &p.definitions;
assert_eq!(
d.bound_outside_std(ModuleId(0), "Some"),
Exported::default()
);
assert!(d.bound_outside_std(ModuleId(0), "Option").ty.is_some());
assert!(matches!(
d.bound_outside_std(ModuleId(0), "println").value,
Some(Res::Builtin(_))
));
assert!(d.is_std_module(ModuleId(2)) && !d.is_std_module(ModuleId(1)));
}
}