Skip to main content

synta_codegen/
c_meson_codegen.rs

1//! Meson build-system file generator for synta-generated C code.
2//!
3//! Produces a `meson.build` file that:
4//! - Defines each ASN.1 module as a Meson `library()` target.
5//! - Declares a companion `*_dep` dependency object for each target.
6//! - Expresses inter-module dependencies via those dependency objects.
7//! - Locates the synta library and enforces the C99 standard.
8
9use crate::ast::Module;
10use crate::naming::{module_file_stem, to_pascal_case};
11use std::fmt::Write;
12
13/// Configuration for Meson file generation.
14#[derive(Debug, Clone, Default)]
15pub struct MesonConfig {
16    /// Path to the synta source tree (the directory that contains `include/`
17    /// and `target/release/`).  When `None` the generated file uses a
18    /// `dependency('synta')` call (resolved by pkg-config or a wrap file).
19    pub synta_root: Option<String>,
20    /// Build the generated library as `'shared_library'` instead of
21    /// `'library'` (which defaults to the build-system preference).
22    pub shared_library: bool,
23}
24
25/// Generate a `meson.build` for one or more ASN.1 modules.
26///
27/// `modules` is the full slice of parsed modules; `order` is the topological
28/// generation order returned by [`crate::import_graph::topological_order`]
29/// (dependencies-first).  Pass `&[0]` when there is only one module.
30pub fn generate_meson(
31    modules: &[Module],
32    order: &[usize],
33    config: MesonConfig,
34) -> Result<String, Box<dyn std::error::Error>> {
35    let mut out = String::new();
36
37    // ── Header ────────────────────────────────────────────────────────────────
38    let module_names: Vec<&str> = order.iter().map(|&i| modules[i].name.as_str()).collect();
39    // Project name: snake_case of the primary (last-in-topo) module.
40    let primary_idx = *order.last().unwrap();
41    let project_name = module_file_stem(&modules[primary_idx].name);
42
43    writeln!(
44        out,
45        "# Generated from ASN.1 module{} {}",
46        if modules.len() == 1 { "" } else { "s" },
47        module_names.join(", ")
48    )?;
49    writeln!(out, "# DO NOT EDIT - auto-generated code")?;
50    writeln!(out)?;
51    writeln!(out, "project('{}', 'c',", project_name)?;
52    writeln!(out, "  default_options: ['c_std=c99', 'warning_level=2'],")?;
53    writeln!(out, ")")?;
54    writeln!(out)?;
55    writeln!(out, "cc = meson.get_compiler('c')")?;
56    writeln!(out)?;
57
58    // ── Synta dependency ──────────────────────────────────────────────────────
59    writeln!(
60        out,
61        "# ── Synta dependency ────────────────────────────────────────────────────────"
62    )?;
63    if let Some(ref root) = config.synta_root {
64        writeln!(out, "_synta_lib = cc.find_library('synta',")?;
65        writeln!(out, "  dirs: '{}' / 'target' / 'release',", root)?;
66        writeln!(out, "  required: true,")?;
67        writeln!(out, ")")?;
68        writeln!(out, "synta_dep = declare_dependency(")?;
69        writeln!(out, "  dependencies: _synta_lib,")?;
70        writeln!(
71            out,
72            "  include_directories: include_directories('{}' / 'include'),",
73            root
74        )?;
75        writeln!(out, ")")?;
76    } else {
77        writeln!(
78            out,
79            "# Resolved via pkg-config, a wrap file, or a parent subproject."
80        )?;
81        writeln!(
82            out,
83            "# If synta is not installed system-wide, specify its location with:"
84        )?;
85        writeln!(out, "#   meson setup build -Dsynta_root=/path/to/synta")?;
86        writeln!(out, "# and add the following option to your meson.options:")?;
87        writeln!(out, "#   option('synta_root', type: 'string', value: '',")?;
88        writeln!(out, "#     description: 'Root of the synta source tree')")?;
89        writeln!(out, "synta_dep = dependency('synta')")?;
90    }
91    writeln!(out)?;
92
93    // ── Library function ──────────────────────────────────────────────────────
94    let lib_fn = if config.shared_library {
95        "shared_library"
96    } else {
97        "library"
98    };
99
100    // Map module name → dep variable name (for dependency edges).
101    let dep_var_for: std::collections::HashMap<&str, String> = modules
102        .iter()
103        .map(|m| {
104            let stem = module_file_stem(&m.name);
105            (m.name.as_str(), format!("{}_dep", stem))
106        })
107        .collect();
108
109    // ── Library targets ───────────────────────────────────────────────────────
110    for &idx in order {
111        let module = &modules[idx];
112        let stem = module_file_stem(&module.name);
113        let pascal = to_pascal_case(&module.name);
114        let lib_var = format!("{}_lib", stem);
115        let dep_var = format!("{}_dep", stem);
116
117        // Inter-module deps that are in the known set.
118        let module_deps: Vec<&str> = module
119            .imports
120            .iter()
121            .filter_map(|imp| {
122                dep_var_for
123                    .get(imp.module_name.as_str())
124                    .map(|s| s.as_str())
125            })
126            .collect();
127
128        writeln!(out, "# ASN.1 module: {}", pascal)?;
129        writeln!(out, "{} = {}('{}',", lib_var, lib_fn, stem)?;
130        writeln!(out, "  sources: ['{}.c'],", stem)?;
131        write!(out, "  dependencies: [synta_dep")?;
132        for dep in &module_deps {
133            write!(out, ", {}", dep)?;
134        }
135        writeln!(out, "],")?;
136        writeln!(out, "  install: true,")?;
137        writeln!(out, ")")?;
138        writeln!(out)?;
139        writeln!(out, "{} = declare_dependency(", dep_var)?;
140        writeln!(
141            out,
142            "  include_directories: include_directories('.', is_system: false),"
143        )?;
144        writeln!(out, "  link_with: {},", lib_var)?;
145        writeln!(out, ")")?;
146        writeln!(out)?;
147    }
148
149    Ok(out)
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::parse;
156
157    #[test]
158    fn test_single_module_meson() {
159        let m = parse("MyModule DEFINITIONS ::= BEGIN Foo ::= INTEGER END").unwrap();
160        let out = generate_meson(&[m], &[0], MesonConfig::default()).unwrap();
161
162        assert!(out.contains("project('my_module', 'c'"));
163        assert!(out.contains("c_std=c99"));
164        assert!(out.contains("library('my_module',"));
165        assert!(out.contains("sources: ['my_module.c']"));
166        assert!(out.contains("synta_dep = dependency('synta')"));
167        assert!(out.contains("my_module_dep = declare_dependency("));
168    }
169
170    #[test]
171    fn test_meson_with_synta_root() {
172        let m = parse("Cert DEFINITIONS ::= BEGIN END").unwrap();
173        let config = MesonConfig {
174            synta_root: Some("/opt/synta".to_string()),
175            shared_library: false,
176        };
177        let out = generate_meson(&[m], &[0], config).unwrap();
178
179        assert!(out.contains("cc.find_library('synta'"));
180        assert!(out.contains("'/opt/synta' / 'target' / 'release'"));
181        assert!(out.contains("'/opt/synta' / 'include'"));
182        // pkg-config fallback should not be present
183        assert!(!out.contains("dependency('synta')"));
184    }
185
186    #[test]
187    fn test_meson_shared_library() {
188        let m = parse("Foo DEFINITIONS ::= BEGIN END").unwrap();
189        let config = MesonConfig {
190            shared_library: true,
191            ..Default::default()
192        };
193        let out = generate_meson(&[m], &[0], config).unwrap();
194        assert!(out.contains("shared_library('foo',"));
195    }
196
197    #[test]
198    fn test_multi_module_meson_deps() {
199        let a = parse("ModA DEFINITIONS ::= BEGIN IMPORTS X FROM ModB; END").unwrap();
200        let b = parse("ModB DEFINITIONS ::= BEGIN END").unwrap();
201        // topological order: ModB (1) before ModA (0)
202        let order = vec![1usize, 0usize];
203        let out = generate_meson(&[a, b], &order, MesonConfig::default()).unwrap();
204
205        // ModB library defined
206        assert!(out.contains("library('mod_b',"));
207        // ModA library defined
208        assert!(out.contains("library('mod_a',"));
209        // ModA depends on mod_b_dep
210        assert!(out.contains("dependencies: [synta_dep, mod_b_dep]"));
211    }
212}