devalang_wasm/language/syntax/parser/driver/
directive.rs

1use crate::language::syntax::ast::{Statement, StatementKind, Value};
2use anyhow::{Result, anyhow};
3use std::path::Path;
4
5pub fn parse_directive(
6    line: &str,
7    line_number: usize,
8    file_path: &std::path::Path,
9) -> Result<Statement> {
10    // Handle @use directive for plugins: @use author.plugin as alias
11    if line.starts_with("@use") {
12        return parse_use_directive(line, line_number);
13    }
14
15    if !line.starts_with("@load") {
16        // handle @import and @export directives
17        if line.starts_with("@import") {
18            // syntax: @import { a, b } from "path"
19            let rest = line["@import".len()..].trim();
20            if let Some(open) = rest.find('{') {
21                if let Some(close) = rest.find('}') {
22                    let names = rest[open + 1..close]
23                        .split(',')
24                        .map(|s| s.trim().to_string())
25                        .filter(|s| !s.is_empty())
26                        .collect::<Vec<_>>();
27                    let after = rest[close + 1..].trim();
28                    let from_prefix = "from";
29                    if after.starts_with(from_prefix) {
30                        let path_part = after[from_prefix.len()..].trim();
31                        let raw = path_part.trim().trim_matches('"');
32                        // Resolve relative to file_path
33                        let base = file_path
34                            .parent()
35                            .unwrap_or_else(|| std::path::Path::new("."));
36                        let joined = base.join(raw);
37                        let path = joined.to_string_lossy().to_string();
38                        return Ok(Statement::new(
39                            StatementKind::Import {
40                                names,
41                                source: path,
42                            },
43                            Value::Null,
44                            0,
45                            line_number,
46                            1,
47                        ));
48                    }
49                }
50            }
51        }
52
53        if line.starts_with("@export") {
54            // syntax: @export { a, b }
55            let rest = line["@export".len()..].trim();
56            if let Some(open) = rest.find('{') {
57                if let Some(close) = rest.find('}') {
58                    let names = rest[open + 1..close]
59                        .split(',')
60                        .map(|s| s.trim().to_string())
61                        .filter(|s| !s.is_empty())
62                        .collect::<Vec<_>>();
63                    return Ok(Statement::new(
64                        StatementKind::Export {
65                            names,
66                            source: String::new(),
67                        },
68                        Value::Null,
69                        0,
70                        line_number,
71                        1,
72                    ));
73                }
74            }
75        }
76
77        return Ok(Statement::new(
78            StatementKind::Unknown,
79            Value::String(line.to_string()),
80            0,
81            line_number,
82            1,
83        ));
84    }
85
86    let mut rest = line["@load".len()..].trim();
87    if rest.is_empty() {
88        return Err(anyhow!("@load directive requires a path"));
89    }
90
91    let path;
92    if rest.starts_with('"') {
93        if let Some(end) = rest[1..].find('"') {
94            let raw = rest[1..1 + end].to_string();
95            // Resolve relative paths relative to the file location
96            let base = file_path
97                .parent()
98                .unwrap_or_else(|| std::path::Path::new("."));
99            let joined = base.join(&raw);
100            path = joined.to_string_lossy().to_string();
101            rest = rest[1 + end + 1..].trim();
102        } else {
103            return Err(anyhow!("unterminated string literal in @load"));
104        }
105    } else {
106        let mut split = rest.splitn(2, char::is_whitespace);
107        path = split.next().unwrap_or_default().trim().to_string();
108        rest = split.next().unwrap_or("").trim();
109    }
110
111    let alias = if rest.starts_with("as ") {
112        rest[3..].trim().to_string()
113    } else {
114        Path::new(&path)
115            .file_stem()
116            .map(|s| s.to_string_lossy().to_string())
117            .unwrap_or_else(|| path.clone())
118    };
119
120    Ok(Statement::new(
121        StatementKind::Load {
122            source: path,
123            alias,
124        },
125        Value::Null,
126        0,
127        line_number,
128        1,
129    ))
130}
131
132/// Parse @use directive for plugins: @use author.plugin as alias
133/// Example: @use devaloop.acid as acid
134fn parse_use_directive(line: &str, line_number: usize) -> Result<Statement> {
135    let rest = line["@use".len()..].trim();
136    if rest.is_empty() {
137        return Err(anyhow!(
138            "@use directive requires a plugin reference (author.plugin)"
139        ));
140    }
141
142    // Parse plugin reference (author.plugin)
143    let plugin_ref;
144    let mut parts = rest.splitn(2, " as ");
145    plugin_ref = parts.next().unwrap_or_default().trim().to_string();
146
147    if !plugin_ref.contains('.') {
148        return Err(anyhow!("@use directive requires format: author.plugin"));
149    }
150
151    let plugin_parts: Vec<&str> = plugin_ref.split('.').collect();
152    if plugin_parts.len() != 2 {
153        return Err(anyhow!(
154            "@use directive requires format: author.plugin (got: {})",
155            plugin_ref
156        ));
157    }
158
159    let author = plugin_parts[0].to_string();
160    let plugin_name = plugin_parts[1].to_string();
161
162    // Parse alias if provided
163    let alias = if let Some(alias_part) = parts.next() {
164        alias_part.trim().to_string()
165    } else {
166        plugin_name.clone()
167    };
168
169    Ok(Statement::new(
170        StatementKind::UsePlugin {
171            author,
172            name: plugin_name,
173            alias,
174        },
175        Value::Null,
176        0,
177        line_number,
178        1,
179    ))
180}