use seqc::Effect;
use seqc::ast::Include;
use seqc::parser::Parser;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
#[derive(Debug, Clone)]
pub struct IncludedWord {
pub name: String,
pub effect: Option<Effect>,
pub source: String,
}
#[derive(Debug, Clone)]
pub struct LocalWord {
pub name: String,
pub effect: Option<Effect>,
}
pub fn find_stdlib_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var("SEQ_STDLIB_PATH") {
let p = PathBuf::from(path);
if p.exists() {
return Some(p);
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
let stdlib = parent.join("../stdlib");
if stdlib.exists() {
return Some(stdlib);
}
let stdlib = parent.join("../../stdlib");
if stdlib.exists() {
return Some(stdlib);
}
}
}
if let Ok(home) = std::env::var("HOME") {
let paths = [
format!("{}/.local/share/seq/stdlib", home),
format!("{}/seq/stdlib", home),
];
for path in paths {
let p = PathBuf::from(path);
if p.exists() {
return Some(p);
}
}
}
let dev_paths = ["./stdlib", "../stdlib", "../../stdlib"];
for path in dev_paths {
let p = PathBuf::from(path);
if p.exists() {
return Some(p.canonicalize().unwrap_or(p));
}
}
None
}
pub fn parse_document(source: &str) -> (Vec<Include>, Vec<LocalWord>) {
let mut parser = Parser::new(source);
match parser.parse() {
Ok(program) => {
let local_words = program
.words
.iter()
.map(|w| LocalWord {
name: w.name.clone(),
effect: w.effect.clone(),
})
.collect();
(program.includes, local_words)
}
Err(_) => (Vec::new(), Vec::new()),
}
}
pub fn resolve_includes(
includes: &[Include],
doc_path: Option<&Path>,
stdlib_path: Option<&Path>,
) -> Vec<IncludedWord> {
let mut words = Vec::new();
let mut visited = HashSet::new();
for include in includes {
resolve_include_recursive(include, doc_path, stdlib_path, &mut words, &mut visited, 0);
}
words
}
fn resolve_include_recursive(
include: &Include,
doc_path: Option<&Path>,
stdlib_path: Option<&Path>,
words: &mut Vec<IncludedWord>,
visited: &mut HashSet<PathBuf>,
depth: usize,
) {
if depth > 10 {
warn!("Include depth limit reached");
return;
}
let (path, source_name) = match resolve_include_path(include, doc_path, stdlib_path) {
Some(result) => result,
None => {
debug!("Could not resolve include: {:?}", include);
return;
}
};
let canonical = match path.canonicalize() {
Ok(p) => p,
Err(_) => {
debug!("Could not canonicalize: {:?}", path);
return;
}
};
if visited.contains(&canonical) {
return;
}
visited.insert(canonical.clone());
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
debug!("Could not read {}: {}", path.display(), e);
return;
}
};
let mut parser = Parser::new(&content);
let program = match parser.parse() {
Ok(p) => p,
Err(e) => {
debug!("Could not parse {}: {}", path.display(), e);
return;
}
};
for word in &program.words {
words.push(IncludedWord {
name: word.name.clone(),
effect: word.effect.clone(),
source: source_name.clone(),
});
}
let include_dir = path.parent();
for nested_include in &program.includes {
resolve_include_recursive(
nested_include,
include_dir,
stdlib_path,
words,
visited,
depth + 1,
);
}
}
fn resolve_include_path(
include: &Include,
doc_dir: Option<&Path>,
stdlib_path: Option<&Path>,
) -> Option<(PathBuf, String)> {
match include {
Include::Std(name) => {
let stdlib = stdlib_path?;
let path = stdlib.join(format!("{}.seq", name));
if path.exists() {
Some((path, format!("std:{}", name)))
} else {
None
}
}
Include::Relative(name) => {
let dir = doc_dir?;
let path = dir.join(format!("{}.seq", name));
if path.exists() {
Some((path, name.clone()))
} else {
None
}
}
}
}
pub fn uri_to_path(uri: &str) -> Option<PathBuf> {
if let Some(path_str) = uri.strip_prefix("file://") {
#[cfg(windows)]
let path_str = path_str.trim_start_matches('/');
let decoded = percent_decode(path_str);
Some(PathBuf::from(decoded))
} else {
None
}
}
fn percent_decode(s: &str) -> String {
let mut bytes = Vec::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
let hex: String = chars.by_ref().take(2).collect();
if hex.len() == 2
&& let Ok(byte) = u8::from_str_radix(&hex, 16)
{
bytes.push(byte);
continue;
}
bytes.push(b'%');
bytes.extend(hex.as_bytes());
} else if c.is_ascii() {
bytes.push(c as u8);
} else {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf);
bytes.extend(encoded.as_bytes());
}
}
String::from_utf8_lossy(&bytes).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uri_to_path_unix() {
let uri = "file:///Users/test/code/example.seq";
let path = uri_to_path(uri).unwrap();
assert_eq!(path, PathBuf::from("/Users/test/code/example.seq"));
}
#[test]
fn test_uri_to_path_with_spaces() {
let uri = "file:///Users/test/my%20code/example.seq";
let path = uri_to_path(uri).unwrap();
assert_eq!(path, PathBuf::from("/Users/test/my code/example.seq"));
}
#[test]
fn test_uri_to_path_with_utf8() {
let uri = "file:///Users/test/caf%C3%A9/example.seq";
let path = uri_to_path(uri).unwrap();
assert_eq!(path, PathBuf::from("/Users/test/café/example.seq"));
}
#[test]
fn test_parse_document_with_includes() {
let source = r#"
include std:json
include "utils"
: main ( -- )
"hello" write_line
;
"#;
let (includes, words) = parse_document(source);
assert_eq!(includes.len(), 2);
assert_eq!(words.len(), 1);
assert_eq!(words[0].name, "main");
}
#[test]
fn test_parse_document_with_effect() {
let source = r#"
: double ( Int -- Int )
dup +
;
"#;
let (_, words) = parse_document(source);
assert_eq!(words.len(), 1);
assert!(words[0].effect.is_some());
}
#[test]
fn test_resolve_stdlib_json() {
let stdlib_path = find_stdlib_path();
assert!(stdlib_path.is_some(), "Could not find stdlib path");
let stdlib_path = stdlib_path.unwrap();
let source = "include std:json\n";
let (includes, _) = parse_document(source);
assert_eq!(includes.len(), 1);
let words = resolve_includes(&includes, None, Some(&stdlib_path));
let names: Vec<&str> = words.iter().map(|w| w.name.as_str()).collect();
assert!(
names.contains(&"json-serialize"),
"Expected json-serialize in {:?}",
names
);
assert!(
names.contains(&"json-parse"),
"Expected json-parse in {:?}",
names
);
}
}