use clap::{CommandFactory, Parser as ClapParser, Subcommand};
use clap_complete::{Shell, generate};
use std::ffi::OsString;
use std::io;
use std::path::{Path, PathBuf};
use std::process;
#[derive(ClapParser)]
#[command(name = "seqc")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Seq compiler - compile .seq programs to executables", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Build {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
keep_ir: bool,
#[arg(long = "ffi-manifest", value_name = "PATH")]
ffi_manifests: Vec<PathBuf>,
#[arg(long)]
pure_inline: bool,
},
Lint {
#[arg(required = true)]
paths: Vec<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
errors_only: bool,
#[arg(long)]
deny_warnings: bool,
},
Completions {
#[arg(value_enum)]
shell: Shell,
},
Test {
#[arg(default_value = ".")]
paths: Vec<PathBuf>,
#[arg(short, long)]
filter: Option<String>,
#[arg(short, long)]
verbose: bool,
},
Venv {
name: PathBuf,
},
}
fn main() {
let args: Vec<OsString> = std::env::args_os().collect();
if args.len() >= 2 {
let first_arg = args[1].to_string_lossy();
if first_arg.ends_with(".seq") && !first_arg.starts_with('-') {
let source_path = PathBuf::from(&args[1]);
let script_args = &args[2..];
match seqc::script::run_script(&source_path, script_args) {
Ok(never) => match never {},
Err(e) => {
eprintln!("Error: {}", e);
process::exit(1);
}
}
}
}
let cli = Cli::parse();
match cli.command {
Commands::Build {
input,
output,
keep_ir,
ffi_manifests,
pure_inline,
} => {
let output = output.unwrap_or_else(|| {
let stem = input.file_stem().unwrap_or_default();
PathBuf::from(stem)
});
run_build(&input, &output, keep_ir, &ffi_manifests, pure_inline);
}
Commands::Lint {
paths,
config,
errors_only,
deny_warnings,
} => {
run_lint(&paths, config.as_deref(), errors_only, deny_warnings);
}
Commands::Completions { shell } => {
run_completions(shell);
}
Commands::Test {
paths,
filter,
verbose,
} => {
run_test(&paths, filter, verbose);
}
Commands::Venv { name } => {
run_venv(&name);
}
}
}
fn run_completions(shell: Shell) {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "seqc", &mut io::stdout());
}
fn run_build(
input: &Path,
output: &Path,
keep_ir: bool,
ffi_manifests: &[PathBuf],
pure_inline: bool,
) {
let mut config = if ffi_manifests.is_empty() {
seqc::CompilerConfig::default()
} else {
seqc::CompilerConfig::new().with_ffi_manifests(ffi_manifests.iter().cloned())
};
config.pure_inline_test = pure_inline;
match seqc::compile_file_with_config(input, output, keep_ir, &config) {
Ok(_) => {
println!("Compiled {} -> {}", input.display(), output.display());
if keep_ir {
let ir_path = output.with_extension("ll");
if ir_path.exists() {
println!("IR saved to {}", ir_path.display());
}
}
}
Err(e) => {
eprintln!("Error: {}", e);
process::exit(1);
}
}
}
fn run_lint(
paths: &[PathBuf],
config_path: Option<&std::path::Path>,
errors_only: bool,
deny_warnings: bool,
) {
use seqc::lint;
use std::fs;
let config = match config_path {
Some(path) => {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading lint config: {}", e);
process::exit(1);
}
};
match lint::LintConfig::from_toml(&content) {
Ok(user_config) => {
let mut default = match lint::LintConfig::default_config() {
Ok(d) => d,
Err(e) => {
eprintln!("Error loading default lint config: {}", e);
process::exit(1);
}
};
default.merge(user_config);
default
}
Err(e) => {
eprintln!("Error parsing lint config: {}", e);
process::exit(1);
}
}
}
None => match lint::LintConfig::default_config() {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading default lint config: {}", e);
process::exit(1);
}
},
};
let linter = match lint::Linter::new(&config) {
Ok(l) => l,
Err(e) => {
eprintln!("Error creating linter: {}", e);
process::exit(1);
}
};
let mut all_diagnostics = Vec::new();
let mut files_checked = 0;
for path in paths {
if path.is_dir() {
for entry in walkdir(path) {
if entry.extension().is_some_and(|e| e == "seq") {
if let Some(parent) = entry.parent() {
let has_manifest = std::fs::read_dir(parent)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
})
.unwrap_or(false);
if has_manifest {
continue;
}
}
lint_file(&entry, &linter, &mut all_diagnostics);
files_checked += 1;
}
}
} else if path.exists() {
lint_file(path, &linter, &mut all_diagnostics);
files_checked += 1;
} else {
eprintln!("Warning: {} does not exist", path.display());
}
}
if errors_only {
all_diagnostics.retain(|d| d.severity == lint::Severity::Error);
}
if all_diagnostics.is_empty() {
println!("No lint issues found in {} file(s)", files_checked);
} else {
print!("{}", lint::format_diagnostics(&all_diagnostics));
let files_with_issues: std::collections::HashSet<_> =
all_diagnostics.iter().map(|d| &d.file).collect();
println!(
"\n{} issue(s) in {} file(s) ({} file(s) checked)",
all_diagnostics.len(),
files_with_issues.len(),
files_checked
);
let has_errors = all_diagnostics
.iter()
.any(|d| d.severity == lint::Severity::Error);
if has_errors || (deny_warnings && !all_diagnostics.is_empty()) {
process::exit(1);
}
}
}
fn lint_file(path: &PathBuf, linter: &seqc::Linter, diagnostics: &mut Vec<seqc::LintDiagnostic>) {
use seqc::{
Parser, ProgramResourceAnalyzer, TypeChecker, call_graph, lint, resolver::Resolver,
};
use std::fs;
let source = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading {}: {}", path.display(), e);
return;
}
};
let mut parser = Parser::new(&source);
let mut program = match parser.parse() {
Ok(p) => p,
Err(e) => {
eprintln!("Parse error in {}: {}", path.display(), e);
return;
}
};
if let Err(e) = program.generate_constructors() {
eprintln!("Constructor error in {}: {}", path.display(), e);
return;
}
let file_diagnostics = linter.lint_program(&program, path);
diagnostics.extend(file_diagnostics);
let mut resource_analyzer = ProgramResourceAnalyzer::new(path);
let resource_diagnostics = resource_analyzer.analyze_program(&program);
diagnostics.extend(resource_diagnostics);
let mut resolver = Resolver::new(None);
let mut resolved = match resolver.resolve(path, program) {
Ok(r) => r,
Err(e) => {
eprintln!("Include resolution error in {}: {}", path.display(), e);
return;
}
};
if !resolved.ffi_includes.is_empty() {
return;
}
if let Err(e) = resolved.program.generate_constructors() {
eprintln!("Constructor error in {}: {}", path.display(), e);
return;
}
let call_graph = call_graph::CallGraph::build(&resolved.program);
let mut type_checker = TypeChecker::new();
type_checker.set_call_graph(call_graph);
if let Err(e) = type_checker.check_program(&resolved.program) {
diagnostics.push(lint::LintDiagnostic {
id: "type-error".to_string(),
severity: lint::Severity::Error,
message: e,
file: path.clone(),
line: 0, start_column: None,
end_line: None,
end_column: None,
word_name: String::new(),
start_index: 0,
end_index: 0,
replacement: String::new(),
});
}
}
fn walkdir(dir: &Path) -> Vec<PathBuf> {
use std::fs;
let mut files = Vec::new();
match fs::read_dir(dir) {
Ok(entries) => {
for entry in entries {
match entry {
Ok(entry) => {
let path = entry.path();
if path.is_dir() {
files.extend(walkdir(&path));
} else {
files.push(path);
}
}
Err(e) => {
eprintln!(
"Warning: Could not read directory entry in {}: {}",
dir.display(),
e
);
}
}
}
}
Err(e) => {
eprintln!("Warning: Could not read directory {}: {}", dir.display(), e);
}
}
files
}
fn run_test(paths: &[PathBuf], filter: Option<String>, verbose: bool) {
use seqc::test_runner::TestRunner;
let runner = TestRunner::new(verbose, filter);
let summary = runner.run(paths);
runner.print_results(&summary);
if summary.has_failures() {
process::exit(1);
} else if summary.total == 0 && summary.compile_failures == 0 {
eprintln!("No tests found");
process::exit(2);
}
}
fn run_venv(name: &Path) {
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
fn cleanup_and_exit(venv_path: &Path, msg: &str) -> ! {
eprintln!("{}", msg);
if let Err(e) = std::fs::remove_dir_all(venv_path) {
eprintln!("Warning: failed to cleanup {}: {}", venv_path.display(), e);
}
process::exit(1);
}
let venv_path: PathBuf = if name.is_absolute() {
name.components().collect()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(name)
.components()
.collect()
};
if venv_path.exists() {
eprintln!("Error: {} already exists", venv_path.display());
process::exit(1);
}
let bin_dir = venv_path.join("bin");
if let Err(e) = fs::create_dir_all(&bin_dir) {
eprintln!("Error creating directory {}: {}", bin_dir.display(), e);
process::exit(1);
}
let current_exe = match std::env::current_exe() {
Ok(path) => path,
Err(e) => {
cleanup_and_exit(
&venv_path,
&format!("Error finding current executable: {}", e),
);
}
};
let exe_dir = match current_exe.parent() {
Some(dir) => dir,
None => {
cleanup_and_exit(
&venv_path,
"Error: could not determine executable directory",
);
}
};
let binaries = ["seqc", "seqr", "seq-lsp"];
let mut copied_count = 0;
for binary in binaries {
let src = exe_dir.join(binary);
let dst = bin_dir.join(binary);
if !src.exists() {
eprintln!("Warning: {} not found, skipping", src.display());
continue;
}
if let Err(e) = fs::copy(&src, &dst) {
cleanup_and_exit(&venv_path, &format!("Error copying {}: {}", binary, e));
}
#[cfg(unix)]
if let Err(e) = fs::set_permissions(&dst, fs::Permissions::from_mode(0o755)) {
eprintln!("Warning: could not set permissions on {}: {}", binary, e);
}
println!(" Copied {}", binary);
copied_count += 1;
}
if copied_count == 0 {
cleanup_and_exit(
&venv_path,
&format!("Error: no seq binaries found in {}", exe_dir.display()),
);
}
let venv_name = venv_path
.components()
.next_back()
.and_then(|c| c.as_os_str().to_str())
.unwrap_or("seq-venv");
if let Err(e) = generate_activate_bash(&venv_path, venv_name) {
cleanup_and_exit(
&venv_path,
&format!("Error generating activate script: {}", e),
);
}
if let Err(e) = generate_activate_fish(&venv_path, venv_name) {
cleanup_and_exit(
&venv_path,
&format!("Error generating activate.fish script: {}", e),
);
}
if let Err(e) = generate_activate_csh(&venv_path, venv_name) {
cleanup_and_exit(
&venv_path,
&format!("Error generating activate.csh script: {}", e),
);
}
println!("\nCreated virtual environment at {}", venv_path.display());
println!("\nTo activate, run:");
println!(" source {}/bin/activate", venv_path.display());
}
fn generate_activate_bash(venv_path: &Path, venv_name: &str) -> std::io::Result<()> {
use std::fs;
let script = format!(
r#"# This file must be sourced with "source activate" from bash/zsh.
# It cannot be run directly.
deactivate () {{
# Reset PATH
if [ -n "${{_OLD_VIRTUAL_PATH:-}}" ]; then
PATH="${{_OLD_VIRTUAL_PATH}}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
# Reset prompt
if [ -n "${{_OLD_VIRTUAL_PS1:-}}" ]; then
PS1="${{_OLD_VIRTUAL_PS1}}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset SEQ_VIRTUAL_ENV
if [ ! "${{1:-}}" = "nondestructive" ]; then
unset -f deactivate
fi
}}
# Unset irrelevant variables
deactivate nondestructive
SEQ_VIRTUAL_ENV="{venv_path}"
export SEQ_VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$SEQ_VIRTUAL_ENV/bin:$PATH"
export PATH
_OLD_VIRTUAL_PS1="${{PS1:-}}"
PS1="({venv_name}) ${{PS1:-}}"
export PS1
"#,
venv_path = venv_path.display(),
venv_name = venv_name
);
fs::write(venv_path.join("bin").join("activate"), script)?;
println!(" Generated bin/activate");
Ok(())
}
fn generate_activate_fish(venv_path: &Path, venv_name: &str) -> std::io::Result<()> {
use std::fs;
let script = format!(
r#"# This file must be sourced with "source activate.fish" from fish.
function deactivate -d "Exit virtual environment"
# Reset PATH
if set -q _OLD_VIRTUAL_PATH
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
# Reset prompt
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
set -e SEQ_VIRTUAL_ENV
if test "$argv[1]" != "nondestructive"
functions -e deactivate
end
end
# Unset irrelevant variables
deactivate nondestructive
set -gx SEQ_VIRTUAL_ENV "{venv_path}"
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$SEQ_VIRTUAL_ENV/bin" $PATH
# Save current prompt
if functions -q fish_prompt
functions -c fish_prompt _old_fish_prompt
end
function fish_prompt
printf "({venv_name}) "
_old_fish_prompt
end
"#,
venv_path = venv_path.display(),
venv_name = venv_name
);
fs::write(venv_path.join("bin").join("activate.fish"), script)?;
println!(" Generated bin/activate.fish");
Ok(())
}
fn generate_activate_csh(venv_path: &Path, venv_name: &str) -> std::io::Result<()> {
use std::fs;
let script = format!(
r#"# This file must be sourced with "source activate.csh" from csh/tcsh.
alias deactivate 'if ($?_OLD_VIRTUAL_PATH) then; setenv PATH "$_OLD_VIRTUAL_PATH"; unsetenv _OLD_VIRTUAL_PATH; endif; unsetenv SEQ_VIRTUAL_ENV; unalias deactivate'
setenv SEQ_VIRTUAL_ENV "{venv_path}"
setenv _OLD_VIRTUAL_PATH "$PATH"
setenv PATH "$SEQ_VIRTUAL_ENV/bin:$PATH"
set prompt = "({venv_name}) $prompt"
"#,
venv_path = venv_path.display(),
venv_name = venv_name
);
fs::write(venv_path.join("bin").join("activate.csh"), script)?;
println!(" Generated bin/activate.csh");
Ok(())
}