aiken_project/
lib.rs

1pub mod blueprint;
2pub mod config;
3pub mod deps;
4pub mod docs;
5pub mod error;
6pub mod export;
7pub mod format;
8pub mod github;
9pub mod module;
10pub mod options;
11pub mod package_name;
12pub mod paths;
13pub mod pretty;
14pub mod telemetry;
15pub mod watch;
16
17mod test_framework;
18
19#[cfg(test)]
20mod tests;
21
22use crate::{
23    blueprint::{
24        Blueprint,
25        schema::{Annotated, Schema},
26    },
27    config::ProjectConfig,
28    error::{Error, Warning},
29    module::{CheckedModule, CheckedModules, ParsedModule, ParsedModules},
30    telemetry::{CoverageMode, Event},
31};
32use aiken_lang::{
33    IdGenerator,
34    ast::{
35        self, DataTypeKey, Definition, FunctionAccessKey, ModuleKind, Tracing, TypedDataType,
36        TypedFunction, UntypedDefinition,
37    },
38    builtins,
39    expr::{TypedExpr, UntypedExpr},
40    format::{Formatter, MAX_COLUMNS},
41    gen_uplc::CodeGenerator,
42    line_numbers::LineNumbers,
43    test_framework::{RunnableKind, Test, TestResult},
44    tipo::{Type, TypeInfo},
45    utils,
46};
47use export::Export;
48use indexmap::IndexMap;
49use miette::NamedSource;
50use options::{CodeGenMode, Options};
51use package_name::PackageName;
52use pallas_addresses::{Address, Network, ShelleyAddress, ShelleyDelegationPart, StakePayload};
53use pallas_primitives::conway::PolicyId;
54use std::{
55    collections::{BTreeSet, HashMap, HashSet},
56    fs::{self, File},
57    io::BufReader,
58    path::{Path, PathBuf},
59    rc::Rc,
60};
61use telemetry::EventListener;
62use uplc::{
63    PlutusData,
64    ast::{Constant, Name, Program},
65};
66
67#[derive(Debug)]
68pub struct Source {
69    pub path: PathBuf,
70    pub name: String,
71    pub code: String,
72    pub kind: ModuleKind,
73}
74
75pub struct Checkpoint {
76    module_types: HashMap<String, TypeInfo>,
77    defined_modules: HashMap<String, PathBuf>,
78}
79
80#[derive(Debug, Clone)]
81enum AddModuleBy {
82    Source { name: String, code: String },
83    Path(PathBuf),
84}
85
86pub struct Project<T>
87where
88    T: EventListener,
89{
90    config: ProjectConfig,
91    defined_modules: HashMap<String, PathBuf>,
92    checked_modules: CheckedModules,
93    id_gen: IdGenerator,
94    module_types: HashMap<String, TypeInfo>,
95    root: PathBuf,
96    sources: Vec<Source>,
97    warnings: Vec<Warning>,
98    checks_count: Option<usize>,
99    event_listener: T,
100    functions: IndexMap<FunctionAccessKey, TypedFunction>,
101    constants: IndexMap<FunctionAccessKey, TypedExpr>,
102    data_types: IndexMap<DataTypeKey, TypedDataType>,
103    module_sources: HashMap<String, (String, LineNumbers)>,
104}
105
106impl<T> Project<T>
107where
108    T: EventListener,
109{
110    #[allow(clippy::result_large_err)]
111    pub fn new(root: PathBuf, event_listener: T) -> Result<Project<T>, Error> {
112        let config = ProjectConfig::load(&root)?;
113
114        let demanded_compiler_version = format!("v{}", config.compiler);
115
116        let mut project = Project::new_with_config(config, root, event_listener);
117
118        let current_compiler_version = config::compiler_version(false);
119
120        if demanded_compiler_version != current_compiler_version {
121            project.warnings.push(Warning::CompilerVersionMismatch {
122                demanded: demanded_compiler_version,
123                current: current_compiler_version,
124            })
125        }
126
127        Ok(project)
128    }
129
130    pub fn new_with_config(config: ProjectConfig, root: PathBuf, event_listener: T) -> Project<T> {
131        let id_gen = IdGenerator::new();
132
133        let mut module_types = HashMap::new();
134
135        module_types.insert("aiken".to_string(), builtins::prelude(&id_gen));
136        module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen));
137
138        let functions = builtins::prelude_functions(&id_gen, &module_types);
139
140        let data_types = builtins::prelude_data_types(&id_gen);
141
142        Project {
143            config,
144            checked_modules: CheckedModules::default(),
145            defined_modules: HashMap::new(),
146            id_gen,
147            module_types,
148            root,
149            sources: vec![],
150            warnings: vec![],
151            checks_count: None,
152            event_listener,
153            functions,
154            constants: IndexMap::new(),
155            data_types,
156            module_sources: HashMap::new(),
157        }
158    }
159
160    pub fn new_generator(&'_ self, tracing: Tracing) -> CodeGenerator<'_> {
161        CodeGenerator::new(
162            self.config.plutus,
163            utils::indexmap::as_ref_values(&self.functions),
164            utils::indexmap::as_ref_values(&self.constants),
165            utils::indexmap::as_ref_values(&self.data_types),
166            utils::indexmap::as_str_ref_values(&self.module_types),
167            utils::indexmap::as_str_ref_values(&self.module_sources),
168            tracing,
169        )
170    }
171
172    pub fn warnings(&mut self) -> Vec<Warning> {
173        std::mem::take(&mut self.warnings)
174    }
175
176    pub fn modules(&self) -> Vec<CheckedModule> {
177        self.checked_modules.values().cloned().collect()
178    }
179
180    pub fn importable_modules(&self) -> Vec<String> {
181        self.module_types.keys().cloned().collect()
182    }
183
184    pub fn checkpoint(&self) -> Checkpoint {
185        Checkpoint {
186            module_types: self.module_types.clone(),
187            defined_modules: self.defined_modules.clone(),
188        }
189    }
190
191    pub fn restore(&mut self, checkpoint: Checkpoint) {
192        self.module_types = checkpoint.module_types;
193        self.defined_modules = checkpoint.defined_modules;
194    }
195
196    pub fn blueprint_path(&self, filepath: Option<&Path>) -> PathBuf {
197        match filepath {
198            Some(filepath) => filepath.to_path_buf(),
199            None => self.root.join(Options::default().blueprint_path),
200        }
201    }
202
203    pub fn build(
204        &mut self,
205        uplc: bool,
206        tracing: Tracing,
207        blueprint_path: PathBuf,
208        env: Option<String>,
209    ) -> Result<(), Vec<Error>> {
210        let options = Options {
211            code_gen_mode: CodeGenMode::Build(uplc),
212            tracing,
213            env,
214            blueprint_path,
215        };
216
217        self.compile(options)
218    }
219
220    pub fn docs(
221        &mut self,
222        destination: Option<PathBuf>,
223        include_dependencies: bool,
224    ) -> Result<(), Vec<Error>> {
225        self.event_listener
226            .handle_event(Event::BuildingDocumentation {
227                root: self.root.clone(),
228                name: self.config.name.to_string(),
229                version: self.config.version.clone(),
230            });
231
232        let config = self.config_definitions(None);
233
234        self.read_source_files(config)?;
235
236        let mut modules = self.parse_sources(self.config.name.clone())?;
237
238        self.type_check(&mut modules, Tracing::silent(), None, false)?;
239
240        let destination = destination.unwrap_or_else(|| self.root.join("docs"));
241
242        self.event_listener.handle_event(Event::GeneratingDocFiles {
243            output_path: destination.clone(),
244        });
245
246        let modules = self
247            .checked_modules
248            .values_mut()
249            .filter(|CheckedModule { package, .. }| {
250                include_dependencies || package == &self.config.name.to_string()
251            })
252            .map(|m| {
253                m.attach_doc_and_module_comments();
254                &*m
255            })
256            .collect();
257
258        let doc_files = docs::generate_all(&self.root, &self.config, modules);
259
260        for file in doc_files {
261            let path = destination.join(file.path);
262            fs::create_dir_all(path.parent().unwrap()).map_err(Error::from)?;
263            fs::write(&path, file.content).map_err(Error::from)?;
264        }
265
266        Ok(())
267    }
268
269    #[allow(clippy::too_many_arguments)]
270    pub fn check(
271        &mut self,
272        skip_tests: bool,
273        match_tests: Option<Vec<String>>,
274        verbose: bool,
275        exact_match: bool,
276        seed: u32,
277        property_max_success: usize,
278        coverage_mode: CoverageMode,
279        tracing: Tracing,
280        env: Option<String>,
281        plain_numbers: bool,
282    ) -> Result<(), Vec<Error>> {
283        let options = Options {
284            tracing,
285            env,
286            code_gen_mode: if skip_tests {
287                CodeGenMode::NoOp
288            } else {
289                CodeGenMode::Test {
290                    match_tests,
291                    verbose,
292                    exact_match,
293                    seed,
294                    property_max_success,
295                    coverage_mode,
296                    plain_numbers,
297                }
298            },
299            blueprint_path: self.blueprint_path(None),
300        };
301
302        self.compile(options)
303    }
304
305    pub fn benchmark(
306        &mut self,
307        match_benchmarks: Option<Vec<String>>,
308        exact_match: bool,
309        seed: u32,
310        max_size: usize,
311        tracing: Tracing,
312        env: Option<String>,
313    ) -> Result<(), Vec<Error>> {
314        let options = Options {
315            tracing,
316            env,
317            code_gen_mode: CodeGenMode::Benchmark {
318                match_benchmarks,
319                exact_match,
320                seed,
321                max_size,
322            },
323            blueprint_path: self.blueprint_path(None),
324        };
325
326        self.compile(options)
327    }
328
329    #[allow(clippy::result_large_err)]
330    pub fn dump_uplc(&self, blueprint: &Blueprint) -> Result<(), Error> {
331        let dir = self.root.join("artifacts");
332
333        self.event_listener
334            .handle_event(Event::DumpingUPLC { path: dir.clone() });
335
336        fs::create_dir_all(&dir)?;
337
338        for validator in &blueprint.validators {
339            let path = dir.clone().join(format!("{}.uplc", validator.title));
340
341            let program = &validator.program;
342            let program: Program<Name> = program.inner().try_into().unwrap();
343
344            fs::write(&path, program.to_pretty()).map_err(|error| Error::FileIo { error, path })?;
345        }
346
347        Ok(())
348    }
349
350    fn config_definitions(&mut self, env: Option<&str>) -> Option<Vec<UntypedDefinition>> {
351        if !self.config.config.is_empty() {
352            let env = env.unwrap_or(ast::DEFAULT_ENV_MODULE);
353
354            match self.config.config.get(env) {
355                None => {
356                    self.warnings.push(Warning::NoConfigurationForEnv {
357                        env: env.to_string(),
358                    });
359                    None
360                }
361                Some(config) => {
362                    let mut conf_definitions = Vec::new();
363
364                    for (identifier, value) in config.iter() {
365                        conf_definitions.push(value.as_definition(identifier));
366                    }
367
368                    Some(conf_definitions)
369                }
370            }
371        } else {
372            None
373        }
374    }
375
376    pub fn compile(&mut self, options: Options) -> Result<(), Vec<Error>> {
377        self.event_listener
378            .handle_event(Event::StartingCompilation {
379                root: self.root.clone(),
380                name: self.config.name.to_string(),
381                version: self.config.version.clone(),
382            });
383
384        let env = options.env.as_deref();
385
386        let config = self.config_definitions(env);
387
388        self.read_source_files(config)?;
389
390        let mut modules = self.parse_sources(self.config.name.clone())?;
391
392        self.type_check(&mut modules, options.tracing, env, true)?;
393
394        match options.code_gen_mode {
395            CodeGenMode::Build(uplc_dump) => {
396                self.event_listener
397                    .handle_event(Event::GeneratingBlueprint {
398                        path: options.blueprint_path.clone(),
399                    });
400
401                self.checked_modules.values_mut().for_each(|m| {
402                    m.attach_doc_and_module_comments();
403                });
404
405                let mut generator = self.new_generator(options.tracing);
406
407                let blueprint = Blueprint::new(&self.config, &self.checked_modules, &mut generator)
408                    .map_err(|err| Error::Blueprint(err.into()))?;
409
410                if blueprint.validators.is_empty() {
411                    self.warnings.push(Warning::NoValidators);
412                }
413
414                if uplc_dump {
415                    self.dump_uplc(&blueprint)?;
416                }
417
418                let json = serde_json::to_string_pretty(&blueprint).unwrap();
419
420                fs::write(options.blueprint_path.as_path(), json).map_err(|error| {
421                    Error::FileIo {
422                        error,
423                        path: options.blueprint_path,
424                    }
425                    .into()
426                })
427            }
428            CodeGenMode::Test {
429                match_tests,
430                verbose,
431                exact_match,
432                seed,
433                property_max_success,
434                coverage_mode,
435                plain_numbers,
436            } => {
437                let tests =
438                    self.collect_tests(verbose, match_tests, exact_match, options.tracing)?;
439
440                if !tests.is_empty() {
441                    self.event_listener.handle_event(Event::RunningTests);
442                }
443
444                let tests = self.run_runnables(tests, seed, property_max_success);
445
446                self.checks_count = if tests.is_empty() {
447                    None
448                } else {
449                    Some(tests.iter().fold(0, |acc, test| {
450                        acc + match test {
451                            TestResult::PropertyTestResult(r) => r.iterations,
452                            _ => 1,
453                        }
454                    }))
455                };
456
457                let errors: Vec<Error> = tests
458                    .iter()
459                    .filter_map(|e| {
460                        if e.is_success() {
461                            None
462                        } else {
463                            Some(Error::from_test_result(e, verbose))
464                        }
465                    })
466                    .collect();
467
468                self.event_listener.handle_event(Event::FinishedTests {
469                    seed,
470                    coverage_mode,
471                    tests,
472                    plain_numbers,
473                });
474
475                if !errors.is_empty() {
476                    Err(errors)
477                } else {
478                    Ok(())
479                }
480            }
481            CodeGenMode::Benchmark {
482                match_benchmarks,
483                exact_match,
484                seed,
485                max_size,
486            } => {
487                let verbose = false;
488
489                let benchmarks = self.collect_benchmarks(
490                    verbose,
491                    match_benchmarks,
492                    exact_match,
493                    options.tracing,
494                )?;
495
496                if !benchmarks.is_empty() {
497                    self.event_listener.handle_event(Event::RunningBenchmarks);
498                }
499
500                let benchmarks = self.run_runnables(benchmarks, seed, max_size);
501
502                let errors: Vec<Error> = benchmarks
503                    .iter()
504                    .filter_map(|e| {
505                        if e.is_success() {
506                            None
507                        } else {
508                            Some(Error::from_test_result(e, verbose))
509                        }
510                    })
511                    .collect();
512
513                self.event_listener
514                    .handle_event(Event::FinishedBenchmarks { seed, benchmarks });
515
516                if !errors.is_empty() {
517                    Err(errors)
518                } else {
519                    Ok(())
520                }
521            }
522            CodeGenMode::NoOp => Ok(()),
523        }
524    }
525
526    #[allow(clippy::result_large_err)]
527    pub fn address(
528        &self,
529        module_name: Option<&str>,
530        validator_name: Option<&str>,
531        stake_address: Option<&str>,
532        blueprint_path: &Path,
533        mainnet: bool,
534    ) -> Result<ShelleyAddress, Error> {
535        // Parse stake address
536        let stake_address = stake_address
537            .map(|s| {
538                Address::from_hex(s)
539                    .or_else(|_| Address::from_bech32(s))
540                    .map_err(|error| Error::MalformedStakeAddress { error: Some(error) })
541                    .and_then(|addr| match addr {
542                        Address::Stake(addr) => Ok(addr),
543                        _ => Err(Error::MalformedStakeAddress { error: None }),
544                    })
545            })
546            .transpose()?;
547        let delegation_part = match stake_address.map(|addr| addr.payload().to_owned()) {
548            None => ShelleyDelegationPart::Null,
549            Some(StakePayload::Stake(key)) => ShelleyDelegationPart::Key(key),
550            Some(StakePayload::Script(script)) => ShelleyDelegationPart::Script(script),
551        };
552
553        let blueprint = Self::blueprint(blueprint_path)?;
554
555        // Calculate the address
556        let when_too_many = |known_validators| {
557            Error::Blueprint(
558                blueprint::error::Error::MoreThanOneValidatorFound { known_validators }.into(),
559            )
560        };
561        let when_missing = |known_validators| {
562            Error::Blueprint(
563                blueprint::error::Error::NoValidatorNotFound { known_validators }.into(),
564            )
565        };
566
567        blueprint.with_validator(
568            module_name,
569            validator_name,
570            when_too_many,
571            when_missing,
572            |validator| {
573                let n = validator.parameters.len();
574
575                if n > 0 {
576                    Err(Error::Blueprint(
577                        blueprint::error::Error::ParameterizedValidator { n }.into(),
578                    ))
579                } else {
580                    let network = if mainnet {
581                        Network::Mainnet
582                    } else {
583                        Network::Testnet
584                    };
585
586                    Ok(validator.program.inner().address(
587                        network,
588                        delegation_part.to_owned(),
589                        &self.config.plutus.into(),
590                    ))
591                }
592            },
593        )
594    }
595
596    #[allow(clippy::result_large_err)]
597    pub fn policy(
598        &self,
599        module_name: Option<&str>,
600        validator_name: Option<&str>,
601        blueprint_path: &Path,
602    ) -> Result<PolicyId, Error> {
603        let blueprint = Self::blueprint(blueprint_path)?;
604
605        // Error handlers for ambiguous / missing validators
606        let when_too_many = |known_validators| {
607            Error::Blueprint(
608                blueprint::error::Error::MoreThanOneValidatorFound { known_validators }.into(),
609            )
610        };
611        let when_missing = |known_validators| {
612            Error::Blueprint(
613                blueprint::error::Error::NoValidatorNotFound { known_validators }.into(),
614            )
615        };
616
617        blueprint.with_validator(
618            module_name,
619            validator_name,
620            when_too_many,
621            when_missing,
622            |validator| {
623                let n = validator.parameters.len();
624                if n > 0 {
625                    Err(Error::Blueprint(
626                        blueprint::error::Error::ParameterizedValidator { n }.into(),
627                    ))
628                } else {
629                    Ok(validator.program.compiled_code_and_hash().0)
630                }
631            },
632        )
633    }
634
635    #[allow(clippy::result_large_err)]
636    pub fn export(&self, module: &str, name: &str, tracing: Tracing) -> Result<Export, Error> {
637        let checked_module =
638            self.checked_modules
639                .get(module)
640                .ok_or_else(|| Error::ModuleNotFound {
641                    module: module.to_string(),
642                    known_modules: self.checked_modules.keys().cloned().collect(),
643                })?;
644
645        checked_module
646            .ast
647            .definitions()
648            .find_map(|def| match def {
649                Definition::Fn(func) if func.name == name => Some((checked_module, func)),
650                _ => None,
651            })
652            .map(|(checked_module, func)| {
653                let mut generator = self.new_generator(tracing);
654
655                Export::from_function(
656                    func,
657                    checked_module,
658                    &mut generator,
659                    &self.checked_modules,
660                    &self.config.plutus,
661                )
662                .map_err(|err| Error::Blueprint(err.into()))
663            })
664            .transpose()?
665            .ok_or_else(|| Error::ExportNotFound {
666                module: module.to_string(),
667                name: name.to_string(),
668            })
669    }
670
671    #[allow(clippy::result_large_err)]
672    pub fn blueprint(path: &Path) -> Result<Blueprint, Error> {
673        let blueprint = File::open(path)
674            .map_err(|_| Error::Blueprint(blueprint::error::Error::InvalidOrMissingFile.into()))?;
675        Ok(serde_json::from_reader(BufReader::new(blueprint))?)
676    }
677
678    fn with_dependencies(&mut self, parsed_packages: &mut ParsedModules) -> Result<(), Vec<Error>> {
679        let manifest = deps::download(&self.event_listener, &self.root, &self.config)?;
680
681        for package in manifest.packages {
682            let lib = self.root.join(paths::build_deps_package(&package.name));
683
684            self.event_listener
685                .handle_event(Event::StartingCompilation {
686                    root: lib.clone(),
687                    name: package.name.to_string(),
688                    version: package.version.clone(),
689                });
690
691            self.read_package_source_files(&lib.join("lib"))?;
692
693            let mut parsed_modules = self.parse_sources(package.name)?;
694
695            use rayon::prelude::*;
696
697            parsed_modules
698                .par_iter_mut()
699                .for_each(|(_module, parsed_module)| {
700                    parsed_module
701                        .ast
702                        .definitions
703                        .retain(|def| !matches!(def, Definition::Test { .. }))
704                });
705
706            parsed_packages.extend(Into::<HashMap<_, _>>::into(parsed_modules));
707        }
708
709        Ok(())
710    }
711
712    #[allow(clippy::result_large_err)]
713    fn read_source_files(&mut self, config: Option<Vec<UntypedDefinition>>) -> Result<(), Error> {
714        let env = self.root.join("env");
715        let lib = self.root.join("lib");
716        let validators = self.root.join("validators");
717        let root = self.root.clone();
718
719        if let Some(defs) = config {
720            self.add_module(
721                AddModuleBy::Source {
722                    name: ast::CONFIG_MODULE.to_string(),
723                    code: Formatter::new()
724                        .definitions(&defs[..])
725                        .to_pretty_string(MAX_COLUMNS),
726                },
727                &root,
728                ModuleKind::Config,
729            )?;
730        }
731
732        self.aiken_files(&validators, ModuleKind::Validator)?;
733        self.aiken_files(&lib, ModuleKind::Lib)?;
734        self.aiken_files(&env, ModuleKind::Env)?;
735
736        Ok(())
737    }
738
739    #[allow(clippy::result_large_err)]
740    fn read_package_source_files(&mut self, lib: &Path) -> Result<(), Error> {
741        self.aiken_files(lib, ModuleKind::Lib)?;
742
743        Ok(())
744    }
745
746    fn parse_sources(&mut self, package_name: PackageName) -> Result<ParsedModules, Vec<Error>> {
747        use rayon::prelude::*;
748
749        let (parsed_modules, parse_errors, duplicates) = self
750            .sources
751            .par_drain(0..)
752            .fold(
753                || (ParsedModules::new(), Vec::new(), Vec::new()),
754                |(mut parsed_modules, mut parse_errors, mut duplicates), elem| {
755                    let Source {
756                        path,
757                        name,
758                        code,
759                        kind,
760                    } = elem;
761
762                    match aiken_lang::parser::module(&code, kind) {
763                        Ok((mut ast, extra)) => {
764                            // Store the name
765                            ast.name.clone_from(&name);
766
767                            let module = ParsedModule {
768                                kind,
769                                ast,
770                                code,
771                                name: name.clone(),
772                                path,
773                                extra,
774                                package: package_name.to_string(),
775                            };
776
777                            let path = module.path.clone();
778
779                            if let Some(first) = parsed_modules.insert(module.name.clone(), module)
780                            {
781                                duplicates.push((name, first.path.clone(), path))
782                            }
783
784                            (parsed_modules, parse_errors, duplicates)
785                        }
786                        Err(errs) => {
787                            for error in errs {
788                                parse_errors.push((
789                                    path.clone(),
790                                    code.clone(),
791                                    NamedSource::new(path.display().to_string(), code.clone()),
792                                    Box::new(error),
793                                ))
794                            }
795
796                            (parsed_modules, parse_errors, duplicates)
797                        }
798                    }
799                },
800            )
801            .reduce(
802                || (ParsedModules::new(), Vec::new(), Vec::new()),
803                |(mut parsed_modules, mut parse_errors, mut duplicates),
804                 (mut parsed, mut errs, mut dups)| {
805                    let keys_left = parsed_modules.keys().collect::<HashSet<_>>();
806                    let keys_right = parsed.keys().collect::<HashSet<_>>();
807
808                    for module in keys_left.intersection(&keys_right) {
809                        duplicates.push((
810                            module.to_string(),
811                            parsed_modules
812                                .get(module.as_str())
813                                .map(|m| m.path.clone())
814                                .unwrap(),
815                            parsed.get(module.as_str()).map(|m| m.path.clone()).unwrap(),
816                        ));
817                    }
818
819                    parsed_modules.extend(parsed.drain());
820
821                    parse_errors.append(&mut errs);
822                    duplicates.append(&mut dups);
823
824                    (parsed_modules, parse_errors, duplicates)
825                },
826            );
827
828        let mut errors: Vec<Error> = Vec::new();
829
830        errors.extend(
831            duplicates
832                .into_iter()
833                .map(|(module, first, second)| Error::DuplicateModule {
834                    module,
835                    first,
836                    second,
837                })
838                .collect::<Vec<_>>(),
839        );
840
841        errors.extend(
842            parse_errors
843                .into_iter()
844                .map(|(path, src, named, error)| Error::Parse {
845                    path,
846                    src,
847                    named: named.into(),
848                    error,
849                })
850                .collect::<Vec<_>>(),
851        );
852
853        for parsed_module in parsed_modules.values() {
854            if let Some(first) = self
855                .defined_modules
856                .insert(parsed_module.name.clone(), parsed_module.path.clone())
857            {
858                errors.push(Error::DuplicateModule {
859                    module: parsed_module.name.clone(),
860                    first,
861                    second: parsed_module.path.clone(),
862                });
863            }
864        }
865
866        if errors.is_empty() {
867            Ok(parsed_modules)
868        } else {
869            Err(errors)
870        }
871    }
872
873    fn type_check(
874        &mut self,
875        modules: &mut ParsedModules,
876        tracing: Tracing,
877        env: Option<&str>,
878        validate_module_name: bool,
879    ) -> Result<(), Vec<Error>> {
880        let our_modules: BTreeSet<String> = modules.keys().cloned().collect();
881
882        self.with_dependencies(modules)?;
883
884        for name in modules.sequence(&our_modules)? {
885            if let Some(module) = modules.remove(&name) {
886                let (checked_module, warnings) = module.infer(
887                    &self.id_gen,
888                    &self.config.name.to_string(),
889                    tracing,
890                    env,
891                    validate_module_name,
892                    &mut self.module_sources,
893                    &mut self.module_types,
894                    &mut self.functions,
895                    &mut self.constants,
896                    &mut self.data_types,
897                )?;
898
899                if our_modules.contains(checked_module.name.as_str())
900                    && checked_module.name.as_str() != ast::CONFIG_MODULE
901                {
902                    self.warnings.extend(warnings);
903                }
904
905                self.checked_modules
906                    .insert(checked_module.name.clone(), checked_module);
907            }
908        }
909
910        Ok(())
911    }
912
913    #[allow(clippy::result_large_err)]
914    fn collect_test_items(
915        &mut self,
916        kind: RunnableKind,
917        verbose: bool,
918        match_tests: Option<Vec<String>>,
919        exact_match: bool,
920        tracing: Tracing,
921    ) -> Result<Vec<Test>, Error> {
922        let mut scripts = Vec::new();
923
924        let match_tests = match_tests.map(|mt| {
925            mt.into_iter()
926                .map(|match_test| {
927                    let mut match_split_dot = match_test.split('.');
928
929                    let match_module = if match_test.contains('.') || match_test.contains('/') {
930                        match_split_dot.next().unwrap_or("")
931                    } else {
932                        ""
933                    };
934
935                    let match_names = match_split_dot.next().and_then(|names| {
936                        let names = names.replace(&['{', '}'][..], "");
937                        let names_split_comma = names.split(',');
938
939                        let result = names_split_comma
940                            .filter_map(|s| {
941                                let s = s.trim();
942                                if s.is_empty() {
943                                    None
944                                } else {
945                                    Some(s.to_string())
946                                }
947                            })
948                            .collect::<Vec<_>>();
949
950                        if result.is_empty() {
951                            None
952                        } else {
953                            Some(result)
954                        }
955                    });
956
957                    self.event_listener.handle_event(Event::CollectingTests {
958                        matching_module: if match_module.is_empty() {
959                            None
960                        } else {
961                            Some(match_module.to_string())
962                        },
963                        matching_names: match_names.clone().unwrap_or_default(),
964                    });
965
966                    (match_module.to_string(), match_names)
967                })
968                .collect::<Vec<(String, Option<Vec<String>>)>>()
969        });
970
971        if match_tests.is_none() {
972            self.event_listener.handle_event(Event::CollectingTests {
973                matching_module: None,
974                matching_names: vec![],
975            });
976        }
977
978        for checked_module in self.checked_modules.values() {
979            if checked_module.package != self.config.name.to_string() {
980                continue;
981            }
982
983            for def in checked_module.ast.definitions() {
984                let func = match (kind, def) {
985                    (RunnableKind::Test, Definition::Test(func)) => Some(func),
986                    (RunnableKind::Bench, Definition::Benchmark(func)) => Some(func),
987                    _ => None,
988                };
989
990                if let Some(func) = func {
991                    if let Some(match_tests) = &match_tests {
992                        let is_match = match_tests.iter().any(|(module, names)| {
993                            let matched_module =
994                                module.is_empty() || checked_module.name.contains(module);
995
996                            let matched_name = match names {
997                                None => true,
998                                Some(names) => names.iter().any(|name| {
999                                    if exact_match {
1000                                        name == &func.name
1001                                    } else {
1002                                        func.name.contains(name)
1003                                    }
1004                                }),
1005                            };
1006
1007                            matched_module && matched_name
1008                        });
1009
1010                        if is_match {
1011                            scripts.push((
1012                                checked_module.input_path.clone(),
1013                                checked_module.name.clone(),
1014                                func,
1015                            ))
1016                        }
1017                    } else {
1018                        scripts.push((
1019                            checked_module.input_path.clone(),
1020                            checked_module.name.clone(),
1021                            func,
1022                        ))
1023                    }
1024                }
1025            }
1026        }
1027
1028        let mut generator = self.new_generator(tracing);
1029
1030        let mut tests = Vec::new();
1031
1032        for (input_path, module_name, test) in scripts.into_iter() {
1033            if verbose {
1034                self.event_listener.handle_event(Event::GeneratingUPLCFor {
1035                    name: test.name.clone(),
1036                    path: input_path.clone(),
1037                })
1038            }
1039
1040            tests.push(Test::from_function_definition(
1041                &mut generator,
1042                test.to_owned(),
1043                module_name,
1044                input_path,
1045                kind,
1046            ));
1047        }
1048
1049        // NOTE: The filtering syntax for tests isn't quite obvious. A common pitfall when willing
1050        // to match over a top-level module is to simple pass in `-m module_name`, which will be
1051        // treated as a match for a test name.
1052        //
1053        // In this case, we raise an additional warning to suggest a different match syntax, which
1054        // may be quite helpful in teaching users how to deal with module filtering.
1055        match match_tests.as_deref() {
1056            Some(&[(ref s, Some(ref names))]) if tests.is_empty() => {
1057                if let [test] = names.as_slice() {
1058                    self.warnings.push(Warning::SuspiciousTestMatch {
1059                        test: if s.is_empty() {
1060                            test.to_string()
1061                        } else {
1062                            s.to_string()
1063                        },
1064                    });
1065                }
1066            }
1067            _ => (),
1068        }
1069
1070        Ok(tests)
1071    }
1072
1073    #[allow(clippy::result_large_err)]
1074    fn collect_tests(
1075        &mut self,
1076        verbose: bool,
1077        match_tests: Option<Vec<String>>,
1078        exact_match: bool,
1079        tracing: Tracing,
1080    ) -> Result<Vec<Test>, Error> {
1081        self.collect_test_items(
1082            RunnableKind::Test,
1083            verbose,
1084            match_tests,
1085            exact_match,
1086            tracing,
1087        )
1088    }
1089
1090    #[allow(clippy::result_large_err)]
1091    fn collect_benchmarks(
1092        &mut self,
1093        verbose: bool,
1094        match_tests: Option<Vec<String>>,
1095        exact_match: bool,
1096        tracing: Tracing,
1097    ) -> Result<Vec<Test>, Error> {
1098        self.collect_test_items(
1099            RunnableKind::Bench,
1100            verbose,
1101            match_tests,
1102            exact_match,
1103            tracing,
1104        )
1105    }
1106
1107    fn run_runnables(
1108        &self,
1109        tests: Vec<Test>,
1110        seed: u32,
1111        max_success: usize,
1112    ) -> Vec<TestResult<UntypedExpr, UntypedExpr>> {
1113        use rayon::prelude::*;
1114
1115        let data_types = utils::indexmap::as_ref_values(&self.data_types);
1116
1117        let plutus_version = &self.config.plutus;
1118
1119        tests
1120            .into_par_iter()
1121            .map(|test| test.run(seed, max_success, plutus_version))
1122            .collect::<Vec<TestResult<(Constant, Rc<Type>), PlutusData>>>()
1123            .into_iter()
1124            .map(|test| test.reify(&data_types))
1125            .collect()
1126    }
1127
1128    #[allow(clippy::result_large_err)]
1129    fn aiken_files(&mut self, dir: &Path, kind: ModuleKind) -> Result<(), Error> {
1130        let mut has_default = None;
1131
1132        walkdir::WalkDir::new(dir)
1133            .follow_links(true)
1134            .into_iter()
1135            .filter_map(Result::ok)
1136            .filter(|e| e.file_type().is_file())
1137            .filter(|e| e.path().extension().is_some_and(|ext| ext == "ak"))
1138            .try_for_each(|d| {
1139                if has_default.is_none() {
1140                    has_default = Some(false);
1141                }
1142
1143                let path = d.into_path();
1144                let keep = is_aiken_path(&path, dir);
1145
1146                if !keep {
1147                    self.warnings
1148                        .push(Warning::InvalidModuleName { path: path.clone() });
1149                }
1150
1151                if keep {
1152                    if self.module_name(dir, &path).as_str() == ast::DEFAULT_ENV_MODULE {
1153                        has_default = Some(true);
1154                    }
1155                    self.add_module(AddModuleBy::Path(path), dir, kind)
1156                } else {
1157                    Ok(())
1158                }
1159            })?;
1160
1161        if kind == ModuleKind::Env && has_default == Some(false) {
1162            return Err(Error::NoDefaultEnvironment);
1163        }
1164
1165        Ok(())
1166    }
1167
1168    #[allow(clippy::result_large_err)]
1169    fn add_module(
1170        &mut self,
1171        add_by: AddModuleBy,
1172        dir: &Path,
1173        kind: ModuleKind,
1174    ) -> Result<(), Error> {
1175        let (name, code, path) = match add_by {
1176            AddModuleBy::Path(path) => {
1177                let name = self.module_name(dir, &path);
1178                let code = fs::read_to_string(&path).map_err(|error| Error::FileIo {
1179                    path: path.clone(),
1180                    error,
1181                })?;
1182                (name, code, path)
1183            }
1184            AddModuleBy::Source { name, code } => (name, code, dir.to_path_buf()),
1185        };
1186
1187        self.sources.push(Source {
1188            name,
1189            code,
1190            kind,
1191            path,
1192        });
1193
1194        Ok(())
1195    }
1196
1197    fn module_name(&self, package_path: &Path, full_module_path: &Path) -> String {
1198        // ../../{config.name}/module.ak
1199
1200        // module.ak
1201        let mut module_path = full_module_path
1202            .strip_prefix(package_path)
1203            .expect("Stripping package prefix from module path")
1204            .to_path_buf();
1205
1206        // module
1207        module_path.set_extension("");
1208
1209        // Stringify
1210        let name = module_path
1211            .to_str()
1212            .expect("Module name path to str")
1213            .to_string();
1214
1215        // normalise windows paths
1216        name.replace('\\', "/").replace('-', "_")
1217    }
1218}
1219
1220fn is_aiken_path(path: &Path, dir: impl AsRef<Path>) -> bool {
1221    use regex::Regex;
1222
1223    let re = Regex::new(&format!(
1224        "^({module}{slash})*{module}(\\.[-_a-z0-9]*)*\\.ak$",
1225        module = "[a-z][-_a-z0-9]*",
1226        slash = "(/|\\\\)",
1227    ))
1228    .expect("is_aiken_path() RE regex");
1229
1230    re.is_match(
1231        path.strip_prefix(dir)
1232            .expect("is_aiken_path(): strip_prefix")
1233            .to_str()
1234            .expect("is_aiken_path(): to_str"),
1235    )
1236}