diff --git a/Cargo.lock b/Cargo.lock index dfb8f37..2bd431f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.4" @@ -291,12 +297,14 @@ dependencies = [ name = "crudlang" version = "0.1.0" dependencies = [ + "arc-swap", "axum", "chrono", "clap", "dotenv", "log", "log4rs", + "notify", "reqwest", "serde", "thiserror", @@ -449,6 +457,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -843,6 +860,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.4", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -881,6 +918,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -899,7 +956,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags", + "bitflags 2.9.4", "libc", "redox_syscall", ] @@ -1014,6 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1041,6 +1099,30 @@ dependencies = [ "tempfile", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.9.4", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1077,7 +1159,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -1296,7 +1378,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.9.4", ] [[package]] @@ -1361,7 +1443,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", @@ -1443,7 +1525,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -1678,7 +1760,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "core-foundation", "system-configuration-sys", ] @@ -1879,7 +1961,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.9.4", "bytes", "futures-core", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index a3d3d0c..95acecd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,5 @@ walkdir = "2.5.0" thiserror = "2.0.17" url = "2.5.7" clap = { version = "4.5.51", features = ["derive"] } +notify = "8.2.0" +arc-swap = "1.7.1" diff --git a/source/hello/web.crud b/source/hello/web.crud index 580afa7..6f3f535 100644 --- a/source/hello/web.crud +++ b/source/hello/web.crud @@ -2,4 +2,4 @@ fn get(path: string) -> string: add("hello", path) fn add(a: string, b: string) -> string: - a + " " + b + a + " " + b \ No newline at end of file diff --git a/src/ast_compiler.rs b/src/ast_compiler.rs index c330a80..ae78ce6 100644 --- a/src/ast_compiler.rs +++ b/src/ast_compiler.rs @@ -5,11 +5,11 @@ use crate::errors::CompilerError::{ }; use crate::errors::CompilerErrorAtLine; use crate::tokens::TokenType::{ - Bang, Bool, Char, Colon, Date, Eof, Eol, Equal, F32, F64, False, FloatingPoint, Fn, Greater, - GreaterEqual, GreaterGreater, I32, I64, Identifier, Indent, Integer, LeftBrace, LeftBracket, - LeftParen, Less, LessEqual, LessLess, Let, ListType, MapType, Minus, Object, Plus, Print, - RightBrace, RightBracket, RightParen, SignedInteger, SingleRightArrow, Slash, Star, StringType, - True, U32, U64, UnsignedInteger, + Bang, Bool, Char, Colon, Date, Eof, Eol, Equal, False, FloatingPoint, Fn, Greater, GreaterEqual, GreaterGreater, + Identifier, Indent, Integer, LeftBrace, LeftBracket, LeftParen, Less, LessEqual, LessLess, + Let, ListType, MapType, Minus, Object, Plus, Print, RightBrace, RightBracket, RightParen, SignedInteger, + SingleRightArrow, Slash, Star, StringType, True, UnsignedInteger, F32, F64, + I32, I64, U32, U64, }; use crate::tokens::{Token, TokenType}; use crate::value::Value; diff --git a/src/file_watch.rs b/src/file_watch.rs new file mode 100644 index 0000000..7ad4816 --- /dev/null +++ b/src/file_watch.rs @@ -0,0 +1,54 @@ +use crate::chunk::Chunk; +use crate::compile_sourcedir; +use log4rs::append::Append; +use notify::{RecursiveMode, Watcher}; +use std::collections::HashMap; +use std::hash::Hash; +use std::path::Path; +use std::sync::Arc; +use std::sync::mpsc::channel; +use std::thread; +use std::time::{Duration, SystemTime}; +use arc_swap::ArcSwap; + +const ONE_SEC: Duration = Duration::from_secs(1); + +pub fn start_watch_daemon(source: &str, registry: Arc>>) { + let source = source.to_string(); + let s = source.to_string(); + let (tx, rx) = channel(); + thread::spawn(move || { + println!("-- File watch started --"); + let path = Path::new(&source); + if !path.exists() { + panic!("source directory {} does not exist", &source); + } + let mut watcher = notify::recommended_watcher(tx).expect("Failed to create watcher"); + watcher + .watch(path, RecursiveMode::Recursive) + .expect("Failed to watch"); + loop { + thread::sleep(Duration::from_secs(1)); + } + }); + thread::spawn(move || { + let mut file_changed = false; + loop { + let start = SystemTime::now(); + loop { + thread::sleep(Duration::from_millis(50)); + if let Ok(_) = rx.recv() { + file_changed = true; + } + if file_changed && SystemTime::now().duration_since(start).unwrap() > ONE_SEC { + break; + } + } + println!("refresh"); // TODO implement refresh source + let new_registry = Arc::new(compile_sourcedir(&s).unwrap()); + + registry.store(new_registry.clone()); + file_changed = false; + } + }); +} diff --git a/src/lib.rs b/src/lib.rs index a6dca5a..dc1495a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ use crate::value::Value; use crate::vm::interpret; use std::collections::HashMap; use std::fs; +use std::sync::Arc; +use arc_swap::ArcSwap; use walkdir::WalkDir; pub mod ast_compiler; @@ -19,6 +21,7 @@ mod tokens; mod value; pub mod vm; pub mod repl; +pub mod file_watch; pub fn compile_sourcedir(source_dir: &str) -> Result, CrudLangError> { let mut registry = HashMap::new(); @@ -26,12 +29,13 @@ pub fn compile_sourcedir(source_dir: &str) -> Result, Cru for entry in WalkDir::new(source_dir).into_iter().filter_map(|e| e.ok()) { let path = entry.path().to_str().unwrap(); if path.ends_with(".crud") { - print!("compiling {:?}: ", path); + print!("-- Compiling "); let source = fs::read_to_string(path).map_err(map_underlying())?; let tokens = scan(&source)?; match ast_compiler::compile(Some(&path), tokens) { Ok(statements) => { - let path = path.strip_prefix("source/").unwrap().replace(".crud", ""); + println!("{}",path); + let path = path.strip_prefix(source_dir).unwrap().replace(".crud", ""); bytecode_compiler::compile(Some(&path), &statements, &mut registry)?; } Err(e) => { @@ -62,5 +66,6 @@ fn run(src: &str) -> Result { let mut registry = HashMap::new(); let ast = ast_compiler::compile(None, tokens)?; bytecode_compiler::compile(None, &ast, &mut registry)?; - interpret(®istry, "main").map_err(CrudLangError::from) + let registry = ArcSwap::from(Arc::new(registry)); + interpret(registry.load(), "main").map_err(CrudLangError::from) } diff --git a/src/main.rs b/src/main.rs index f2308f3..43b6c5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,10 @@ use crudlang::errors::CrudLangError; use crudlang::errors::CrudLangError::Platform; use crudlang::vm::interpret_async; use crudlang::{compile_sourcedir, map_underlying}; +use notify::Watcher; use std::collections::HashMap; use std::sync::Arc; +use arc_swap::ArcSwap; /// A simple CLI tool to greet users #[derive(Parser, Debug)] @@ -19,6 +21,9 @@ struct Args { #[arg(short, long)] source: Option, + + #[arg(short, long)] + watch: bool, } #[tokio::main] @@ -26,17 +31,19 @@ async fn main() -> Result<(), CrudLangError> { println!("-- Crudlang --"); tracing_subscriber::fmt::init(); let args = Args::parse(); - let source = args.source.unwrap_or("source".to_string()); + let source = args.source.unwrap_or("./source".to_string()); let registry = compile_sourcedir(&source)?; - let registry = Arc::new(registry); if !registry.is_empty() { - println!("-- Compilation successful -- Starting server --"); - let state = Arc::new(AppState { - registry: registry.clone(), - }); - + let swap = Arc::new(ArcSwap::from(Arc::new(registry))); + if args.watch { + crudlang::file_watch::start_watch_daemon(&source, swap.clone()); + } + println!("-- Compilation successful --"); + let state =AppState { + registry: swap.clone(), + }; let app = Router::new() .route("/", any(handle_any).with_state(state.clone())) .route("/{*path}", any(handle_any).with_state(state.clone())); @@ -46,12 +53,12 @@ async fn main() -> Result<(), CrudLangError> { .map_err(map_underlying())?; println!( - "listening on {}\n", + "-- Listening on {} --\n", listener.local_addr().map_err(map_underlying())? ); if args.repl { - std::thread::spawn(move || crudlang::repl::start(registry).unwrap()); + std::thread::spawn(move || crudlang::repl::start(swap.load()).unwrap()); } axum::serve(listener, app).await.map_err(map_underlying())?; @@ -65,11 +72,11 @@ async fn main() -> Result<(), CrudLangError> { #[derive(Clone)] struct AppState { - registry: Arc>, + registry: Arc>>, } async fn handle_any( - State(state): State>, + State(state): State, req: Request, ) -> Result, StatusCode> { let method = req.method().to_string().to_ascii_lowercase(); @@ -84,7 +91,7 @@ async fn handle_any( .collect() }) .unwrap_or_default(); - let component = format!("{}/web", &uri.path()[1..]); + let component = format!("{}/web", &uri.path()); let function_qname = format!("{}.{}", component, method); let mut headers = HashMap::new(); @@ -93,7 +100,7 @@ async fn handle_any( } let path = &req.uri().to_string(); match interpret_async( - &state.registry, + state.registry.load(), &function_qname, path, query_params, @@ -104,7 +111,7 @@ async fn handle_any( Ok(value) => Ok(Json(value.to_string())), Err(e) => { // url checks out but function for method not found - if state.registry.get(&format!("{}.main", component)).is_some() { + if state.registry.load().get(&format!("{}.main", component)).is_some() { Err(StatusCode::METHOD_NOT_ALLOWED) } else { Err(StatusCode::NOT_FOUND) diff --git a/src/repl.rs b/src/repl.rs index 9bc2ae2..b944e74 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -5,8 +5,9 @@ use std::collections::HashMap; use std::io; use std::io::Write; use std::sync::Arc; +use arc_swap::{ArcSwap, Guard}; -pub fn start(registry: Arc>) -> Result<(), CrudLangError> { +pub fn start(registry: Guard>>) -> Result<(), CrudLangError> { println!("REPL started -- Type ctrl-c to exit (both the repl and the server)"); println!(":h for help"); loop { @@ -20,7 +21,7 @@ pub fn start(registry: Arc>) -> Result<(), CrudLangError> match input { ":h" => help(), ":le" => list_endpoints(registry.clone()), - ":lf" => list_functions(registry.clone()), + ":lf" => list_endpoints(registry.clone()), _ => {} } // println!("[{}]",input); diff --git a/src/vm.rs b/src/vm.rs index 01a0cb6..03fdf6f 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -5,17 +5,19 @@ use crate::tokens::TokenType; use crate::value::Value; use axum::http::{Uri}; use std::collections::HashMap; +use std::sync::Arc; +use arc_swap::Guard; use tracing::debug; -pub struct Vm<'a> { +pub struct Vm { ip: usize, stack: Vec, local_vars: HashMap, error_occurred: bool, - registry: &'a HashMap, + registry: Arc>, } -pub fn interpret(registry: &HashMap, function: &str) -> Result { +pub fn interpret(registry: Guard>>, function: &str) -> Result { let chunk = registry.get(function).unwrap().clone(); // for (key,value) in registry.iter() { // println!("{}", key); @@ -26,13 +28,13 @@ pub fn interpret(registry: &HashMap, function: &str) -> Result, + registry: Guard>>, function: &str, uri: &str, query_params: HashMap, @@ -46,7 +48,7 @@ pub async fn interpret_async( stack: vec![], local_vars: HashMap::new(), error_occurred: false, - registry, + registry:registry.clone(), }; vm.local_vars .insert("path".to_string(), Value::String(uri.into())); @@ -73,13 +75,13 @@ pub fn interpret_function(chunk: &Chunk, args: Vec) -> Result Vm<'a> { +impl Vm { fn run_function(&mut self, chunk: &Chunk, mut args: Vec) -> Result { // arguments -> locals for (_, name) in chunk.vars.iter() {