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 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 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 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 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 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 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_path.set_extension("");
1208
1209 let name = module_path
1211 .to_str()
1212 .expect("Module name path to str")
1213 .to_string();
1214
1215 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}