Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions src/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ use chumsky::container::Container;

use crate::error::{Error, ErrorCollector, RichError, Span};
use crate::parse::{self, ParseFromStrWithErrors};
use crate::resolution::{CanonPath, DependencyMap, SourceFile};
use crate::resolution::DependencyMap;
use crate::source::{CanonPath, SourceFile};

pub use crate::driver::resolve_order::{FileScoped, Program, SymbolTable};

Expand Down Expand Up @@ -182,12 +183,12 @@ impl DependencyGraph {
/// This function will return an `Err(String)` only for critical internal compiler errors
/// (e.g., if a provided `SourceFile` is unexpectedly missing its underlying file path).
pub fn new(
root_source: SourceFile,
root_source: CanonSourceFile,
dependency_map: Arc<DependencyMap>,
root_program: &parse::Program,
handler: &mut ErrorCollector,
) -> Result<Option<Self>, String> {
let root_canon_source = CanonSourceFile::try_from(root_source)?;
let root_canon_source = root_source;

let mut graph = Self {
modules: vec![Module {
Expand Down Expand Up @@ -389,15 +390,11 @@ pub(crate) mod tests {
let lib_dir = canon(&ws.create_dir("workspace/libs/lib"));

// Set up the dependency map for imports (e.g. `use lib::...`)
let mut map = DependencyMap::new();
map.insert(workspace_dir.clone(), "lib".to_string(), lib_dir.clone())
.expect("Failed to insert dependency map");

// Register the strict crate boundaries so local files are forced to use `crate::`
map.insert(workspace_dir.clone(), CRATE_STR.to_string(), workspace_dir)
.expect("Failed to insert workspace crate boundary");
map.insert(lib_dir.clone(), CRATE_STR.to_string(), lib_dir)
.expect("Failed to insert library crate boundary");
let map = crate::resolution::DependencyMapBuilder::new()
.with_entry_root(workspace_dir.clone())
.add_dependency(workspace_dir.clone(), "lib".to_string(), lib_dir.clone())
.build()
.expect("Failed to create dependency map");
let map = Arc::new(map);

let mut root_file_path = None;
Expand All @@ -416,7 +413,7 @@ pub(crate) mod tests {

let root_p = root_file_path.expect("main.simf must be defined in file list");
let main_canon_source = CanonSourceFile::new(root_p, Arc::from(root_content));
let main_source = SourceFile::from(main_canon_source);
let main_source = SourceFile::from(main_canon_source.clone());

let main_program_option =
parse::Program::parse_from_str_with_errors(main_source.clone(), &mut handler);
Expand All @@ -426,7 +423,7 @@ pub(crate) mod tests {
};

let graph_option =
DependencyGraph::new(main_source, map, &main_program, &mut handler).unwrap();
DependencyGraph::new(main_canon_source, map, &main_program, &mut handler).unwrap();

let mut file_ids = HashMap::new();

Expand Down Expand Up @@ -648,4 +645,19 @@ pub(crate) mod tests {
.map_or(true, |deps| deps.is_empty());
assert!(b_has_no_deps, "B depends on nothing");
}

#[test]
fn test_canon_source_file_rejects_anonymous() {
let anonymous_source = SourceFile::anonymous(Arc::from("fn main() {}"));
let result = CanonSourceFile::try_from(anonymous_source);

assert!(
result.is_err(),
"CanonSourceFile must explicitly reject anonymous files"
);
assert_eq!(
result.unwrap_err(),
"Cannot canonicalize the SourceFile because it is missing a file name."
);
}
}
12 changes: 11 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use simplicity::elements;

use crate::lexer::Token;
use crate::parse::MatchPattern;
use crate::resolution::SourceFile;
use crate::source::SourceFile;
use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName};
use crate::types::{ResolvedType, UIntType};

Expand Down Expand Up @@ -474,6 +474,11 @@ impl fmt::Display for ErrorCollector {
/// Records _what_ happened but not where.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Error {
DependencyPathNotFound(String),
DependencyNotADirectory(String),
ReservedDependencyKeyword(String),
DuplicateDependencyAlias(String, String),
InvalidDependencyIdentifier(String),
Internal(String),
UnknownLibrary(String),
ArraySizeNonZero(usize),
Expand Down Expand Up @@ -533,6 +538,11 @@ pub enum Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::DependencyPathNotFound(path) => write!(f, "Path not found: {}", path),
Error::DependencyNotADirectory(path) => write!(f, "Path must be a directory: {}", path),
Error::ReservedDependencyKeyword(kw) => write!(f, "The '{}' keyword is reserved and cannot be manually mapped. Use the builder's context definitions instead.", kw),
Error::DuplicateDependencyAlias(alias, context) => write!(f, "Duplicate dependency mapping: alias '{}' is defined multiple times for context '{}'", alias, context),
Error::InvalidDependencyIdentifier(alias) => write!(f, "Invalid dependency alias '{}': must be a valid identifier and not a reserved keyword", alias),
Error::Internal(err) => write!(
f,
"INTERNAL ERROR: {err}"
Expand Down
22 changes: 6 additions & 16 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,24 +255,14 @@ pub fn lex<'src>(input: &'src str) -> (Option<Tokens<'src>>, Vec<crate::error::R
)
}

/// A list of all reserved keywords.
pub const KEYWORDS: &[&str] = &[
"pub", "use", "as", "fn", "let", "type", "mod", "const", "match", CRATE_STR, "true", "false",
];

/// Checks whether a given string is a keyword.
#[cfg(feature = "arbitrary")]
pub fn is_keyword(s: &str) -> bool {
matches!(
s,
"pub"
| "use"
| "as"
| "fn"
| "let"
| "type"
| "mod"
| "const"
| "match"
| CRATE_STR
| "true"
| "false"
)
KEYWORDS.contains(&s)
}

#[cfg(test)]
Expand Down
89 changes: 46 additions & 43 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod num;
pub mod parse;
pub mod pattern;
pub mod resolution;
pub mod source;

#[cfg(feature = "serde")]
mod serde;
pub mod str;
Expand All @@ -36,10 +38,12 @@ pub extern crate simplicity;
pub use simplicity::elements;

use crate::debug::DebugSymbols;
use crate::driver::CanonSourceFile;
use crate::driver::DependencyGraph;
use crate::error::{ErrorCollector, WithContent, WithSource as _};
use crate::parse::ParseFromStrWithErrors;
use crate::resolution::{DependencyMap, SourceFile};
use crate::resolution::DependencyMap;
use crate::source::SourceFile;
pub use crate::types::ResolvedType;
pub use crate::value::Value;
pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues};
Expand All @@ -61,19 +65,20 @@ impl TemplateProgram {
///
/// The string is not a valid SimplicityHL program.
pub fn new_with_dep(
source: SourceFile,
source: CanonSourceFile,
dependency_map: &DependencyMap,
) -> Result<Self, String> {
let mut error_handler = ErrorCollector::new();
let source_file = SourceFile::from(source.clone());

// 1. Parse root file
let parsed_program =
parse::Program::parse_from_str_with_errors(source.clone(), &mut error_handler)
parse::Program::parse_from_str_with_errors(source_file.clone(), &mut error_handler)
.ok_or_else(|| error_handler.to_string())?;

// 2. Create the driver program
let driver_program: driver::Program = if dependency_map.is_empty() {
driver::Program::from_parse(&parsed_program, source.content(), &mut error_handler)
driver::Program::from_parse(&parsed_program, source_file.content(), &mut error_handler)
.ok_or_else(|| error_handler.to_string())?
} else {
let graph = DependencyGraph::new(
Expand All @@ -90,7 +95,7 @@ impl TemplateProgram {
};

// 3. AST Analysis
let ast_program = ast::Program::analyze(&driver_program).with_source(source.clone())?;
let ast_program = ast::Program::analyze(&driver_program).with_source(source_file)?;
Ok(Self {
simfony: ast_program,
file: source.content(),
Expand Down Expand Up @@ -188,7 +193,7 @@ impl CompiledProgram {
/// - [`TemplateProgram::new_with_dep`]
/// - [`TemplateProgram::instantiate`]
pub fn new_with_dep(
source: SourceFile,
source: CanonSourceFile,
dependency_map: &DependencyMap,
arguments: Arguments,
include_debug_symbols: bool,
Expand Down Expand Up @@ -422,7 +427,10 @@ pub(crate) mod tests {

pub fn template_deps(prog_path: &Path, dependency_map: &DependencyMap) -> Self {
let program_text = std::fs::read_to_string(prog_path).unwrap();
let source = SourceFile::new(prog_path, Arc::from(program_text));
let source = CanonSourceFile::new(
crate::source::CanonPath::canonicalize(prog_path).unwrap(),
Arc::from(program_text),
);

let program = match TemplateProgram::new_with_dep(source, dependency_map) {
Ok(x) => x,
Expand Down Expand Up @@ -494,33 +502,22 @@ pub(crate) mod tests {
I: IntoIterator<Item = (P, K, P)>,
K: Into<String>,
{
let mut dependency_map = DependencyMap::new();
let mut builder = crate::resolution::DependencyMapBuilder::new();

if let Some(parent) = prog_path.as_ref().parent() {
let canon_root = crate::resolution::tests::canon(parent);
let _ = dependency_map.insert(
canon_root.clone(),
crate::driver::CRATE_STR.to_string(),
canon_root,
);
builder = builder.with_entry_root(canon_root);
}

for (context, alias, target) in dependencies {
let context = crate::resolution::tests::canon(context.as_ref());
let target = crate::resolution::tests::canon(target.as_ref());

dependency_map
.insert(context.clone(), alias.into(), target.clone())
.unwrap();

// Treat each mapped dependency as an isolated external package to satisfy strict local-file checks
let _ = dependency_map.insert(
target.clone(),
crate::driver::CRATE_STR.to_string(),
target,
);
builder = builder.add_dependency(context, alias.into(), target);
}

let dependency_map = builder.build().unwrap();

TestCase::<TemplateProgram>::template_deps(prog_path.as_ref(), &dependency_map)
.with_arguments(Arguments::default())
}
Expand Down Expand Up @@ -725,7 +722,6 @@ pub(crate) mod tests {

#[test]
fn test_crate_keyword_compilation_success() {
use crate::resolution::{CanonPath, DependencyMap};
use crate::test_utils::TempWorkspace;

let ws = TempWorkspace::new("crate_success");
Expand All @@ -740,14 +736,10 @@ pub(crate) mod tests {
);

let main_path = root.join("main.simf");
let mut dependency_map = DependencyMap::new();
let canon_root = CanonPath::canonicalize(&root).unwrap();
dependency_map
.insert(
canon_root.clone(),
crate::driver::CRATE_STR.to_string(),
canon_root,
)
let canon_root = crate::source::CanonPath::canonicalize(&root).unwrap();
let dependency_map = crate::resolution::DependencyMapBuilder::new()
.with_entry_root(canon_root)
.build()
.unwrap();

TestCase::<TemplateProgram>::template_deps(&main_path, &dependency_map)
Expand All @@ -756,6 +748,16 @@ pub(crate) mod tests {
.assert_run_success();
}

#[test]
fn test_anonymous_source_compiles_without_dependencies() {
let code = "fn main() { assert!(true); }";
let program = TemplateProgram::new(code);
assert!(
program.is_ok(),
"TemplateProgram::new should successfully compile anonymous source files without requiring canonical paths"
);
}

#[test]
fn cat() {
TestCase::program_file("./examples/cat.simf")
Expand Down Expand Up @@ -1162,23 +1164,23 @@ mod error_tests {
use super::*;

use crate::resolution::tests::canon;
use crate::resolution::CanonPath;
use crate::source::CanonPath;
use crate::test_utils::TempWorkspace;

fn dependency_map(root_dir: &Path, drp: &str, lib_dir: &Path) -> DependencyMap {
let mut dependency_map = DependencyMap::new();

let context = CanonPath::canonicalize(root_dir).unwrap();
let target = CanonPath::canonicalize(lib_dir).unwrap();

dependency_map.insert(context, drp.into(), target).unwrap();

dependency_map
crate::resolution::DependencyMapBuilder::new()
.with_entry_root(context.clone())
.add_dependency(context, drp.into(), target)
.build()
.unwrap()
}

fn source_file(path: &Path) -> SourceFile {
fn source_file(path: &Path) -> CanonSourceFile {
let content = std::fs::read_to_string(path).expect("Failed to read test file");
SourceFile::new(path, Arc::from(content))
CanonSourceFile::new(canon(path), Arc::from(content))
}

#[test]
Expand Down Expand Up @@ -1211,6 +1213,7 @@ mod error_tests {
#[test]
fn omitted_context_dependency_applies_inside_dependency_files() {
let ws = TempWorkspace::new("omitted_context_dependency");
let root_dir = ws.create_dir("workspace");
let lib_dir = ws.create_dir("workspace/lib");
let main_path = ws.create_file(
"workspace/main.simf",
Expand All @@ -1222,7 +1225,7 @@ mod error_tests {
);
ws.create_file("workspace/lib/base.simf", "pub fn one() -> u32 { 1 }\n");

let dependencies = dependency_map(&main_path, "lib", &lib_dir);
let dependencies = dependency_map(&root_dir, "lib", &lib_dir);
let _err = TemplateProgram::new_with_dep(source_file(&main_path), &dependencies)
.expect_err("omitted-context dependencies");
}
Expand Down Expand Up @@ -1333,7 +1336,7 @@ mod functional_tests {
}

#[test]
#[should_panic(expected = "not found")]
#[should_panic(expected = "DependencyPathNotFound")]
fn file_not_found_error() {
run_dependency_test(
format!("{}/file-not-found", ERROR_TESTS_DIR).as_str(),
Expand All @@ -1342,7 +1345,7 @@ mod functional_tests {
}

#[test]
#[should_panic(expected = "not found")]
#[should_panic(expected = "DependencyPathNotFound")]
fn lib_not_found_error() {
run_dependency_test(format!("{}/lib-not-found", ERROR_TESTS_DIR).as_str(), "lib");
}
Expand Down
Loading
Loading