Skip to main content

modrpc_codegen/codegen/
mod.rs

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