diff --git a/front/parser/src/import.rs b/front/parser/src/import.rs index 26a306a0..10cdf5cd 100644 --- a/front/parser/src/import.rs +++ b/front/parser/src/import.rs @@ -1,4 +1,4 @@ -use crate::ast::ASTNode; +use crate::ast::{ASTNode, StatementNode}; use crate::parse; use error::error::{WaveError, WaveErrorKind}; use lexer::Lexer; @@ -26,11 +26,7 @@ pub fn local_import_unit( } if path.starts_with("std::") { - already_imported.insert(path.to_string()); - return Ok(ImportedUnit { - abs_path: base_dir.to_path_buf(), - ast: vec![], - }); + return std_import_unit(path, already_imported); } if path.contains("::") { @@ -60,11 +56,72 @@ pub fn local_import_unit( )); } + parse_wave_file(&found_path, &target_file_name, already_imported) +} + +pub fn local_import( + path: &str, + already_imported: &mut HashSet, + base_dir: &Path, +) -> Result, WaveError> { + Ok(local_import_unit(path, already_imported, base_dir)?.ast) +} + +fn std_import_unit(path: &str, already_imported: &mut HashSet) -> Result { + let rel = path.strip_prefix("std::").unwrap(); + if rel.trim().is_empty() { + return Err(WaveError::new( + WaveErrorKind::SyntaxError("Empty std import".to_string()), + "std import path cannot be empty (example: import(\"std::io::format\"))", + path, + 0, + 0, + )); + } + + let std_root = std_root_dir(path)?; + + // std::io::format -> ~/.wave/lib/wave/std/io/format.wave + let rel_path = rel.replace("::", "/"); + let found_path = std_root.join(format!("{}.wave", rel_path)); + + if !found_path.exists() || !found_path.is_file() { + return Err(WaveError::new( + WaveErrorKind::SyntaxError("File not found".to_string()), + format!("Could not find std import target '{}'", found_path.display()), + path, + 0, + 0, + )); + } + + parse_wave_file(&found_path, path, already_imported) +} + +fn std_root_dir(import_path: &str) -> Result { + let home = std::env::var("HOME").map_err(|_| { + WaveError::new( + WaveErrorKind::SyntaxError("std not installed".to_string()), + "HOME env not set; cannot locate std at ~/.wave/lib/wave/std", + import_path, + 0, + 0, + ) + })?; + + Ok(PathBuf::from(home).join(".wave/lib/wave/std")) +} + +fn parse_wave_file( + found_path: &Path, + display_name: &str, + already_imported: &mut HashSet, +) -> Result { let abs_path = found_path.canonicalize().map_err(|e| { WaveError::new( WaveErrorKind::SyntaxError("Canonicalization failed".to_string()), format!("Failed to canonicalize path: {}", e), - target_file_name.clone(), + display_name, 0, 0, ) @@ -76,7 +133,7 @@ pub fn local_import_unit( WaveError::new( WaveErrorKind::UnexpectedChar('?'), "Invalid path encoding", - target_file_name.clone(), + display_name, 0, 0, ) @@ -88,11 +145,11 @@ pub fn local_import_unit( } already_imported.insert(abs_path_str); - let content = std::fs::read_to_string(&found_path).map_err(|e| { + let content = std::fs::read_to_string(&abs_path).map_err(|e| { WaveError::new( WaveErrorKind::SyntaxError("Read error".to_string()), - format!("Failed to read '{}': {}", target_file_name, e), - target_file_name.clone(), + format!("Failed to read '{}': {}", abs_path.display(), e), + display_name, 0, 0, ) @@ -104,8 +161,8 @@ pub fn local_import_unit( let ast = parse(&tokens).ok_or_else(|| { WaveError::new( WaveErrorKind::SyntaxError("Parse failed".to_string()), - format!("Failed to parse '{}'", target_file_name), - target_file_name.clone(), + format!("Failed to parse '{}'", abs_path.display()), + display_name, 1, 1, ) @@ -115,11 +172,3 @@ pub fn local_import_unit( Ok(ImportedUnit { abs_path, ast }) } - -pub fn local_import( - path: &str, - already_imported: &mut HashSet, - base_dir: &Path, -) -> Result, WaveError> { - Ok(local_import_unit(path, already_imported, base_dir)?.ast) -} diff --git a/src/commands.rs b/src/commands.rs index 3fe8061a..57a28f51 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,9 @@ use crate::errors::CliError; +use std::{env, fs}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; use crate::{compile_and_img, compile_and_run}; -use std::path::Path; #[derive(Default)] pub struct DebugFlags { @@ -57,3 +60,144 @@ pub fn img_run(file_path: &Path) -> Result<(), CliError> { } Ok(()) } + +pub fn handle_install_std() -> Result<(), CliError> { + install_or_update_std(false) +} + +pub fn handle_update_std() -> Result<(), CliError> { + install_or_update_std(true) +} + +fn install_or_update_std(is_update: bool) -> Result<(), CliError> { + let install_dir = resolve_std_install_dir()?; + + if install_dir.exists() { + if !is_update { + return Err(CliError::StdAlreadyInstalled { path: install_dir }); + } + fs::remove_dir_all(&install_dir)?; + } + + fs::create_dir_all(&install_dir)?; + + install_std_from_wave_repo_sparse(&install_dir)?; + + if is_update { + println!("✅ std updated: {}", install_dir.display()); + } else { + println!("✅ std installed: {}", install_dir.display()); + } + + Ok(()) +} + +fn install_std_from_wave_repo_sparse(stage_dir: &Path) -> Result<(), CliError> { + if !tool_exists("git") { + return Err(CliError::ExternalToolMissing("git")); + } + + let repo = "https://github.com/wavefnd/Wave.git"; + let reference = "master"; + + let tmp = make_tmp_dir("wave-std")?; + + run_cmd( + Command::new("git") + .arg("clone") + .arg("--depth").arg("1") + .arg("--filter=blob:none") + .arg("--sparse") + .arg("--branch").arg(reference) + .arg(repo) + .arg(&tmp), + "git clone", + )?; + + run_cmd( + Command::new("git") + .arg("-C").arg(&tmp) + .arg("sparse-checkout") + .arg("set") + .arg("std"), + "git sparse-checkout set std", + )?; + + let src_std = tmp.join("std"); + + let manifest_path = src_std.join("manifest.json"); + if !manifest_path.exists() { + return Err(CliError::CommandFailed( + "manifest.json not found in repo/std (add std/manifest.json)".to_string(), + )); + } + + let text = fs::read_to_string(&manifest_path)?; + let manifest = utils::json::parse(&text) + .map_err(|e| CliError::CommandFailed(format!("invalid manifest.json: {}", e)))?; + + if manifest.get_str("name") != Some("std") { + return Err(CliError::CommandFailed("manifest.json name != 'std'".to_string())); + } + + copy_dir_all(&src_std, stage_dir)?; + + fs::write( + stage_dir.join("INSTALL_META"), + format!("repo={}\nref={}\n", repo, reference), + )?; + + let _ = fs::remove_dir_all(&tmp); + Ok(()) +} + +fn resolve_std_install_dir() -> Result { + let home = env::var("HOME").map_err(|_| CliError::HomeNotSet)?; + Ok(PathBuf::from(home).join(".wave/lib/wave/std")) +} + + +fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), CliError> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let from = entry.path(); + let to = dst.join(entry.file_name()); + + if ty.is_dir() { + copy_dir_all(&from, &to)?; + } else if ty.is_file() { + if let Some(parent) = to.parent() { + fs::create_dir_all(parent)?; + } + fs::copy(&from, &to)?; + } + } + Ok(()) +} + +fn tool_exists(name: &str) -> bool { + Command::new(name).arg("--version").output().is_ok() +} + +fn run_cmd(cmd: &mut Command, label: &str) -> Result<(), CliError> { + let out = cmd.output()?; + if out.status.success() { + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); + let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string(); + Err(CliError::CommandFailed(format!( + "{} (status={})\nstdout: {}\nstderr: {}", + label, out.status, stdout, stderr + ))) + } +} + +fn make_tmp_dir(prefix: &str) -> Result { + let t = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let p = env::temp_dir().join(format!("{}-{}", prefix, t)); + fs::create_dir_all(&p)?; + Ok(p) +} \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs index 849f90fc..602f310b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::path::PathBuf; #[derive(Debug)] pub enum CliError { @@ -8,6 +9,18 @@ pub enum CliError { command: &'static str, expected: &'static str, }, + + // install/update + UnknownInstallTarget(String), + UnknownUpdateTarget(String), + StdAlreadyInstalled { path: PathBuf }, + InvalidExecutablePath, + ExternalToolMissing(&'static str), + CommandFailed(String), + HomeNotSet, + + // io + Io(std::io::Error), } impl fmt::Display for CliError { @@ -20,6 +33,22 @@ impl fmt::Display for CliError { "Error: Missing argument for '{}'. Expected: {}", command, expected ), + + CliError::UnknownInstallTarget(t) => write!(f, "Error: Unknown install target '{}'", t), + CliError::UnknownUpdateTarget(t) => write!(f, "Error: Unknown update target '{}'", t), + CliError::StdAlreadyInstalled { path } => write!(f, "Error: std already installed at '{}'", path.display()), + CliError::InvalidExecutablePath => write!(f, "Error: Invalid executable path"), + CliError::ExternalToolMissing(t) => write!(f, "Error: required tool not found: {}", t), + CliError::CommandFailed(cmd) => write!(f, "Error: command failed: {}", cmd), + CliError::HomeNotSet => write!(f, "Error: HOME environment variable not set"), + + CliError::Io(e) => write!(f, "IO Error: {}", e), } } } + +impl From for CliError { + fn from(e: std::io::Error) -> Self { + CliError::Io(e) + } +} diff --git a/src/main.rs b/src/main.rs index 761da36f..500779b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::{env, process}; use utils::colorex::*; -use wavec::commands::{handle_build, handle_run, DebugFlags}; +use wavec::commands::{handle_build, handle_install_std, handle_run, handle_update_std, DebugFlags}; use wavec::errors::CliError; use wavec::version_wave; @@ -68,6 +68,32 @@ fn run() -> Result<(), CliError> { handle_build(Path::new(&file), &opt_flag, &debug_flags)?; } + "install" => { + let target = iter.next().ok_or(CliError::MissingArgument { + command: "install", + expected: "", + })?; + + match target.as_str() { + "std" => { + handle_install_std()?; + } + _ => return Err(CliError::UnknownInstallTarget(target)), + } + } + + "update" => { + let target = iter.next().ok_or(CliError::MissingArgument { + command: "update", + expected: "", + })?; + + match target.as_str() { + "std" => handle_update_std()?, + _ => return Err(CliError::UnknownUpdateTarget(target)), + } + } + "--help" => print_help(), _ => return Err(CliError::UnknownCommand(command)), @@ -100,6 +126,16 @@ fn print_help() { "build ".color("38,139,235"), "Compile Wave file" ); + println!( + " {:<18} {}", + "install std".color("38,139,235"), + "Install Wave standard library (std)" + ); + println!( + " {:<18} {}", + "update std".color("38,139,235"), + "Update Wave standard library (std)" + ); println!( " {:<18} {}", "--help".color("38,139,235"), diff --git a/src/runner.rs b/src/runner.rs index 657a8eba..e44cc2ec 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -25,26 +25,15 @@ fn expand_imports_for_codegen( for node in ast { match node { ASTNode::Statement(StatementNode::Import(module)) => { - let imported_ast = local_import(&module, already, base_dir)?; + let unit = local_import_unit(&module, already, base_dir)?; - if imported_ast.is_empty() { + if unit.ast.is_empty() { continue; } - let target_file_name = if module.ends_with(".wave") { - module.clone() - } else { - format!("{}.wave", module) - }; + let next_dir = unit.abs_path.parent().unwrap_or(base_dir); - let found_path = base_dir.join(&target_file_name); - let next_dir = found_path - .canonicalize() - .ok() - .and_then(|p| p.parent().map(|d| d.to_path_buf())) - .unwrap_or_else(|| base_dir.to_path_buf()); - - let expanded = expand_from_dir(&next_dir, imported_ast, already)?; + let expanded = expand_from_dir(next_dir, unit.ast, already)?; out.extend(expanded); } diff --git a/std/io/format.wave b/std/io/format.wave new file mode 100644 index 00000000..9a66ebe3 --- /dev/null +++ b/std/io/format.wave @@ -0,0 +1,3 @@ +fun std_format_dummy() { + println("std::io::format loaded"); +} \ No newline at end of file diff --git a/std/manifest.json b/std/manifest.json new file mode 100644 index 00000000..30d3b55e --- /dev/null +++ b/std/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "std", + "format": 1, + "modules": ["io", "fs", "net", "math", "sys", "json"] +} diff --git a/test/test75.wave b/test/test75.wave new file mode 100644 index 00000000..20586b1b --- /dev/null +++ b/test/test75.wave @@ -0,0 +1,64 @@ +struct Bytes { + data: ptr; + len: i32; +} + +fun string_bytes(s: str) -> Bytes { + var p: ptr = s; + var len: i32 = 0; + + while (p[len] != 0) { + len = len + 1; + } + + var b: Bytes; + b.data = p; + b.len = len; + return b; +} + +fun count_placeholders(input: str) -> i32 { + var bytes: Bytes = string_bytes(input); + var i: i32 = 0; + var count: i32 = 0; + + while (i < bytes.len) { + if (bytes.data[i] == '{') { + var start: i32 = i; + i = i + 1; + + while (i < bytes.len) { + if (bytes.data[i] == '}') { + count = count + 1; + i = i + 1; + break; + } + i = i + 1; + } + + if (i >= bytes.len && bytes.data[start] == '{') { + break; + } + } else { + i = i + 1; + } + } + + return count; +} + +fun main() { + var s1: str = "hello {} world"; + var s2: str = "{} {} {}"; + var s3: str = "{ hello } { world }"; + var s4: str = "{ { }"; + var s5: str = "no placeholders here"; + var s6: str = "{}{}}{"; + + println("s1 = {}", count_placeholders(s1)); + println("s2 = {}", count_placeholders(s2)); + println("s3 = {}", count_placeholders(s3)); + println("s4 = {}", count_placeholders(s4)); + println("s5 = {}", count_placeholders(s5)); + println("s6 = {}", count_placeholders(s6)); +} diff --git a/utils/src/json.rs b/utils/src/json.rs new file mode 100644 index 00000000..75208dae --- /dev/null +++ b/utils/src/json.rs @@ -0,0 +1,226 @@ +#[derive(Debug, Clone)] +pub enum Json { + Null, + Bool(bool), + Num(f64), + Str(String), + Arr(Vec), + Obj(Vec<(String, Json)>), +} + +impl Json { + pub fn get(&self, key: &str) -> Option<&Json> { + match self { + Json::Obj(kv) => kv.iter().find(|(k, _)| k == key).map(|(_, v)| v), + _ => None, + } + } + pub fn get_str(&self, key: &str) -> Option<&str> { + match self.get(key) { + Some(Json::Str(s)) => Some(s), + _ => None, + } + } + pub fn get_num(&self, key: &str) -> Option { + match self.get(key) { + Some(Json::Num(n)) => Some(*n), + _ => None, + } + } + pub fn get_arr(&self, key: &str) -> Option<&[Json]> { + match self.get(key) { + Some(Json::Arr(a)) => Some(a), + _ => None, + } + } +} + +pub fn parse(input: &str) -> Result { + let mut p = Parser::new(input.as_bytes()); + let v = p.parse_value()?; + p.skip_ws(); + if !p.eof() { + return Err("trailing characters".into()); + } + Ok(v) +} + +struct Parser<'a> { + s: &'a [u8], + i: usize, +} + +impl<'a> Parser<'a> { + fn new(s: &'a [u8]) -> Self { + Self { s, i: 0 } + } + + fn eof(&self) -> bool { + self.i >= self.s.len() + } + + fn peek(&self) -> Option { + self.s.get(self.i).copied() + } + + fn next(&mut self) -> Option { + let c = self.peek()?; + self.i += 1; + Some(c) + } + + fn skip_ws(&mut self) { + while let Some(c) = self.peek() { + if c == b' ' || c == b'\n' || c == b'\r' || c == b'\t' { + self.i += 1; + } else { + break; + } + } + } + + fn expect(&mut self, ch: u8) -> Result<(), String> { + self.skip_ws(); + match self.next() { + Some(c) if c == ch => Ok(()), + _ => Err(format!("expected '{}'", ch as char)), + } + } + + fn parse_value(&mut self) -> Result { + self.skip_ws(); + let c = self.peek().ok_or("unexpected eof")?; + match c { + b'n' => { self.consume_bytes(b"null")?; Ok(Json::Null) } + b't' => { self.consume_bytes(b"true")?; Ok(Json::Bool(true)) } + b'f' => { self.consume_bytes(b"false")?; Ok(Json::Bool(false)) } + b'"' => Ok(Json::Str(self.parse_string()?)), + b'[' => Ok(Json::Arr(self.parse_array()?)), + b'{' => Ok(Json::Obj(self.parse_object()?)), + b'-' | b'0'..=b'9' => Ok(Json::Num(self.parse_number()?)), + _ => Err("invalid value".into()), + } + } + + fn consume_bytes(&mut self, lit: &[u8]) -> Result<(), String> { + self.skip_ws(); + if self.s.get(self.i..self.i + lit.len()) == Some(lit) { + self.i += lit.len(); + Ok(()) + } else { + Err(format!("expected '{}'", String::from_utf8_lossy(lit))) + } + } + + fn parse_string(&mut self) -> Result { + self.expect(b'"')?; + let mut out = String::new(); + while let Some(c) = self.next() { + match c { + b'"' => return Ok(out), + b'\\' => { + let esc = self.next().ok_or("unfinished escape")?; + let ch = match esc { + b'"' => '"', + b'\\' => '\\', + b'/' => '/', + b'b' => '\x08', + b'f' => '\x0c', + b'n' => '\n', + b'r' => '\r', + b't' => '\t', + + b'u' => return Err("unicode escape (\\uXXXX) not supported".into()), + _ => return Err("invalid escape".into()), + }; + out.push(ch); + } + _ => out.push(c as char), + } + } + Err("unterminated string".into()) + } + + fn parse_number(&mut self) -> Result { + self.skip_ws(); + let start = self.i; + + if self.peek() == Some(b'-') { self.i += 1; } + + // int + match self.peek() { + Some(b'0') => self.i += 1, + Some(b'1'..=b'9') => { + self.i += 1; + while matches!(self.peek(), Some(b'0'..=b'9')) { self.i += 1; } + } + _ => return Err("invalid number".into()), + } + + // frac + if self.peek() == Some(b'.') { + self.i += 1; + if !matches!(self.peek(), Some(b'0'..=b'9')) { + return Err("invalid fraction".into()); + } + while matches!(self.peek(), Some(b'0'..=b'9')) { self.i += 1; } + } + + // exp + if matches!(self.peek(), Some(b'e') | Some(b'E')) { + self.i += 1; + if matches!(self.peek(), Some(b'+') | Some(b'-')) { self.i += 1; } + if !matches!(self.peek(), Some(b'0'..=b'9')) { + return Err("invalid exponent".into()); + } + while matches!(self.peek(), Some(b'0'..=b'9')) { self.i += 1; } + } + + let s = std::str::from_utf8(&self.s[start..self.i]).map_err(|_| "utf8 error")?; + s.parse::().map_err(|_| "number parse failed".into()) + } + + fn parse_array(&mut self) -> Result, String> { + self.expect(b'[')?; + self.skip_ws(); + let mut out = Vec::new(); + if self.peek() == Some(b']') { self.i += 1; return Ok(out); } + + loop { + let v = self.parse_value()?; + out.push(v); + self.skip_ws(); + match self.next().ok_or("unexpected eof in array")? { + b',' => continue, + b']' => break, + _ => return Err("expected ',' or ']'".into()), + } + } + Ok(out) + } + + fn parse_object(&mut self) -> Result, String> { + self.expect(b'{')?; + self.skip_ws(); + let mut out = Vec::new(); + if self.peek() == Some(b'}') { self.i += 1; return Ok(out); } + + loop { + self.skip_ws(); + if self.peek() != Some(b'"') { + return Err("object key must be string".into()); + } + let key = self.parse_string()?; + self.expect(b':')?; + let val = self.parse_value()?; + out.push((key, val)); + self.skip_ws(); + match self.next().ok_or("unexpected eof in object")? { + b',' => continue, + b'}' => break, + _ => return Err("expected ',' or '}'".into()), + } + } + Ok(out) + } +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 7f894b76..c5a8fbce 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,4 +1,5 @@ pub mod colorex; pub mod formatx; +pub mod json; pub use colorex::Colorize; \ No newline at end of file