Skip to content

Commit 773d7d9

Browse files
authored
feat: implement standard library (std) management and import system (#269)
1 parent d174058 commit 773d7d9

File tree

10 files changed

+584
-38
lines changed

10 files changed

+584
-38
lines changed

front/parser/src/import.rs

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::ast::ASTNode;
1+
use crate::ast::{ASTNode, StatementNode};
22
use crate::parse;
33
use error::error::{WaveError, WaveErrorKind};
44
use lexer::Lexer;
@@ -26,11 +26,7 @@ pub fn local_import_unit(
2626
}
2727

2828
if path.starts_with("std::") {
29-
already_imported.insert(path.to_string());
30-
return Ok(ImportedUnit {
31-
abs_path: base_dir.to_path_buf(),
32-
ast: vec![],
33-
});
29+
return std_import_unit(path, already_imported);
3430
}
3531

3632
if path.contains("::") {
@@ -60,11 +56,72 @@ pub fn local_import_unit(
6056
));
6157
}
6258

59+
parse_wave_file(&found_path, &target_file_name, already_imported)
60+
}
61+
62+
pub fn local_import(
63+
path: &str,
64+
already_imported: &mut HashSet<String>,
65+
base_dir: &Path,
66+
) -> Result<Vec<ASTNode>, WaveError> {
67+
Ok(local_import_unit(path, already_imported, base_dir)?.ast)
68+
}
69+
70+
fn std_import_unit(path: &str, already_imported: &mut HashSet<String>) -> Result<ImportedUnit, WaveError> {
71+
let rel = path.strip_prefix("std::").unwrap();
72+
if rel.trim().is_empty() {
73+
return Err(WaveError::new(
74+
WaveErrorKind::SyntaxError("Empty std import".to_string()),
75+
"std import path cannot be empty (example: import(\"std::io::format\"))",
76+
path,
77+
0,
78+
0,
79+
));
80+
}
81+
82+
let std_root = std_root_dir(path)?;
83+
84+
// std::io::format -> ~/.wave/lib/wave/std/io/format.wave
85+
let rel_path = rel.replace("::", "/");
86+
let found_path = std_root.join(format!("{}.wave", rel_path));
87+
88+
if !found_path.exists() || !found_path.is_file() {
89+
return Err(WaveError::new(
90+
WaveErrorKind::SyntaxError("File not found".to_string()),
91+
format!("Could not find std import target '{}'", found_path.display()),
92+
path,
93+
0,
94+
0,
95+
));
96+
}
97+
98+
parse_wave_file(&found_path, path, already_imported)
99+
}
100+
101+
fn std_root_dir(import_path: &str) -> Result<PathBuf, WaveError> {
102+
let home = std::env::var("HOME").map_err(|_| {
103+
WaveError::new(
104+
WaveErrorKind::SyntaxError("std not installed".to_string()),
105+
"HOME env not set; cannot locate std at ~/.wave/lib/wave/std",
106+
import_path,
107+
0,
108+
0,
109+
)
110+
})?;
111+
112+
Ok(PathBuf::from(home).join(".wave/lib/wave/std"))
113+
}
114+
115+
fn parse_wave_file(
116+
found_path: &Path,
117+
display_name: &str,
118+
already_imported: &mut HashSet<String>,
119+
) -> Result<ImportedUnit, WaveError> {
63120
let abs_path = found_path.canonicalize().map_err(|e| {
64121
WaveError::new(
65122
WaveErrorKind::SyntaxError("Canonicalization failed".to_string()),
66123
format!("Failed to canonicalize path: {}", e),
67-
target_file_name.clone(),
124+
display_name,
68125
0,
69126
0,
70127
)
@@ -76,7 +133,7 @@ pub fn local_import_unit(
76133
WaveError::new(
77134
WaveErrorKind::UnexpectedChar('?'),
78135
"Invalid path encoding",
79-
target_file_name.clone(),
136+
display_name,
80137
0,
81138
0,
82139
)
@@ -88,11 +145,11 @@ pub fn local_import_unit(
88145
}
89146
already_imported.insert(abs_path_str);
90147

91-
let content = std::fs::read_to_string(&found_path).map_err(|e| {
148+
let content = std::fs::read_to_string(&abs_path).map_err(|e| {
92149
WaveError::new(
93150
WaveErrorKind::SyntaxError("Read error".to_string()),
94-
format!("Failed to read '{}': {}", target_file_name, e),
95-
target_file_name.clone(),
151+
format!("Failed to read '{}': {}", abs_path.display(), e),
152+
display_name,
96153
0,
97154
0,
98155
)
@@ -104,8 +161,8 @@ pub fn local_import_unit(
104161
let ast = parse(&tokens).ok_or_else(|| {
105162
WaveError::new(
106163
WaveErrorKind::SyntaxError("Parse failed".to_string()),
107-
format!("Failed to parse '{}'", target_file_name),
108-
target_file_name.clone(),
164+
format!("Failed to parse '{}'", abs_path.display()),
165+
display_name,
109166
1,
110167
1,
111168
)
@@ -115,11 +172,3 @@ pub fn local_import_unit(
115172

116173
Ok(ImportedUnit { abs_path, ast })
117174
}
118-
119-
pub fn local_import(
120-
path: &str,
121-
already_imported: &mut HashSet<String>,
122-
base_dir: &Path,
123-
) -> Result<Vec<ASTNode>, WaveError> {
124-
Ok(local_import_unit(path, already_imported, base_dir)?.ast)
125-
}

src/commands.rs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use crate::errors::CliError;
2+
use std::{env, fs};
3+
use std::path::{Path, PathBuf};
4+
use std::process::Command;
5+
use std::time::{SystemTime, UNIX_EPOCH};
26
use crate::{compile_and_img, compile_and_run};
3-
use std::path::Path;
47

58
#[derive(Default)]
69
pub struct DebugFlags {
@@ -57,3 +60,144 @@ pub fn img_run(file_path: &Path) -> Result<(), CliError> {
5760
}
5861
Ok(())
5962
}
63+
64+
pub fn handle_install_std() -> Result<(), CliError> {
65+
install_or_update_std(false)
66+
}
67+
68+
pub fn handle_update_std() -> Result<(), CliError> {
69+
install_or_update_std(true)
70+
}
71+
72+
fn install_or_update_std(is_update: bool) -> Result<(), CliError> {
73+
let install_dir = resolve_std_install_dir()?;
74+
75+
if install_dir.exists() {
76+
if !is_update {
77+
return Err(CliError::StdAlreadyInstalled { path: install_dir });
78+
}
79+
fs::remove_dir_all(&install_dir)?;
80+
}
81+
82+
fs::create_dir_all(&install_dir)?;
83+
84+
install_std_from_wave_repo_sparse(&install_dir)?;
85+
86+
if is_update {
87+
println!("✅ std updated: {}", install_dir.display());
88+
} else {
89+
println!("✅ std installed: {}", install_dir.display());
90+
}
91+
92+
Ok(())
93+
}
94+
95+
fn install_std_from_wave_repo_sparse(stage_dir: &Path) -> Result<(), CliError> {
96+
if !tool_exists("git") {
97+
return Err(CliError::ExternalToolMissing("git"));
98+
}
99+
100+
let repo = "https://github.com/wavefnd/Wave.git";
101+
let reference = "master";
102+
103+
let tmp = make_tmp_dir("wave-std")?;
104+
105+
run_cmd(
106+
Command::new("git")
107+
.arg("clone")
108+
.arg("--depth").arg("1")
109+
.arg("--filter=blob:none")
110+
.arg("--sparse")
111+
.arg("--branch").arg(reference)
112+
.arg(repo)
113+
.arg(&tmp),
114+
"git clone",
115+
)?;
116+
117+
run_cmd(
118+
Command::new("git")
119+
.arg("-C").arg(&tmp)
120+
.arg("sparse-checkout")
121+
.arg("set")
122+
.arg("std"),
123+
"git sparse-checkout set std",
124+
)?;
125+
126+
let src_std = tmp.join("std");
127+
128+
let manifest_path = src_std.join("manifest.json");
129+
if !manifest_path.exists() {
130+
return Err(CliError::CommandFailed(
131+
"manifest.json not found in repo/std (add std/manifest.json)".to_string(),
132+
));
133+
}
134+
135+
let text = fs::read_to_string(&manifest_path)?;
136+
let manifest = utils::json::parse(&text)
137+
.map_err(|e| CliError::CommandFailed(format!("invalid manifest.json: {}", e)))?;
138+
139+
if manifest.get_str("name") != Some("std") {
140+
return Err(CliError::CommandFailed("manifest.json name != 'std'".to_string()));
141+
}
142+
143+
copy_dir_all(&src_std, stage_dir)?;
144+
145+
fs::write(
146+
stage_dir.join("INSTALL_META"),
147+
format!("repo={}\nref={}\n", repo, reference),
148+
)?;
149+
150+
let _ = fs::remove_dir_all(&tmp);
151+
Ok(())
152+
}
153+
154+
fn resolve_std_install_dir() -> Result<PathBuf, CliError> {
155+
let home = env::var("HOME").map_err(|_| CliError::HomeNotSet)?;
156+
Ok(PathBuf::from(home).join(".wave/lib/wave/std"))
157+
}
158+
159+
160+
fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), CliError> {
161+
fs::create_dir_all(dst)?;
162+
for entry in fs::read_dir(src)? {
163+
let entry = entry?;
164+
let ty = entry.file_type()?;
165+
let from = entry.path();
166+
let to = dst.join(entry.file_name());
167+
168+
if ty.is_dir() {
169+
copy_dir_all(&from, &to)?;
170+
} else if ty.is_file() {
171+
if let Some(parent) = to.parent() {
172+
fs::create_dir_all(parent)?;
173+
}
174+
fs::copy(&from, &to)?;
175+
}
176+
}
177+
Ok(())
178+
}
179+
180+
fn tool_exists(name: &str) -> bool {
181+
Command::new(name).arg("--version").output().is_ok()
182+
}
183+
184+
fn run_cmd(cmd: &mut Command, label: &str) -> Result<(), CliError> {
185+
let out = cmd.output()?;
186+
if out.status.success() {
187+
Ok(())
188+
} else {
189+
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
190+
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
191+
Err(CliError::CommandFailed(format!(
192+
"{} (status={})\nstdout: {}\nstderr: {}",
193+
label, out.status, stdout, stderr
194+
)))
195+
}
196+
}
197+
198+
fn make_tmp_dir(prefix: &str) -> Result<PathBuf, CliError> {
199+
let t = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
200+
let p = env::temp_dir().join(format!("{}-{}", prefix, t));
201+
fs::create_dir_all(&p)?;
202+
Ok(p)
203+
}

src/errors.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::fmt;
2+
use std::path::PathBuf;
23

34
#[derive(Debug)]
45
pub enum CliError {
@@ -8,6 +9,18 @@ pub enum CliError {
89
command: &'static str,
910
expected: &'static str,
1011
},
12+
13+
// install/update
14+
UnknownInstallTarget(String),
15+
UnknownUpdateTarget(String),
16+
StdAlreadyInstalled { path: PathBuf },
17+
InvalidExecutablePath,
18+
ExternalToolMissing(&'static str),
19+
CommandFailed(String),
20+
HomeNotSet,
21+
22+
// io
23+
Io(std::io::Error),
1124
}
1225

1326
impl fmt::Display for CliError {
@@ -20,6 +33,22 @@ impl fmt::Display for CliError {
2033
"Error: Missing argument for '{}'. Expected: {}",
2134
command, expected
2235
),
36+
37+
CliError::UnknownInstallTarget(t) => write!(f, "Error: Unknown install target '{}'", t),
38+
CliError::UnknownUpdateTarget(t) => write!(f, "Error: Unknown update target '{}'", t),
39+
CliError::StdAlreadyInstalled { path } => write!(f, "Error: std already installed at '{}'", path.display()),
40+
CliError::InvalidExecutablePath => write!(f, "Error: Invalid executable path"),
41+
CliError::ExternalToolMissing(t) => write!(f, "Error: required tool not found: {}", t),
42+
CliError::CommandFailed(cmd) => write!(f, "Error: command failed: {}", cmd),
43+
CliError::HomeNotSet => write!(f, "Error: HOME environment variable not set"),
44+
45+
CliError::Io(e) => write!(f, "IO Error: {}", e),
2346
}
2447
}
2548
}
49+
50+
impl From<std::io::Error> for CliError {
51+
fn from(e: std::io::Error) -> Self {
52+
CliError::Io(e)
53+
}
54+
}

0 commit comments

Comments
 (0)