mod lexer;
mod parser;
mod analyzer;
mod codegen;
mod elf;
mod errors;
mod lib_file;
#[cfg(test)]
mod compile_fail_tests;
#[cfg(test)]
mod declare_create_type_coverage;
use std::env;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::Command;
use lexer::Lexer;
use parser::Parser;
use parser::ast::{Program, Statement};
use analyzer::Analyzer;
use codegen::{CodeGenerator, format_lib_name, mangle_library_symbol, render_lib_file};
fn resolve_core_env_override(
vox_core_path: Option<&str>,
ec_core_path: Option<&str>,
) -> (Option<PathBuf>, bool) {
let vox = vox_core_path.filter(|s| !s.is_empty());
let ec = ec_core_path.filter(|s| !s.is_empty());
let deprecated_only = vox.is_none() && ec.is_some();
(vox.or(ec).map(PathBuf::from), deprecated_only)
}
fn resolve_config_file_path(
vox_config: Option<PathBuf>,
ec_config: Option<PathBuf>,
) -> (Option<PathBuf>, bool) {
let deprecated_only = vox_config.is_none() && ec_config.is_some();
(vox_config.or(ec_config), deprecated_only)
}
include!(concat!(env!("OUT_DIR"), "/embedded_coreasm.rs"));
fn materialised_embedded_coreasm() -> Option<PathBuf> {
if EMBEDDED_COREASM.is_empty() {
return None;
}
let cache_root = env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))?
.join("vox");
let version_dir = cache_root.join(env!("CARGO_PKG_VERSION"));
let target = version_dir.join("coreasm");
if target.is_dir() {
return Some(target);
}
fs::create_dir_all(&cache_root).ok()?;
let staging_parent = cache_root.join(format!(
".{}.{}.partial",
env!("CARGO_PKG_VERSION"),
std::process::id()
));
let _ = fs::remove_dir_all(&staging_parent);
let staging = staging_parent.join("coreasm");
let write_all = || -> std::io::Result<()> {
for (relative_path, contents) in EMBEDDED_COREASM {
let destination = staging.join(relative_path);
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&destination, contents)?;
}
Ok(())
};
if write_all().is_err() {
let _ = fs::remove_dir_all(&staging_parent);
return None;
}
match fs::rename(&staging_parent, &version_dir) {
Ok(()) => Some(target),
Err(_) => {
let _ = fs::remove_dir_all(&staging_parent);
target.is_dir().then_some(target)
}
}
}
fn find_coreasm_path() -> Option<PathBuf> {
let (env_path, deprecate) = resolve_core_env_override(
env::var("VOX_CORE_PATH").ok().as_deref(),
env::var("EC_CORE_PATH").ok().as_deref(),
);
if deprecate {
eprintln!(
"note: EC_CORE_PATH is deprecated; set VOX_CORE_PATH instead \
(still read as a fallback for now)."
);
}
if let Some(core_path) = env_path {
let path = PathBuf::from(&core_path);
if path.exists() {
return Some(path);
}
let coreasm = path.join("coreasm");
if coreasm.exists() {
return Some(coreasm);
}
}
if let Some(config_path) = get_config_lib_path() {
if config_path.exists() {
return Some(config_path);
}
}
let system_paths = [
"/usr/local/share/vox/coreasm",
"/usr/share/vox/coreasm",
"/opt/vox/coreasm",
];
for path in &system_paths {
let p = PathBuf::from(path);
if p.exists() {
return Some(p);
}
}
if let Ok(exe) = env::current_exe() {
let mut dir = exe.parent();
while let Some(d) = dir {
let candidate = d.join("coreasm");
if candidate.exists() {
return Some(candidate);
}
dir = d.parent();
}
}
let cwd_coreasm = PathBuf::from("coreasm");
if cwd_coreasm.exists() {
return Some(cwd_coreasm);
}
materialised_embedded_coreasm()
}
fn get_config_lib_path() -> Option<PathBuf> {
let config_dir = env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
env::var("HOME")
.map(|h| PathBuf::from(h).join(".config"))
.unwrap_or_default()
});
let vox_cfg = config_dir.join("vox").join("config");
let ec_cfg = config_dir.join("ec").join("config");
let (config_file, deprecate) = resolve_config_file_path(
vox_cfg.exists().then_some(vox_cfg),
ec_cfg.exists().then_some(ec_cfg),
);
if deprecate {
eprintln!(
"note: ~/.config/ec/config is deprecated; use ~/.config/vox/config \
instead (still read as a fallback for now)."
);
}
let Some(config_file) = config_file else {
return None;
};
if let Ok(file) = fs::File::open(&config_file) {
let reader = BufReader::new(file);
for line in reader.lines().map_while(Result::ok) {
let line = line.trim();
if line.starts_with('#') || line.is_empty() {
continue;
}
if let Some(value) = line.strip_prefix("core_path=") {
let path = PathBuf::from(value.trim());
let coreasm = if path.ends_with("coreasm") {
path
} else {
path.join("coreasm")
};
return Some(coreasm);
}
}
}
None
}
fn show_version() {
eprintln!("vox v{} By Josjuar Lister 2026", env!("CARGO_PKG_VERSION"));
}
fn show_help() {
eprintln!("Usage: vox <source.vox> [options]");
eprintln!();
eprintln!("Options:");
eprintln!(" --emit-asm Output assembly only (don't assemble/link)");
eprintln!(" --keep-asm Keep assembly file after linking");
eprintln!(" --run Compile and run the program");
eprintln!(" --shared Build a shared library (.so) instead of executable");
eprintln!(" --link <libs> Link against shared libraries (comma-separated)");
eprintln!(" --lib-path <paths> Additional library search paths (comma-separated)");
eprintln!(" --target <arch> Target architecture (default: x86_64)");
eprintln!(" -o <file> Output file name");
eprintln!(" -v | --verbose Verbose output");
eprintln!(" -h | --help Show help");
eprintln!(" -V | --version Show version");
eprintln!();
show_version();
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
show_help();
std::process::exit(1);
}
if args.len() == 2 {
match args[1].as_str() {
"--help" | "-h" => {
show_help();
std::process::exit(0);
}
"--version" | "-V" => {
show_version();
std::process::exit(0);
}
_ => {}
}
}
let mut source_paths: Vec<String> = Vec::new();
let mut emit_asm_only = false;
let mut keep_asm = false;
let mut run_after = false;
let mut build_shared = false;
let mut output_name = None;
let mut verbose = false;
let mut link_libs: Vec<String> = Vec::new();
let mut lib_paths: Vec<String> = Vec::new();
let mut target_arch = option_env!("TARGET_ARCH").unwrap_or("x86_64").to_string();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--help" | "-h" => {
show_help();
std::process::exit(0);
}
"--version" | "-V" => {
show_version();
std::process::exit(0);
}
"--emit-asm" => emit_asm_only = true,
"--keep-asm" => keep_asm = true,
"--run" => run_after = true,
"--shared" => build_shared = true,
"--verbose" | "-v" => verbose = true,
"-o" => {
i += 1;
if i < args.len() {
output_name = Some(args[i].clone());
}
}
"--link" => {
i += 1;
if i < args.len() {
link_libs.extend(args[i].split(',').map(|s| s.trim().to_string()));
}
}
"--lib-path" => {
i += 1;
if i < args.len() {
lib_paths.extend(args[i].split(',').map(|s| s.trim().to_string()));
}
}
"--target" => {
i += 1;
if i < args.len() {
target_arch = args[i].clone();
}
}
_ => {
source_paths.push(args[i].clone());
}
}
i += 1;
}
if source_paths.is_empty() {
show_help();
std::process::exit(1);
}
if source_paths.len() > 1 && !build_shared {
eprintln!(
"Multiple source files ({}) are only valid with --shared, which links \
them into one library in a single link step. An executable build takes \
a single source — pass --shared, or compile each source separately.",
source_paths.join(", ")
);
std::process::exit(1);
}
let first_source = source_paths[0].clone();
let mut combined_statements: Vec<Statement> = Vec::new();
let mut identities: Vec<(String, String, String, String)> = Vec::new();
let mut all_imported_functions: Vec<lib_file::ImportedFunction> = Vec::new();
let mut imported_sos: Vec<PathBuf> = Vec::new();
for source_path in &source_paths {
let source = match fs::read_to_string(source_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", source_path, e);
std::process::exit(1);
}
};
if verbose {
println!("Compiling {}...", source_path);
}
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens)
.with_source(source_path, &source)
.with_shared_mode(build_shared);
let program = match parser.parse() {
Ok(p) => p,
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
};
for warning in &parser.warnings {
eprintln!("{}", warning);
}
if verbose {
for included in &parser.included_paths {
println!("Including: {}", included);
}
}
let source_path_buf = PathBuf::from(source_path);
let source_dir = source_path_buf
.parent()
.unwrap_or(Path::new("."))
.to_path_buf();
match lib_file::resolve_program_imports(&program, &source_dir, &lib_paths) {
Ok(imports) => {
for import in imports {
all_imported_functions.extend(import.functions);
if !imported_sos.contains(&import.so_path) {
imported_sos.push(import.so_path);
}
}
}
Err(message) => {
eprintln!("Error: {}", message);
std::process::exit(1);
}
}
if build_shared && source_paths.len() > 1 {
let identity = program.statements.iter().find_map(|s| {
if let Statement::LibraryDecl { name, version } = s {
Some((name.clone(), version.clone()))
} else {
None
}
});
match identity {
Some((lib, ver)) => {
let prefix = mangle_library_symbol(&lib, &ver, "");
if let Some((plib, pver, prev_file)) = identities
.iter()
.find_map(|(p, l, v, f)| {
if p == &prefix {
Some((l.clone(), v.clone(), f.clone()))
} else {
None
}
})
{
if plib == lib && pver == ver {
eprintln!(
"Duplicate library identity: '{}' and '{}' both declare \
Library {} version \"{}\". Two sources linked into one \
.so must each name a distinct library and version, or the \
second's signatures silently overwrite the first's. Rename \
one.",
prev_file, source_path, format_lib_name(&lib), ver
);
} else {
eprintln!(
"Duplicate library identity: '{}' declares Library {} \
version \"{}\" and '{}' declares Library {} version \"{}\", \
but both mangle to the symbol prefix '{}'. The mangler folds \
every character outside [A-Za-z0-9_] to '_', so these are the \
same to the linker and the second's signatures silently \
overwrite the first's. Rename one so the library and version \
stay distinct after mangling.",
prev_file, format_lib_name(&plib), pver, source_path, format_lib_name(&lib), ver, prefix
);
}
std::process::exit(1);
}
identities.push((prefix, lib, ver, source_path.clone()));
}
None => {
eprintln!(
"'{}' has no `Library` declaration. A source linked into a shared \
library alongside others must declare its identity — \
`Library name version \"x.y\".` — so its symbols are mangled \
apart from the other libraries' and a `.lib` can be written for it. \
Add one before the function definitions.",
source_path
);
std::process::exit(1);
}
}
}
combined_statements.extend(program.statements);
}
let mut program = Program::new(combined_statements);
let first_source_content = fs::read_to_string(&first_source).unwrap_or_default();
let mut analyzer = Analyzer::new()
.with_source(&first_source, &first_source_content)
.with_shared_mode(build_shared)
.with_imports(all_imported_functions.clone());
analyzer.analyze(&mut program);
for warning in &analyzer.warnings {
eprintln!("warning: {}", warning);
}
if !analyzer.errors.is_empty() {
for err in &analyzer.errors {
eprintln!("{}", err);
}
std::process::exit(1);
}
let mut codegen = CodeGenerator::new();
codegen.set_shared_lib_mode(build_shared);
codegen.set_target_arch(&target_arch);
codegen.set_imports(all_imported_functions);
let assembly = codegen.generate(&program);
let base_name = Path::new(&first_source)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let asm_path = format!("{}.asm", base_name);
let obj_path = format!("{}.o", base_name);
let output_path = output_name.unwrap_or_else(|| {
if build_shared {
format!("lib{}.so", base_name)
} else {
base_name.to_string()
}
});
if let Err(e) = fs::write(&asm_path, &assembly) {
eprintln!("Error writing assembly: {}", e);
std::process::exit(1);
}
if verbose {
println!("Generated {}", asm_path);
}
if emit_asm_only {
return;
}
let cleanup_asm_on_failure = || {
if !keep_asm {
let _ = fs::remove_file(&asm_path);
}
};
let lib_path = if build_shared {
Some(Path::new(&output_path).with_extension("lib"))
} else {
None
};
let coreasm_include = match find_coreasm_path() {
Some(path) => {
if let Some(parent) = path.parent() {
format!("-I{}/", parent.display())
} else {
format!("-I{}/", path.display())
}
}
None => {
eprintln!("Warning: coreasm library not found. Set VOX_CORE_PATH (or the deprecated EC_CORE_PATH) or install to /usr/local/share/vox/");
"-I./".to_string()
}
};
if verbose {
println!("Assembling...");
}
let nasm_args = if build_shared {
vec!["-f", "elf64", "-DPIC", &coreasm_include, "-o", &obj_path, &asm_path]
} else {
vec!["-f", "elf64", &coreasm_include, "-o", &obj_path, &asm_path]
};
let nasm_result = Command::new("nasm")
.args(&nasm_args)
.status();
match nasm_result {
Ok(status) if status.success() => {}
Ok(_) => {
eprintln!("NASM assembly failed");
cleanup_asm_on_failure();
std::process::exit(1);
}
Err(e) => {
eprintln!("Failed to run NASM: {}", e);
eprintln!("Make sure NASM is installed: sudo apt install nasm");
cleanup_asm_on_failure();
std::process::exit(1);
}
}
if verbose {
println!("Linking...");
}
let map_path = if build_shared {
Some(env::temp_dir().join(format!("vox-{}-{}.map", base_name, std::process::id())))
} else {
None
};
let mut import_ld_args: Vec<String> = Vec::new();
let mut import_rpaths: Vec<String> = Vec::new();
{
let mut so_dirs: Vec<String> = Vec::new();
let mut so_names: Vec<String> = Vec::new();
for so in &imported_sos {
let dir = so
.parent()
.map(|d| d.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let dir = dir.canonicalize().unwrap_or(dir);
let dir_s = dir.display().to_string();
if !so_dirs.contains(&dir_s) {
so_dirs.push(dir_s.clone());
import_ld_args.push(format!("-L{}", dir_s));
import_rpaths.push(dir_s);
}
if let Some(fname) = so.file_name().and_then(|f| f.to_str()) {
so_names.push(format!("-l:{}", fname));
}
}
import_ld_args.extend(so_names);
}
let ld_result = if build_shared {
let map_path = map_path.as_ref().unwrap();
let mut script = String::from("{ global:");
for func in codegen.exported_functions() {
script.push_str(&format!(" {};", func));
}
script.push_str(" local:*; };\n");
if let Err(e) = fs::write(&map_path, &script) {
eprintln!("Error writing version script: {}", e);
cleanup_asm_on_failure();
std::process::exit(1);
}
let mut all_args: Vec<String> = vec![
"-shared".to_string(),
format!("--version-script={}", map_path.display()),
"-o".to_string(),
output_path.clone(),
obj_path.clone(),
];
for p in lib_paths.iter().map(|p| format!("-L{}", p)) {
all_args.push(p);
}
for l in link_libs.iter().map(|l| format!("-l{}", l)) {
all_args.push(l);
}
for a in &import_ld_args {
all_args.push(a.clone());
}
for r in &import_rpaths {
all_args.push("-rpath".to_string());
all_args.push(r.clone());
}
let arg_refs: Vec<&str> = all_args.iter().map(|s| s.as_str()).collect();
Command::new("ld")
.args(&arg_refs)
.status()
} else {
let ld_args = vec!["-o", &output_path, &obj_path];
let lib_path_args: Vec<String> = lib_paths.iter()
.map(|p| format!("-L{}", p))
.collect();
let link_args: Vec<String> = link_libs.iter()
.map(|l| format!("-l{}", l))
.collect();
let mut dynamic_args: Vec<String> = Vec::new();
if !link_libs.is_empty() || !imported_sos.is_empty() {
dynamic_args.push("-dynamic-linker".to_string());
dynamic_args.push("/lib64/ld-linux-x86-64.so.2".to_string());
for p in lib_paths.iter() {
dynamic_args.push("-rpath".to_string());
dynamic_args.push(p.clone());
}
for r in &import_rpaths {
dynamic_args.push("-rpath".to_string());
dynamic_args.push(r.clone());
}
}
let mut all_args: Vec<&str> = ld_args;
for a in &dynamic_args {
all_args.push(a);
}
for p in &lib_path_args {
all_args.push(p);
}
for l in &link_args {
all_args.push(l);
}
for a in &import_ld_args {
all_args.push(a);
}
Command::new("ld")
.args(&all_args)
.status()
};
if let Some(ref p) = map_path {
let _ = fs::remove_file(p);
}
match ld_result {
Ok(status) if status.success() => {}
Ok(_) => {
eprintln!("Linking failed");
cleanup_asm_on_failure();
std::process::exit(1);
}
Err(e) => {
eprintln!("Failed to run ld: {}", e);
cleanup_asm_on_failure();
std::process::exit(1);
}
}
let _ = fs::remove_file(&obj_path);
if let Some(ref lib_path) = lib_path {
let so_filename = Path::new(&output_path)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(&output_path);
let lib_text = render_lib_file(codegen.library_blocks(), so_filename);
if let Err(e) = fs::write(lib_path, &lib_text) {
let _ = fs::remove_file(&output_path);
eprintln!("Error writing .lib '{}': {}", lib_path.display(), e);
std::process::exit(1);
}
if verbose {
println!("Created library interface: {}", lib_path.display());
}
}
if verbose {
if build_shared {
println!("Created shared library: {}", output_path);
} else {
println!("Created executable: {}", output_path);
}
}
if keep_asm {
if verbose {
println!("Kept assembly file: {}", asm_path);
}
} else {
if verbose {
println!("Removed assembly file: {}", asm_path);
}
let _ = fs::remove_file(&asm_path);
}
if run_after {
if build_shared {
eprintln!("Cannot run a shared library directly");
std::process::exit(1);
}
if verbose {
println!("\nRunning {}...\n", output_path);
}
let run_result = Command::new(format!("./{}", output_path))
.status();
if let Ok(status) = run_result {
std::process::exit(status.code().unwrap_or(0));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vox_core_path_resolves() {
let (path, deprecate) = resolve_core_env_override(Some("/opt/vox"), None);
assert_eq!(path, Some(PathBuf::from("/opt/vox")));
assert!(!deprecate);
}
#[test]
fn ec_core_path_still_resolves() {
let (path, deprecate) = resolve_core_env_override(None, Some("/opt/ec"));
assert_eq!(path, Some(PathBuf::from("/opt/ec")));
assert!(deprecate);
}
#[test]
fn vox_core_path_takes_precedence_over_ec_core_path() {
let (path, deprecate) = resolve_core_env_override(Some("/opt/vox"), Some("/opt/ec"));
assert_eq!(path, Some(PathBuf::from("/opt/vox")));
assert!(!deprecate);
}
#[test]
fn neither_env_set_is_no_override() {
let (path, deprecate) = resolve_core_env_override(None, None);
assert_eq!(path, None);
assert!(!deprecate);
}
#[test]
fn empty_values_are_treated_as_unset() {
let (path, deprecate) = resolve_core_env_override(Some(""), Some(""));
assert_eq!(path, None);
assert!(!deprecate);
}
#[test]
fn vox_config_file_resolves() {
let (path, deprecate) =
resolve_config_file_path(Some(PathBuf::from("/home/u/.config/vox/config")), None);
assert_eq!(path, Some(PathBuf::from("/home/u/.config/vox/config")));
assert!(!deprecate);
}
#[test]
fn ec_config_file_still_resolves() {
let (path, deprecate) =
resolve_config_file_path(None, Some(PathBuf::from("/home/u/.config/ec/config")));
assert_eq!(path, Some(PathBuf::from("/home/u/.config/ec/config")));
assert!(deprecate);
}
#[test]
fn vox_config_file_takes_precedence_over_ec_config_file() {
let (path, deprecate) = resolve_config_file_path(
Some(PathBuf::from("/home/u/.config/vox/config")),
Some(PathBuf::from("/home/u/.config/ec/config")),
);
assert_eq!(path, Some(PathBuf::from("/home/u/.config/vox/config")));
assert!(!deprecate);
}
#[test]
fn neither_config_file_present_is_no_override() {
let (path, deprecate) = resolve_config_file_path(None, None);
assert_eq!(path, None);
assert!(!deprecate);
}
}