diff --git a/Cargo.toml b/Cargo.toml index fb8b83e4..403c93fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ package.license = "MIT OR Apache-2.0" package.homepage = "https://device-driver.com" package.repository = "https://github.com/diondokter/device-driver" package.edition = "2024" -package.rust-version = "1.94" +package.rust-version = "1.96" [workspace.dependencies] device-driver-codegen = { path = "compiler/dd-codegen", version = "=2.0.0" } diff --git a/compiler/dd-cli/src/main.rs b/compiler/dd-cli/src/main.rs index 4b25bf3d..a88d2599 100644 --- a/compiler/dd-cli/src/main.rs +++ b/compiler/dd-cli/src/main.rs @@ -27,9 +27,16 @@ struct BuildArgs { /// Path to the input file. #[arg(short = 's', long = "source", value_name = "FILE", global = true)] source_path: Option, - /// Path to output location. Any existing file is overwritten. If not provided, the output is written to stdout. + /// Path to output location. Any existing file is overwritten. + /// If no output file or target folder is provided and only one file is generated, the output is written to stdout. + /// + /// Can only be used with targets that generate a single file. #[arg(short = 'o', long = "output", value_name = "FILE", global = true)] output_path: Option, + /// Path to target folder location. Any existing file in the folder is overwritten. + /// If no output file or target folder is provided and only one file is generated, the output is written to stdout. + #[arg(short = 't', long = "target", value_name = "FOLDER", global = true)] + target_folder: Option, #[command(flatten)] options: CompileOptions, } @@ -83,11 +90,16 @@ fn build(args: BuildArgs) -> Result { let Some(source_path) = args.source_path else { return Err(DynError::new("no source path provided")); }; + if args.output_path.is_some() && args.target_folder.is_some() { + return Err(DynError::new( + "both single file output path and target folder are specified. Both cannot be used at the same time. Pick one", + )); + } let source = std::fs::read_to_string(&source_path) .with_message(|| format!("Failed to open input file at: {:?}", source_path.display()))?; - let (output, diagnostics) = device_driver_core::compile(&source, args.options) + let (output_files, diagnostics) = device_driver_core::compile(&source, args.options) .with_message(|| "internal compilation error")?; let diagnostics_has_error = diagnostics.has_error(); @@ -110,26 +122,40 @@ fn build(args: BuildArgs) -> Result { return Ok(ExitCode::FAILURE); } - let output_writer: &mut dyn Write = match &args.output_path { - Some(path) => &mut std::fs::File::create(path).with_message(|| { - format!( - "could not create the output file at: {:?}. Does its directory exist?", - path.display() - ) - })?, - None => &mut std::io::stdout().lock(), - }; + if let Some(output_folder) = args.target_folder.as_ref() { + std::fs::create_dir_all(output_folder).with_message(|| "could not create target folder")?; - let mut output_writer = std::io::BufWriter::new(output_writer); - output_writer - .write_all(output.as_bytes()) - .with_message(|| { - format!( - "could not write output to {}", - args.output_path - .map_or_else(|| "stdout".into(), |path| format!("{:?}", path.display())) - ) - })?; + for file in output_files { + let file_path = output_folder.join(file.name); + std::fs::write(&file_path, file.contents) + .with_message(|| format!("could not write output to {}", file_path.display()))?; + } + } else if let [output_file] = output_files.as_slice() { + let output_writer: &mut dyn Write = match &args.output_path { + Some(path) => &mut std::fs::File::create(path).with_message(|| { + format!( + "could not create the output file at: {:?}. Does its directory exist?", + path.display() + ) + })?, + None => &mut std::io::stdout().lock(), + }; + + let mut output_writer = std::io::BufWriter::new(output_writer); + output_writer + .write_all(output_file.contents.as_bytes()) + .with_message(|| { + format!( + "could not write output to {}", + args.output_path + .map_or_else(|| "stdout".into(), |path| format!("{:?}", path.display())) + ) + })?; + } else { + return Err(DynError::new( + "more than one file is generated, but no target folder is given", + )); + } Ok(ExitCode::SUCCESS) } @@ -174,6 +200,13 @@ fn gen_docs(args: GenDocsArgs) -> Result { .to_string(), ) .with_message(|| "writing rust-help")?; + std::fs::write( + cli_folder.join("docs-help.txt"), + device_driver_core::DocsCodegenOptions::command() + .render_long_help() + .to_string(), + ) + .with_message(|| "writing docs-help")?; device_driver_core::gen_docs(&args.output_path).map(|()| ExitCode::SUCCESS) } diff --git a/compiler/dd-codegen/Cargo.toml b/compiler/dd-codegen/Cargo.toml index b018e3b5..8bf61506 100644 --- a/compiler/dd-codegen/Cargo.toml +++ b/compiler/dd-codegen/Cargo.toml @@ -15,9 +15,13 @@ readme = "README.md" [dependencies] device-driver-common.workspace = true device-driver-lir.workspace = true +device-driver-diagnostics.workspace = true convert_case.workspace = true clap.workspace = true itertools.workspace = true +syn.workspace = true + +prettyplease = { version = "0.3.0" } askama = { version = "0.16.0", default-features = false, features = ["derive", "alloc"] } diff --git a/compiler/dd-codegen/src/docs.rs b/compiler/dd-codegen/src/docs.rs new file mode 100644 index 00000000..e0fce6df --- /dev/null +++ b/compiler/dd-codegen/src/docs.rs @@ -0,0 +1,423 @@ +use std::{borrow::Cow, collections::HashMap, range::Range}; + +use askama::Template; +use clap::Parser; +use device_driver_common::{ + span::Spanned, + specifiers::{Access, ByteOrder}, +}; +use device_driver_diagnostics::DynError; +use device_driver_lir::model::{ + Block, BlockMethod, BlockMethodType, Driver, Field, FieldConversionMethod, FieldSet, Repeat, +}; +use itertools::Itertools; + +use crate::File; + +#[derive(Parser, Debug, Clone, Default)] +#[command(no_binary_name = true, bin_name = "")] +pub struct DocsCodegenOptions { + /// How many bits wide the tables can be at most. + /// If 0, then the value is guessed. + #[arg( + long = "docs-max-table-bit-width", + value_name = "NUMBER", + require_equals = true, + default_value = "0" + )] + pub max_table_bit_width: u32, +} + +pub fn codegen( + mut codegen_options: DocsCodegenOptions, + lir_driver: &Driver, + source: &str, +) -> Result, DynError> { + if codegen_options.max_table_bit_width == 0 { + let mut counts = HashMap::::new(); + *counts.entry(8).or_default() += 1; + + for size in lir_driver.field_sets.iter().map(|fs| fs.size_bytes) { + if size.is_multiple_of(4) { + *counts.entry(32).or_default() += 1; + } + if size.is_multiple_of(3) { + *counts.entry(24).or_default() += 1; + } + if size.is_multiple_of(2) { + *counts.entry(16).or_default() += 1; + } + if size == 1 { + *counts.entry(8).or_default() += 1; + } + } + codegen_options.max_table_bit_width = counts + .into_iter() + // Grab the most commonly used, but with a little favor for the bigger sizes + .max_by_key(|(val, count)| *count + (val / 8)) + .unwrap_or_default() + .0; + } + + let mut output_files = Vec::new(); + + for block in lir_driver + .devices + .iter() + .flat_map(|device| device.blocks.iter()) + { + output_files.push(File { + name: format!("block.{}.html", block.name.original()), + contents: BlockPageTemplateDocs { block }.to_string(), + }); + } + + // Write out all operations + for method in lir_driver + .devices + .iter() + .flat_map(|device| device.blocks.iter()) + .flat_map(|block| block.methods.iter()) + { + match &method.method_type { + BlockMethodType::Block { .. } => {} + BlockMethodType::Register { + field_set_name, + access, + reset_value, + } => { + output_files.push(File { + name: format!("register.{}.html", method.name.original()), + contents: RegisterPageTemplateDocs { + method, + fieldset_template: FieldsetTemplateDocs { + fieldset: lir_driver + .field_sets + .iter() + .find(|fs| fs.name == *field_set_name) + .ok_or_else(|| { + DynError::new(format!( + "could not find fieldset {}", + field_set_name.original() + )) + })?, + reset_value: Some(reset_value), + codegen_options: &codegen_options, + source, + }, + access, + } + .to_string(), + }); + } + BlockMethodType::Command { + field_set_name_in: _, + field_set_name_out: _, + } => { + // TODO + } + BlockMethodType::Buffer { access: _ } => { + // TODO + } + } + } + + for fs in &lir_driver.field_sets { + output_files.push(File { + name: format!("type.{}.html", fs.name.original()), + contents: FieldsetPageTemplateDocs { + fieldset_template: FieldsetTemplateDocs { + fieldset: fs, + reset_value: None, + codegen_options: &codegen_options, + source, + }, + } + .to_string(), + }); + } + + Ok(output_files) +} + +#[derive(Template)] +#[template( + path = "docs/register_page.html.j2", + escape = "none", + whitespace = "minimize" +)] +pub struct RegisterPageTemplateDocs<'a> { + method: &'a BlockMethod, + fieldset_template: FieldsetTemplateDocs<'a>, + access: &'a Access, +} + +#[derive(Template)] +#[template( + path = "docs/fieldset_page.html.j2", + escape = "none", + whitespace = "minimize" +)] +pub struct FieldsetPageTemplateDocs<'a> { + fieldset_template: FieldsetTemplateDocs<'a>, +} + +#[derive(Template)] +#[template( + path = "docs/fieldset.html.j2", + escape = "none", + whitespace = "minimize" +)] +pub struct FieldsetTemplateDocs<'a> { + fieldset: &'a FieldSet, + reset_value: Option<&'a Option>>>, + codegen_options: &'a DocsCodegenOptions, + source: &'a str, +} + +impl<'a> FieldsetTemplateDocs<'a> { + fn get_reset_value_text(&self) -> Option<&str> { + match self.reset_value { + Some(Some(reset_value)) => { + Some(&self.source[reset_value.span.start..reset_value.span.end]) + } + Some(None) => Some("0"), + None => None, + } + } + + fn overview_tables(&self) -> impl Iterator> { + (0..self.fieldset.size_bytes * 8) + .step_by(self.codegen_options.max_table_bit_width as usize) + .map(|bit_start| { + (bit_start + ..(bit_start + self.codegen_options.max_table_bit_width) + .min(self.fieldset.size_bytes * 8)) + .into() + }) + .rev() + } + + fn fields_in_range( + &self, + mut bit_range: Range, + ) -> impl Iterator, u32)> { + std::iter::from_fn(move || { + // TODO: What about overlapping fields? + + if bit_range.start >= bit_range.end || bit_range.end == 0 { + return None; + } + + let last_bit = bit_range.end - 1; + let Some(field) = self + .fieldset + .fields + .iter() + .find(|field| (field.address.start..=field.address.end).contains(&last_bit)) + else { + let first_next_bit = self + .fieldset + .fields + .iter() + .map(|field| field.address.end + 1) + .filter(|end| *end < bit_range.end) + .max() + .unwrap_or_default(); + let change = bit_range.end - first_next_bit.max(bit_range.start); + bit_range.end -= change; + + return Some((None, change)); + }; + + let first_next_bit = field.address.start; + let change = bit_range.end - first_next_bit.max(bit_range.start); + bit_range.end -= change; + + Some((Some(field), change)) + }) + } + + fn reset_values_in_range( + &self, + mut bit_range: Range, + ) -> impl Iterator, u32)> { + std::iter::from_fn(move || { + // TODO: What about overlapping fields? + + if bit_range.start >= bit_range.end || bit_range.end == 0 { + return None; + } + + let last_bit = bit_range.end - 1; + let Some(field) = self + .fieldset + .fields + .iter() + .find(|field| (field.address.start..=field.address.end).contains(&last_bit)) + else { + let first_next_bit = self + .fieldset + .fields + .iter() + .map(|field| field.address.end + 1) + .filter(|end| *end < bit_range.end) + .max() + .unwrap_or_default(); + let change = bit_range.end - first_next_bit.max(bit_range.start); + bit_range.end -= change; + + let reset_value_range = bit_range.end..bit_range.end + change; + + return Some(( + self.reset_value.map(|reset_value| { + let reset_value = reset_value + .as_ref() + .map(|reset_value| reset_value.value.clone()) + .unwrap_or_else(|| vec![0; self.fieldset.size_bytes as usize]); + load_bits( + &reset_value, + self.fieldset.byte_order, + reset_value_range.into(), + ) + }), + change, + )); + }; + + let first_next_bit = field.address.start; + let change = bit_range.end - first_next_bit.max(bit_range.start); + bit_range.end -= change; + let reset_value_range = bit_range.end..bit_range.end + change; + + Some(( + self.reset_value.map(|reset_value| { + let reset_value = reset_value + .as_ref() + .map(|reset_value| reset_value.value.clone()) + .unwrap_or_else(|| vec![0; self.fieldset.size_bytes as usize]); + load_bits( + &reset_value, + self.fieldset.byte_order, + reset_value_range.into(), + ) + }), + change, + )) + }) + } + + fn field_conversion_display(&self, field: &Field) -> Cow<'static, str> { + match &field.conversion_method { + FieldConversionMethod::None => "".into(), + FieldConversionMethod::Into(identifier) => { + format!("{}
from ", identifier.original()).into() + } + FieldConversionMethod::UnsafeInto(identifier) => { + format!("{}
from ", identifier.original()).into() + } + FieldConversionMethod::TryInto(identifier) => { + format!("{}
try from ", identifier.original()).into() + } + FieldConversionMethod::Bool => "".into(), + } + } + + fn field_reset_value(&self, field: &Field) -> Cow<'static, str> { + let Some(Some(reset_value)) = self.reset_value else { + return "0h".into(); + }; + + let bits = load_bits( + &reset_value.value, + self.fieldset.byte_order, + (field.address.start..field.address.end + 1).into(), + ); + + // TODO: Show converted value + + format!("{bits:X}h").into() + } +} + +fn load_bits(reset_value: &[u8], byte_order: ByteOrder, mut bit_range: Range) -> u64 { + let mut val = 0; + + while bit_range.end > bit_range.start { + bit_range.end -= 1; + let bit = bit_range.end; + + let byte_index = match byte_order { + ByteOrder::LE => bit / 8, + ByteOrder::BE => reset_value.len() as u32 - 1 - bit / 8, + }; + let bit_index = bit % 8; + + let bit_value = reset_value[byte_index as usize] & (1 << bit_index); + + if bit_value != 0 { + val |= 1 << (bit - bit_range.start); + } + } + val +} + +fn description_to_html(description: &str) -> String { + description.lines().join("
") +} + +#[derive(Template)] +#[template( + path = "docs/block_page.html.j2", + escape = "none", + whitespace = "minimize" +)] +pub struct BlockPageTemplateDocs<'a> { + block: &'a Block, +} + +impl<'a> BlockPageTemplateDocs<'a> { + pub fn blocks(&self) -> impl Iterator { + self.block + .methods + .iter() + .filter(|method| matches!(method.method_type, BlockMethodType::Block { .. })) + } + + pub fn has_blocks(&self) -> bool { + self.blocks().next().is_some() + } + + pub fn registers(&self) -> impl Iterator { + self.block + .methods + .iter() + .filter(|method| matches!(method.method_type, BlockMethodType::Register { .. })) + } + + pub fn has_registers(&self) -> bool { + self.registers().next().is_some() + } + + pub fn commands(&self) -> impl Iterator { + self.block + .methods + .iter() + .filter(|method| matches!(method.method_type, BlockMethodType::Command { .. })) + } + + pub fn has_commands(&self) -> bool { + self.commands().next().is_some() + } + + pub fn buffers(&self) -> impl Iterator { + self.block + .methods + .iter() + .filter(|method| matches!(method.method_type, BlockMethodType::Buffer { .. })) + } + + pub fn has_buffers(&self) -> bool { + self.buffers().next().is_some() + } +} diff --git a/compiler/dd-codegen/src/lib.rs b/compiler/dd-codegen/src/lib.rs index 45fce2fc..103198c2 100644 --- a/compiler/dd-codegen/src/lib.rs +++ b/compiler/dd-codegen/src/lib.rs @@ -1,15 +1,20 @@ use clap::Subcommand; +use device_driver_diagnostics::DynError; use device_driver_lir::model::Driver; use itertools::Itertools; +pub use crate::docs::DocsCodegenOptions; pub use crate::rust::RustCodegenOptions; +mod docs; mod rust; #[derive(Debug, Clone, Subcommand)] pub enum Target { /// Generate Rust code Rust(RustCodegenOptions), + /// Generate a documentation website + Docs(DocsCodegenOptions), } impl Target { @@ -18,6 +23,9 @@ impl Target { Target::Rust(_) => { "compile_error!(\"The device driver input has errors that need to be solved!\");" } + Target::Docs(_) => { + "