From 5022f8ca8cfbc22c4aa6a1a43225b1b54ecc0bd4 Mon Sep 17 00:00:00 2001 From: Angus Bethke Date: Thu, 16 Oct 2025 12:13:33 +0000 Subject: [PATCH 01/21] feat: Added CLI options for generating headers --- compiler/plc_driver/src/cli.rs | 126 +++++++++++++++++++- compiler/plc_driver/src/lib.rs | 28 +++++ compiler/plc_driver/src/pipelines.rs | 65 +++++++++- tests/integration/header_generator_tests.rs | 22 ++++ tests/tests.rs | 1 + 5 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 tests/integration/header_generator_tests.rs diff --git a/compiler/plc_driver/src/cli.rs b/compiler/plc_driver/src/cli.rs index 8a863cc642..8fec8e8424 100644 --- a/compiler/plc_driver/src/cli.rs +++ b/compiler/plc_driver/src/cli.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Result}; // Copyright (c) 2021 Ghaith Hachem and Mathias Rieder -use clap::{ArgGroup, Parser, Subcommand}; +use clap::{ArgEnum, ArgGroup, Parser, Subcommand}; use encoding_rs::Encoding; use plc_diagnostics::diagnostics::{diagnostics_registry::DiagnosticsConfiguration, Diagnostic}; use std::{env, ffi::OsStr, num::ParseIntError, path::PathBuf}; @@ -311,6 +311,20 @@ pub enum SubCommands { #[clap(help = "Error code to explain, for example `E001`")] error: String, }, + + /// Generates code for a given project + /// + /// Sub-command(s): + /// Header : Generates the Header files + Generate { + #[clap( + parse(try_from_str = validate_config) + )] + build_config: Option, + + #[clap(subcommand)] + option: GenerateOption, + } } #[derive(Copy, Clone, PartialEq, Eq, Debug, Subcommand)] @@ -321,11 +335,54 @@ pub enum ConfigOption { Diagnostics, } +#[derive(Debug, Subcommand)] +pub enum GenerateOption { + Headers { + #[clap( + name = "include-stubs", + long, + help = "Whether or not to include generated code stubs for the library." + )] + include_stubs: bool, + + #[clap( + name = "header-language", + long, + arg_enum, + help = "The language used to generate the header file. Currently supported language(s) are: C", + default_value = "c" + )] + language: GenerateLanguage, + + #[clap( + name = "header-output", + long, + help = "The output folder where generated headers and stubs will be placed." + )] + output: Option, + + #[clap( + name = "header-prefix", + long, + help = "The prefix for the generated header file(s). Will default to the project name if not supplied." + )] + prefix: Option, + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy, ArgEnum, Default)] +pub enum GenerateLanguage { + #[default] + C, + Rust, +} + impl SubCommands { pub fn get_build_configuration(&self) -> Option<&str> { let (SubCommands::Build { build_config, .. } | SubCommands::Check { build_config } - | SubCommands::Config { build_config, .. }) = self + | SubCommands::Config { build_config, .. } + | SubCommands::Generate { build_config, .. }) = self else { return None; }; @@ -414,6 +471,11 @@ impl CompileParameters { self.check_only || matches!(self.commands, Some(SubCommands::Check { .. })) } + /// If set, header files will be generated + pub fn is_header_generator(&self) -> bool { + matches!(self.commands, Some(SubCommands::Generate { option: GenerateOption::Headers { .. }, .. })) + } + /// return the selected output format, or the default if none. #[cfg(test)] pub fn output_format_or_default(&self) -> FormatOption { @@ -480,7 +542,7 @@ impl CompileParameters { fn has_config(&self) -> Result { let res = match &self.commands { None | Some(SubCommands::Explain { .. }) => false, - Some(SubCommands::Build { .. }) | Some(SubCommands::Check { .. }) => true, + Some(SubCommands::Build { .. }) | Some(SubCommands::Check { .. }) | Some(SubCommands::Generate { .. }) => true, Some(SubCommands::Config { build_config, .. }) => { let current_dir = env::current_dir()?; build_config.is_some() || super::get_config(¤t_dir).exists() @@ -506,7 +568,7 @@ impl CompileParameters { #[cfg(test)] mod cli_tests { - use crate::cli::ConfigOption; + use crate::cli::{ConfigOption, GenerateLanguage, GenerateOption}; use super::{CompileParameters, SubCommands}; use clap::ErrorKind; @@ -884,6 +946,62 @@ mod cli_tests { } } + #[test] + fn generate_subcommand() { + let parameters = CompileParameters::parse(vec_of_strings!( + "generate", + "src/ProjectPlc.json", + "headers" + )) + .unwrap(); + if let Some(commands) = parameters.commands { + match commands { + SubCommands::Generate { build_config, option, .. } => { + assert_eq!(build_config, Some("src/ProjectPlc.json".to_string())); + match option { + GenerateOption::Headers { include_stubs, language, .. } => + { + assert_eq!(include_stubs, false); + assert_eq!(language, GenerateLanguage::C); + } + } + } + _ => panic!("Unexpected command"), + }; + } + + let parameters = CompileParameters::parse(vec_of_strings!( + "generate", + "src/ProjectPlc.json", + "headers", + "--include-stubs", + "--header-language", + "rust", + "--header-output", + "some_dir", + "--header-prefix", + "myLib" + )) + .unwrap(); + if let Some(commands) = parameters.commands { + match commands { + SubCommands::Generate { build_config, option, .. } => { + assert_eq!(build_config, Some("src/ProjectPlc.json".to_string())); + match option { + GenerateOption::Headers { include_stubs, language, output, prefix } => + { + assert_eq!(include_stubs, true); + assert_eq!(language, GenerateLanguage::Rust); + assert_eq!(output, Some("some_dir".to_string())); + assert_eq!(prefix, Some("myLib".to_string())); + } + } + } + _ => panic!("Unexpected command"), + }; + } + } + #[test] fn include_files_added() { let parameters = CompileParameters::parse(vec_of_strings!( diff --git a/compiler/plc_driver/src/lib.rs b/compiler/plc_driver/src/lib.rs index e6267a1637..8206e607a5 100644 --- a/compiler/plc_driver/src/lib.rs +++ b/compiler/plc_driver/src/lib.rs @@ -30,6 +30,8 @@ use plc_index::GlobalContext; use project::project::Project; use source_code::SourceContainer; +use crate::cli::GenerateLanguage; + pub mod cli; pub mod pipelines; @@ -86,6 +88,32 @@ pub struct LinkOptions { pub module_name: Option, } +#[derive(Debug)] +pub struct GenerateHeaderOptions { + /// Whether or not to include generated code stubs for the library. + pub include_stubs: bool, + + /// The language used to generate the header file. + pub language: GenerateLanguage, + + /// The output folder where generated headers and stubs will be placed. Will default by convention. + pub output_path: PathBuf, + + /// The prefix for the generated header file(s). Will default to the project name if not supplied. + pub prefix: String, +} + +impl Default for GenerateHeaderOptions { + fn default() -> Self { + GenerateHeaderOptions { + include_stubs: false, + language: GenerateLanguage::C, + output_path: PathBuf::from(String::new()), + prefix: String::new() + } + } +} + #[derive(Clone, Default, Debug)] pub enum LinkerScript { #[default] diff --git a/compiler/plc_driver/src/pipelines.rs b/compiler/plc_driver/src/pipelines.rs index 7bc1b4b209..603799b90a 100644 --- a/compiler/plc_driver/src/pipelines.rs +++ b/compiler/plc_driver/src/pipelines.rs @@ -9,8 +9,7 @@ use std::{ }; use crate::{ - cli::{self, CompileParameters, ConfigOption, SubCommands}, - get_project, CompileOptions, LinkOptions, LinkerScript, + cli::{self, CompileParameters, ConfigOption, GenerateLanguage, GenerateOption, SubCommands}, get_project, CompileOptions, GenerateHeaderOptions, LinkOptions, LinkerScript }; use ast::{ ast::{pre_process, CompilationUnit, LinkageType}, @@ -28,7 +27,7 @@ use plc::{ calls::AggregateTypeLowerer, polymorphism::PolymorphicCallLowerer, property::PropertyLowerer, vtable::VirtualTableGenerator, InitVisitor, }, - output::FormatOption, + output::{FormatOption}, parser::parse_file, resolver::{ const_evaluator::UnresolvableConstant, AnnotationMapImpl, AstAnnotations, Dependency, StringLiterals, @@ -74,6 +73,7 @@ pub trait Pipeline { fn index(&mut self, project: ParsedProject) -> Result; fn annotate(&mut self, project: IndexedProject) -> Result; fn generate(&mut self, context: &CodegenContext, project: AnnotatedProject) -> Result<(), Diagnostic>; + fn generate_headers(&mut self, project: AnnotatedProject) -> Result<(), Diagnostic>; } impl TryFrom for BuildPipeline { @@ -222,6 +222,28 @@ impl BuildPipeline { }) } + pub fn get_generate_header_options(&self) -> Option { + self.compile_parameters.as_ref().map(|params| { + let location = &self.project.get_location().map(|it| it.to_path_buf()); + let project_name = self.project.get_name(); + match params.commands.as_ref().unwrap() { + SubCommands::Generate { option, .. } => { + match option { + GenerateOption::Headers { include_stubs, language, output, prefix, .. } => { + GenerateHeaderOptions { + include_stubs: *include_stubs, + language: *language, + output_path: if output.is_some() { PathBuf::from(output.clone().unwrap()) } else { location.clone().unwrap_or(PathBuf::from(String::new())) }, + prefix: prefix.clone().unwrap_or_else(|| project_name.to_string()), + } + } + } + }, + _ => GenerateHeaderOptions::default(), + } + }) + } + fn print_config_options(&self, option: ConfigOption) -> Result<(), Diagnostic> { match option { cli::ConfigOption::Schema => { @@ -333,11 +355,17 @@ impl Pipeline for BuildPipeline { annotated_project.generate_hardware_information(format, location)?; } - // 5. Codegen + // Skip code-gen if it is check if self.compile_parameters.as_ref().is_some_and(CompileParameters::is_check) { return Ok(()); } + // Generate Header files + if self.compile_parameters.as_ref().is_some_and(CompileParameters::is_header_generator) { + return self.generate_headers(annotated_project); + } + + // 5. Codegen self.generate(&CodegenContext::create(), annotated_project) } @@ -429,6 +457,35 @@ impl Pipeline for BuildPipeline { .unwrap_or(Ok(()))?; Ok(()) } + + fn generate_headers(&mut self, _project: AnnotatedProject) -> Result<(), Diagnostic> { + let Some(generate_header_options) = self.get_generate_header_options() else { + log::debug!("No generate header options provided!"); + return Ok(()); + }; + + // TODO: Load template file and perform content replacement to inject various items to the header + let contents = "No content yet..."; + + // TODO: Should we split the header into multiple files? If so, then this should be modified. + let (header_path, header_dir) = match generate_header_options.language { + GenerateLanguage::C => { + let mut output_path = generate_header_options.output_path.clone(); + let output_dir = generate_header_options.output_path.clone(); + + output_path.push(format!("{}.h", generate_header_options.prefix)); + + (output_path, output_dir) + } + language => { + log::debug!("{language:?} language not yet supported!"); + return Ok(()); + } + }; + + fs::create_dir_all(header_dir)?; + fs::write(header_path, contents).map_err(|_| Diagnostic::new("Unable to generate header file...")) + } } pub fn read_got_layout(location: &str, format: ConfigFormat) -> Result, Diagnostic> { diff --git a/tests/integration/header_generator_tests.rs b/tests/integration/header_generator_tests.rs new file mode 100644 index 0000000000..40e75a4a7e --- /dev/null +++ b/tests/integration/header_generator_tests.rs @@ -0,0 +1,22 @@ +use crate::get_test_file; +use driver::compile; + +#[test] +#[serial] +fn generate_header_for_empty_project() { + let dir = tempfile::tempdir().unwrap(); + let header_dir = dir.path().join("headers").to_str().unwrap().to_string(); + + let parameters = &[ + "plc", + "generate", + &get_test_file("empty_proj/conf/plc.json"), + "headers", + "--header-output", + &header_dir + ]; + compile(parameters).unwrap(); + + println!("{}", dir.path().join("headers").join("EmptyProject.h").to_str().unwrap()); + assert!(dir.path().join("headers").join("EmptyProject.h").is_file()); +} diff --git a/tests/tests.rs b/tests/tests.rs index 226d3eb71a..4a48b02774 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -53,6 +53,7 @@ mod integration { mod cfc; mod command_line_compile; mod external_files; + mod header_generator_tests; mod linking; mod multi_files; } From af1a86d06782d45452cc3b0d1d15f691ec7cca53 Mon Sep 17 00:00:00 2001 From: Angus Bethke Date: Thu, 16 Oct 2025 12:35:53 +0000 Subject: [PATCH 02/21] fix: cargo fmt --- compiler/plc_driver/src/cli.rs | 22 ++++++++---------- compiler/plc_driver/src/lib.rs | 2 +- compiler/plc_driver/src/pipelines.rs | 25 ++++++++++++--------- tests/integration/header_generator_tests.rs | 2 +- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/compiler/plc_driver/src/cli.rs b/compiler/plc_driver/src/cli.rs index 8fec8e8424..196e6b9f8b 100644 --- a/compiler/plc_driver/src/cli.rs +++ b/compiler/plc_driver/src/cli.rs @@ -324,7 +324,7 @@ pub enum SubCommands { #[clap(subcommand)] option: GenerateOption, - } + }, } #[derive(Copy, Clone, PartialEq, Eq, Debug, Subcommand)] @@ -367,7 +367,7 @@ pub enum GenerateOption { help = "The prefix for the generated header file(s). Will default to the project name if not supplied." )] prefix: Option, - } + }, } #[derive(PartialEq, Eq, Debug, Clone, Copy, ArgEnum, Default)] @@ -542,7 +542,9 @@ impl CompileParameters { fn has_config(&self) -> Result { let res = match &self.commands { None | Some(SubCommands::Explain { .. }) => false, - Some(SubCommands::Build { .. }) | Some(SubCommands::Check { .. }) | Some(SubCommands::Generate { .. }) => true, + Some(SubCommands::Build { .. }) + | Some(SubCommands::Check { .. }) + | Some(SubCommands::Generate { .. }) => true, Some(SubCommands::Config { build_config, .. }) => { let current_dir = env::current_dir()?; build_config.is_some() || super::get_config(¤t_dir).exists() @@ -948,19 +950,14 @@ mod cli_tests { #[test] fn generate_subcommand() { - let parameters = CompileParameters::parse(vec_of_strings!( - "generate", - "src/ProjectPlc.json", - "headers" - )) - .unwrap(); + let parameters = + CompileParameters::parse(vec_of_strings!("generate", "src/ProjectPlc.json", "headers")).unwrap(); if let Some(commands) = parameters.commands { match commands { SubCommands::Generate { build_config, option, .. } => { assert_eq!(build_config, Some("src/ProjectPlc.json".to_string())); match option { - GenerateOption::Headers { include_stubs, language, .. } => - { + GenerateOption::Headers { include_stubs, language, .. } => { assert_eq!(include_stubs, false); assert_eq!(language, GenerateLanguage::C); } @@ -988,8 +985,7 @@ mod cli_tests { SubCommands::Generate { build_config, option, .. } => { assert_eq!(build_config, Some("src/ProjectPlc.json".to_string())); match option { - GenerateOption::Headers { include_stubs, language, output, prefix } => - { + GenerateOption::Headers { include_stubs, language, output, prefix } => { assert_eq!(include_stubs, true); assert_eq!(language, GenerateLanguage::Rust); assert_eq!(output, Some("some_dir".to_string())); diff --git a/compiler/plc_driver/src/lib.rs b/compiler/plc_driver/src/lib.rs index 8206e607a5..2f9d726087 100644 --- a/compiler/plc_driver/src/lib.rs +++ b/compiler/plc_driver/src/lib.rs @@ -109,7 +109,7 @@ impl Default for GenerateHeaderOptions { include_stubs: false, language: GenerateLanguage::C, output_path: PathBuf::from(String::new()), - prefix: String::new() + prefix: String::new(), } } } diff --git a/compiler/plc_driver/src/pipelines.rs b/compiler/plc_driver/src/pipelines.rs index 603799b90a..578b55a975 100644 --- a/compiler/plc_driver/src/pipelines.rs +++ b/compiler/plc_driver/src/pipelines.rs @@ -9,7 +9,8 @@ use std::{ }; use crate::{ - cli::{self, CompileParameters, ConfigOption, GenerateLanguage, GenerateOption, SubCommands}, get_project, CompileOptions, GenerateHeaderOptions, LinkOptions, LinkerScript + cli::{self, CompileParameters, ConfigOption, GenerateLanguage, GenerateOption, SubCommands}, + get_project, CompileOptions, GenerateHeaderOptions, LinkOptions, LinkerScript, }; use ast::{ ast::{pre_process, CompilationUnit, LinkageType}, @@ -27,7 +28,7 @@ use plc::{ calls::AggregateTypeLowerer, polymorphism::PolymorphicCallLowerer, property::PropertyLowerer, vtable::VirtualTableGenerator, InitVisitor, }, - output::{FormatOption}, + output::FormatOption, parser::parse_file, resolver::{ const_evaluator::UnresolvableConstant, AnnotationMapImpl, AstAnnotations, Dependency, StringLiterals, @@ -227,15 +228,17 @@ impl BuildPipeline { let location = &self.project.get_location().map(|it| it.to_path_buf()); let project_name = self.project.get_name(); match params.commands.as_ref().unwrap() { - SubCommands::Generate { option, .. } => { - match option { - GenerateOption::Headers { include_stubs, language, output, prefix, .. } => { - GenerateHeaderOptions { - include_stubs: *include_stubs, - language: *language, - output_path: if output.is_some() { PathBuf::from(output.clone().unwrap()) } else { location.clone().unwrap_or(PathBuf::from(String::new())) }, - prefix: prefix.clone().unwrap_or_else(|| project_name.to_string()), - } + SubCommands::Generate { option, .. } => match option { + GenerateOption::Headers { include_stubs, language, output, prefix, .. } => { + GenerateHeaderOptions { + include_stubs: *include_stubs, + language: *language, + output_path: if output.is_some() { + PathBuf::from(output.clone().unwrap()) + } else { + location.clone().unwrap_or(PathBuf::from(String::new())) + }, + prefix: prefix.clone().unwrap_or_else(|| project_name.to_string()), } } }, diff --git a/tests/integration/header_generator_tests.rs b/tests/integration/header_generator_tests.rs index 40e75a4a7e..f94b775dd6 100644 --- a/tests/integration/header_generator_tests.rs +++ b/tests/integration/header_generator_tests.rs @@ -13,7 +13,7 @@ fn generate_header_for_empty_project() { &get_test_file("empty_proj/conf/plc.json"), "headers", "--header-output", - &header_dir + &header_dir, ]; compile(parameters).unwrap(); From fda1c442577209f42d720c422628827cc84bafd2 Mon Sep 17 00:00:00 2001 From: Angus Bethke Date: Thu, 23 Oct 2025 13:55:15 +0000 Subject: [PATCH 03/21] feat: Added primitive types and functions that use primitive types to the header generator --- Cargo.lock | 153 +++++++++++ Cargo.toml | 1 + compiler/plc_driver/Cargo.toml | 1 + compiler/plc_driver/src/cli.rs | 10 +- compiler/plc_driver/src/lib.rs | 28 --- compiler/plc_driver/src/pipelines.rs | 37 +-- compiler/plc_header_generator/Cargo.toml | 13 + .../src/header_generator.rs | 237 ++++++++++++++++++ compiler/plc_header_generator/src/lib.rs | 39 +++ .../src/templates/function_template.h | 1 + .../src/templates/header_template.h | 18 ++ .../src/templates/struct_template.h | 3 + compiler/plc_header_generator/src/type_map.rs | 77 ++++++ tests/integration/data/var_proj/conf/plc.json | 11 + .../data/var_proj/src/my_test_interface.pli | 85 +++++++ .../data/var_proj/src/my_test_interface_2.pli | 19 ++ tests/integration/header_generator_tests.rs | 9 +- 17 files changed, 676 insertions(+), 66 deletions(-) create mode 100644 compiler/plc_header_generator/Cargo.toml create mode 100644 compiler/plc_header_generator/src/header_generator.rs create mode 100644 compiler/plc_header_generator/src/lib.rs create mode 100644 compiler/plc_header_generator/src/templates/function_template.h create mode 100644 compiler/plc_header_generator/src/templates/header_template.h create mode 100644 compiler/plc_header_generator/src/templates/struct_template.h create mode 100644 compiler/plc_header_generator/src/type_map.rs create mode 100644 tests/integration/data/var_proj/conf/plc.json create mode 100644 tests/integration/data/var_proj/src/my_test_interface.pli create mode 100644 tests/integration/data/var_proj/src/my_test_interface_2.pli diff --git a/Cargo.lock b/Cargo.lock index a3f580cc82..676c27b94b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,6 +518,28 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clap" version = "3.2.25" @@ -868,6 +890,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "diff" version = "0.1.13" @@ -1368,6 +1396,17 @@ dependencies = [ "regex-syntax 0.8.6", ] +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.9.4", + "ignore", + "walkdir", +] + [[package]] name = "gloo-timers" version = "0.3.0" @@ -1607,6 +1646,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "humantime" version = "2.3.0" @@ -2685,6 +2733,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -2909,6 +2966,7 @@ dependencies = [ "log", "plc_ast", "plc_diagnostics", + "plc_header_generator", "plc_index", "plc_lowering", "plc_project", @@ -2924,6 +2982,19 @@ dependencies = [ "toml", ] +[[package]] +name = "plc_header_generator" +version = "0.1.0" +dependencies = [ + "clap 3.2.25", + "log", + "plc_ast", + "plc_diagnostics", + "plc_source", + "rusty", + "tera", +] + [[package]] name = "plc_index" version = "0.1.0" @@ -3697,6 +3768,16 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -4120,6 +4201,28 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -4446,6 +4549,56 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.8.1" diff --git a/Cargo.toml b/Cargo.toml index e8e715b9b8..d93e26645f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ members = [ "compiler/plc_index", "compiler/section_mangler", "compiler/plc_lowering", + "compiler/plc_header_generator", "tests/test_utils", ] default-members = [".", "compiler/plc_driver", "compiler/plc_xml"] diff --git a/compiler/plc_driver/Cargo.toml b/compiler/plc_driver/Cargo.toml index 1d6323e7c3..b32360eec6 100644 --- a/compiler/plc_driver/Cargo.toml +++ b/compiler/plc_driver/Cargo.toml @@ -15,6 +15,7 @@ cfc = { path = "../plc_xml/", package = "plc_xml" } plc_diagnostics = { path = "../plc_diagnostics/" } plc_index = { path = "../plc_index" } plc_lowering = { path = "../plc_lowering" } +plc_header_generator = { path = "../plc_header_generator" } serde = { version = "1.0", features = ["derive"] } serde_json = "1" diff --git a/compiler/plc_driver/src/cli.rs b/compiler/plc_driver/src/cli.rs index 196e6b9f8b..c8d25defe1 100644 --- a/compiler/plc_driver/src/cli.rs +++ b/compiler/plc_driver/src/cli.rs @@ -1,8 +1,9 @@ use anyhow::{bail, Result}; // Copyright (c) 2021 Ghaith Hachem and Mathias Rieder -use clap::{ArgEnum, ArgGroup, Parser, Subcommand}; +use clap::{ArgGroup, Parser, Subcommand}; use encoding_rs::Encoding; use plc_diagnostics::diagnostics::{diagnostics_registry::DiagnosticsConfiguration, Diagnostic}; +use plc_header_generator::GenerateLanguage; use std::{env, ffi::OsStr, num::ParseIntError, path::PathBuf}; use plc::output::FormatOption; @@ -370,13 +371,6 @@ pub enum GenerateOption { }, } -#[derive(PartialEq, Eq, Debug, Clone, Copy, ArgEnum, Default)] -pub enum GenerateLanguage { - #[default] - C, - Rust, -} - impl SubCommands { pub fn get_build_configuration(&self) -> Option<&str> { let (SubCommands::Build { build_config, .. } diff --git a/compiler/plc_driver/src/lib.rs b/compiler/plc_driver/src/lib.rs index 2f9d726087..e6267a1637 100644 --- a/compiler/plc_driver/src/lib.rs +++ b/compiler/plc_driver/src/lib.rs @@ -30,8 +30,6 @@ use plc_index::GlobalContext; use project::project::Project; use source_code::SourceContainer; -use crate::cli::GenerateLanguage; - pub mod cli; pub mod pipelines; @@ -88,32 +86,6 @@ pub struct LinkOptions { pub module_name: Option, } -#[derive(Debug)] -pub struct GenerateHeaderOptions { - /// Whether or not to include generated code stubs for the library. - pub include_stubs: bool, - - /// The language used to generate the header file. - pub language: GenerateLanguage, - - /// The output folder where generated headers and stubs will be placed. Will default by convention. - pub output_path: PathBuf, - - /// The prefix for the generated header file(s). Will default to the project name if not supplied. - pub prefix: String, -} - -impl Default for GenerateHeaderOptions { - fn default() -> Self { - GenerateHeaderOptions { - include_stubs: false, - language: GenerateLanguage::C, - output_path: PathBuf::from(String::new()), - prefix: String::new(), - } - } -} - #[derive(Clone, Default, Debug)] pub enum LinkerScript { #[default] diff --git a/compiler/plc_driver/src/pipelines.rs b/compiler/plc_driver/src/pipelines.rs index 578b55a975..ae0c713ad2 100644 --- a/compiler/plc_driver/src/pipelines.rs +++ b/compiler/plc_driver/src/pipelines.rs @@ -9,8 +9,8 @@ use std::{ }; use crate::{ - cli::{self, CompileParameters, ConfigOption, GenerateLanguage, GenerateOption, SubCommands}, - get_project, CompileOptions, GenerateHeaderOptions, LinkOptions, LinkerScript, + cli::{self, CompileParameters, ConfigOption, GenerateOption, SubCommands}, + get_project, CompileOptions, LinkOptions, LinkerScript, }; use ast::{ ast::{pre_process, CompilationUnit, LinkageType}, @@ -41,6 +41,7 @@ use plc_diagnostics::{ diagnostician::Diagnostician, diagnostics::{Diagnostic, Severity}, }; +use plc_header_generator::{header_generator, GenerateHeaderOptions}; use plc_index::GlobalContext; use plc_lowering::inheritance::InheritanceLowerer; use project::{ @@ -225,8 +226,6 @@ impl BuildPipeline { pub fn get_generate_header_options(&self) -> Option { self.compile_parameters.as_ref().map(|params| { - let location = &self.project.get_location().map(|it| it.to_path_buf()); - let project_name = self.project.get_name(); match params.commands.as_ref().unwrap() { SubCommands::Generate { option, .. } => match option { GenerateOption::Headers { include_stubs, language, output, prefix, .. } => { @@ -236,9 +235,9 @@ impl BuildPipeline { output_path: if output.is_some() { PathBuf::from(output.clone().unwrap()) } else { - location.clone().unwrap_or(PathBuf::from(String::new())) + PathBuf::from(String::new()) }, - prefix: prefix.clone().unwrap_or_else(|| project_name.to_string()), + prefix: prefix.clone().unwrap_or(String::new()), } } }, @@ -461,33 +460,17 @@ impl Pipeline for BuildPipeline { Ok(()) } - fn generate_headers(&mut self, _project: AnnotatedProject) -> Result<(), Diagnostic> { + fn generate_headers(&mut self, project: AnnotatedProject) -> Result<(), Diagnostic> { let Some(generate_header_options) = self.get_generate_header_options() else { log::debug!("No generate header options provided!"); return Ok(()); }; - // TODO: Load template file and perform content replacement to inject various items to the header - let contents = "No content yet..."; - - // TODO: Should we split the header into multiple files? If so, then this should be modified. - let (header_path, header_dir) = match generate_header_options.language { - GenerateLanguage::C => { - let mut output_path = generate_header_options.output_path.clone(); - let output_dir = generate_header_options.output_path.clone(); - - output_path.push(format!("{}.h", generate_header_options.prefix)); - - (output_path, output_dir) - } - language => { - log::debug!("{language:?} language not yet supported!"); - return Ok(()); - } - }; + for unit in project.units { + header_generator::generate_headers(&generate_header_options, unit.unit)?; + } - fs::create_dir_all(header_dir)?; - fs::write(header_path, contents).map_err(|_| Diagnostic::new("Unable to generate header file...")) + Ok(()) } } diff --git a/compiler/plc_header_generator/Cargo.toml b/compiler/plc_header_generator/Cargo.toml new file mode 100644 index 0000000000..946e299845 --- /dev/null +++ b/compiler/plc_header_generator/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "plc_header_generator" +version = "0.1.0" +edition = "2021" + +[dependencies] +plc = { path = "../..", package = "rusty" } +plc_diagnostics = { path = "../plc_diagnostics/" } +plc_ast = { path = "../plc_ast/" } +plc_source = { path = "../plc_source/" } +tera = "1" +clap = { version = "3.0", features = ["derive"] } +log.workspace = true diff --git a/compiler/plc_header_generator/src/header_generator.rs b/compiler/plc_header_generator/src/header_generator.rs new file mode 100644 index 0000000000..bd55dead82 --- /dev/null +++ b/compiler/plc_header_generator/src/header_generator.rs @@ -0,0 +1,237 @@ +use std::{fs, path::{PathBuf}}; +use plc::typesystem::{get_builtin_types, DataType}; +use plc_ast::ast::{self, ArgumentProperty, CompilationUnit, DataTypeDeclaration, PouType, UserTypeDeclaration, Variable, VariableBlock, VariableBlockType}; +use plc_diagnostics::diagnostics::Diagnostic; +use plc_source::source_location::FileMarker; +use tera::{Context, Tera}; + +use crate::{type_map::get_type_name_for_type_by_language, GenerateHeaderOptions, GenerateLanguage}; + +pub fn generate_headers(generate_header_options: &GenerateHeaderOptions, compilation_unit: CompilationUnit) -> Result<(), Diagnostic> { + let (header_path, header_dir, header_file_name) = match generate_header_options.language { + GenerateLanguage::C => { + let file_path = match compilation_unit.file { + FileMarker::File(file_path) => PathBuf::from(file_path), + _ => PathBuf::from(String::new()) + }; + + let mut output_path = if generate_header_options.output_path.as_os_str().is_empty() { + if file_path.parent().is_some() { PathBuf::from(file_path.parent().unwrap()) } else { PathBuf::from(String::new()) } + } + else { + generate_header_options.output_path.clone() + }; + + if output_path.as_os_str().is_empty() { + return Err(Diagnostic::new("Unable to determine an output path!")); + } + + let output_dir = output_path.clone(); + let output_name = if generate_header_options.prefix.is_empty() { + let file_name = get_file_name_from_path_buf_without_extension(file_path); + if file_name.is_some() { + format!("{}.h", file_name.unwrap()) + } + else { + String::new() + } + } + else { + format!("{}.h", generate_header_options.prefix) + }; + + if output_name.is_empty() { + // TODO: I'm not sure how I feel about swallowing this... + // ... this usually means this compilation unit is not associated with a file, + // but I don't know if that will always be the case + return Ok(()); + } + + output_path.push(&output_name); + + (output_path, output_dir, output_name) + }, + language => { + return Err(Diagnostic::new(format!("{language:?} language not yet supported!"))); + } + }; + + // Create the directories to the output path + fs::create_dir_all(&header_dir)?; + + // Copy the template file to the output path + let template_file = include_str!("templates/header_template.h"); + fs::write(&header_path, template_file)?; + + // Use tera to render the header + let header_path = header_path.to_str().unwrap(); + let header_dir = format!("{}/**/*", header_dir.to_str().unwrap()); + + let tera = Tera::new(&header_dir).unwrap(); + let mut context = Context::new(); + let builtin_types = get_builtin_types(); + + // Add global variables + let global_variables = build_global_variables(&compilation_unit, generate_header_options.language, &builtin_types); + context.insert("global_variables", &global_variables); + + // Add user-defined data types + let user_defined_data_types = build_user_types(&compilation_unit, generate_header_options.language, &builtin_types); + context.insert("user_defined_data_types", &user_defined_data_types); + + // Add functions + let functions = build_functions(&compilation_unit, generate_header_options.language, &builtin_types); + context.insert("functions", &functions); + + // TODO: Add function blocks + let test = "// TODO"; + let function_blocks = format!("{test}: I'm not a function block... that's weird??"); + context.insert("function_blocks", &function_blocks); + + // Write the file + let content = tera.render(&header_file_name, &context).unwrap(); + fs::write(header_path, content)?; + + Ok(()) +} + +fn build_global_variables(compilation_unit: &CompilationUnit, language: GenerateLanguage, builtin_types: &[DataType]) -> String { + let global_variables = get_variables_from_variable_blocks( + language, + &compilation_unit.global_vars, + builtin_types, + &[VariableBlockType::Global]) + .join(";\nextern "); + + format!("extern {global_variables};") +} + +fn build_functions(compilation_unit: &CompilationUnit, language: GenerateLanguage, builtin_types: &[DataType]) -> String { + let mut functions: Vec = Vec::new(); + + for pou in &compilation_unit.pous { + match pou.kind { + PouType::Function => { + let mut tera = Tera::default(); + let function_template_content = include_str!("templates/function_template.h"); + // TODO: This sucks - do something about it + let _ = tera.add_raw_template("function_template.h", function_template_content); + let mut context = Context::new(); + + context.insert("data_type", &get_type_name_for_type_by_language( + language, + &get_type_from_data_type_decleration(&pou.return_type), + builtin_types)); + context.insert("name", &pou.name); + + let input_variables = get_variables_from_variable_blocks( + language, + &pou.variable_blocks, + builtin_types, + &[ + VariableBlockType::Input(ArgumentProperty::ByRef), + VariableBlockType::Input(ArgumentProperty::ByVal), + VariableBlockType::InOut, + VariableBlockType::Output + ]) + .join(", "); + + context.insert("variables", &input_variables); + + let content = tera.render("function_template.h", &context).unwrap(); + functions.push(content); + } + _ => continue + } + } + + functions.join("\n") +} + +fn build_user_types(compilation_unit: &CompilationUnit, language: GenerateLanguage, builtin_types: &[DataType]) -> String { + let mut user_types: Vec = Vec::new(); + + for user_type in &compilation_unit.user_types { + user_types.push(get_type_from_user_type_decleration(language, user_type, builtin_types)); + } + + user_types.join("\n") +} + +fn get_type_from_user_type_decleration(language: GenerateLanguage, user_type: &UserTypeDeclaration, builtin_types: &[DataType]) -> String { + match &user_type.data_type { + ast::DataType::StructType { name, variables} => { + let mut tera = Tera::default(); + let struct_template_content = include_str!("templates/struct_template.h"); + // TODO: This sucks - do something about it + let _ = tera.add_raw_template("struct_template.h", struct_template_content); + let mut context = Context::new(); + + let formatted_variables = get_formatted_variables_from_variables(language, variables, builtin_types, false); + context.insert("name", &name.clone().unwrap_or(String::new()),); + context.insert("variables", &format!("{};", formatted_variables.join(";\n\t"))); + + tera.render("struct_template.h", &context).unwrap() + }, + _ => String::new() + } +} + +fn get_type_from_data_type_decleration(data_type_declaration: &Option) -> String { + match data_type_declaration { + Some(DataTypeDeclaration::Reference { referenced_type, .. }) => { + referenced_type.clone() + } + _ => String::new() + } +} + +fn get_variables_from_variable_blocks(language: GenerateLanguage, variable_blocks: &[VariableBlock], + builtin_types: &[DataType], variable_block_types: &[VariableBlockType]) -> Vec { + let mut variables: Vec = Vec::new(); + + for variable_block in variable_blocks { + if variable_block_types.contains(&variable_block.kind) { + let is_reference = variable_block.kind == VariableBlockType::Input(ArgumentProperty::ByRef) + || variable_block.kind == VariableBlockType::InOut || variable_block.kind == VariableBlockType::Output; + + variables.append(&mut get_formatted_variables_from_variables(language, &variable_block.variables, builtin_types, is_reference)); + } + } + + variables +} + +fn get_formatted_variables_from_variables(language: GenerateLanguage, variable_block_variables: &[Variable], builtin_types: &[DataType], is_reference: bool) -> Vec { + let mut variables: Vec = Vec::new(); + + let reference_sign = if is_reference { "*" } else { "" }; + + for variable in variable_block_variables { + let variable_str = format!("{}{reference_sign} {}", + get_type_name_for_type_by_language(language, &get_type_from_data_type_decleration(&Some(variable.data_type_declaration.clone())), builtin_types), + variable.get_name()); + variables.push(variable_str.to_string()); + } + + variables +} + +fn get_file_name_from_path_buf_without_extension(file_path: PathBuf) -> Option { + if file_path.file_name().is_some() + { + let file_name = file_path.file_name().unwrap().to_str(); + file_name?; + + let file_name = file_name.unwrap().split('.').next().unwrap_or(""); + + if file_name.is_empty() { + return None; + } + + Some(String::from(file_name)) + } + else { + None + } +} diff --git a/compiler/plc_header_generator/src/lib.rs b/compiler/plc_header_generator/src/lib.rs new file mode 100644 index 0000000000..ec2ab158af --- /dev/null +++ b/compiler/plc_header_generator/src/lib.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; + +use clap::ArgEnum; + +pub mod header_generator; +pub mod type_map; + +#[derive(PartialEq, Eq, Debug, Clone, Copy, ArgEnum, Default)] +pub enum GenerateLanguage { + #[default] + C, + Rust, +} + +#[derive(Debug)] +pub struct GenerateHeaderOptions { + /// Whether or not to include generated code stubs for the library. + pub include_stubs: bool, + + /// The language used to generate the header file. + pub language: GenerateLanguage, + + /// The output folder where generated headers and stubs will be placed. Will default by convention. + pub output_path: PathBuf, + + /// The prefix for the generated header file(s). Will default to the project name if not supplied. + pub prefix: String, +} + +impl Default for GenerateHeaderOptions { + fn default() -> Self { + GenerateHeaderOptions { + include_stubs: false, + language: GenerateLanguage::C, + output_path: PathBuf::from(String::new()), + prefix: String::new(), + } + } +} diff --git a/compiler/plc_header_generator/src/templates/function_template.h b/compiler/plc_header_generator/src/templates/function_template.h new file mode 100644 index 0000000000..0fe41a1fb9 --- /dev/null +++ b/compiler/plc_header_generator/src/templates/function_template.h @@ -0,0 +1 @@ +{{ data_type }} {{ name }}({{ variables }}); diff --git a/compiler/plc_header_generator/src/templates/header_template.h b/compiler/plc_header_generator/src/templates/header_template.h new file mode 100644 index 0000000000..30b0db8259 --- /dev/null +++ b/compiler/plc_header_generator/src/templates/header_template.h @@ -0,0 +1,18 @@ +// ---------------------------------------------------- // +// This file is auto-generated // +// Manual changes made to this file will be overwritten // +// ---------------------------------------------------- // + +#include + +// Global Variables +{{ global_variables }} + +// User-defined data types +{{ user_defined_data_types }} + +// Functions +{{ functions }} + +// TODO: Function blocks +{{ function_blocks }} diff --git a/compiler/plc_header_generator/src/templates/struct_template.h b/compiler/plc_header_generator/src/templates/struct_template.h new file mode 100644 index 0000000000..4c8abaf631 --- /dev/null +++ b/compiler/plc_header_generator/src/templates/struct_template.h @@ -0,0 +1,3 @@ +typedef struct { + {{ variables }} +} {{ name }}; diff --git a/compiler/plc_header_generator/src/type_map.rs b/compiler/plc_header_generator/src/type_map.rs new file mode 100644 index 0000000000..735f9ab7d3 --- /dev/null +++ b/compiler/plc_header_generator/src/type_map.rs @@ -0,0 +1,77 @@ +use plc::typesystem::{DataType, DataTypeInformation, BOOL_TYPE, REAL_SIZE}; +use plc_ast::ast::TypeNature; + +use crate::GenerateLanguage; + +pub fn get_type_name_for_type_by_language(language: GenerateLanguage, type_name: &str, builtin_types: &[DataType]) -> String { + + // TODO: Handle complex and user-defined types + + match language { + GenerateLanguage::C => get_type_name_for_type_in_c(type_name, builtin_types), + _ => String::new() + } +} + +fn get_type_name_for_type_in_c(type_name: &str, builtin_types: &[DataType]) -> String { + if type_name.is_empty() { + return String::from(C_VOID); + } + + let builtin_type = builtin_types + .iter() + .find(|builtin_type| builtin_type.name == type_name); + + if builtin_type.is_none() { + // TODO: Maybe we should log here? But this probably means it is a complex type... + return String::from(type_name); + } + + let builtin_type = builtin_type.unwrap(); + match &builtin_type.information { + DataTypeInformation::Integer { signed, size, .. } => { + // Booleans have their own type + if type_name == BOOL_TYPE { + return String::from(C_BOOL); + } + + // Dates have their own type + if builtin_type.nature == TypeNature::Date || builtin_type.nature == TypeNature::Duration { + return String::from(C_TIME); + } + + let signed_operator = if *signed { "" } else { "u" }; + let constructed_type_name = format!("{signed_operator}int{size}_t"); + + constructed_type_name + }, + DataTypeInformation::Float { size, .. } => { + if *size == REAL_SIZE { String::from(C_FLOAT) } else { String::from(C_DOUBLE) } + }, + DataTypeInformation::Alias { referenced_type, .. } => get_type_name_for_type_in_c(referenced_type, builtin_types), + _ => { + log::debug!("{type_name} this type is not yet supported!"); + String::new() + } + } +} + +// ------------------- // +// -- "C" Constants -- // + +/// The constant value for the "c" type: float +const C_FLOAT: &str = "float_t"; + +/// The constant value for the "c" type: double +const C_DOUBLE: &str = "double_t"; + +/// The constant value for the "c" type: time +const C_TIME: &str = "time_t"; + +/// The constant value for the "c" type: bool +const C_BOOL: &str = "bool"; + +/// The constant value for the "c" type: void +const C_VOID: &str = "void"; + +// ------------------- // diff --git a/tests/integration/data/var_proj/conf/plc.json b/tests/integration/data/var_proj/conf/plc.json new file mode 100644 index 0000000000..2f854815a0 --- /dev/null +++ b/tests/integration/data/var_proj/conf/plc.json @@ -0,0 +1,11 @@ +{ + "name" : "VariableProject", + "files" : [ + "../src/**/*.pli" + ], + "compile_type" : "Shared", + "output" : "prog.so", + "libraries" : [ + ] +} + diff --git a/tests/integration/data/var_proj/src/my_test_interface.pli b/tests/integration/data/var_proj/src/my_test_interface.pli new file mode 100644 index 0000000000..b9b2bba008 --- /dev/null +++ b/tests/integration/data/var_proj/src/my_test_interface.pli @@ -0,0 +1,85 @@ +VAR_GLOBAL + varBool: BOOL; + + varSInt: SINT; + varInt: INT; + varDInt: DINT; + varLInt: LINT; + + varByte: BYTE; + varWord: WORD; + varDWord: DWORD; + varLWord: LWORD; + + varReal: REAL; + varLReal: LREAL; + + varDate: DATE; + varDateAndTime: DATE_AND_TIME; +END_VAR + +(******************** +* +* Converts DT/LDT to DATE +* +*********************) +{external} +FUNCTION DATE_AND_TIME_TO_DATE : DATE +VAR_INPUT + in : DT; +END_VAR +END_FUNCTION + +(******************** +* +* Converts DT/LDT to TOD/LTOD +* +*********************) +{external} +FUNCTION DATE_AND_TIME_TO_TIME_OF_DAY : TOD +VAR_INPUT + in : DT; +END_VAR +END_FUNCTION + +{external} +FUNCTION IamAWeIrdLYCaSEdFunCTIon: INT +VAR_INPUT + varRealInputToFunc: REAL; + varIntInputToFunc: INT; + varDateAndTimeInputToFunc: DATE_AND_TIME; +END_VAR +VAR_IN_OUT + varRealInOutFunc: REAL; +END_VAR +VAR_OUTPUT + varIntOutputFunc: INT; +END_VAR +END_FUNCTION + +FUNCTION IAmAVoidFunction +VAR_INPUT + varIntInput: INT; +END_VAR +END_FUNCTION + +(******************** +* +* Basic Struct with Primitive Types +* +*********************) +TYPE StructWithPrimitiveTypes: + STRUCT + Field1 : BYTE; + Field2 : INT; + Field3 : DINT; + Field4 : STRING[255]; + Field5 : WSTRING[6000]; + END_STRUCT +END_TYPE + +FUNCTION fnThatUsesStructWithPrimitiveTypes: StructWithPrimitiveTypes +VAR_INPUT + varStruct: StructWithPrimitiveTypes; +END_VAR +END_FUNCTION diff --git a/tests/integration/data/var_proj/src/my_test_interface_2.pli b/tests/integration/data/var_proj/src/my_test_interface_2.pli new file mode 100644 index 0000000000..286e56a38c --- /dev/null +++ b/tests/integration/data/var_proj/src/my_test_interface_2.pli @@ -0,0 +1,19 @@ +VAR_GLOBAL + varBool2: BOOL; + + varSInt2: SINT; + varInt2: INT; + varDInt2: DINT; + varLInt2: LINT; + + varByte2: BYTE; + varWord2: WORD; + varDWord2: DWORD; + varLWord2: LWORD; + + varReal2: REAL; + varLReal2: LREAL; + + varDate2: DATE; + varDateAndTime2: DATE_AND_TIME; +END_VAR diff --git a/tests/integration/header_generator_tests.rs b/tests/integration/header_generator_tests.rs index f94b775dd6..39a045ab31 100644 --- a/tests/integration/header_generator_tests.rs +++ b/tests/integration/header_generator_tests.rs @@ -10,13 +10,16 @@ fn generate_header_for_empty_project() { let parameters = &[ "plc", "generate", - &get_test_file("empty_proj/conf/plc.json"), + &get_test_file("var_proj/conf/plc.json"), "headers", "--header-output", &header_dir, ]; compile(parameters).unwrap(); - println!("{}", dir.path().join("headers").join("EmptyProject.h").to_str().unwrap()); - assert!(dir.path().join("headers").join("EmptyProject.h").is_file()); + println!("{}", dir.path().join("headers").join("my_test_interface.h").to_str().unwrap()); + println!("{}", dir.path().join("headers").join("my_test_interface_2.h").to_str().unwrap()); + + assert!(dir.path().join("headers").join("my_test_interface.h").is_file()); + assert!(dir.path().join("headers").join("my_test_interface_2.h").is_file()); } From 3853b44c554b95300fdd965a171d33d1c2f582f3 Mon Sep 17 00:00:00 2001 From: Angus Bethke Date: Wed, 29 Oct 2025 14:56:24 +0000 Subject: [PATCH 04/21] feat: Added several complex types and refactored to allow for easier additions of further languages --- compiler/plc_driver/src/pipelines.rs | 50 +- .../plc_header_generator/src/file_manager.rs | 106 +++ .../src/header_generator.rs | 747 +++++++++++++----- compiler/plc_header_generator/src/lib.rs | 4 +- .../src/template_manager.rs | 82 ++ .../src/templates/c/function_template.h | 1 + .../src/templates/{ => c}/header_template.h | 0 .../src/templates/c/param_array_template.h | 1 + .../src/templates/c/param_enum_template.h | 1 + .../src/templates/c/param_struct_template.h | 1 + .../templates/c/user_type_array_template.h | 1 + .../src/templates/c/user_type_enum_template.h | 3 + .../user_type_struct_template.h} | 2 +- .../src/templates/c/variable_template.h | 1 + .../src/templates/function_template.h | 1 - .../plc_header_generator/src/type_manager.rs | 126 +++ compiler/plc_header_generator/src/type_map.rs | 77 -- .../data/var_proj/src/my_test_interface.pli | 88 ++- 18 files changed, 1000 insertions(+), 292 deletions(-) create mode 100644 compiler/plc_header_generator/src/file_manager.rs create mode 100644 compiler/plc_header_generator/src/template_manager.rs create mode 100644 compiler/plc_header_generator/src/templates/c/function_template.h rename compiler/plc_header_generator/src/templates/{ => c}/header_template.h (100%) create mode 100644 compiler/plc_header_generator/src/templates/c/param_array_template.h create mode 100644 compiler/plc_header_generator/src/templates/c/param_enum_template.h create mode 100644 compiler/plc_header_generator/src/templates/c/param_struct_template.h create mode 100644 compiler/plc_header_generator/src/templates/c/user_type_array_template.h create mode 100644 compiler/plc_header_generator/src/templates/c/user_type_enum_template.h rename compiler/plc_header_generator/src/templates/{struct_template.h => c/user_type_struct_template.h} (72%) create mode 100644 compiler/plc_header_generator/src/templates/c/variable_template.h delete mode 100644 compiler/plc_header_generator/src/templates/function_template.h create mode 100644 compiler/plc_header_generator/src/type_manager.rs delete mode 100644 compiler/plc_header_generator/src/type_map.rs diff --git a/compiler/plc_driver/src/pipelines.rs b/compiler/plc_driver/src/pipelines.rs index ae0c713ad2..a709b0b449 100644 --- a/compiler/plc_driver/src/pipelines.rs +++ b/compiler/plc_driver/src/pipelines.rs @@ -41,7 +41,7 @@ use plc_diagnostics::{ diagnostician::Diagnostician, diagnostics::{Diagnostic, Severity}, }; -use plc_header_generator::{header_generator, GenerateHeaderOptions}; +use plc_header_generator::{header_generator::GeneratedHeader, GenerateHeaderOptions}; use plc_index::GlobalContext; use plc_lowering::inheritance::InheritanceLowerer; use project::{ @@ -225,24 +225,22 @@ impl BuildPipeline { } pub fn get_generate_header_options(&self) -> Option { - self.compile_parameters.as_ref().map(|params| { - match params.commands.as_ref().unwrap() { - SubCommands::Generate { option, .. } => match option { - GenerateOption::Headers { include_stubs, language, output, prefix, .. } => { - GenerateHeaderOptions { - include_stubs: *include_stubs, - language: *language, - output_path: if output.is_some() { - PathBuf::from(output.clone().unwrap()) - } else { - PathBuf::from(String::new()) - }, - prefix: prefix.clone().unwrap_or(String::new()), - } + self.compile_parameters.as_ref().map(|params| match params.commands.as_ref().unwrap() { + SubCommands::Generate { option, .. } => match option { + GenerateOption::Headers { include_stubs, language, output, prefix, .. } => { + GenerateHeaderOptions { + include_stubs: *include_stubs, + language: *language, + output_path: if output.is_some() { + PathBuf::from(output.clone().unwrap()) + } else { + PathBuf::from(String::new()) + }, + prefix: prefix.clone().unwrap_or(String::new()), } - }, - _ => GenerateHeaderOptions::default(), - } + } + }, + _ => GenerateHeaderOptions::default(), }) } @@ -466,8 +464,22 @@ impl Pipeline for BuildPipeline { return Ok(()); }; + let mut generated_headers: Vec = Vec::new(); + for unit in project.units { - header_generator::generate_headers(&generate_header_options, unit.unit)?; + let mut generated_header = GeneratedHeader::new(); + generated_header.generate_headers(&generate_header_options, unit.unit)?; + generated_headers.push(generated_header); + } + + for generated_header in generated_headers { + if !generated_header.is_empty() { + // Create the directories to the output path + fs::create_dir_all(generated_header.directory)?; + + // Write the header file + fs::write(generated_header.path, generated_header.contents)?; + } } Ok(()) diff --git a/compiler/plc_header_generator/src/file_manager.rs b/compiler/plc_header_generator/src/file_manager.rs new file mode 100644 index 0000000000..8c8a007c06 --- /dev/null +++ b/compiler/plc_header_generator/src/file_manager.rs @@ -0,0 +1,106 @@ +use std::path::PathBuf; + +use plc_ast::ast::CompilationUnit; +use plc_diagnostics::diagnostics::Diagnostic; +use plc_source::source_location::FileMarker; + +use crate::{GenerateHeaderOptions, GenerateLanguage}; + +pub struct FileManager { + pub language: GenerateLanguage, + pub output_path: String, + pub output_dir: String, +} + +impl FileManager { + pub const fn new() -> Self { + FileManager { language: GenerateLanguage::C, output_path: String::new(), output_dir: String::new() } + } + + pub fn prepare_file_and_directory( + &mut self, + generate_header_options: &GenerateHeaderOptions, + compilation_unit: &CompilationUnit, + ) -> Result { + match self.language { + GenerateLanguage::C => { + self.prepare_file_and_directory_for_c(generate_header_options, compilation_unit) + } + language => Err(Diagnostic::new(format!("{language:?} language not yet supported!"))), + } + } + + fn prepare_file_and_directory_for_c( + &mut self, + generate_header_options: &GenerateHeaderOptions, + compilation_unit: &CompilationUnit, + ) -> Result { + let file_path = match compilation_unit.file { + FileMarker::File(file_path) => PathBuf::from(file_path), + _ => PathBuf::from(String::new()), + }; + + let mut output_path = if generate_header_options.output_path.as_os_str().is_empty() { + if file_path.parent().is_some() { + PathBuf::from(file_path.parent().unwrap()) + } else { + PathBuf::from(String::new()) + } + } else { + generate_header_options.output_path.clone() + }; + + if output_path.as_os_str().is_empty() { + return Err(Diagnostic::new("Unable to determine an output path!")); + } + + let output_dir = output_path.clone(); + let output_name = if generate_header_options.prefix.is_empty() { + let file_name = get_file_name_from_path_buf_without_extension(file_path); + if file_name.is_some() { + format!("{}.h", file_name.unwrap()) + } else { + String::new() + } + } else { + format!("{}.h", generate_header_options.prefix) + }; + + if output_name.is_empty() { + // TODO: I'm not sure how I feel about swallowing this... + // This means this compilation unit is not associated with a file in this case we aren't interested in drilling into it + return Ok(false); + } + + output_path.push(&output_name); + + self.output_path = String::from(output_path.to_str().expect("Unable to determine the output path!")); + self.output_dir = + String::from(output_dir.to_str().expect("Unable to determine the output directory!")); + + Ok(true) + } +} + +impl Default for FileManager { + fn default() -> Self { + Self::new() + } +} + +fn get_file_name_from_path_buf_without_extension(file_path: PathBuf) -> Option { + if file_path.file_name().is_some() { + let file_name = file_path.file_name().unwrap().to_str(); + file_name?; + + let file_name = file_name.unwrap().split('.').next().unwrap_or(""); + + if file_name.is_empty() { + return None; + } + + Some(String::from(file_name)) + } else { + None + } +} diff --git a/compiler/plc_header_generator/src/header_generator.rs b/compiler/plc_header_generator/src/header_generator.rs index bd55dead82..d6b9152796 100644 --- a/compiler/plc_header_generator/src/header_generator.rs +++ b/compiler/plc_header_generator/src/header_generator.rs @@ -1,237 +1,628 @@ -use std::{fs, path::{PathBuf}}; use plc::typesystem::{get_builtin_types, DataType}; -use plc_ast::ast::{self, ArgumentProperty, CompilationUnit, DataTypeDeclaration, PouType, UserTypeDeclaration, Variable, VariableBlock, VariableBlockType}; +use plc_ast::{ + ast::{ + self, ArgumentProperty, AstNode, AstStatement, CompilationUnit, DataTypeDeclaration, PouType, + ReferenceAccess, UserTypeDeclaration, Variable, VariableBlock, VariableBlockType, + }, + literals::AstLiteral, +}; use plc_diagnostics::diagnostics::Diagnostic; -use plc_source::source_location::FileMarker; use tera::{Context, Tera}; -use crate::{type_map::get_type_name_for_type_by_language, GenerateHeaderOptions, GenerateLanguage}; - -pub fn generate_headers(generate_header_options: &GenerateHeaderOptions, compilation_unit: CompilationUnit) -> Result<(), Diagnostic> { - let (header_path, header_dir, header_file_name) = match generate_header_options.language { - GenerateLanguage::C => { - let file_path = match compilation_unit.file { - FileMarker::File(file_path) => PathBuf::from(file_path), - _ => PathBuf::from(String::new()) - }; - - let mut output_path = if generate_header_options.output_path.as_os_str().is_empty() { - if file_path.parent().is_some() { PathBuf::from(file_path.parent().unwrap()) } else { PathBuf::from(String::new()) } - } - else { - generate_header_options.output_path.clone() - }; +use crate::{ + file_manager::FileManager, + template_manager::{TemplateManager, TemplateType}, + type_manager::TypeManager, + GenerateHeaderOptions, +}; + +pub struct GeneratedHeader { + pub directory: String, + pub path: String, + pub contents: String, + + declared_user_types: Vec, + template_manager: TemplateManager, + type_manager: TypeManager, + file_manager: FileManager, +} - if output_path.as_os_str().is_empty() { - return Err(Diagnostic::new("Unable to determine an output path!")); - } +pub enum GenerationSource { + GlobalVariable, + UserType, + Struct, + FunctionParameter, +} - let output_dir = output_path.clone(); - let output_name = if generate_header_options.prefix.is_empty() { - let file_name = get_file_name_from_path_buf_without_extension(file_path); - if file_name.is_some() { - format!("{}.h", file_name.unwrap()) - } - else { - String::new() - } - } - else { - format!("{}.h", generate_header_options.prefix) - }; +impl Default for GeneratedHeader { + fn default() -> Self { + Self::new() + } +} - if output_name.is_empty() { - // TODO: I'm not sure how I feel about swallowing this... - // ... this usually means this compilation unit is not associated with a file, - // but I don't know if that will always be the case - return Ok(()); - } +impl GeneratedHeader { + pub const fn new() -> Self { + GeneratedHeader { + declared_user_types: Vec::new(), + directory: String::new(), + path: String::new(), + contents: String::new(), + template_manager: TemplateManager::new(), + type_manager: TypeManager::new(), + file_manager: FileManager::new(), + } + } - output_path.push(&output_name); + pub fn is_empty(&self) -> bool { + self.directory.is_empty() && self.path.is_empty() && self.contents.is_empty() + } - (output_path, output_dir, output_name) - }, - language => { - return Err(Diagnostic::new(format!("{language:?} language not yet supported!"))); + pub fn generate_headers( + &mut self, + generate_header_options: &GenerateHeaderOptions, + compilation_unit: CompilationUnit, + ) -> Result<(), Diagnostic> { + // Setup managers + self.template_manager.language = generate_header_options.language; + self.type_manager.language = generate_header_options.language; + self.file_manager.language = generate_header_options.language; + + // If the directories could not be configured with an acceptable outcome, then we exit without performing generation for this compilation unit + if !self.file_manager.prepare_file_and_directory(generate_header_options, &compilation_unit)? { + return Ok(()); } - }; - - // Create the directories to the output path - fs::create_dir_all(&header_dir)?; - // Copy the template file to the output path - let template_file = include_str!("templates/header_template.h"); - fs::write(&header_path, template_file)?; + // Configure tera + let mut tera = Tera::default(); + let mut context = Context::new(); - // Use tera to render the header - let header_path = header_path.to_str().unwrap(); - let header_dir = format!("{}/**/*", header_dir.to_str().unwrap()); + let template = self + .template_manager + .get_template(TemplateType::Header) + .expect("Unable to find the 'header' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'header' template to tera!"); - let tera = Tera::new(&header_dir).unwrap(); - let mut context = Context::new(); - let builtin_types = get_builtin_types(); + let builtin_types = get_builtin_types(); - // Add global variables - let global_variables = build_global_variables(&compilation_unit, generate_header_options.language, &builtin_types); - context.insert("global_variables", &global_variables); + // 1 - Add global variables + let global_variables = self.build_global_variables(&compilation_unit, &builtin_types); + context.insert("global_variables", &global_variables); - // Add user-defined data types - let user_defined_data_types = build_user_types(&compilation_unit, generate_header_options.language, &builtin_types); - context.insert("user_defined_data_types", &user_defined_data_types); + // 2 - Add functions + let functions = self.build_functions(&compilation_unit, &builtin_types); + context.insert("functions", &functions); - // Add functions - let functions = build_functions(&compilation_unit, generate_header_options.language, &builtin_types); - context.insert("functions", &functions); + // TODO: 3 - Add function blocks + let test = "// TODO"; + let function_blocks = format!("{test}: I'm not a function block... that's weird??"); + context.insert("function_blocks", &function_blocks); - // TODO: Add function blocks - let test = "// TODO"; - let function_blocks = format!("{test}: I'm not a function block... that's weird??"); - context.insert("function_blocks", &function_blocks); + // 4 - Add user-defined data types + let user_defined_data_types = self.build_user_types(&compilation_unit, &builtin_types); + context.insert("user_defined_data_types", &user_defined_data_types); - // Write the file - let content = tera.render(&header_file_name, &context).unwrap(); - fs::write(header_path, content)?; + // Set the outputs + self.directory = self.file_manager.output_dir.clone(); + self.path = self.file_manager.output_path.clone(); + self.contents = tera.render(&template.name, &context).unwrap(); - Ok(()) -} + Ok(()) + } -fn build_global_variables(compilation_unit: &CompilationUnit, language: GenerateLanguage, builtin_types: &[DataType]) -> String { - let global_variables = get_variables_from_variable_blocks( - language, + fn build_global_variables( + &mut self, + compilation_unit: &CompilationUnit, + builtin_types: &[DataType], + ) -> String { + let global_variables = self.get_variables_from_variable_blocks( &compilation_unit.global_vars, builtin_types, - &[VariableBlockType::Global]) - .join(";\nextern "); + &[VariableBlockType::Global], + &compilation_unit.user_types, + &GenerationSource::GlobalVariable, + ); - format!("extern {global_variables};") -} + format!("extern {};", global_variables.join(";\nextern ")) + } -fn build_functions(compilation_unit: &CompilationUnit, language: GenerateLanguage, builtin_types: &[DataType]) -> String { - let mut functions: Vec = Vec::new(); - - for pou in &compilation_unit.pous { - match pou.kind { - PouType::Function => { - let mut tera = Tera::default(); - let function_template_content = include_str!("templates/function_template.h"); - // TODO: This sucks - do something about it - let _ = tera.add_raw_template("function_template.h", function_template_content); - let mut context = Context::new(); - - context.insert("data_type", &get_type_name_for_type_by_language( - language, - &get_type_from_data_type_decleration(&pou.return_type), - builtin_types)); - context.insert("name", &pou.name); - - let input_variables = get_variables_from_variable_blocks( - language, - &pou.variable_blocks, - builtin_types, - &[ - VariableBlockType::Input(ArgumentProperty::ByRef), - VariableBlockType::Input(ArgumentProperty::ByVal), - VariableBlockType::InOut, - VariableBlockType::Output - ]) - .join(", "); - - context.insert("variables", &input_variables); - - let content = tera.render("function_template.h", &context).unwrap(); - functions.push(content); + fn build_functions(&mut self, compilation_unit: &CompilationUnit, builtin_types: &[DataType]) -> String { + let mut functions: Vec = Vec::new(); + + for pou in &compilation_unit.pous { + match pou.kind { + PouType::Function => { + let mut tera = Tera::default(); + let mut context = Context::new(); + + let template = self + .template_manager + .get_template(TemplateType::Function) + .expect("Unable to find the 'function' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'function' template to tera!"); + + let (_, type_name) = &self.type_manager.get_type_name_for_type( + &get_type_from_data_type_decleration(&pou.return_type), + builtin_types, + ); + + context.insert("data_type", type_name); + context.insert("name", &pou.name); + + let input_variables = self + .get_variables_from_variable_blocks( + &pou.variable_blocks, + builtin_types, + &[ + VariableBlockType::Input(ArgumentProperty::ByRef), + VariableBlockType::Input(ArgumentProperty::ByVal), + VariableBlockType::InOut, + VariableBlockType::Output, + ], + &compilation_unit.user_types, + &GenerationSource::FunctionParameter, + ) + .join(", "); + + context.insert("variables", &input_variables); + + let content = tera.render(&template.name, &context).unwrap().trim().to_string(); + functions.push(content); + } + _ => continue, } - _ => continue } + + format!("{};", functions.join(";\n\n")) } - functions.join("\n") -} + fn build_user_types(&mut self, compilation_unit: &CompilationUnit, builtin_types: &[DataType]) -> String { + let mut user_types: Vec = Vec::new(); + + for user_type in &compilation_unit.user_types { + let formatted_user_type = self.build_user_type( + user_type, + builtin_types, + &compilation_unit.user_types, + None, + None, + &GenerationSource::UserType, + ); + + if let Some(value) = formatted_user_type { + user_types.push(value); + } + } + + format!("{};", user_types.join(";\n\n")) + } + + fn build_user_type( + &mut self, + user_type: &UserTypeDeclaration, + builtin_types: &[DataType], + user_types: &[UserTypeDeclaration], + field_name_override: Option<&String>, + type_name_override: Option<&String>, + generation_source: &GenerationSource, + ) -> Option { + let mut tera = Tera::default(); + let mut context = Context::new(); + + let type_name = String::from(user_type.data_type.get_name().unwrap_or("")); + if self.declared_user_types.contains(&type_name) + && matches!(generation_source, GenerationSource::UserType) + { + return None; + } + + match &user_type.data_type { + ast::DataType::StructType { name, variables } => { + let template = match generation_source { + GenerationSource::GlobalVariable | GenerationSource::UserType => { + let template = self + .template_manager + .get_template(TemplateType::UserTypeStruct) + .expect("Unable to find the 'user type struct' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'user type struct' template to tera!"); + + let formatted_variables = self.get_formatted_variables_from_variables( + variables, + builtin_types, + false, + user_types, + &GenerationSource::Struct, + ); + context.insert( + "name", + &coalesce_optional_strings_with_default(name, field_name_override), + ); + context.insert("variables", &format!("{};", formatted_variables.join(";\n\t"))); + + template + } + GenerationSource::Struct | GenerationSource::FunctionParameter => { + let template = self + .template_manager + .get_template(TemplateType::ParamStruct) + .expect("Unable to find the 'param struct' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'param struct' template to tera!"); + + context.insert( + "name", + &field_name_override + .expect("Field name expected for generation source type: 'Parameter'!"), + ); + context.insert( + "data_type", + &type_name_override + .expect("Data Type expected for generation source type: 'Parameter'!"), + ); + + template + } + }; + + Some(tera.render(&template.name, &context).unwrap().trim().to_string()) + } + ast::DataType::EnumType { name, elements, .. } => { + let template = match generation_source { + GenerationSource::GlobalVariable | GenerationSource::UserType => { + let template = self + .template_manager + .get_template(TemplateType::UserTypeEnum) + .expect("Unable to find the 'user type enum' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'user type enum' template to tera!"); + + let formatted_enum_declerations = + self.extract_enum_declaration_from_elements(elements); + context.insert( + "name", + &coalesce_optional_strings_with_default(name, field_name_override), + ); + context.insert("variables", &formatted_enum_declerations); + + template + } + GenerationSource::Struct | GenerationSource::FunctionParameter => { + let template = self + .template_manager + .get_template(TemplateType::ParamEnum) + .expect("Unable to find the 'param enum' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'param enum' template to tera!"); + + context.insert( + "name", + &field_name_override + .expect("Field name expected for generation source type: 'Parameter'!"), + ); + context.insert( + "data_type", + &type_name_override + .expect("Data Type expected for generation source type: 'Parameter'!"), + ); + + template + } + }; + + Some(tera.render(&template.name, &context).unwrap().trim().to_string()) + } + ast::DataType::StringType { name, size, is_wide } => { + let template = match generation_source { + GenerationSource::GlobalVariable + | GenerationSource::Struct + | GenerationSource::UserType => { + let template = self + .template_manager + .get_template(TemplateType::UserTypeArray) + .expect("Unable to find the 'user type array' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'user type array' template to tera!"); + + let string_size = extract_string_size(size); + context.insert( + "name", + &coalesce_optional_strings_with_default(name, field_name_override), + ); + context.insert("data_type", &self.type_manager.get_type_name_for_string(is_wide)); + context.insert("size", &string_size); + + template + } + GenerationSource::FunctionParameter => { + let template = self + .template_manager + .get_template(TemplateType::ParamArray) + .expect("Unable to find the 'param array' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'param array' template to tera!"); + + context.insert( + "name", + &field_name_override + .expect("Field name expected for generation source type: 'Parameter'!"), + ); + context.insert("data_type", &self.type_manager.get_type_name_for_string(is_wide)); + + template + } + }; + + Some(tera.render(&template.name, &context).unwrap().trim().to_string()) + } + ast::DataType::ArrayType { name, bounds, referenced_type, .. } => { + let template = match generation_source { + GenerationSource::GlobalVariable + | GenerationSource::Struct + | GenerationSource::UserType => { + let template = self + .template_manager + .get_template(TemplateType::UserTypeArray) + .expect("Unable to find the 'user type array' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'user type array' template to tera!"); + + let string_size = extract_array_size(bounds); + context.insert( + "name", + &coalesce_optional_strings_with_default(name, field_name_override), + ); + + let (_, type_name) = self + .type_manager + .get_type_name_for_type(referenced_type.get_name().unwrap(), builtin_types); + context.insert("data_type", &type_name); + context.insert("size", &string_size); + + template + } + GenerationSource::FunctionParameter => { + let template = self + .template_manager + .get_template(TemplateType::ParamArray) + .expect("Unable to find the 'param array' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'param array' template to tera!"); + + context.insert( + "name", + &field_name_override + .expect("Field name expected for generation source type: 'Parameter'!"), + ); + + let (_, type_name) = self + .type_manager + .get_type_name_for_type(referenced_type.get_name().unwrap(), builtin_types); + context.insert("data_type", &type_name); + + template + } + }; + + Some(tera.render(&template.name, &context).unwrap().trim().to_string()) + } + ast::DataType::PointerType { .. } => { + // TODO: Implement Pointers + None + } + ast::DataType::SubRangeType { .. } => { + // TODO: Implement Sub Range -- This is just an integer + None + } + ast::DataType::VarArgs { .. } => { + // TODO: Implement Var Args -- This is limited to functions https://www.geeksforgeeks.org/c/printf-in-c/ + None + } + ast::DataType::GenericType { .. } => { + // Currently out of scope + None + } + } + } + + fn extract_enum_declaration_from_elements(&self, node: &AstNode) -> String { + match &node.stmt { + AstStatement::ExpressionList(exp_nodes) => { + let mut formatted_enum_declarations: Vec = Vec::new(); + for exp_node in exp_nodes { + match &exp_node.stmt { + AstStatement::Assignment(assignment) => { + let left = extract_enum_field_name_from_statement(&assignment.left.stmt); + let right = extract_enum_field_value_from_statement(&assignment.right.stmt); + let right = if right.is_empty() { String::new() } else { format!(" = {right}") }; + + formatted_enum_declarations.push(format!("{left}{right}")); + } + _ => continue, + } + } + formatted_enum_declarations.join(",\n\t") + } + _ => String::new(), + } + } -fn build_user_types(compilation_unit: &CompilationUnit, language: GenerateLanguage, builtin_types: &[DataType]) -> String { - let mut user_types: Vec = Vec::new(); + fn get_variables_from_variable_blocks( + &mut self, + variable_blocks: &[VariableBlock], + builtin_types: &[DataType], + variable_block_types: &[VariableBlockType], + user_types: &[UserTypeDeclaration], + generation_source: &GenerationSource, + ) -> Vec { + let mut variables: Vec = Vec::new(); + + for variable_block in variable_blocks { + if variable_block_types.contains(&variable_block.kind) { + let is_reference = variable_block.kind == VariableBlockType::Input(ArgumentProperty::ByRef) + || variable_block.kind == VariableBlockType::InOut + || variable_block.kind == VariableBlockType::Output; + + variables.append(&mut self.get_formatted_variables_from_variables( + &variable_block.variables, + builtin_types, + is_reference, + user_types, + generation_source, + )); + } + } - for user_type in &compilation_unit.user_types { - user_types.push(get_type_from_user_type_decleration(language, user_type, builtin_types)); + variables } - user_types.join("\n") -} + fn get_formatted_variables_from_variables( + &mut self, + variable_block_variables: &[Variable], + builtin_types: &[DataType], + is_reference: bool, + user_types: &[UserTypeDeclaration], + generation_source: &GenerationSource, + ) -> Vec { + let mut tera = Tera::default(); + let mut context = Context::new(); + let mut variables: Vec = Vec::new(); + + let reference_symbol = if is_reference { "*" } else { "" }; + + for variable in variable_block_variables { + let (is_user_generated, type_name) = self.type_manager.get_type_name_for_type( + &get_type_from_data_type_decleration(&Some(variable.data_type_declaration.clone())), + builtin_types, + ); + + if is_user_generated { + let option_user_type = get_user_generated_type_by_name(&type_name, user_types); + + if let Some(user_type) = option_user_type { + let formatted_user_type = self.build_user_type( + user_type, + builtin_types, + user_types, + Some(&String::from(variable.get_name())), + Some(&type_name), + generation_source, + ); + + if let Some(value) = formatted_user_type { + if !self.type_manager.user_type_can_be_declared_outside_of_a_function(user_type) { + self.declared_user_types.push(type_name.clone()); + } + variables.push(value); + } + } + } else { + let template = self + .template_manager + .get_template(TemplateType::Variable) + .expect("Unable to find the 'variable' template!"); + tera.add_raw_template(&template.name, &template.content) + .expect("Unable to add the 'variable' template to tera!"); + + context.insert("data_type", &type_name); + context.insert("name", variable.get_name()); + context.insert("reference_symbol", reference_symbol); + + variables.push(tera.render(&template.name, &context).unwrap().trim().to_string()); + } + } -fn get_type_from_user_type_decleration(language: GenerateLanguage, user_type: &UserTypeDeclaration, builtin_types: &[DataType]) -> String { - match &user_type.data_type { - ast::DataType::StructType { name, variables} => { - let mut tera = Tera::default(); - let struct_template_content = include_str!("templates/struct_template.h"); - // TODO: This sucks - do something about it - let _ = tera.add_raw_template("struct_template.h", struct_template_content); - let mut context = Context::new(); + variables + } +} - let formatted_variables = get_formatted_variables_from_variables(language, variables, builtin_types, false); - context.insert("name", &name.clone().unwrap_or(String::new()),); - context.insert("variables", &format!("{};", formatted_variables.join(";\n\t"))); +fn coalesce_optional_strings_with_default( + name: &Option, + field_name_override: Option<&String>, +) -> String { + if let Some(field_name_ovr) = field_name_override { + field_name_ovr.to_string() + } else { + name.clone().unwrap_or_default() + } +} - tera.render("struct_template.h", &context).unwrap() +fn extract_enum_field_name_from_statement(statement: &AstStatement) -> String { + match statement { + AstStatement::ReferenceExpr(reference_expression) => match &reference_expression.access { + ReferenceAccess::Member(member_node) => { + let member_statement = member_node.get_stmt(); + match member_statement { + AstStatement::Identifier(enum_field) => enum_field.to_string(), + _ => String::new(), + } + } + _ => String::new(), }, - _ => String::new() + _ => String::new(), + } +} + +fn extract_enum_field_value_from_statement(statement: &AstStatement) -> String { + match statement { + AstStatement::Literal(literal) => literal.get_literal_value(), + _ => String::new(), } } fn get_type_from_data_type_decleration(data_type_declaration: &Option) -> String { match data_type_declaration { - Some(DataTypeDeclaration::Reference { referenced_type, .. }) => { - referenced_type.clone() - } - _ => String::new() + Some(DataTypeDeclaration::Reference { referenced_type, .. }) => referenced_type.clone(), + _ => String::new(), } } -fn get_variables_from_variable_blocks(language: GenerateLanguage, variable_blocks: &[VariableBlock], - builtin_types: &[DataType], variable_block_types: &[VariableBlockType]) -> Vec { - let mut variables: Vec = Vec::new(); - - for variable_block in variable_blocks { - if variable_block_types.contains(&variable_block.kind) { - let is_reference = variable_block.kind == VariableBlockType::Input(ArgumentProperty::ByRef) - || variable_block.kind == VariableBlockType::InOut || variable_block.kind == VariableBlockType::Output; - - variables.append(&mut get_formatted_variables_from_variables(language, &variable_block.variables, builtin_types, is_reference)); +fn get_user_generated_type_by_name<'a>( + name: &'a str, + user_types: &'a [UserTypeDeclaration], +) -> Option<&'a UserTypeDeclaration> { + for user_type in user_types { + if let Some(data_type_name) = user_type.data_type.get_name() { + if data_type_name == name { + return Some(user_type); + } } } - variables + None } -fn get_formatted_variables_from_variables(language: GenerateLanguage, variable_block_variables: &[Variable], builtin_types: &[DataType], is_reference: bool) -> Vec { - let mut variables: Vec = Vec::new(); +fn extract_string_size(size: &Option) -> String { + if size.is_none() { + return String::new(); + } - let reference_sign = if is_reference { "*" } else { "" }; + let size = size.clone().unwrap(); - for variable in variable_block_variables { - let variable_str = format!("{}{reference_sign} {}", - get_type_name_for_type_by_language(language, &get_type_from_data_type_decleration(&Some(variable.data_type_declaration.clone())), builtin_types), - variable.get_name()); - variables.push(variable_str.to_string()); + match size.stmt { + // TODO: Verify this is necessary + // +1 character for the string-termination-marker + AstStatement::Literal(AstLiteral::Integer(value)) => format!("{}", value + 1), + _ => String::new(), } - - variables } -fn get_file_name_from_path_buf_without_extension(file_path: PathBuf) -> Option { - if file_path.file_name().is_some() - { - let file_name = file_path.file_name().unwrap().to_str(); - file_name?; +fn extract_array_size(bounds: &AstNode) -> String { + match &bounds.stmt { + AstStatement::RangeStatement(range_stmt) => { + let start_value = match range_stmt.start.get_stmt() { + AstStatement::Literal(AstLiteral::Integer(value)) => *value, + _ => i128::default(), + }; - let file_name = file_name.unwrap().split('.').next().unwrap_or(""); + let end_value = match range_stmt.end.get_stmt() { + AstStatement::Literal(AstLiteral::Integer(value)) => *value, + _ => i128::default(), + }; - if file_name.is_empty() { - return None; + format!("{}", end_value - start_value + 1) } - - Some(String::from(file_name)) - } - else { - None + _ => String::new(), } } diff --git a/compiler/plc_header_generator/src/lib.rs b/compiler/plc_header_generator/src/lib.rs index ec2ab158af..9d3b14bf33 100644 --- a/compiler/plc_header_generator/src/lib.rs +++ b/compiler/plc_header_generator/src/lib.rs @@ -2,8 +2,10 @@ use std::path::PathBuf; use clap::ArgEnum; +pub mod file_manager; pub mod header_generator; -pub mod type_map; +pub mod template_manager; +pub mod type_manager; #[derive(PartialEq, Eq, Debug, Clone, Copy, ArgEnum, Default)] pub enum GenerateLanguage { diff --git a/compiler/plc_header_generator/src/template_manager.rs b/compiler/plc_header_generator/src/template_manager.rs new file mode 100644 index 0000000000..0100d3d78d --- /dev/null +++ b/compiler/plc_header_generator/src/template_manager.rs @@ -0,0 +1,82 @@ +use crate::GenerateLanguage; + +pub struct TemplateManager { + pub language: GenerateLanguage, +} + +impl TemplateManager { + pub const fn new() -> Self { + TemplateManager { language: GenerateLanguage::C } + } + + pub fn get_template(&self, template_type: TemplateType) -> Option