modrpc_codegen/codegen/
mod.rs

1use std::collections::HashSet;
2
3use crate::{
4    Database, Module,
5    ast::{Interface, Schema},
6};
7
8pub mod js;
9pub mod rust;
10pub mod wasm;
11
12pub fn interface_init_state(db: &Database, interface: &Interface) -> mproto_codegen::ast::TypeDef {
13    let mut init_fields = Vec::new();
14
15    for state in &interface.state {
16        init_fields.push(mproto_codegen::ast::NamedField {
17            name: state.name.clone(),
18            ty: state.ty.clone(),
19        });
20    }
21
22    for object in &interface.objects {
23        let Some(object_interface) = db.lookup_interface(&object.construct) else {
24            panic!("failed to lookup interface '{}'", object.construct.name);
25        };
26
27        if !object_interface.has_state(db) {
28            continue;
29        }
30
31        init_fields.push(mproto_codegen::ast::NamedField {
32            name: object.name.clone(),
33            ty: mproto_codegen::ast::Type::Defined {
34                // TODO import InitState from construct's crate
35                ident: mproto_codegen::ast::QualifiedIdentifier {
36                    name: format!("{}InitState", object.construct.name),
37                    module: object.construct.module.clone(),
38                },
39                args: object.type_args.clone(),
40            },
41        });
42    }
43
44    let params = if interface.has_state(db) {
45        interface.type_params.clone()
46    } else {
47        vec![]
48    };
49
50    mproto_codegen::ast::TypeDef {
51        name: format!("{}InitState", interface.name),
52        params,
53        body: mproto_codegen::ast::TypeBody::Struct(mproto_codegen::ast::Struct {
54            fields: init_fields,
55        }),
56    }
57}
58
59pub fn interface_role_config(
60    db: &Database,
61    mproto_cx: &mproto_codegen::codegen::CodegenCx<'_>,
62    interface: &Interface,
63    role_name: &str,
64) -> mproto_codegen::ast::TypeDef {
65    let mut fields = Vec::new();
66    let mut used_type_params = HashSet::new();
67
68    for config_list in &interface.config {
69        if !config_list.roles.iter().find(|r| *r == role_name).is_some() {
70            continue;
71        }
72
73        for config_item in &config_list.items {
74            for type_param in &interface.type_params {
75                if mproto_codegen::codegen::type_uses_param(mproto_cx, &config_item.ty, type_param)
76                {
77                    used_type_params.insert(type_param.clone());
78                }
79            }
80
81            fields.push(mproto_codegen::ast::NamedField {
82                name: config_item.name.clone(),
83                ty: config_item.ty.clone(),
84            });
85        }
86    }
87
88    for object in &interface.objects {
89        let Some(object_interface) = db.lookup_interface(&object.construct) else {
90            panic!("failed to lookup interface '{}'", object.construct.name);
91        };
92
93        let object_role_names = object.get_roles_for_parent_role(object_interface, role_name);
94        if object_role_names.is_empty() {
95            panic!(
96                "object '{}' failed to find roles '{}'",
97                object.name, role_name
98            );
99        };
100
101        for object_role_name in &object_role_names {
102            if !object_interface.has_config_for_role(object_role_name) {
103                continue;
104            }
105
106            let object_interface_role_name =
107                &format!("{}{}", object.construct.name, object_role_name);
108            let object_role_config_def = db
109                .mproto_db()
110                .lookup_type_def(&mproto_codegen::ast::QualifiedIdentifier {
111                    name: format!("{object_interface_role_name}Config"),
112                    module: object.construct.module.clone(),
113                })
114                .expect("lookup object role config");
115
116            let mut type_args = vec![];
117            for (type_param, type_arg) in object_interface
118                .type_params
119                .iter()
120                .zip(object.type_args.iter())
121            {
122                let object_role_config_uses_type_param = object_role_config_def
123                    .params
124                    .iter()
125                    .find(|p| p == &type_param)
126                    .is_some();
127                if object_role_config_uses_type_param {
128                    type_args.push(type_arg.clone());
129                }
130            }
131
132            fields.push(mproto_codegen::ast::NamedField {
133                name: object.name.clone(),
134                ty: mproto_codegen::ast::Type::Defined {
135                    ident: mproto_codegen::ast::QualifiedIdentifier {
136                        name: format!("{object_interface_role_name}Config"),
137                        module: object.construct.module.clone(),
138                    },
139                    args: type_args,
140                },
141            });
142        }
143    }
144
145    let params = interface
146        .type_params
147        .iter()
148        .filter(|p| used_type_params.contains(p.as_str()))
149        .cloned()
150        .collect();
151
152    mproto_codegen::ast::TypeDef {
153        name: format!("{}{}Config", interface.name, role_name),
154        params,
155        body: mproto_codegen::ast::TypeBody::Struct(mproto_codegen::ast::Struct { fields }),
156    }
157}
158
159pub fn define_interface_config_and_state_structs(db: &mut Database, interface: &Interface) {
160    // Add init state for interface to mproto module.
161    let init_state_struct = interface_init_state(db, interface);
162    let _ = db
163        .mproto_db_mut()
164        .local_mut()
165        .new_type_def(init_state_struct);
166    // Add interface role config structs to mproto module.
167    for role_name in &interface.roles {
168        let role_config_struct = interface_role_config(
169            db,
170            &mproto_codegen::codegen::CodegenCx::new_with_type_params(
171                db.mproto_db(),
172                Some("crate::proto"),
173                true,
174                &interface.type_params,
175            ),
176            &interface,
177            role_name,
178        );
179        let _ = db
180            .mproto_db_mut()
181            .local_mut()
182            .new_type_def(role_config_struct);
183    }
184}
185
186pub fn load_imports_recursive(db: &mut Database, schema: &Schema) -> Result<(), ()> {
187    for import in &schema.imports {
188        if import.path.ends_with(".mproto") {
189            let type_defs = mproto_codegen::parse::parse_file(&import.path)
190                .expect(&format!("failed to parse import '{}'.", import.path));
191            // TODO load mproto imports once mproto supports it.
192            //mproto_codegen::load_imports_recursive(db, &import_schema)?;
193            // Create mproto module
194            let mproto_module = mproto_codegen::Module::from_type_defs(type_defs.clone());
195            // Add mproto module to mproto DB
196            db.mproto_db_mut()
197                .add_module(import.name.clone(), "mproto", mproto_module);
198        } else if import.path.ends_with(".modrpc") {
199            let mut import_schema = crate::parse::parse_file(&import.path)
200                .expect(&format!("failed to parse import '{}'.", import.path));
201
202            load_imports_recursive(db, &import_schema)?;
203
204            // Create mproto module corresponding to modrpc module
205            let mproto_module =
206                mproto_codegen::Module::from_type_defs(import_schema.type_defs.clone());
207            let lower_cx = LowerCx {
208                module_name: &import.name,
209                module: &mproto_module,
210            };
211            let lowered_type_defs = import_schema
212                .type_defs
213                .into_iter()
214                .map(|type_def| lower_cx.lower_type_def(type_def))
215                .collect();
216            let lowered_mproto_module = mproto_codegen::Module::from_type_defs(lowered_type_defs);
217
218            // Create mproto module
219            for interface in &mut import_schema.interfaces {
220                // Modify the imported schema's mproto local type references to be fully qualified.
221                for state_field in &mut interface.state {
222                    state_field.ty = lower_cx.lower_type(state_field.ty.clone());
223                }
224                for config_list in &mut interface.config {
225                    for config_item in &mut config_list.items {
226                        config_item.ty = lower_cx.lower_type(config_item.ty.clone());
227                    }
228                }
229            }
230            // Add mproto module to mproto DB
231            db.mproto_db_mut()
232                .add_module(import.name.clone(), "modrpc", lowered_mproto_module);
233            // Add init and config structs after module is installed in the DB since they require
234            // that locally referenced types are defined.
235            for interface in &mut import_schema.interfaces {
236                // TODO re-use define_interface_config_and_state_structs. Requires splitting
237                // mproto_codgen::Database out of modrpc_codegen::Database.
238
239                // Add init state for interface to mproto module.
240                let init_state_struct = interface_init_state(db, &interface);
241                let _ = db
242                    .mproto_db_mut()
243                    .imported_module_mut(&import.name)
244                    .expect("lookup mproto module")
245                    .new_type_def(init_state_struct);
246                // Add interface role config structs to mproto module.
247                for role_name in &interface.roles {
248                    let role_config_struct = interface_role_config(
249                        db,
250                        &mproto_codegen::codegen::CodegenCx::new_with_type_params(
251                            db.mproto_db(),
252                            None,
253                            true,
254                            &interface.type_params,
255                        ),
256                        &interface,
257                        role_name,
258                    );
259                    let _ = db
260                        .mproto_db_mut()
261                        .imported_module_mut(&import.name)
262                        .expect("lookup mproto module")
263                        .new_type_def(role_config_struct);
264                }
265            }
266
267            // Create modrpc module
268            let mut module = Module::new();
269            for interface in import_schema.interfaces {
270                let _ = module.add_interface(interface.clone());
271            }
272            // Add modrpc module to modrpc DB
273            db.add_module(import.name.clone(), module);
274        } else {
275            // TODO return a descriptive error
276            panic!(
277                "imports must end with '.mproto' or '.modrpc' - got '{}'",
278                import.path
279            );
280        }
281    }
282
283    Ok(())
284}
285
286struct LowerCx<'a> {
287    module_name: &'a str,
288    module: &'a mproto_codegen::Module,
289}
290
291impl<'a> LowerCx<'a> {
292    /// For now this just ensures all QualifiedIdentifiers for things in the local module have their
293    /// module set to `Some(module_name)`. In the future we should actually lower to an IR that has
294    /// type references resolved into TypeDefIds.
295    fn lower_type_def(
296        &self,
297        type_def: mproto_codegen::ast::TypeDef,
298    ) -> mproto_codegen::ast::TypeDef {
299        let body = match type_def.body {
300            mproto_codegen::ast::TypeBody::Struct(struct_body) => {
301                mproto_codegen::ast::TypeBody::Struct(mproto_codegen::ast::Struct {
302                    fields: self.lower_named_fields(struct_body.fields),
303                })
304            }
305            mproto_codegen::ast::TypeBody::Enum(enum_body) => {
306                mproto_codegen::ast::TypeBody::Enum(mproto_codegen::ast::Enum {
307                    variants: enum_body
308                        .variants
309                        .into_iter()
310                        .map(|(name, variant)| {
311                            let variant = match variant {
312                                mproto_codegen::ast::EnumVariant::Empty => {
313                                    mproto_codegen::ast::EnumVariant::Empty
314                                }
315                                mproto_codegen::ast::EnumVariant::NamedFields { fields } => {
316                                    mproto_codegen::ast::EnumVariant::NamedFields {
317                                        fields: self.lower_named_fields(fields),
318                                    }
319                                }
320                            };
321                            (name, variant)
322                        })
323                        .collect(),
324                })
325            }
326        };
327
328        mproto_codegen::ast::TypeDef {
329            name: type_def.name,
330            params: type_def.params,
331            body,
332        }
333    }
334
335    fn lower_named_fields(
336        &self,
337        fields: Vec<mproto_codegen::ast::NamedField>,
338    ) -> Vec<mproto_codegen::ast::NamedField> {
339        fields
340            .into_iter()
341            .map(|f| mproto_codegen::ast::NamedField {
342                name: f.name,
343                ty: self.lower_type(f.ty),
344            })
345            .collect()
346    }
347
348    fn lower_type(&self, ty: mproto_codegen::ast::Type) -> mproto_codegen::ast::Type {
349        use mproto_codegen::ast::{PrimitiveType, QualifiedIdentifier, Type};
350        match ty {
351            Type::Primitive(PrimitiveType::Box(inner_ty)) => {
352                Type::Primitive(PrimitiveType::Box(Box::new(self.lower_type(*inner_ty))))
353            }
354            Type::Primitive(PrimitiveType::List(item_ty)) => {
355                Type::Primitive(PrimitiveType::List(Box::new(self.lower_type(*item_ty))))
356            }
357            Type::Primitive(PrimitiveType::Option(item_ty)) => {
358                Type::Primitive(PrimitiveType::Option(Box::new(self.lower_type(*item_ty))))
359            }
360            Type::Primitive(PrimitiveType::Result(ok_ty, err_ty)) => {
361                Type::Primitive(PrimitiveType::Result(
362                    Box::new(self.lower_type(*ok_ty)),
363                    Box::new(self.lower_type(*err_ty)),
364                ))
365            }
366            Type::Defined { ident, args } => {
367                let args = args.into_iter().map(|a| self.lower_type(a)).collect();
368                if ident.module.is_none() && self.module.type_def_by_name(&ident.name).is_some() {
369                    Type::Defined {
370                        ident: QualifiedIdentifier {
371                            module: Some(self.module_name.into()),
372                            name: ident.name,
373                        },
374                        args,
375                    }
376                } else {
377                    Type::Defined { ident, args }
378                }
379            }
380            ty @ _ => ty,
381        }
382    }
383}
384
385fn write_js_file(
386    path: impl AsRef<std::path::Path> + std::fmt::Debug,
387    header: &str,
388    tokens: &genco::lang::js::Tokens,
389) -> std::io::Result<()> {
390    use std::io::Write;
391
392    let fmt = genco::fmt::Config::from_lang::<genco::lang::JavaScript>()
393        .with_indentation(genco::fmt::Indentation::Space(2));
394    let config = genco::lang::js::Config::default();
395    let mut file = std::fs::File::create(&path)?;
396
397    file.write_all(header.as_bytes())?;
398
399    let mut w = genco::fmt::IoWriter::new(file);
400    tokens
401        .format_file(&mut w.as_formatter(&fmt), &config)
402        .expect(&format!("format {:?} file", path));
403
404    Ok(())
405}