pub mod ast;
pub mod codegen;
pub mod errors;
pub mod lexer;
pub mod parser;
pub mod project;
pub mod resources;
pub mod stdlib;
pub mod types;
pub use project::{compile_project, Project, ProjectError};
pub use types::PreferredOverride;
use errors::Diagnostic;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct CompileOptions {
pub output_dir: std::path::PathBuf,
pub target: String,
pub format: OutputFormat,
pub dry_run: bool,
pub debug_ast: bool,
pub debug_tokens: bool,
pub no_hcl: bool,
}
impl Default for CompileOptions {
fn default() -> Self {
Self {
output_dir: std::path::PathBuf::from("."),
target: "production".to_string(),
format: OutputFormat::Hcl,
dry_run: false,
debug_ast: false,
debug_tokens: false,
no_hcl: false,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OutputFormat {
#[default]
Hcl,
Json,
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"hcl" | "tf" => Ok(OutputFormat::Hcl),
"json" => Ok(OutputFormat::Json),
_ => Err(format!("unknown format: {} (expected 'hcl' or 'json')", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnsafeBlockInfo {
pub file: String,
pub line: usize,
pub column: usize,
pub reason: String,
}
pub fn compile_file(path: &Path, options: &CompileOptions) -> Result<String, Vec<Diagnostic>> {
let source = std::fs::read_to_string(path)
.map_err(|e| vec![Diagnostic::error(format!("failed to read file: {}", e))])?;
let filename = path.to_string_lossy().to_string();
compile_source(&source, &filename, options)
}
pub use types::{extract_exports, GlobalSymbolTable, ModuleExports};
#[derive(Debug)]
pub struct CompileResult {
pub output: String,
pub preferred_overrides: Vec<PreferredOverride>,
pub unsafe_count: usize,
}
pub fn compile_source(
source: &str,
filename: &str,
options: &CompileOptions,
) -> Result<String, Vec<Diagnostic>> {
let result = compile_source_with_globals(source, filename, options, None)?;
Ok(result.output)
}
pub fn compile_source_with_globals(
source: &str,
filename: &str,
options: &CompileOptions,
globals: Option<&GlobalSymbolTable>,
) -> Result<CompileResult, Vec<Diagnostic>> {
let tokens = lexer::tokenize(source, filename)?;
if options.debug_tokens {
eprintln!("=== Tokens ===");
for token in &tokens {
eprintln!(" {:?}", token);
}
eprintln!();
}
let ast = parser::parse(tokens, source, filename)?;
if options.debug_ast {
eprintln!("=== AST ===");
eprintln!("{:#?}", ast);
eprintln!();
}
let type_check_result =
types::check_with_globals(&ast, source, filename, globals, options.no_hcl)?;
let output = match options.format {
OutputFormat::Hcl => codegen::emit_hcl(&type_check_result.program, &options.target),
OutputFormat::Json => codegen::emit_json(&type_check_result.program, &options.target),
};
let unsafe_count = type_check_result.program.count_unsafe_blocks();
Ok(CompileResult {
output,
preferred_overrides: type_check_result.preferred_overrides,
unsafe_count,
})
}
pub fn compile_and_extract(
source: &str,
filename: &str,
options: &CompileOptions,
globals: Option<&GlobalSymbolTable>,
) -> Result<(String, types::TypedProgram, Vec<PreferredOverride>), Vec<Diagnostic>> {
let tokens = lexer::tokenize(source, filename)?;
let ast = parser::parse(tokens, source, filename)?;
let type_check_result =
types::check_with_globals(&ast, source, filename, globals, options.no_hcl)?;
let output = match options.format {
OutputFormat::Hcl => codegen::emit_hcl(&type_check_result.program, &options.target),
OutputFormat::Json => codegen::emit_json(&type_check_result.program, &options.target),
};
Ok((
output,
type_check_result.program,
type_check_result.preferred_overrides,
))
}
pub fn check_file(path: &Path) -> Result<(), Vec<Diagnostic>> {
let source = std::fs::read_to_string(path)
.map_err(|e| vec![Diagnostic::error(format!("failed to read file: {}", e))])?;
let filename = path.to_string_lossy().to_string();
check_source(&source, &filename)
}
pub fn check_source(source: &str, filename: &str) -> Result<(), Vec<Diagnostic>> {
let tokens = lexer::tokenize(source, filename)?;
let ast = parser::parse(tokens, source, filename)?;
let _typed_ast = types::check(&ast, source, filename, false)?;
Ok(())
}
pub fn find_unsafe_blocks(path: &Path) -> Result<Vec<UnsafeBlockInfo>, String> {
let mut results = Vec::new();
if path.is_file() {
if path.extension().is_some_and(|e| e == "hk") {
results.extend(find_unsafe_in_file(path)?);
}
} else if path.is_dir() {
for entry in walkdir(path)? {
if entry.extension().is_some_and(|e| e == "le") {
results.extend(find_unsafe_in_file(&entry)?);
}
}
}
Ok(results)
}
fn find_unsafe_in_file(path: &Path) -> Result<Vec<UnsafeBlockInfo>, String> {
let source = std::fs::read_to_string(path)
.map_err(|e| format!("failed to read {}: {}", path.display(), e))?;
let filename = path.to_string_lossy().to_string();
let tokens = lexer::tokenize(&source, &filename)
.map_err(|d| format!("lex error in {}: {:?}", path.display(), d))?;
let ast = parser::parse(tokens, &source, &filename)
.map_err(|d| format!("parse error in {}: {:?}", path.display(), d))?;
Ok(ast::find_unsafe_blocks(&ast)
.into_iter()
.map(|ub| UnsafeBlockInfo {
file: filename.clone(),
line: ub.line,
column: ub.column,
reason: ub.reason,
})
.collect())
}
fn walkdir(path: &Path) -> Result<Vec<std::path::PathBuf>, String> {
let mut files = Vec::new();
fn visit(dir: &Path, files: &mut Vec<std::path::PathBuf>) -> Result<(), String> {
let entries = std::fs::read_dir(dir)
.map_err(|e| format!("failed to read directory {}: {}", dir.display(), e))?;
for entry in entries {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.starts_with('.') && name != "node_modules" && name != "target" {
visit(&path, files)?;
}
} else {
files.push(path);
}
}
Ok(())
}
visit(path, &mut files)?;
Ok(files)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compile_options_default() {
let opts = CompileOptions::default();
assert_eq!(opts.target, "production");
assert!(!opts.dry_run);
}
#[test]
fn test_output_format_parse() {
assert_eq!("hcl".parse::<OutputFormat>().unwrap(), OutputFormat::Hcl);
assert_eq!("json".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
assert!("invalid".parse::<OutputFormat>().is_err());
}
}