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
5/// Parse directive keywords (import, export, use, load) without "@" prefix
6pub fn parse_directive_keyword(
7    line: &str,
8    keyword: &str,
9    line_number: usize,
10    file_path: &std::path::Path,
11) -> Result<Statement> {
12    match keyword {
13        "use" => parse_use_directive(line, line_number),
14        "load" => parse_load_directive(line, line_number, file_path),
15        "import" => parse_import_directive(line, line_number, file_path),
16        "export" => parse_export_directive(line, line_number),
17        _ => Err(anyhow!("Unknown directive: {}", keyword)),
18    }
19}
20
21fn parse_import_directive(
22    line: &str,
23    line_number: usize,
24    file_path: &std::path::Path,
25) -> Result<Statement> {
26    // syntax: import { a, b } from "path"
27    let rest = line["import".len()..].trim();
28    if let Some(open) = rest.find('{') {
29        if let Some(close) = rest.find('}') {
30            let names = rest[open + 1..close]
31                .split(',')
32                .map(|s| s.trim().to_string())
33                .filter(|s| !s.is_empty())
34                .collect::<Vec<_>>();
35            let after = rest[close + 1..].trim();
36            let from_prefix = "from";
37            if after.starts_with(from_prefix) {
38                let path_part = after[from_prefix.len()..].trim();
39                let raw = path_part.trim().trim_matches('"');
40                // Resolve relative to file_path
41                let base = file_path
42                    .parent()
43                    .unwrap_or_else(|| std::path::Path::new("."));
44                let joined = base.join(raw);
45                let path = joined.to_string_lossy().to_string();
46                return Ok(Statement::new(
47                    StatementKind::Import {
48                        names,
49                        source: path,
50                    },
51                    Value::Null,
52                    0,
53                    line_number,
54                    1,
55                ));
56            }
57        }
58    }
59    Err(anyhow!(
60        "Invalid import syntax. Use: import {{ a, b }} from \"path\""
61    ))
62}
63
64fn parse_export_directive(line: &str, line_number: usize) -> Result<Statement> {
65    // syntax: export { a, b }
66    let rest = line["export".len()..].trim();
67    if let Some(open) = rest.find('{') {
68        if let Some(close) = rest.find('}') {
69            let names = rest[open + 1..close]
70                .split(',')
71                .map(|s| s.trim().to_string())
72                .filter(|s| !s.is_empty())
73                .collect::<Vec<_>>();
74            return Ok(Statement::new(
75                StatementKind::Export {
76                    names,
77                    source: String::new(),
78                },
79                Value::Null,
80                0,
81                line_number,
82                1,
83            ));
84        }
85    }
86    Err(anyhow!("Invalid export syntax. Use: export {{ a, b }}"))
87}
88
89fn parse_load_directive(
90    line: &str,
91    line_number: usize,
92    file_path: &std::path::Path,
93) -> Result<Statement> {
94    let mut rest = line["load".len()..].trim();
95    if rest.is_empty() {
96        return Err(anyhow!("load directive requires a path"));
97    }
98
99    let path;
100    if rest.starts_with('"') {
101        if let Some(end) = rest[1..].find('"') {
102            let raw = rest[1..1 + end].to_string();
103            // Resolve relative paths relative to the file location
104            let base = file_path
105                .parent()
106                .unwrap_or_else(|| std::path::Path::new("."));
107            let joined = base.join(&raw);
108            path = joined.to_string_lossy().to_string();
109            rest = rest[1 + end + 1..].trim();
110        } else {
111            return Err(anyhow!("unterminated string literal in load"));
112        }
113    } else {
114        let mut split = rest.splitn(2, char::is_whitespace);
115        path = split.next().unwrap_or_default().trim().to_string();
116        rest = split.next().unwrap_or("").trim();
117    }
118
119    let alias = if rest.starts_with("as ") {
120        rest[3..].trim().to_string()
121    } else {
122        Path::new(&path)
123            .file_stem()
124            .map(|s| s.to_string_lossy().to_string())
125            .unwrap_or_else(|| path.clone())
126    };
127
128    Ok(Statement::new(
129        StatementKind::Load {
130            source: path,
131            alias,
132        },
133        Value::Null,
134        0,
135        line_number,
136        1,
137    ))
138}
139
140/// Parse use directive for plugins: use author.plugin as alias
141/// Example: use devaloop.acid as acid
142fn parse_use_directive(line: &str, line_number: usize) -> Result<Statement> {
143    let rest = line["use".len()..].trim();
144    if rest.is_empty() {
145        return Err(anyhow!(
146            "use directive requires a plugin reference (author.plugin)"
147        ));
148    }
149
150    // Parse plugin reference (author.plugin)
151    let plugin_ref;
152    let mut parts = rest.splitn(2, " as ");
153    plugin_ref = parts.next().unwrap_or_default().trim().to_string();
154
155    if !plugin_ref.contains('.') {
156        return Err(anyhow!("use directive requires format: author.plugin"));
157    }
158
159    let plugin_parts: Vec<&str> = plugin_ref.split('.').collect();
160    if plugin_parts.len() != 2 {
161        return Err(anyhow!(
162            "use directive requires format: author.plugin (got: {})",
163            plugin_ref
164        ));
165    }
166
167    let author = plugin_parts[0].to_string();
168    let plugin_name = plugin_parts[1].to_string();
169
170    // Parse alias if provided
171    let alias = if let Some(alias_part) = parts.next() {
172        alias_part.trim().to_string()
173    } else {
174        plugin_name.clone()
175    };
176
177    Ok(Statement::new(
178        StatementKind::UsePlugin {
179            author,
180            name: plugin_name,
181            alias,
182        },
183        Value::Null,
184        0,
185        line_number,
186        1,
187    ))
188}
189
190// Old @ prefix support kept for backwards compatibility, but deprecated
191pub fn parse_directive(
192    line: &str,
193    line_number: usize,
194    file_path: &std::path::Path,
195) -> Result<Statement> {
196    eprintln!(
197        "⚠️  WARNING: @ prefix for directives is deprecated. Use 'import', 'export', 'use', 'load' instead."
198    );
199
200    if line.starts_with("@use") {
201        return parse_use_directive(line, line_number);
202    }
203
204    if !line.starts_with("@load") {
205        if line.starts_with("@import") {
206            let rest = line["@import".len()..].trim();
207            if let Some(open) = rest.find('{') {
208                if let Some(close) = rest.find('}') {
209                    let names = rest[open + 1..close]
210                        .split(',')
211                        .map(|s| s.trim().to_string())
212                        .filter(|s| !s.is_empty())
213                        .collect::<Vec<_>>();
214                    let after = rest[close + 1..].trim();
215                    let from_prefix = "from";
216                    if after.starts_with(from_prefix) {
217                        let path_part = after[from_prefix.len()..].trim();
218                        let raw = path_part.trim().trim_matches('"');
219                        let base = file_path
220                            .parent()
221                            .unwrap_or_else(|| std::path::Path::new("."));
222                        let joined = base.join(raw);
223                        let path = joined.to_string_lossy().to_string();
224                        return Ok(Statement::new(
225                            StatementKind::Import {
226                                names,
227                                source: path,
228                            },
229                            Value::Null,
230                            0,
231                            line_number,
232                            1,
233                        ));
234                    }
235                }
236            }
237        }
238
239        if line.starts_with("@export") {
240            let rest = line["@export".len()..].trim();
241            if let Some(open) = rest.find('{') {
242                if let Some(close) = rest.find('}') {
243                    let names = rest[open + 1..close]
244                        .split(',')
245                        .map(|s| s.trim().to_string())
246                        .filter(|s| !s.is_empty())
247                        .collect::<Vec<_>>();
248                    return Ok(Statement::new(
249                        StatementKind::Export {
250                            names,
251                            source: String::new(),
252                        },
253                        Value::Null,
254                        0,
255                        line_number,
256                        1,
257                    ));
258                }
259            }
260        }
261
262        return Ok(Statement::new(
263            StatementKind::Unknown,
264            Value::String(line.to_string()),
265            0,
266            line_number,
267            1,
268        ));
269    }
270
271    let mut rest = line["@load".len()..].trim();
272    if rest.is_empty() {
273        return Err(anyhow!("@load directive requires a path"));
274    }
275
276    let path;
277    if rest.starts_with('"') {
278        if let Some(end) = rest[1..].find('"') {
279            let raw = rest[1..1 + end].to_string();
280            let base = file_path
281                .parent()
282                .unwrap_or_else(|| std::path::Path::new("."));
283            let joined = base.join(&raw);
284            path = joined.to_string_lossy().to_string();
285            rest = rest[1 + end + 1..].trim();
286        } else {
287            return Err(anyhow!("unterminated string literal in @load"));
288        }
289    } else {
290        let mut split = rest.splitn(2, char::is_whitespace);
291        path = split.next().unwrap_or_default().trim().to_string();
292        rest = split.next().unwrap_or("").trim();
293    }
294
295    let alias = if rest.starts_with("as ") {
296        rest[3..].trim().to_string()
297    } else {
298        Path::new(&path)
299            .file_stem()
300            .map(|s| s.to_string_lossy().to_string())
301            .unwrap_or_else(|| path.clone())
302    };
303
304    Ok(Statement::new(
305        StatementKind::Load {
306            source: path,
307            alias,
308        },
309        Value::Null,
310        0,
311        line_number,
312        1,
313    ))
314}