use nepali_core::{Interpreter, Parser, Resolver};
use std::env;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::rc::Rc;
#[cfg(feature = "ai-interop")]
mod host_ai;
#[cfg(feature = "cache")]
mod host_cache;
#[cfg(feature = "js-interop")]
mod host_js;
mod host_linux;
#[cfg(feature = "python-interop")]
mod host_python;
#[cfg(feature = "rust-interop")]
mod host_rust;
mod loader;
#[cfg(feature = "ai-interop")]
use host_ai::LocalAi;
#[cfg(feature = "cache")]
use host_cache::RedisCache;
#[cfg(feature = "js-interop")]
use host_js::QuickJsHost;
use host_linux::{LinuxFs, RealCommand, SqliteDb};
#[cfg(feature = "python-interop")]
use host_python::PyHost;
#[cfg(feature = "rust-interop")]
use host_rust::RustPluginHost;
fn main() -> ExitCode {
match env::args().nth(1) {
Some(path) => run_script(&path),
None => run_shell(),
}
}
fn database_path() -> PathBuf {
if let Ok(p) = env::var("NEPALI_DB") {
return PathBuf::from(p);
}
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".nepali").join("os.db")
}
fn open_host_db() -> Option<Rc<SqliteDb>> {
let path = database_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
match SqliteDb::open(&path.to_string_lossy()) {
Ok(db) => Some(Rc::new(db)),
Err(e) => {
eprintln!("warning: could not open database at {}: {e}", path.display());
None
}
}
}
fn new_interpreter() -> Interpreter {
let mut interp = Interpreter::new();
interp.set_host_fs(Rc::new(LinuxFs));
interp.set_host_command(Rc::new(RealCommand));
if let Some(db) = open_host_db() {
interp.set_host_db(db);
}
#[cfg(feature = "python-interop")]
interp.set_host_python(Rc::new(PyHost));
#[cfg(feature = "rust-interop")]
interp.set_host_rust(Rc::new(RustPluginHost::new()));
#[cfg(feature = "js-interop")]
interp.set_host_js(Rc::new(QuickJsHost));
#[cfg(feature = "cache")]
{
let redis_url =
env::var("NEPALI_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
interp.set_host_cache(Rc::new(RedisCache::new(redis_url)));
}
#[cfg(feature = "ai-interop")]
interp.set_host_ai(Rc::new(LocalAi::new()));
interp
}
fn run_script(path: &str) -> ExitCode {
let program = match loader::load(Path::new(path)) {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
if let Err(errors) = Resolver::resolve(&program) {
for e in &errors {
eprintln!("resolution error: {e}");
}
return ExitCode::FAILURE;
}
let mut interp = new_interpreter();
if let Err(e) = interp.run(&program) {
for line in &interp.output {
println!("{line}");
}
eprintln!("runtime error: {e}");
return ExitCode::FAILURE;
}
for line in &interp.output {
println!("{line}");
}
ExitCode::SUCCESS
}
fn run_shell() -> ExitCode {
let mut interp = new_interpreter();
let stdin = io::stdin();
loop {
print_prompt();
let mut line = String::new();
let bytes_read = match stdin.read_line(&mut line) {
Ok(n) => n,
Err(e) => {
eprintln!("nepali: {e}");
return ExitCode::FAILURE;
}
};
if bytes_read == 0 {
println!();
return ExitCode::SUCCESS; }
let line = line.trim();
if line.is_empty() {
continue;
}
if line == "exit" || line == "बाहिर" {
return ExitCode::SUCCESS;
}
if let Some(forced) = line.strip_prefix('!') {
run_external(forced.trim());
continue;
}
if let Some(dir) = line.strip_prefix("cd ").or_else(|| (line == "cd").then_some("")) {
change_dir(dir.trim());
continue;
}
let first_word = line.split_whitespace().next().unwrap_or("");
if is_real_executable(first_word) {
run_external(line);
continue;
}
match try_run_as_nepali(&mut interp, line) {
Some(()) => {}
None => run_external(line),
}
}
}
fn is_real_executable(name: &str) -> bool {
use std::os::unix::fs::PermissionsExt;
fn is_executable_file(path: &Path) -> bool {
std::fs::metadata(path)
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
if name.is_empty() {
return false;
}
if name.contains('/') {
return is_executable_file(Path::new(name));
}
let Ok(path_var) = env::var("PATH") else { return false };
env::split_paths(&path_var).any(|dir| is_executable_file(&dir.join(name)))
}
fn print_prompt() {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("?"));
print!("nep:{} $ ", cwd.display());
let _ = io::stdout().flush();
}
fn try_run_as_nepali(interp: &mut Interpreter, line: &str) -> Option<()> {
let mut parser = Parser::new(line);
let program = parser.parse_program().ok()?;
interp.output.clear();
if let Err(e) = interp.run(&program) {
for out_line in &interp.output {
println!("{out_line}");
}
eprintln!("runtime error: {e}");
return Some(());
}
for out_line in &interp.output {
println!("{out_line}");
}
Some(())
}
fn change_dir(dir: &str) {
let target = if dir.is_empty() {
env::var("HOME").unwrap_or_else(|_| "/".to_string())
} else {
dir.to_string()
};
if let Err(e) = env::set_current_dir(&target) {
eprintln!("cd: {target}: {e}");
}
}
fn run_external(line: &str) {
let mut parts = line.split_whitespace();
let Some(cmd) = parts.next() else { return };
let args: Vec<&str> = parts.collect();
match Command::new(cmd).args(&args).status() {
Ok(status) if !status.success() => {
if let Some(code) = status.code() {
eprintln!("{cmd}: exited with status {code}");
}
}
Ok(_) => {}
Err(e) => eprintln!("{cmd}: {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loader_resolves_import_across_files() {
let entry = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/import_demo/main.nep");
let program = loader::load(&entry).expect("load error");
Resolver::resolve(&program).expect("resolution error");
let mut interp = Interpreter::new();
interp.run(&program).expect("runtime error");
assert_eq!(
interp.output,
vec!["square(4) = 16".to_string(), "cube(3) = 27".to_string()]
);
}
}