Skip to main content

firmion_process/
process.rs

1// Top-level pipeline orchestrator for firmion.
2//
3// The process function is the single entry point that drives the entire
4// compiler pipeline.  It sequences the four stages in order — Ast, LayoutDb,
5// IRDb and Engine — passing each stage's output as input to the next, and
6// converting any stage-level Err(()) result into an anyhow error so that the
7// caller receives a descriptive failure message.  It also handles the output
8// file name, creating the file before handing it to Engine for writing.
9//
10// Order of operations: process.rs sits above all four pipeline stages.
11// main.rs calls process() once per invocation after reading the source file.
12
13// Don't clutter upstream docs.rs for an otherwise private library.
14#![doc(hidden)]
15
16pub mod map_phase;
17use anyhow::{Context, Result, anyhow};
18use parse_int::parse;
19use std::collections::HashMap;
20use std::fs::File;
21use std::io::Write;
22
23// Local libraries
24use ast::Ast;
25use ast::astdb::AstDb;
26use diags::Diags;
27use exec_phase::ExecPhase;
28use firmion_extension::extension_registry::ExtensionRegistry;
29use firmion_extension::test_mocks::register_test_extensions;
30use ir::{ConstBuiltins, ParameterValue};
31use irdb::IRDb;
32use layout_phase::LayoutPhase;
33use irdb::layoutdb::LayoutDb;
34use crate::map_phase::{format_c99, format_csv, format_json, format_rs};
35use irdb::regiondb::RegionDb;
36use exec_phase::validation_phase::ValidationPhase;
37
38#[allow(unused_imports)]
39use tracing::{debug, error, info, trace, warn};
40
41/// Parses a single `-D` define string of the form `NAME=value` or `NAME`
42/// into a `(name, ParameterValue)` pair.
43///
44/// Value type inference:
45/// - No `=`                          → `Integer(1)` (GCC convention for bare -DFLAG)
46/// - Ends with `u`                   → `U64`
47/// - Ends with `i`                   → `I64`
48/// - Starts with `"` / `'`          → `QuotedString` (strip surrounding quotes)
49/// - Starts with `-`                 → `I64`
50/// - Starts with `0x`/`0b`     `     → `U64` (matches source const behavior)
51/// - Otherwise                       → `Integer`
52fn parse_define(s: &str) -> Result<(String, ParameterValue)> {
53    if s.is_empty() {
54        return Err(anyhow!("Empty name in define '{}'", s));
55    }
56    let (name, val_str) = match s.find('=') {
57        None => return Ok((s.to_string(), ParameterValue::Integer(1))),
58        Some(pos) => (&s[..pos], &s[pos + 1..]),
59    };
60    if name.is_empty() {
61        return Err(anyhow!("Empty name in define '{}'", s));
62    }
63    let value = if val_str.starts_with('"') || val_str.starts_with('\'') {
64        // trim_matches only removes prefixes and suffixes, not interior chars.
65        let inner = val_str.trim_matches(&['"', '\''][..]);
66        ParameterValue::QuotedString(inner.to_string())
67    } else if let Some(stripped) = val_str.strip_suffix('u') {
68        let v =
69            parse::<u64>(stripped).map_err(|e| anyhow!("Error parsing define '{}': {}", s, e))?;
70        ParameterValue::U64(v)
71    } else if let Some(stripped) = val_str.strip_suffix('i') {
72        let v = parse::<i64>(stripped)
73            .map_err(|_| anyhow!("Invalid I64 value in define '{}': '{}'", s, stripped))?;
74        ParameterValue::I64(v)
75    } else if val_str.starts_with('-') {
76        let v = parse::<i64>(val_str)
77            .map_err(|_| anyhow!("Invalid I64 value in define '{}': '{}'", s, val_str))?;
78        ParameterValue::I64(v)
79    } else if val_str.starts_with("0x")
80        || val_str.starts_with("0X")
81        || val_str.starts_with("0b")
82        || val_str.starts_with("0B")
83    {
84        let v = parse::<u64>(val_str)
85            .map_err(|_| anyhow!("Invalid U64 value in define '{}': '{}'", s, val_str))?;
86        ParameterValue::U64(v)
87    } else if val_str.chars().any(|c| !c.is_ascii_digit()) {
88        // Non-numeric characters: treat as a bare string rather than erroring.
89        // This lets -DPATH=./file.elf work without shell-level quote gymnastics.
90        ParameterValue::QuotedString(val_str.to_string())
91    } else {
92        let v = parse::<i64>(val_str)
93            .map_err(|_| anyhow!("Invalid integer value in define '{}': '{}'", s, val_str))?;
94        ParameterValue::Integer(v)
95    };
96    Ok((name.to_string(), value))
97}
98
99/// Returns all compiled-in extension names in sorted order.
100/// Excludes test-only mock extensions.
101pub fn list_extensions() -> Vec<String> {
102    let mut registry = ExtensionRegistry::new();
103    extensions::register_all(&mut registry);
104    registry
105        .sorted_names()
106        .iter()
107        .map(|s| s.to_string())
108        .collect()
109}
110
111/// Entry point for all processing on the input source file.
112/// This function drives the compilation pipeline.
113///
114/// `name`        — source file path
115/// `fstr`        — source file contents
116/// `output_file` — binary output path (default: "output.bin")
117/// `verbosity`   — log level (0 = quiet, 1 = default, 2+ = verbose)
118/// `noprint`     — suppress print statements in source
119/// `defines`     — command-line const defines, e.g. `["BASE=0x1000", "COUNT=4"]`
120/// `map_hf`           — human-friendly map destination: None = skip,
121///                      Some("-") = stdout, Some(path) = file
122/// `map_json`         — JSON map destination: None = skip,
123///                      Some("-") = stdout, Some(path) = file
124/// `max_output_size`  — reject images larger than this many bytes (ERR_179)
125#[allow(clippy::too_many_arguments)]
126pub fn process(
127    name: &str,
128    fstr: &str,
129    output_file: Option<&str>,
130    verbosity: u64,
131    noprint: bool,
132    defines: &[String],
133    max_output_size: u64,
134    map_csv: Option<&str>,
135    map_json: Option<&str>,
136    map_c99: Option<&str>,
137    map_rs: Option<&str>,
138) -> Result<()> {
139    info!("Processing {}", name);
140    ConstBuiltins::init();
141
142    let mut diags = Diags::new(name, fstr, verbosity, noprint);
143
144    // Parse -D defines into a map of pre-resolved const values.
145    let mut const_defines: HashMap<String, ParameterValue> = HashMap::new();
146    for d in defines {
147        let (n, v) = parse_define(d)?;
148        const_defines.insert(n, v);
149    }
150
151    let ast = Ast::new(name, fstr, &mut diags).context("[ERR_218]: Error detected, halting.")?;
152
153    if verbosity > 2 {
154        ast.dump("ast.dot")?;
155    }
156
157    // First AstDb: lenient (no nesting validation) — used only by const_eval.
158    // Nesting validation is deferred to the post-prune AstDb below, where
159    // sections promoted from top-level if/else blocks are visible.
160    let ast_db = AstDb::new(&mut diags, &ast, false)?;
161
162    let (mut symbol_table, pruned_ast) = const_eval::evaluate_and_prune(&mut diags, &ast, &ast_db, &const_defines)
163        .context("[ERR_219]: Error detected, halting.")?;
164
165    // Second AstDb: built from the pruned AST with full nesting validation.
166    // Sections promoted from top-level if/else blocks are now at root level.
167    let pruned_ast_db =
168        AstDb::new(&mut diags, &pruned_ast, true).context("[ERR_220]: Error detected, halting.")?;
169
170    let Some(region_bindings) =
171        const_eval::evaluate_regions(&mut diags, &pruned_ast, &pruned_ast_db, &mut symbol_table)
172    else {
173        return Err(anyhow!("[ERR_226]: Error detected, halting."));
174    };
175
176    let Some(obj_props) =
177        const_eval::evaluate_obj_props(&mut diags, &pruned_ast, &pruned_ast_db, &mut symbol_table)
178    else {
179        return Err(anyhow!("[ERR_232]: Error detected, halting."));
180    };
181
182    // Build the section-to-region-name map (foreign key into region_bindings).
183    let mut section_region_names: HashMap<String, String> = HashMap::new();
184    for (sec_name, section) in &pruned_ast_db.sections {
185        if let Some(region_name) = &section.region {
186            section_region_names.insert(sec_name.to_string(), region_name.clone());
187        }
188    }
189
190    let layout_db = LayoutDb::new(&mut diags, &pruned_ast, &pruned_ast_db, &symbol_table, obj_props)
191        .context("[ERR_221]: Error detected, halting.")?;
192    if verbosity > 2 {
193        layout_db.dump();
194    }
195
196    let mut ext_registry = ExtensionRegistry::new();
197    register_test_extensions(&mut ext_registry);
198    extensions::register_all(&mut ext_registry);
199
200    let ir_db = IRDb::new(
201        symbol_table,
202        &layout_db,
203        &mut diags,
204        &ext_registry,
205        section_region_names,
206        region_bindings,
207    )
208    .context("[ERR_222]: Error detected, halting.")?;
209
210    debug!("Dumping ir_db");
211    if verbosity > 2 {
212        ir_db.dump();
213    }
214
215    let region_db = RegionDb::build(&ir_db, &mut diags)
216        .ok_or_else(|| anyhow!("[ERR_227]: Error detected, halting."))?;
217
218    let (location_db, argval_db) =
219        LayoutPhase::build(&ir_db, &region_db, &ext_registry, &mut diags)
220            .context("[ERR_223]: Error detected, halting.")?;
221    if verbosity > 2 {
222        // LayoutPhase debug dump removed
223    }
224
225    // Check image size against --max-output-size before writing any bytes.
226    // Determine if the user specified an output file on the command line
227    // Trim whitespace
228    let fname_str = String::from(output_file.unwrap_or("output.bin").trim_matches(' '));
229    debug!("process: output file name is {}", fname_str);
230
231    let map_db = map_phase::build(&location_db, &ir_db, &fname_str, &mut diags);
232    let final_size = map_db.sections.last().map_or(0, |d| d.file_offset + d.size);
233    if final_size > max_output_size {
234        let msg = format!(
235            "Output image size {} bytes exceeds maximum {} bytes. \
236             Use --max-output-size to increase the limit.",
237            final_size, max_output_size
238        );
239        diags.err0("ERR_224", &msg);
240        return Err(anyhow!("[ERR_224]: Error detected, halting."));
241    }
242
243    ValidationPhase::validate(&argval_db, &ir_db, &mut diags)
244        .context("[ERR_225]: Error detected, halting.")?;
245
246    let mut file = std::fs::OpenOptions::new()
247        .read(true)
248        .write(true)
249        .create(true)
250        .truncate(true)
251        .open(&fname_str)
252        .context(format!("Unable to create output file {}", fname_str))?;
253
254    if ExecPhase::execute(
255        &location_db,
256        &argval_db,
257        &map_db,
258        &ir_db,
259        &mut diags,
260        &mut file,
261        &ext_registry,
262    )
263    .is_err()
264    {
265        return Err(anyhow!("[ERR_223]: Error detected, halting."));
266    }
267
268    // Generate map output if requested.  MapDb derives all data from the
269    // post-iterate engine and irdb; no additional compiler passes run.
270    if map_csv.is_some() || map_json.is_some() || map_c99.is_some() || map_rs.is_some() {
271        emit_map(map_csv, &format_csv(&map_db))?;
272        emit_map(map_json, &format_json(&map_db))?;
273        emit_map(map_c99, &format_c99(&map_db))?;
274        emit_map(map_rs, &format_rs(&map_db))?;
275    }
276    Ok(())
277}
278
279/// Writes `content` to stdout when `dest` is `Some("-")`, or to the named
280/// file when `dest` is `Some(path)`.  Does nothing when `dest` is `None`.
281fn emit_map(dest: Option<&str>, content: &str) -> Result<()> {
282    match dest {
283        None => {}
284        Some("-") => print!("{content}"),
285        Some(path) => {
286            let mut f = File::create(path).context(format!("Unable to create map file {path}"))?;
287            f.write_all(content.as_bytes())
288                .context(format!("Unable to write map file {path}"))?;
289        }
290    }
291    Ok(())
292}
293
294#[cfg(test)]
295mod tests {
296    use super::parse_define;
297    use ir::ParameterValue;
298
299    fn name_val(s: &str) -> (String, ParameterValue) {
300        parse_define(s).expect("parse_define failed")
301    }
302
303    // --- hex values ---
304
305    #[test]
306    fn hex_no_suffix_is_u64() {
307        let (n, v) = name_val("BASE=0x1000");
308        assert_eq!(n, "BASE");
309        assert_eq!(v, ParameterValue::U64(0x1000));
310    }
311
312    #[test]
313    fn hex_u_suffix_is_u64() {
314        let (n, v) = name_val("BASE=0x1000u");
315        assert_eq!(n, "BASE");
316        assert_eq!(v, ParameterValue::U64(0x1000));
317    }
318
319    #[test]
320    fn hex_i_suffix_is_i64() {
321        let (n, v) = name_val("OFFSET=0x40i");
322        assert_eq!(n, "OFFSET");
323        assert_eq!(v, ParameterValue::I64(0x40));
324    }
325
326    #[test]
327    fn hex_uppercase_digits() {
328        let (n, v) = name_val("MASK=0xFF");
329        assert_eq!(n, "MASK");
330        assert_eq!(v, ParameterValue::U64(0xFF));
331    }
332
333    #[test]
334    fn hex_large_u64() {
335        // 0xFFFFFFFF fits in both i64 and u64; with u suffix must be U64.
336        let (n, v) = name_val("LIMIT=0xFFFFFFFFu");
337        assert_eq!(n, "LIMIT");
338        assert_eq!(v, ParameterValue::U64(0xFFFF_FFFF));
339    }
340
341    #[test]
342    fn hex_u64_max() {
343        // u64::MAX requires u suffix; without it parse::<i64> would fail.
344        let (n, v) = name_val("TOP=0xFFFFFFFFFFFFFFFFu");
345        assert_eq!(n, "TOP");
346        assert_eq!(v, ParameterValue::U64(u64::MAX));
347    }
348
349    #[test]
350    fn hex_u64_max_no_suffix() {
351        // 0xFFFFFFFFFFFFFFFF is valid U64 without any suffix.
352        let (n, v) = name_val("TOP=0xFFFFFFFFFFFFFFFF");
353        assert_eq!(n, "TOP");
354        assert_eq!(v, ParameterValue::U64(u64::MAX));
355    }
356
357    // --- decimal and other cases (regression) ---
358
359    #[test]
360    fn decimal_no_suffix_is_integer() {
361        let (n, v) = name_val("COUNT=42");
362        assert_eq!(n, "COUNT");
363        assert_eq!(v, ParameterValue::Integer(42));
364    }
365
366    #[test]
367    fn decimal_u_suffix_is_u64() {
368        let (n, v) = name_val("SIZE=64u");
369        assert_eq!(n, "SIZE");
370        assert_eq!(v, ParameterValue::U64(64));
371    }
372
373    #[test]
374    fn decimal_negative_is_i64() {
375        let (n, v) = name_val("SHIFT=-4");
376        assert_eq!(n, "SHIFT");
377        assert_eq!(v, ParameterValue::I64(-4));
378    }
379
380    #[test]
381    fn bare_name_is_integer_one() {
382        let (n, v) = name_val("FLAG");
383        assert_eq!(n, "FLAG");
384        assert_eq!(v, ParameterValue::Integer(1));
385    }
386
387    #[test]
388    fn empty_name_is_error() {
389        assert!(parse_define("").is_err());
390        assert!(parse_define("=").is_err());
391        assert!(parse_define("=42").is_err());
392    }
393
394    #[test]
395    fn quoted_string_is_parsed() {
396        let (n, v) = name_val("VERSION=\"1.0\"");
397        assert_eq!(n, "VERSION");
398        assert_eq!(v, ParameterValue::QuotedString("1.0".to_string()));
399
400        let (n2, v2) = name_val("LABEL='stable'");
401        assert_eq!(n2, "LABEL");
402        assert_eq!(v2, ParameterValue::QuotedString("stable".to_string()));
403    }
404
405    #[test]
406    fn bare_non_numeric_is_string() {
407        // Unquoted values containing non-digit characters become bare strings.
408        // This lets -DPATH=./some/file.elf work without shell-level quoting.
409        let (n, v) = name_val("PATH=./.pio/build/firmware.elf");
410        assert_eq!(n, "PATH");
411        assert_eq!(v, ParameterValue::QuotedString("./.pio/build/firmware.elf".to_string()));
412
413        let (n2, v2) = name_val(r"PATH=.\.pio\build\firmware.elf");
414        assert_eq!(n2, "PATH");
415        assert_eq!(v2, ParameterValue::QuotedString(r".\.pio\build\firmware.elf".to_string()));
416    }
417}