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