use std::fs;
use std::path::Path;
use syntaqlite_buildtools::codegen_api::MacroStyle;
fn ensure_dir(path: &Path, label: &str) -> Result<(), String> {
fs::create_dir_all(path).map_err(|e| format!("Failed to create {label}: {e}"))
}
fn write_file(path: &Path, content: impl AsRef<[u8]>) -> Result<(), String> {
fs::write(path, content).map_err(|e| format!("Failed to write {}: {}", path.display(), e))
}
#[derive(clap::ValueEnum, Clone)]
pub(crate) enum OutputType {
TypedDialectEnv,
Raw,
Full,
RuntimeOnly,
}
#[derive(clap::Parser)]
pub(crate) struct DialectArgs {
#[arg(long, required = true)]
name: String,
#[arg(long)]
output_dir: Option<String>,
#[arg(long)]
actions_dir: Option<String>,
#[arg(long)]
nodes_dir: Option<String>,
#[arg(long, value_enum, default_value_t = OutputType::TypedDialectEnv)]
output_type: OutputType,
#[arg(long, default_value = "syntaqlite_runtime.h")]
runtime_header: String,
#[arg(long, default_value = "syntaqlite_dialect.h")]
ext_header: String,
#[arg(long, value_enum, default_value_t = CliMacroStyle::None)]
macro_style: CliMacroStyle,
}
#[derive(Clone, Copy, Default, clap::ValueEnum)]
pub(crate) enum CliMacroStyle {
#[default]
None,
Rust,
}
#[derive(clap::Subcommand)]
pub(crate) enum ToolCommand {
#[command(hide = true)]
Lemon {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(hide = true)]
Mkkeyword {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
pub(crate) fn dispatch_dialect(args: &DialectArgs) -> Result<(), String> {
let name = &args.name;
let actions_dir = args.actions_dir.as_deref();
let nodes_dir = args.nodes_dir.as_deref();
let macro_style_val = match args.macro_style {
CliMacroStyle::None => MacroStyle::None,
CliMacroStyle::Rust => MacroStyle::Rust,
};
let require_output_dir = |type_name: &str| -> Result<String, String> {
args.output_dir
.clone()
.ok_or_else(|| format!("--output-dir is required for --output-type={type_name}"))
};
match &args.output_type {
OutputType::TypedDialectEnv => cmd_generate_dialect(
name,
actions_dir,
nodes_dir,
&require_output_dir("dialect")?,
&args.runtime_header,
&args.ext_header,
macro_style_val,
),
OutputType::Raw => cmd_generate_dialect_raw(
name,
actions_dir,
nodes_dir,
&require_output_dir("raw")?,
macro_style_val,
),
OutputType::Full => cmd_generate_dialect_full(
name,
actions_dir,
nodes_dir,
&require_output_dir("full")?,
macro_style_val,
),
OutputType::RuntimeOnly => cmd_generate_runtime(&require_output_dir("runtime-only")?),
}
}
pub(crate) fn dispatch_tool(cmd: ToolCommand) -> Result<(), String> {
match cmd {
ToolCommand::Lemon { args } => syntaqlite_buildtools::run_lemon(&args),
ToolCommand::Mkkeyword { args } => syntaqlite_buildtools::run_mkkeyword(&args),
}
}
fn cmd_generate_dialect(
dialect: &str,
actions_dir: Option<&str>,
nodes_dir: Option<&str>,
output_dir: &str,
runtime_header: &str,
ext_header: &str,
macro_style: MacroStyle,
) -> Result<(), String> {
use syntaqlite_buildtools::amalgamate;
let temp_dir = tempfile::TempDir::new().map_err(|e| format!("creating temp directory: {e}"))?;
let temp = temp_dir.path();
let (merged_y, merged_synq) = load_extensions(actions_dir, nodes_dir)?;
codegen_to_dir_with_base(&merged_y, &merged_synq, temp, dialect, macro_style)?;
let out = Path::new(output_dir);
ensure_dir(out, "output dir")?;
let result =
amalgamate::amalgamate_dialect(dialect, temp, Some(runtime_header), Some(ext_header))?;
write_file(&out.join(format!("syntaqlite_{dialect}.h")), &result.header)?;
write_file(&out.join(format!("syntaqlite_{dialect}.c")), &result.source)?;
eprintln!("wrote {}/syntaqlite_{dialect}.{{h,c}}", out.display());
Ok(())
}
fn cmd_generate_dialect_full(
dialect: &str,
actions_dir: Option<&str>,
nodes_dir: Option<&str>,
output_dir: &str,
macro_style: MacroStyle,
) -> Result<(), String> {
use syntaqlite_buildtools::amalgamate;
let runtime_temp =
tempfile::TempDir::new().map_err(|e| format!("creating runtime temp directory: {e}"))?;
syntaqlite_buildtools::base_files::write_runtime_headers_to_dir(runtime_temp.path())
.map_err(|e| format!("writing runtime headers: {e}"))?;
let dialect_temp =
tempfile::TempDir::new().map_err(|e| format!("creating dialect temp directory: {e}"))?;
let (merged_y, merged_synq) = load_extensions(actions_dir, nodes_dir)?;
codegen_to_dir_with_base(
&merged_y,
&merged_synq,
dialect_temp.path(),
dialect,
macro_style,
)?;
let out = Path::new(output_dir);
ensure_dir(out, "output dir")?;
let result = amalgamate::amalgamate_full(dialect, runtime_temp.path(), dialect_temp.path())?;
write_file(&out.join(format!("syntaqlite_{dialect}.h")), &result.header)?;
write_file(&out.join(format!("syntaqlite_{dialect}.c")), &result.source)?;
eprintln!(
"wrote {}/syntaqlite_{dialect}.{{h,c}} (full)",
out.display()
);
Ok(())
}
fn cmd_generate_runtime(output_dir: &str) -> Result<(), String> {
use syntaqlite_buildtools::amalgamate;
let temp_dir = tempfile::TempDir::new().map_err(|e| format!("creating temp directory: {e}"))?;
let temp = temp_dir.path();
syntaqlite_buildtools::base_files::write_runtime_headers_to_dir(temp)
.map_err(|e| format!("writing runtime headers: {e}"))?;
let out = Path::new(output_dir);
ensure_dir(out, "output dir")?;
let result = amalgamate::amalgamate_runtime(temp)?;
write_file(&out.join("syntaqlite_runtime.h"), &result.header)?;
write_file(&out.join("syntaqlite_runtime.c"), &result.source)?;
if let Some(ext) = &result.ext_header {
write_file(&out.join("syntaqlite_dialect.h"), ext)?;
}
eprintln!(
"wrote {}/syntaqlite_runtime.{{h,c}} + syntaqlite_dialect.h",
out.display()
);
Ok(())
}
fn cmd_generate_dialect_raw(
dialect: &str,
actions_dir: Option<&str>,
nodes_dir: Option<&str>,
output_dir: &str,
macro_style: MacroStyle,
) -> Result<(), String> {
use syntaqlite_buildtools::codegen_api::{DialectCodegenJob, DialectNaming};
use syntaqlite_buildtools::output_resolver::OutputLayout;
let (merged_y, merged_synq) = load_extensions(actions_dir, nodes_dir)?;
let dialect_spec = DialectNaming::new(dialect);
let layout = OutputLayout::for_external(
Path::new(output_dir),
dialect,
&dialect_spec.include_dir_name(),
);
DialectCodegenJob::new(&dialect_spec, &merged_y, &merged_synq)
.with_base_synq(syntaqlite_buildtools::base_files::base_synq_files())
.with_macro_style(macro_style)
.write_to(
&layout,
&|dir| ensure_dir(dir, "output directory"),
&|path, content| write_file(path, content),
)?;
eprintln!("wrote raw dialect files to {output_dir}");
Ok(())
}
type NamedFiles = Vec<(String, String)>;
fn load_extensions(
actions_dir: Option<&str>,
nodes_dir: Option<&str>,
) -> Result<(NamedFiles, NamedFiles), String> {
use syntaqlite_buildtools::base_files;
use syntaqlite_buildtools::codegen_api::read_named_files_from_dir;
let read_ext = |dir: Option<&str>, ext: &str| -> Result<NamedFiles, String> {
Ok(match dir {
Some(d) => read_named_files_from_dir(d, ext)?,
None => Vec::new(),
})
};
let merged_y =
base_files::merge_file_sets(base_files::base_y_files(), &read_ext(actions_dir, "y")?);
let merged_synq =
base_files::merge_file_sets(base_files::base_synq_files(), &read_ext(nodes_dir, "synq")?);
Ok((merged_y, merged_synq))
}
fn codegen_to_dir_with_base(
y_files: &NamedFiles,
synq_files: &NamedFiles,
temp_root: &Path,
dialect_name: &str,
macro_style: MacroStyle,
) -> Result<(), String> {
use syntaqlite_buildtools::codegen_api::{DialectCodegenJob, DialectNaming};
use syntaqlite_buildtools::output_resolver::OutputLayout;
let dialect_spec = DialectNaming::new(dialect_name);
let layout =
OutputLayout::for_amalg_temp(temp_root, dialect_name, &dialect_spec.include_dir_name());
DialectCodegenJob::new(&dialect_spec, y_files, synq_files)
.with_base_synq(syntaqlite_buildtools::base_files::base_synq_files())
.with_macro_style(macro_style)
.write_to(
&layout,
&|dir| ensure_dir(dir, "output directory"),
&|path, content| write_file(path, content),
)
}