1use crate::{
2 blueprint::{
3 self,
4 definitions::Definitions,
5 parameter::Parameter,
6 schema::{Annotated, Declaration, Schema},
7 },
8 module::{CheckedModule, CheckedModules},
9};
10use aiken_lang::{
11 ast::{ArgName, Span, TypedArg, TypedFunction},
12 gen_uplc::CodeGenerator,
13 plutus_version::PlutusVersion,
14};
15use miette::NamedSource;
16use uplc::ast::SerializableProgram;
17
18#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
19pub struct Export {
20 pub name: String,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub doc: Option<String>,
24
25 #[serde(skip_serializing_if = "Vec::is_empty")]
26 #[serde(default)]
27 pub parameters: Vec<Parameter>,
28
29 pub return_type: Parameter,
30
31 #[serde(flatten)]
32 pub program: SerializableProgram,
33
34 #[serde(skip_serializing_if = "Definitions::is_empty")]
35 #[serde(default)]
36 pub definitions: Definitions<Annotated<Schema>>,
37}
38
39impl Export {
40 pub fn from_function(
41 func: &TypedFunction,
42 module: &CheckedModule,
43 generator: &mut CodeGenerator,
44 modules: &CheckedModules,
45 plutus_version: &PlutusVersion,
46 ) -> Result<Export, blueprint::Error> {
47 let mut definitions = Definitions::new();
48
49 let parameters = func
50 .arguments
51 .iter()
52 .map(|param| {
53 Annotated::from_type(
54 modules.into(),
55 blueprint::validator::tipo_or_annotation(module, param),
56 &mut definitions,
57 )
58 .map(|schema| Parameter {
59 title: Some(param.arg_name.get_label()),
60 schema: Declaration::Referenced(schema),
61 })
62 .map_err(|error| blueprint::Error::Schema {
63 error,
64 location: param.location,
65 source_code: NamedSource::new(
66 module.input_path.display().to_string(),
67 module.code.clone(),
68 ),
69 })
70 })
71 .collect::<Result<_, _>>()?;
72
73 let return_type = Annotated::from_type(
74 modules.into(),
75 blueprint::validator::tipo_or_annotation(
76 module,
77 &TypedArg {
78 arg_name: ArgName::Discarded {
79 name: "".to_string(),
80 label: "".to_string(),
81 location: Span::empty(),
82 },
83 location: Span::empty(),
84 annotation: func.return_annotation.clone(),
85 doc: None,
86 is_validator_param: false,
87 tipo: func.return_type.clone(),
88 },
89 ),
90 &mut definitions,
91 )
92 .map(|schema| Parameter {
93 title: Some("return_type".to_string()),
94 schema: Declaration::Referenced(schema),
95 })
96 .map_err(|error| blueprint::Error::Schema {
97 error,
98 location: func.location,
99 source_code: NamedSource::new(
100 module.input_path.display().to_string(),
101 module.code.clone(),
102 ),
103 })?;
104
105 let program = generator
106 .generate_raw(&func.body, &func.arguments, &module.name)
107 .to_debruijn()
108 .unwrap();
109
110 let program = match plutus_version {
111 PlutusVersion::V1 => SerializableProgram::PlutusV1Program(program),
112 PlutusVersion::V2 => SerializableProgram::PlutusV2Program(program),
113 PlutusVersion::V3 => SerializableProgram::PlutusV3Program(program),
114 };
115
116 Ok(Export {
117 name: format!("{}.{}", &module.name, &func.name),
118 doc: func.doc.clone(),
119 parameters,
120 return_type,
121 program,
122 definitions,
123 })
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::{CheckedModules, Export};
130 use crate::tests::TestProject;
131 use aiken_lang::{
132 self,
133 ast::{TraceLevel, Tracing},
134 plutus_version::PlutusVersion,
135 };
136
137 macro_rules! assert_export {
138 ($code:expr) => {
139 let mut project = TestProject::new();
140
141 let modules = CheckedModules::singleton(project.check(project.parse(indoc::indoc! { $code })));
142
143 let mut generator = project.new_generator(
144 Tracing::All(TraceLevel::Verbose),
145 );
146
147 let (module, func) = modules
148 .functions()
149 .next()
150 .expect("source code did no yield any exports");
151
152 let export = Export::from_function(func, module, &mut generator, &modules, &PlutusVersion::default());
153
154 match export {
155 Err(e) => insta::with_settings!({
156 description => concat!("Code:\n\n", indoc::indoc! { $code }),
157 omit_expression => true
158 }, {
159 insta::assert_debug_snapshot!(e);
160 }),
161
162 Ok(validator) => insta::with_settings!({
163 description => concat!("Code:\n\n", indoc::indoc! { $code }),
164 omit_expression => true
165 }, {
166 insta::assert_json_snapshot!(validator);
167 }),
168 };
169 };
170 }
171
172 #[test]
173 fn basic_export() {
174 assert_export!(
175 r#"
176 pub fn add(a: Int, b: Int) -> Int {
177 a + b
178 }
179 "#
180 );
181 }
182
183 #[test]
184 fn illegal_opaque_type() {
185 assert_export!(
186 r#"
187 pub opaque type Thing {
188 a: Int
189 }
190
191 pub fn add(a: Thing, b: Int) -> Int {
192 a.a + b
193 }
194 "#
195 );
196 }
197
198 #[test]
199 fn recursive_types() {
200 assert_export!(
201 r#"
202 pub type Foo<a> {
203 Empty
204 Bar(a, Foo<a>)
205 }
206
207 pub fn add(a: Foo<Int>, b: Foo<Int>) -> Int {
208 when (a, b) is {
209 (Empty, Empty) -> 0
210 (Bar(x, y), Bar(c, d)) -> x + c + add(y, d)
211 (Empty, Bar(c, d)) -> c + add(Empty, d)
212 (Bar(x, y), Empty) -> x + add(y, Empty)
213 }
214 }
215 "#
216 );
217 }
218
219 #[test]
220 fn cannot_export_generics() {
221 assert_export!(
222 r#"
223 pub fn add(_a: a, _b: b) -> Bool {
224 True
225 }
226 "#
227 );
228 }
229}