use anyhow::Result;
use clap::{Parser, Subcommand};
use colored::Colorize;
use pascal::build_system::{BuildSystem, Manifest};
use pascal::ppu::PpuFile;
use pascal::ParallelConfig;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "pascal")]
#[command(about = "Pascal - A production-ready optimizing Pascal compiler and package manager")]
#[command(version = "0.1.2")]
#[command(author = "Pascal Team")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init {
name: String,
#[arg(short, long, default_value = ".")]
dir: PathBuf,
#[arg(short, long, default_value = "default")]
template: String,
},
Build {
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
quiet: bool,
#[arg(short = 'O', long)]
optimization: Option<u8>,
},
Add {
name: String,
#[arg(short = 'V', long)]
version: Option<String>,
#[arg(long)]
path: Option<String>,
#[arg(long)]
git: Option<String>,
},
Remove {
name: String,
},
Compile {
input: PathBuf,
#[arg(short, long, default_value = ".")]
output: PathBuf,
#[arg(short = 'I', long = "include")]
search_paths: Vec<PathBuf>,
#[arg(short = 'O', long, default_value = "0")]
optimization: u8,
#[arg(short, long)]
debug: bool,
#[arg(long)]
no_ppu: bool,
#[arg(long)]
no_cache: bool,
#[arg(short = 'S', long)]
asm: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
quiet: bool,
#[arg(short = 'j', long)]
parallel: bool,
#[arg(long, default_value = "0")]
threads: usize,
#[arg(short = 'D', long = "define", value_name = "SYMBOL")]
defines: Vec<String>,
},
Info {
ppu_file: PathBuf,
},
Run {
input: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
quiet: bool,
#[arg(short, long)]
watch: bool,
#[arg(long)]
profile: bool,
#[arg(long, default_value = "flamegraph.svg")]
profile_output: PathBuf,
},
Debug {
input: PathBuf,
#[arg(short, long)]
breakpoint: Vec<String>,
#[arg(short, long)]
watch: Vec<String>,
#[arg(short, long)]
verbose: bool,
},
Fmt {
#[arg(default_value = ".")]
path: Vec<PathBuf>,
#[arg(long)]
check: bool,
#[arg(long)]
init_config: bool,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long, default_value = "true")]
recursive: bool,
#[arg(long, conflicts_with_all = ["tabs", "indent_4"])]
indent_2: bool,
#[arg(long, conflicts_with_all = ["tabs", "indent_2"])]
indent_4: bool,
#[arg(long, conflicts_with_all = ["indent_2", "indent_4"])]
tabs: bool,
#[arg(long)]
max_line_length: Option<usize>,
#[arg(short, long)]
verbose: bool,
},
Check {
input: PathBuf,
},
Doc {
#[arg(default_value = ".")]
path: Vec<PathBuf>,
#[arg(short, long, default_value = "markdown")]
format: String,
#[arg(short, long, default_value = "docs")]
output: PathBuf,
#[arg(long, default_value = "true")]
recursive: bool,
},
Clean {
#[arg(default_value = ".")]
directory: PathBuf,
},
}
fn open_project(verbose: bool) -> Result<BuildSystem> {
let cwd = std::env::current_dir()?;
let manifest_path = Manifest::find(&cwd).ok_or_else(|| {
anyhow::anyhow!(
"No pascal.toml found in {} or any parent directory.\n\
Run `pascal init <name>` to create a new project.",
cwd.display()
)
})?;
let project_root = manifest_path.parent().unwrap();
BuildSystem::open(project_root, verbose)
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Init { name, dir, template } => BuildSystem::init_with_template(&dir, &name, &template),
Commands::Build { verbose, quiet, optimization } => {
let mut bs = open_project(verbose && !quiet)?;
if let Some(opt) = optimization {
bs.manifest_mut().build.optimization = opt;
}
bs.build(quiet)
}
Commands::Add {
name,
version,
path,
git,
} => {
let mut bs = open_project(false)?;
bs.add_dependency(&name, version.as_deref(), path.as_deref(), git.as_deref())
}
Commands::Remove { name } => {
let mut bs = open_project(false)?;
bs.remove_dependency(&name)
}
Commands::Compile {
input,
output,
search_paths,
optimization,
debug,
no_ppu,
no_cache,
asm,
verbose,
quiet,
parallel,
threads,
defines,
} => compile_file(
input,
output,
search_paths,
optimization,
debug,
!no_ppu,
!no_cache,
asm,
verbose && !quiet,
quiet,
parallel,
threads,
defines,
),
Commands::Info { ppu_file } => show_ppu_info(ppu_file),
Commands::Run {
input,
verbose,
quiet,
watch,
profile,
profile_output,
} => {
if watch {
if let Some(file) = input {
run_file_watch(file, verbose && !quiet)
} else {
run_project_watch(verbose && !quiet)?;
Ok(())
}
} else if let Some(file) = input {
run_file(file, verbose && !quiet, profile.then(|| profile_output))
} else {
open_project(verbose && !quiet)?.run(quiet, profile.then(|| profile_output))
}
}
Commands::Debug {
input,
breakpoint,
watch,
verbose,
} => run_debug(input, breakpoint, watch, verbose),
Commands::Fmt { path, check, .. } => fmt_files(path, check),
Commands::Check { input } => check_file(input),
Commands::Doc {
path,
format,
output,
recursive,
} => doc_files(path, format, output, recursive),
Commands::Clean { directory } => clean_directory(directory),
}
}
fn compile_file(
input: PathBuf,
output: PathBuf,
_search_paths: Vec<PathBuf>,
optimization: u8,
_debug: bool,
_generate_ppu: bool,
_use_ppu: bool,
generate_asm: bool,
verbose: bool,
quiet: bool,
parallel: bool,
threads: usize,
defines: Vec<String>,
) -> Result<()> {
if !input.exists() {
eprintln!(
"{} File not found: {}",
"Error:".red().bold(),
input.display()
);
std::process::exit(1);
}
if verbose {
println!("{} {}", "Compiling:".green().bold(), input.display());
println!("{}", "Configuration:".cyan().bold());
println!(" Output directory: {}", output.display());
println!(" Optimization level: {}", optimization);
if parallel {
println!(" Parallel compilation: {}", "enabled".green());
if threads > 0 {
println!(" Threads: {}", threads);
} else {
println!(" Threads: {} (auto-detected)", num_cpus::get());
}
}
}
if parallel {
let config = ParallelConfig::new()
.with_threads(threads)
.with_parallel_modules(true)
.with_parallel_optimization(true);
if let Err(e) = config.init_thread_pool() {
eprintln!(
"{} Failed to initialize thread pool: {}",
"Error:".red().bold(),
e
);
std::process::exit(1);
}
if verbose {
println!("{} Thread pool initialized", "Info:".cyan().bold());
}
}
let raw_source = std::fs::read_to_string(&input)?;
if verbose {
println!("{} Read {} bytes", "Info:".cyan().bold(), raw_source.len());
}
let defines_set: std::collections::HashSet<String> =
defines.into_iter().map(|s| s.split('=').next().unwrap_or(&s).to_uppercase()).collect();
let source = pascal::preprocess(&raw_source, &defines_set);
if verbose && !defines_set.is_empty() {
println!("{} Defines: {:?}", "Info:".cyan().bold(), defines_set);
}
let mut parser = pascal::parser::Parser::new(&source);
let program = match parser.parse_program() {
Ok(prog) => {
if verbose {
println!("{} Parsed program '{}'", "Info:".cyan().bold(), prog.name);
println!(" Uses: {:?}", prog.uses);
println!(" Statements: {}", prog.block.statements.len());
println!(" Variables: {}", prog.block.vars.len());
println!(" Constants: {}", prog.block.consts.len());
}
prog
}
Err(e) => {
eprintln!("{} {}", "Parse error:".red().bold(), e);
if !parser.errors().is_empty() {
for err in parser.errors() {
eprintln!(" {}", err);
}
}
std::process::exit(1);
}
};
if optimization > 0 && verbose {
println!(
"{} Optimization level {}",
"Info:".cyan().bold(),
optimization
);
}
if generate_asm {
let stem = input.file_stem().unwrap().to_str().unwrap();
let asm_path = output.join(format!("{}.asm", stem));
let unit = pascal::ast::Unit {
name: program.name.clone(),
uses: program.uses.clone(),
interface: pascal::ast::UnitInterface {
uses: vec![],
types: vec![],
constants: vec![],
variables: vec![],
procedures: vec![],
functions: vec![],
classes: vec![],
interfaces: vec![],
},
implementation: pascal::ast::UnitImplementation {
uses: vec![],
types: program.block.types.clone(),
constants: program.block.consts.clone(),
variables: program.block.vars.clone(),
procedures: program.block.procedures.clone(),
functions: program.block.functions.clone(),
classes: vec![],
interfaces: vec![],
initialization: Some(program.block.statements.clone()),
finalization: None,
},
};
let mut codegen = pascal::UnitCodeGenerator::new();
let asm_output = codegen.generate_unit(&unit)?;
std::fs::create_dir_all(&output)?;
std::fs::write(&asm_path, asm_output)?;
if !quiet {
println!(
"{} Generated assembly: {}",
"Success:".green().bold(),
asm_path.display()
);
}
}
if !quiet {
println!(
"{} Compiled '{}' successfully",
"Success:".green().bold(),
program.name
);
}
Ok(())
}
fn run_file(input: PathBuf, verbose: bool, profile_output: Option<PathBuf>) -> Result<()> {
if !input.exists() {
eprintln!(
"{} File not found: {}",
"Error:".red().bold(),
input.display()
);
std::process::exit(1);
}
let source = std::fs::read_to_string(&input)?;
if verbose {
println!("{} {}", "Running:".green().bold(), input.display());
}
let mut parser = pascal::parser::Parser::new(&source);
let program = match parser.parse_program() {
Ok(prog) => prog,
Err(e) => {
eprintln!("{} {}", "Parse error:".red().bold(), e);
for err in parser.errors() {
eprintln!(" {}", err);
}
std::process::exit(1);
}
};
if verbose {
println!(
"{} Parsed program '{}'",
"Info:".cyan().bold(),
program.name
);
}
run_program_impl(&program, verbose, profile_output)
}
fn run_file_watch(input: PathBuf, verbose: bool) -> Result<()> {
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::sync::mpsc;
use std::time::Duration;
if !input.exists() {
eprintln!("{} File not found: {}", "Error:".red().bold(), input.display());
std::process::exit(1);
}
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default().with_poll_interval(Duration::from_secs(1)))?;
let watch_path = input.canonicalize().unwrap_or_else(|_| input.clone());
watcher.watch(&watch_path, RecursiveMode::NonRecursive)?;
if !verbose {
println!("{} Watching {} (Ctrl+C to stop)", "Info:".cyan().bold(), input.display());
}
loop {
run_file(input.clone(), verbose, None)?;
if verbose {
println!("{} Waiting for changes...", "Info:".cyan().bold());
}
match rx.recv() {
Ok(Ok(_)) => {
if verbose {
println!("{} Change detected, re-running...", "Info:".cyan().bold());
}
}
Err(_) => return Ok(()),
_ => {}
}
}
}
fn run_project_watch(verbose: bool) -> Result<()> {
let bs = open_project(verbose)?;
let manifest = bs.manifest();
let main_path = manifest
.package
.main
.as_ref()
.map(|m| bs.project_root().join(&manifest.package.src).join(m))
.ok_or_else(|| anyhow::anyhow!("No main file in pascal.toml"))?;
run_file_watch(main_path, verbose)?;
Ok(())
}
fn run_debug(
input: PathBuf,
breakpoints: Vec<String>,
watch_vars: Vec<String>,
verbose: bool,
) -> Result<()> {
if !input.exists() {
eprintln!("{} File not found: {}", "Error:".red().bold(), input.display());
std::process::exit(1);
}
let source = std::fs::read_to_string(&input)?;
let mut parser = pascal::parser::Parser::new(&source);
let program = match parser.parse_program() {
Ok(p) => p,
Err(e) => {
eprintln!("{} {}", "Parse error:".red().bold(), e);
std::process::exit(1);
}
};
let mut session = pascal::debugger::DebugSession::new();
for b in &breakpoints {
session.add_breakpoint(b);
}
for w in &watch_vars {
session.add_watch(w);
}
if verbose {
println!(
"{} Debugging '{}' (breakpoints: {:?}, watch: {:?})",
"Info:".cyan().bold(),
input.display(),
breakpoints,
watch_vars
);
}
pascal::debugger::run_with_debugger(&program, &mut session, verbose)
}
fn run_program_impl(
program: &pascal::ast::Program,
verbose: bool,
profile_output: Option<PathBuf>,
) -> Result<()> {
let run = || {
let mut interpreter = pascal::interpreter::Interpreter::new(verbose);
match interpreter.run_program(program) {
Ok(()) => {
if verbose {
println!("\n{} Program finished", "Info:".cyan().bold());
}
}
Err(e) => {
eprintln!("\n{} {}", "Runtime error:".red().bold(), e);
std::process::exit(1);
}
}
};
if let Some(ref out) = profile_output {
#[cfg(feature = "profile")]
{
pascal::profile::run_profiled(out, run)?;
if verbose {
println!(
"{} Profile written to {}",
"Info:".cyan().bold(),
out.display()
);
}
}
#[cfg(not(feature = "profile"))]
{
eprintln!(
"{} Use `cargo build --features profile` to enable profiling",
"Warning:".yellow().bold()
);
run();
}
} else {
run();
}
Ok(())
}
fn fmt_files(paths: Vec<PathBuf>, check_only: bool) -> Result<()> {
use std::fs;
use std::io::Write;
fn format_pascal(source: &str) -> String {
let lines: Vec<&str> = source.lines().collect();
let mut out = String::with_capacity(source.len());
let mut prev_blank = false;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim_end();
let is_blank = trimmed.is_empty();
if is_blank {
if !prev_blank {
out.push('\n');
}
prev_blank = true;
} else {
prev_blank = false;
out.push_str(trimmed);
if i + 1 < lines.len() {
out.push('\n');
}
}
}
if !out.ends_with('\n') && !out.is_empty() {
out.push('\n');
}
out
}
let mut formatted = 0;
for path in paths {
if path.is_dir() {
for entry in fs::read_dir(&path)? {
let entry = entry?;
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) == Some("pas") {
let source = fs::read_to_string(&p)?;
let formatted_source = format_pascal(&source);
if source != formatted_source {
formatted += 1;
if !check_only {
let mut f = fs::File::create(&p)?;
f.write_all(formatted_source.as_bytes())?;
println!(" Formatted: {}", p.display());
} else {
println!(" Would format: {}", p.display());
}
}
}
}
} else if path.extension().and_then(|s| s.to_str()) == Some("pas") {
let source = fs::read_to_string(&path)?;
let formatted_source = format_pascal(&source);
if source != formatted_source {
formatted += 1;
if !check_only {
let mut f = fs::File::create(&path)?;
f.write_all(formatted_source.as_bytes())?;
println!(" Formatted: {}", path.display());
} else {
println!(" Would format: {}", path.display());
}
}
}
}
if check_only && formatted > 0 {
println!(
"{} {} file(s) would be reformatted",
"Warning:".yellow().bold(),
formatted
);
std::process::exit(1);
} else if formatted > 0 {
println!(
"{} Formatted {} file(s)",
"Success:".green().bold(),
formatted
);
}
Ok(())
}
fn doc_files(
paths: Vec<PathBuf>,
format_str: String,
output_dir: PathBuf,
recursive: bool,
) -> Result<()> {
use std::fs;
use std::io::Write;
let format = format_str
.parse::<pascal::DocFormat>()
.map_err(|e| anyhow::anyhow!("{}", e))?;
let ext = match format {
pascal::DocFormat::Markdown => "md",
pascal::DocFormat::Html => "html",
};
std::fs::create_dir_all(&output_dir)?;
let mut collected: Vec<PathBuf> = vec![];
for path in &paths {
if path.is_dir() {
fn collect_pas(dir: &std::path::Path, out: &mut Vec<PathBuf>, recursive: bool) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() && recursive {
collect_pas(&p, out, recursive);
} else if p.extension().and_then(|s| s.to_str()) == Some("pas") {
out.push(p);
}
}
}
}
collect_pas(path, &mut collected, recursive);
} else if path.extension().and_then(|s| s.to_str()) == Some("pas") {
collected.push(path.clone());
}
}
let mut generated = 0;
for input in collected {
let source = match fs::read_to_string(&input) {
Ok(s) => s,
Err(e) => {
eprintln!("{} {}: {}", "Warning:".yellow().bold(), input.display(), e);
continue;
}
};
match pascal::generate_docs_from_source(&source, &input, format) {
Ok(docs) => {
let stem = input.file_stem().and_then(|s| s.to_str()).unwrap_or("doc");
let out_path = output_dir.join(format!("{}.{}", stem, ext));
if let Err(e) = fs::File::create(&out_path).and_then(|mut f| f.write_all(docs.as_bytes())) {
eprintln!("{} Failed to write {}: {}", "Error:".red().bold(), out_path.display(), e);
} else {
println!(" Generated: {} -> {}", input.display(), out_path.display());
generated += 1;
}
}
Err(e) => {
eprintln!("{} {}: {}", "Warning:".yellow().bold(), input.display(), e);
}
}
}
println!(
"{} Generated documentation for {} file(s) in {}",
"Success:".green().bold(),
generated,
output_dir.display()
);
Ok(())
}
fn check_file(input: PathBuf) -> Result<()> {
if !input.exists() {
eprintln!(
"{} File not found: {}",
"Error:".red().bold(),
input.display()
);
std::process::exit(1);
}
let source = std::fs::read_to_string(&input)?;
let mut parser = pascal::parser::Parser::new(&source);
match parser.parse_program() {
Ok(program) => {
println!(
"{} Check passed for '{}' (parse OK)",
"Success:".green().bold(),
program.name
);
Ok(())
}
Err(e) => {
eprintln!("{} {}", "Parse error:".red().bold(), e);
for err in parser.errors() {
eprintln!(" {}", err);
}
std::process::exit(1);
}
}
}
fn show_ppu_info(ppu_file: PathBuf) -> Result<()> {
if !ppu_file.exists() {
eprintln!(
"{} File not found: {}",
"Error:".red().bold(),
ppu_file.display()
);
std::process::exit(1);
}
println!("{} {}", "PPU File:".cyan().bold(), ppu_file.display());
match PpuFile::load(&ppu_file) {
Ok(ppu) => {
println!("{}", "Version:".green().bold());
println!(" Version: {}", ppu.version);
println!("{}", "\nModule:".green().bold());
println!(" Name: {}", ppu.module.name);
println!(" Dependencies: {:?}", ppu.module.dependencies);
println!("{}", "\nUnit:".green().bold());
println!(" Name: {}", ppu.module.unit.name);
println!("{}", "\nInterface:".green().bold());
println!(" Uses: {:?}", ppu.module.unit.interface.uses);
println!(" Types: {}", ppu.module.unit.interface.types.len());
println!(" Constants: {}", ppu.module.unit.interface.constants.len());
println!(" Variables: {}", ppu.module.unit.interface.variables.len());
println!(" Functions: {}", ppu.module.unit.interface.functions.len());
println!(
" Procedures: {}",
ppu.module.unit.interface.procedures.len()
);
println!("{}", "\nImplementation:".green().bold());
println!(
" Functions: {}",
ppu.module.unit.implementation.functions.len()
);
println!(
" Procedures: {}",
ppu.module.unit.implementation.procedures.len()
);
println!(
" Has initialization: {}",
ppu.module.unit.implementation.initialization.is_some()
);
println!(
" Has finalization: {}",
ppu.module.unit.implementation.finalization.is_some()
);
Ok(())
}
Err(e) => {
eprintln!("{} {}", "Error reading PPU:".red().bold(), e);
std::process::exit(1);
}
}
}
fn clean_directory(directory: PathBuf) -> Result<()> {
use std::fs;
if !directory.exists() {
eprintln!(
"{} Directory not found: {}",
"Error:".red().bold(),
directory.display()
);
std::process::exit(1);
}
println!("{} {}", "Cleaning:".cyan().bold(), directory.display());
let mut count = 0;
for entry in fs::read_dir(&directory)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("ppu") {
if let Err(e) = fs::remove_file(&path) {
eprintln!(
"{} Failed to remove {}: {}",
"Warning:".yellow().bold(),
path.display(),
e
);
} else {
println!(" Removed: {}", path.display());
count += 1;
}
}
}
println!("{} Removed {} PPU file(s)", "Done:".green().bold(), count);
Ok(())
}