use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io;
use std::path::Path;
use std::sync::Arc;
use brink_compiler::{CompileError, CompileOutput, ResolvedDiagnostic};
use brink_driver::{AnalysisOptions, Dialect, Driver, LintLevel, TypePolicy};
use brink_ir::Diagnostic;
use brink_project_config::{ConfigError, discover_from_entry_in_tree, parse_str_at};
use brink_source_tree::SourceTree;
fn fnv1a_64(bytes: &[u8]) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET;
for &byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct ContentHash(u64);
impl ContentHash {
#[must_use]
pub fn of(text: &str) -> Self {
Self(fnv1a_64(text.as_bytes()))
}
}
impl std::fmt::Display for ContentHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:016x}", self.0)
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct EnvHash(u64);
impl std::fmt::Display for EnvHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:016x}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ContentStore {
Inline(BTreeMap<ContentHash, String>),
}
impl ContentStore {
fn get(&self, hash: ContentHash) -> Option<&str> {
match self {
Self::Inline(map) => map.get(&hash).map(String::as_str),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ResolvedDep {
pub module: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Environment {
manifest: BTreeMap<String, ContentHash>,
content: ContentStore,
pub entry: String,
pub options: AnalysisOptions,
pub resolved_deps: Vec<ResolvedDep>,
}
impl Environment {
pub fn source_keys(&self) -> impl Iterator<Item = &str> {
self.manifest.keys().map(String::as_str)
}
pub fn source_text(&self, key: &str) -> Option<Cow<'_, str>> {
let hash = *self.manifest.get(key)?;
self.content.get(hash).map(Cow::Borrowed)
}
pub fn content_hash(&self) -> EnvHash {
let mut buf: Vec<u8> = Vec::new();
for (key, hash) in &self.manifest {
buf.extend_from_slice(key.as_bytes());
buf.push(0);
buf.extend_from_slice(&hash.0.to_le_bytes());
}
buf.push(0xff);
buf.extend_from_slice(self.entry.as_bytes());
buf.push(0xff);
buf.extend_from_slice(&serde_json::to_vec(&self.options).unwrap_or_default());
buf.push(0xff);
buf.extend_from_slice(&serde_json::to_vec(&self.resolved_deps).unwrap_or_default());
EnvHash(fnv1a_64(&buf))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OptionOverrides {
pub dialect: Option<Dialect>,
pub types: Option<TypePolicy>,
pub lints: BTreeMap<String, LintLevel>,
pub deny_warnings: Option<bool>,
}
pub struct Project;
impl Project {
pub fn load(
tree: &dyn SourceTree,
entry: &str,
overrides: &OptionOverrides,
) -> Result<Environment, LoadError> {
let sources = collect_sources(tree, entry)?;
let mut manifest = BTreeMap::new();
let mut inline = BTreeMap::new();
for (key, text) in sources {
let hash = ContentHash::of(&text);
inline.insert(hash, text);
manifest.insert(key, hash);
}
mount_stdlib(&mut manifest, &mut inline);
let options = resolve_options(tree, entry, overrides)?;
Ok(Environment {
manifest,
content: ContentStore::Inline(inline),
entry: entry.to_string(),
options,
resolved_deps: Vec::new(),
})
}
}
const STDLIB_SOURCES: &[(&str, &str)] = &[(
"std/conventions/screenplay.brink",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/std/conventions/screenplay.brink"
)),
)];
#[must_use]
pub fn stdlib_sources() -> &'static [(&'static str, &'static str)] {
STDLIB_SOURCES
}
fn mount_stdlib(
manifest: &mut BTreeMap<String, ContentHash>,
inline: &mut BTreeMap<ContentHash, String>,
) {
for (key, text) in STDLIB_SOURCES {
if manifest.contains_key(*key) {
continue;
}
let hash = ContentHash::of(text);
inline.entry(hash).or_insert_with(|| (*text).to_string());
manifest.insert((*key).to_string(), hash);
}
}
fn is_dotdot_polluted(key: &str) -> bool {
key.split('/').any(|segment| segment == "..")
}
const NATIVE_EXTENSION: &str = "brink";
fn collect_sources(
tree: &dyn SourceTree,
entry: &str,
) -> Result<BTreeMap<String, String>, LoadError> {
if brink_driver::is_native(Path::new(entry)) {
let mut map = BTreeMap::new();
for key in tree.list()? {
if Path::new(&key)
.extension()
.is_none_or(|ext| ext != NATIVE_EXTENSION)
{
continue;
}
if is_dotdot_polluted(&key) {
return Err(LoadError::InvalidSourceKey(key));
}
let text = tree.read(&key)?;
map.insert(key, text);
}
Ok(map)
} else {
let mut driver = Driver::new();
driver.discover(entry, |key| tree.read(key))?;
let db = driver.db();
let mut map = BTreeMap::new();
for id in db.file_ids() {
if let (Some(path), Some(source)) = (db.file_path(id), db.source(id)) {
map.insert(path.to_string(), source.to_string());
}
}
Ok(map)
}
}
fn resolve_options(
tree: &dyn SourceTree,
entry: &str,
overrides: &OptionOverrides,
) -> Result<AnalysisOptions, LoadError> {
let mut options = AnalysisOptions::default();
if let Some(config_key) = discover_from_entry_in_tree(tree, entry)? {
let text = tree
.read(&config_key)
.map_err(|source| LoadError::ConfigRead {
path: config_key.clone(),
source,
})?;
let (config, warnings) =
parse_str_at(config_key.clone(), &text).map_err(|source| LoadError::Config {
path: config_key.clone(),
source: Box::new(source),
})?;
for warning in &warnings {
tracing::warn!("[{config_key}] {warning}");
}
let config_warnings = options.apply_project_config(
&config,
overrides.dialect.is_some(),
overrides.types.is_some(),
);
for warning in &config_warnings {
tracing::warn!("[{config_key}] {warning}");
}
}
if let Some(dialect) = overrides.dialect {
options.dialect = dialect;
}
if let Some(types) = overrides.types {
options.types = Some(types);
}
let lint_override_warnings =
options.apply_lint_overrides(&overrides.lints, overrides.deny_warnings);
for warning in &lint_override_warnings {
tracing::warn!("{warning}");
}
Ok(options)
}
pub fn compile(env: &Environment) -> Result<CompileOutput, CompileError> {
let mut driver = Driver::new();
driver.set_analysis_options(env.options.clone());
for key in env.source_keys() {
if let Some(text) = env.source_text(key) {
driver.db_mut().set_file(key, text.into_owned());
}
}
if driver.db_mut().set_entry(&env.entry).is_none() {
return Err(CompileError::Io(io::Error::new(
io::ErrorKind::NotFound,
format!("entry file not in environment: {}", env.entry),
)));
}
let product = driver.db().story_data().cloned().unwrap_or_default();
let Some(story) = product.story else {
let mut all = product.errors;
all.extend(product.warnings);
return Err(CompileError::Diagnostics(resolve_diagnostics(
driver.db(),
all,
)));
};
Ok(CompileOutput {
data: Arc::unwrap_or_clone(story),
warnings: resolve_diagnostics(driver.db(), product.warnings),
})
}
fn resolve_diagnostics(
db: &brink_driver::ProjectDb,
diags: Vec<Diagnostic>,
) -> Vec<ResolvedDiagnostic> {
let opts = db.analysis_options();
let types = opts.type_policy();
diags
.into_iter()
.map(|d| ResolvedDiagnostic {
path: db.file_path(d.file).unwrap_or_default().to_string(),
file: d.file,
range: d.range,
message: d.message,
severity: brink_driver::effective_severity(d.code, types, &opts.lints),
code: d.code,
})
.collect()
}
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("discovery error: {0}")]
Discover(#[from] brink_driver::DiscoverError),
#[error("{source}")]
Config {
path: String,
#[source]
source: Box<ConfigError>,
},
#[error("failed to read project config {path}: {source}")]
ConfigRead {
path: String,
#[source]
source: io::Error,
},
#[error("invalid source key `{0}` (must be root-relative, no `..`)")]
InvalidSourceKey(String),
}
#[cfg(test)]
mod tests {
use super::*;
use brink_driver::LintLevel;
use brink_source_tree::InMemory;
fn tree(files: &[(&str, &str)]) -> InMemory {
InMemory::new(
files
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect::<BTreeMap<_, _>>(),
)
}
#[test]
fn content_hash_is_deterministic_and_content_addressed() {
assert_eq!(ContentHash::of("hello"), ContentHash::of("hello"));
assert_ne!(ContentHash::of("hello"), ContentHash::of("world"));
}
#[test]
fn source_text_resolves_through_key_hash_store() {
let t = tree(&[("main.brink", "flow main() {}")]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
assert_eq!(
env.source_text("main.brink").as_deref(),
Some("flow main() {}")
);
assert_eq!(env.source_text("absent.brink"), None);
}
#[test]
fn identical_content_is_stored_once_but_keyed_twice() {
let t = tree(&[("a.brink", "flow a() {}"), ("b.brink", "flow a() {}")]);
let env = Project::load(&t, "a.brink", &OptionOverrides::default()).expect("loads");
let ContentStore::Inline(store) = &env.content;
assert_eq!(env.source_keys().count(), 3);
assert_eq!(store.len(), 2);
}
#[test]
fn source_keys_are_sorted() {
let t = tree(&[
("z.brink", "flow z() {}"),
("a.brink", "flow a() {}"),
("m.brink", "flow m() {}"),
]);
let env = Project::load(&t, "a.brink", &OptionOverrides::default()).expect("loads");
let keys: Vec<_> = env.source_keys().collect();
assert_eq!(
keys,
vec![
"a.brink",
"m.brink",
"std/conventions/screenplay.brink",
"z.brink"
]
);
}
#[test]
fn environment_round_trips_through_json_unchanged() {
let t = tree(&[("main.brink", "flow main() {}")]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
let json = serde_json::to_string(&env).expect("serializes");
let back: Environment = serde_json::from_str(&json).expect("deserializes");
assert_eq!(env, back);
assert_eq!(env.content_hash(), back.content_hash());
}
#[test]
fn content_hash_changes_when_a_source_changes() {
let a = Project::load(
&tree(&[("m.brink", "flow m() {}")]),
"m.brink",
&OptionOverrides::default(),
)
.expect("loads");
let b = Project::load(
&tree(&[("m.brink", "flow m() { Hi. }")]),
"m.brink",
&OptionOverrides::default(),
)
.expect("loads");
assert_ne!(a.content_hash(), b.content_hash());
}
#[test]
fn content_hash_changes_when_options_change() {
let base = tree(&[("m.brink", "flow m() {}")]);
let default = Project::load(&base, "m.brink", &OptionOverrides::default()).expect("loads");
let overridden = Project::load(
&base,
"m.brink",
&OptionOverrides {
dialect: Some(Dialect::Brink),
..OptionOverrides::default()
},
)
.expect("loads");
assert_ne!(default.content_hash(), overridden.content_hash());
}
#[test]
fn brink_toml_dialect_is_discovered_over_the_tree() {
let t = tree(&[
("brink.toml", "[project]\ndialect = \"brink\"\n"),
("main.brink", "flow main() {}"),
]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
assert_eq!(env.options.dialect, Dialect::Brink);
}
#[test]
fn brink_toml_is_discovered_by_walking_up_from_the_entry() {
let t = tree(&[
("brink.toml", "[project]\ndialect = \"brink\"\n"),
("chapters/main.brink", "flow main() {}"),
]);
let env =
Project::load(&t, "chapters/main.brink", &OptionOverrides::default()).expect("loads");
assert_eq!(env.options.dialect, Dialect::Brink);
}
#[test]
fn explicit_override_wins_over_brink_toml() {
let t = tree(&[
("brink.toml", "[project]\ndialect = \"brink\"\n"),
("main.brink", "flow main() {}"),
]);
let env = Project::load(
&t,
"main.brink",
&OptionOverrides {
dialect: Some(Dialect::StrictInk),
..OptionOverrides::default()
},
)
.expect("loads");
assert_eq!(env.options.dialect, Dialect::StrictInk);
}
#[test]
fn no_brink_toml_yields_default_options() {
let t = tree(&[("main.brink", "flow main() {}")]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
assert_eq!(env.options, AnalysisOptions::default());
}
#[test]
fn repeat_compiles_do_not_leak_options_across_project_load_calls() {
let brink_tree = tree(&[
("brink.toml", "[project]\ndialect = \"brink\"\n"),
("main.brink", "flow main() {}"),
]);
let first =
Project::load(&brink_tree, "main.brink", &OptionOverrides::default()).expect("loads");
assert_eq!(first.options.dialect, Dialect::Brink);
let default_tree = tree(&[("main2.brink", "flow main() {}")]);
let second = Project::load(&default_tree, "main2.brink", &OptionOverrides::default())
.expect("loads");
assert_eq!(
second.options,
AnalysisOptions::default(),
"a later, unrelated Project::load call must never observe an \
earlier call's resolved AnalysisOptions -- each call must \
resolve from a fresh AnalysisOptions::default(), not a \
reused/mutated one; got {:?}",
second.options
);
}
#[test]
fn malformed_brink_toml_is_a_load_error() {
let t = tree(&[
("brink.toml", "[project]\ndialect = \"sideways\"\n"),
("main.brink", "flow main() {}"),
]);
let err = Project::load(&t, "main.brink", &OptionOverrides::default())
.expect_err("invalid dialect value must fail load");
assert!(matches!(err, LoadError::Config { .. }));
}
#[test]
fn malformed_brink_toml_error_names_its_path() {
let t = tree(&[
("brink.toml", "[project]\ndialect = \"sideways\"\n"),
("main.brink", "flow main() {}"),
]);
let err = Project::load(&t, "main.brink", &OptionOverrides::default())
.expect_err("invalid dialect value must fail load");
let LoadError::Config { path, .. } = &err else {
unreachable!("expected LoadError::Config, got {err:?}");
};
assert_eq!(path, "brink.toml");
assert!(
err.to_string().contains("brink.toml"),
"error message must name the malformed file, got: {err}"
);
}
#[test]
fn malformed_brink_toml_error_names_its_nested_path() {
let t = tree(&[
("chapters/brink.toml", "[project]\ndialect = \"sideways\"\n"),
("chapters/deep/main.brink", "flow main() {}"),
]);
let err = Project::load(&t, "chapters/deep/main.brink", &OptionOverrides::default())
.expect_err("invalid dialect value must fail load");
let LoadError::Config { path, .. } = &err else {
unreachable!("expected LoadError::Config, got {err:?}");
};
assert_eq!(path, "chapters/brink.toml");
assert!(
err.to_string().contains("chapters/brink.toml"),
"error message must name the nested malformed file, got: {err}"
);
}
#[test]
fn unreadable_brink_toml_error_names_its_path() {
let dir = std::env::temp_dir().join(format!(
"brink-environment-unreadable-config-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("brink.toml"), [0x80_u8, 0x81, 0x82]).unwrap();
std::fs::write(dir.join("main.brink"), "flow main() {}").unwrap();
let t = brink_driver::RealFs::new(&dir);
let err = Project::load(&t, "main.brink", &OptionOverrides::default())
.expect_err("non-UTF-8 brink.toml must fail load");
let LoadError::ConfigRead { path, .. } = &err else {
unreachable!("expected LoadError::ConfigRead, got {err:?}");
};
assert_eq!(path, "brink.toml");
assert!(
err.to_string().contains("brink.toml"),
"error message must name the unreadable file, got: {err}"
);
std::fs::remove_dir_all(&dir).ok();
}
const E014_SOURCE: &str = "Hello.\n~\n-> END\n";
#[test]
fn brink_toml_lints_table_resolves_into_options() {
let t = tree(&[
("brink.toml", "[lints]\nE014 = \"deny\"\n"),
("main.ink", E014_SOURCE),
]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
assert_eq!(
env.options.lints.overrides.get("E014"),
Some(&LintLevel::Deny)
);
}
#[test]
fn brink_toml_deny_warnings_resolves_into_options() {
let t = tree(&[
("brink.toml", "[lints]\ndeny-warnings = true\n"),
("main.ink", E014_SOURCE),
]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
assert!(env.options.lints.deny_warnings);
}
#[test]
fn absent_lints_table_leaves_options_lints_at_default() {
let t = tree(&[("main.ink", E014_SOURCE)]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
assert_eq!(env.options.lints, brink_driver::LintPolicy::default());
}
#[test]
fn e014_warning_compiles_cleanly_by_default() {
let t = tree(&[("main.ink", E014_SOURCE)]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let out = compile(&env).expect("a Warning-only diagnostic must not block compilation");
assert!(
out.warnings
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E014),
"expected E014 among the warnings: {:?}",
out.warnings
);
}
#[test]
fn brink_toml_lints_deny_relevels_e014_and_blocks_compile() {
let t = tree(&[
("brink.toml", "[lints]\nE014 = \"deny\"\n"),
("main.ink", E014_SOURCE),
]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let err = compile(&env).expect_err("a denied E014 must block compilation");
let CompileError::Diagnostics(diags) = err else {
unreachable!("expected CompileError::Diagnostics, got {err:?}");
};
assert!(
diags
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E014),
"expected E014 among the surfaced diagnostics: {diags:?}"
);
}
#[test]
fn brink_toml_deny_warnings_blocks_compile_on_an_unconfigured_warning() {
let t = tree(&[
("brink.toml", "[lints]\ndeny-warnings = true\n"),
("main.ink", E014_SOURCE),
]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let err = compile(&env).expect_err("deny-warnings must promote E014 to a compile error");
assert!(matches!(err, CompileError::Diagnostics(_)));
}
#[test]
fn override_lints_resolves_into_options() {
let t = tree(&[("main.ink", E014_SOURCE)]);
let mut lints = BTreeMap::new();
lints.insert("E014".to_owned(), LintLevel::Deny);
let env = Project::load(
&t,
"main.ink",
&OptionOverrides {
lints,
..OptionOverrides::default()
},
)
.expect("loads");
assert_eq!(
env.options.lints.overrides.get("E014"),
Some(&LintLevel::Deny)
);
}
#[test]
fn override_deny_e014_blocks_compile_with_no_brink_toml() {
let t = tree(&[("main.ink", E014_SOURCE)]);
let mut lints = BTreeMap::new();
lints.insert("E014".to_owned(), LintLevel::Deny);
let env = Project::load(
&t,
"main.ink",
&OptionOverrides {
lints,
..OptionOverrides::default()
},
)
.expect("loads");
let err = compile(&env).expect_err("CLI --deny E014 must block compilation");
let CompileError::Diagnostics(diags) = err else {
unreachable!("expected CompileError::Diagnostics, got {err:?}");
};
assert!(
diags
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E014),
"expected E014 among the surfaced diagnostics: {diags:?}"
);
}
#[test]
fn override_deny_warnings_blocks_compile_with_no_brink_toml() {
let t = tree(&[("main.ink", E014_SOURCE)]);
let env = Project::load(
&t,
"main.ink",
&OptionOverrides {
deny_warnings: Some(true),
..OptionOverrides::default()
},
)
.expect("loads");
let err = compile(&env).expect_err("CLI -D warnings must promote E014 to a compile error");
assert!(matches!(err, CompileError::Diagnostics(_)));
}
#[test]
fn override_lints_wins_over_a_conflicting_brink_toml_entry() {
let t = tree(&[
("brink.toml", "[lints]\nE014 = \"deny\"\n"),
("main.ink", E014_SOURCE),
]);
let mut lints = BTreeMap::new();
lints.insert("E014".to_owned(), LintLevel::Allow);
let env = Project::load(
&t,
"main.ink",
&OptionOverrides {
lints,
..OptionOverrides::default()
},
)
.expect("loads");
assert_eq!(
env.options.lints.overrides.get("E014"),
Some(&LintLevel::Allow),
"the CLI override must replace the file's E014 = deny"
);
compile(&env).expect("CLI --allow E014 must win over brink.toml's E014 = deny");
}
#[test]
fn native_universe_is_the_whole_tree_excluding_config() {
let t = tree(&[
("brink.toml", "[project]\n"),
("main.brink", "flow main() {}"),
("lib/util.brink", "flow util() {}"),
("README.md", "not source"),
]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
let keys: Vec<_> = env.source_keys().collect();
assert_eq!(
keys,
vec![
"lib/util.brink",
"main.brink",
"std/conventions/screenplay.brink"
]
);
}
#[test]
fn dotdot_native_key_is_rejected() {
struct Hostile;
impl SourceTree for Hostile {
fn list(&self) -> io::Result<Vec<String>> {
Ok(vec!["a.brink".to_string(), "../escape.brink".to_string()])
}
fn read(&self, key: &str) -> io::Result<String> {
Ok(format!("-- {key} --"))
}
}
let err = Project::load(&Hostile, "a.brink", &OptionOverrides::default())
.expect_err("dotdot key must be rejected");
assert!(matches!(err, LoadError::InvalidSourceKey(k) if k == "../escape.brink"));
}
#[test]
fn ink_universe_follows_the_include_graph() {
let t = tree(&[
("main.ink", "INCLUDE lib.ink\nHello.\n-> END\n"),
("lib.ink", "== helper ==\n-> DONE\n"),
("unreferenced.ink", "== orphan ==\n-> DONE\n"),
]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let keys: Vec<_> = env.source_keys().collect();
assert_eq!(
keys,
vec!["lib.ink", "main.ink", "std/conventions/screenplay.brink"]
);
}
#[test]
fn stdlib_screenplay_preset_is_mounted_into_every_environment() {
let t = tree(&[("main.brink", "flow main() {}")]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
let mounted = env
.source_text("std/conventions/screenplay.brink")
.expect("the built-in screenplay preset must be mounted into every Environment");
assert!(
mounted.contains("heading"),
"mounted stdlib text looks wrong (embed path misconfigured?): {mounted}"
);
}
#[test]
fn stdlib_mount_is_present_for_a_native_and_an_ink_entry_alike() {
let native = Project::load(
&tree(&[("main.brink", "flow main() {}")]),
"main.brink",
&OptionOverrides::default(),
)
.expect("loads");
let ink = Project::load(
&tree(&[("main.ink", "Hello.\n-> END\n")]),
"main.ink",
&OptionOverrides::default(),
)
.expect("loads");
assert!(
native
.source_text("std/conventions/screenplay.brink")
.is_some()
);
assert!(
ink.source_text("std/conventions/screenplay.brink")
.is_some()
);
}
#[test]
fn a_project_source_at_the_stdlib_key_wins_over_the_embedded_copy() {
let t = tree(&[
("main.brink", "flow main() {}"),
(
"std/conventions/screenplay.brink",
"// project-authored override\nflow overridden() {}",
),
]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
assert_eq!(
env.source_text("std/conventions/screenplay.brink")
.as_deref(),
Some("// project-authored override\nflow overridden() {}"),
"a project's own file at the stdlib's key must win, not be silently clobbered \
by the embedded copy"
);
}
#[test]
fn mounted_stdlib_compiles_cleanly_alongside_an_ordinary_native_project() {
let t = tree(&[("main.brink", "flow main() { Hello. }")]);
let env = Project::load(&t, "main.brink", &OptionOverrides::default()).expect("loads");
let out = compile(&env).expect(
"a plain native project must compile cleanly with the stdlib mounted alongside it",
);
assert!(
out.warnings.is_empty(),
"the mounted stdlib module must not itself introduce diagnostics: {:?}",
out.warnings
);
let has_scene_entered_extern = out.data.externals.iter().any(|ext| {
out.data
.name_table
.get(ext.name.0 as usize)
.is_some_and(|name| name == "scene_entered")
});
assert!(
has_scene_entered_extern,
"the mounted screenplay preset's `heading` handler declares \
`extern scene_entered(title, slug)` — its absence from the \
compiled externs means the mount never reached the compile at \
all, got externs: {:?}",
out.data.externals
);
}
#[test]
fn stdlib_mount_is_manifest_only_for_an_ink_entry() {
let t = tree(&[("main.ink", "Hello.\n-> END\n")]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let out = compile(&env).expect(
"a plain ink project must compile cleanly with the stdlib mounted alongside it",
);
assert!(
out.warnings.is_empty(),
"the mounted stdlib module must not itself introduce diagnostics \
for an ink entry either: {:?}",
out.warnings
);
let has_scene_entered_extern = out.data.externals.iter().any(|ext| {
out.data
.name_table
.get(ext.name.0 as usize)
.is_some_and(|name| name == "scene_entered")
});
assert!(
!has_scene_entered_extern,
"an ink entry's compilation closure must NOT include the \
mounted, manifest-only stdlib module (no INCLUDE edge reaches \
it) — its presence here would mean the mount reaches ink \
compiles too, contradicting the manifest-only scope fence: \
{:?}",
out.data.externals
);
}
#[test]
fn mounted_stdlib_introduces_no_diagnostics_under_types_strict() {
let t = tree(&[("main.brink", "flow main() { Hello. }")]);
let overrides = OptionOverrides {
types: Some(TypePolicy::Strict),
..OptionOverrides::default()
};
let env = Project::load(&t, "main.brink", &overrides).expect("loads");
let out = compile(&env).expect(
"a plain native project must compile cleanly under types = strict \
with the stdlib mounted alongside it",
);
assert!(
out.warnings.is_empty(),
"the mounted stdlib module must not itself introduce diagnostics \
under types = strict: {:?}",
out.warnings
);
}
#[test]
fn e181_is_reachable_from_an_ordinary_ink_project_colliding_with_a_std_preset_name() {
let t = tree(&[(
"story.ink",
"STRUCT Cue = #{\n speaker: string,\n}\nHello.\n-> END\n",
)]);
let overrides = OptionOverrides {
dialect: Some(Dialect::Brink),
..OptionOverrides::default()
};
let env = Project::load(&t, "story.ink", &overrides).expect("loads");
let err = compile(&env).expect_err(
"a project struct colliding with a std-declared homonym must raise \
E181 and fail compilation, not silently drop the struct",
);
let CompileError::Diagnostics(diags) = err else {
unreachable!("expected a Diagnostics compile error, got: {err:?}");
};
assert!(
diags
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E181),
"expected E181 among the blocking diagnostics: {diags:?}"
);
}
#[test]
fn external_self_declaration_silently_drops_when_colliding_with_a_std_preset_name() {
let t = tree(&[(
"story.ink",
"EXTERNAL scene_entered(title, slug)\nHello.\n-> END\n",
)]);
let env = Project::load(&t, "story.ink", &OptionOverrides::default()).expect("loads");
let err = compile(&env).expect_err(
"a project EXTERNAL colliding with a std-declared homonym must raise \
a diagnostic and fail compilation, not silently drop the external",
);
let CompileError::Diagnostics(diags) = err else {
unreachable!("expected a Diagnostics compile error, got: {err:?}");
};
assert!(
diags
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E184),
"expected E184 (the EXTERNAL/CONST/VAR twin of E181) among the \
blocking diagnostics: {diags:?}"
);
}
#[test]
fn compile_over_environment_produces_story_data() {
let t = tree(&[("main.ink", "Hello, world.\n-> END\n")]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let out = compile(&env).expect("compiles");
assert!(
!out.data.containers.is_empty(),
"expected compiled containers"
);
}
#[test]
fn compile_surfaces_diagnostics_as_a_compile_error() {
let t = tree(&[("main.ink", "VAR arr = 0\n~ { arr = #[1, 2, 3] }\n-> END\n")]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let err = compile(&env).expect_err("strict-ink must reject extension syntax");
assert!(matches!(err, CompileError::Diagnostics(_)));
}
#[test]
fn load_then_compile_matches_across_a_serialize_round_trip() {
let t = tree(&[("main.ink", "Hello.\n-> END\n")]);
let env = Project::load(&t, "main.ink", &OptionOverrides::default()).expect("loads");
let json = serde_json::to_string(&env).expect("serializes");
let back: Environment = serde_json::from_str(&json).expect("deserializes");
let a = compile(&env).expect("compiles");
let b = compile(&back).expect("compiles from round-tripped env");
let mut buf_a = String::new();
let mut buf_b = String::new();
brink_format::write_inkt(&a.data, &mut buf_a).expect("inkt a");
brink_format::write_inkt(&b.data, &mut buf_b).expect("inkt b");
assert_eq!(buf_a, buf_b);
}
}