tan/
api.rs

1// #todo find a better name, e.g. `lang`, `sys`, `runtime`, tbh api is a good name though.
2
3use std::path::Path;
4
5use crate::{
6    check::check,
7    context::Context,
8    error::Error,
9    eval::eval,
10    expr::Expr,
11    lexer::{token::Token, Lexer},
12    macro_expand::macro_expand,
13    optimize::optimize,
14    parser::Parser,
15    prune::prune,
16    range::Position,
17    util::fs::get_full_extension,
18};
19
20pub const TAN_FILE_EXTENSION: &str = "tan";
21
22pub const TAN_FILE_EMOJI_EXTENSION: &str = "👅";
23
24// #todo Add unit test.
25// #todo Implement strict version that checks exactly *.tan and skips *.*.tan, e.g. *.data.tan
26pub fn has_tan_extension(path: impl AsRef<Path>) -> bool {
27    let path = path.as_ref();
28    if let Some(extension) = path.extension() {
29        extension == TAN_FILE_EXTENSION || extension == TAN_FILE_EMOJI_EXTENSION
30    } else {
31        false
32    }
33}
34
35// #todo Add unit test
36/// A strict version of has_tan_extension that checks exactly `*.tan` and
37/// skips `*.*.tan`, e.g. `*.data.tan``.
38pub fn has_tan_extension_strict(path: impl AsRef<Path>) -> bool {
39    let path = path.as_ref();
40    if let Some(extension) = get_full_extension(path) {
41        extension == TAN_FILE_EXTENSION || extension == TAN_FILE_EMOJI_EXTENSION
42    } else {
43        false
44    }
45}
46
47// #todo Optimize this!
48pub fn strip_tan_extension(path: impl Into<String>) -> String {
49    let path = path.into();
50
51    if let Some(path) = path.strip_suffix(&format!(".{TAN_FILE_EXTENSION}")) {
52        path.to_owned()
53    } else if let Some(path) = path.strip_suffix(&format!(".{TAN_FILE_EMOJI_EXTENSION}")) {
54        path.to_owned()
55    } else {
56        path
57    }
58}
59
60/// Lexes a Tan expression encoded as a text string.
61pub fn lex_string(input: impl AsRef<str>) -> Result<Vec<Token>, Vec<Error>> {
62    let input = input.as_ref();
63    let mut lexer = Lexer::new(input);
64    lexer.lex()
65}
66
67// #todo temp solution for compatibility.
68// #todo remove this!
69/// Parses a Tan expression encoded as a text string, returns first expression.
70pub fn parse_string(input: impl AsRef<str>) -> Result<Expr, Vec<Error>> {
71    let input = input.as_ref();
72
73    let mut lexer = Lexer::new(input);
74    let tokens = lexer.lex()?;
75
76    let mut parser = Parser::new(&tokens);
77    let mut expr = parser.parse()?;
78
79    // #todo temp solution
80    let expr = expr.swap_remove(0);
81
82    Ok(expr)
83}
84
85// #todo Merge with parse_string.
86pub fn parse_string_with_position(
87    input: impl AsRef<str>,
88    start_position: Position,
89) -> Result<Expr, Vec<Error>> {
90    let input = input.as_ref();
91
92    let mut lexer = Lexer::new(input).with_position(start_position);
93    let tokens = lexer.lex()?;
94
95    let mut parser = Parser::new(&tokens).with_position(start_position);
96    let mut expr = parser.parse()?;
97
98    // #todo temp solution
99    let expr = expr.swap_remove(0);
100
101    Ok(expr)
102}
103
104// #insight Use the compile* functions if you don't need transient expressions in the AST.
105/// Parses a Tan expression encoded as a text string, returns all expressions parsed.
106pub fn parse_string_all(input: impl AsRef<str>) -> Result<Vec<Expr>, Vec<Error>> {
107    let input = input.as_ref();
108
109    let mut lexer = Lexer::new(input);
110    let tokens = lexer.lex()?;
111
112    let mut parser = Parser::new(&tokens);
113    let exprs = parser.parse()?;
114
115    Ok(exprs)
116}
117
118// #todo find a better name.
119pub fn compile(expr: Expr, context: &mut Context) -> Result<Expr, Vec<Error>> {
120    // #insight this is the main read/analysis pipeline, it consists of passes or stages.
121    // #todo better use the term `stage` (multi-stage programming)
122
123    // #insight
124    // Macro expansion should be performed before resolving.
125
126    // Prune pass
127
128    // #insight first prune pass needed before macro_expand.
129    // #todo find a better name for the `prune` stage.
130
131    let Some(expr) = prune(expr) else {
132        // The expression is pruned (elided)
133        // #todo what should be returned here?
134        return Ok(Expr::None);
135    };
136
137    // Expand macros.
138
139    // #todo pass a dummy scope here? no need to polute the dyn-time environment with macro stuff.
140    let expr = macro_expand(expr, context);
141
142    // #todo bug, macro_expand strips let annotation!
143
144    // #todo temp hack until macro_expand returns multiple errors.
145    let Ok(expr) = expr else {
146        return Err(vec![expr.unwrap_err()]);
147    };
148
149    // #todo maybe a second `prune` pass is needed?
150
151    let Some(expr) = expr else {
152        // The expression is pruned (elided)
153        // #insight elision can happen also in macro_expand!
154        return Ok(Expr::None);
155    };
156
157    // Check pass
158    // #todo confusion with upcoming `unchecked` keyword/concept.
159    // #todo find a better name (validation?)
160    // #todo move check after optimize? in resolve?
161    let expr = check(expr);
162    let Ok(expr) = expr else {
163        return Err(vec![expr.unwrap_err()]);
164    };
165
166    // Optimization pass
167
168    // #todo should run after resolve?
169    let expr = optimize(expr);
170
171    // Resolve pass (typechecking, definitions, etc)
172
173    // #todo should we push a new env?
174    // let mut resolver = Resolver::new();
175    // let expr = resolver.resolve(expr, context)?;
176
177    Ok(expr)
178}
179
180// #todo should it really update the context?
181// #todo should refactor
182/// Reads a Tan expression encoded as a text string, and 'compiles' it for evaluation.
183/// Updates the context with definitions.
184pub fn compile_string(
185    input: impl AsRef<str>,
186    context: &mut Context,
187) -> Result<Vec<Expr>, Vec<Error>> {
188    let exprs = parse_string_all(input)?;
189
190    // #todo also resolve static-use (normal use) here!
191
192    // #insight
193    //
194    // AST -> Executable pipeline:
195    //
196    // - macro-expand
197    // - resolve
198    // - optimize
199
200    // // Nice debugging tool!
201    // for ex in &exprs {
202    //     for e in ex.iter() {
203    //         println!("-- {e:?}");
204    //     }
205    // }
206
207    let mut compiled_exprs = Vec::new();
208
209    for expr in exprs {
210        // #todo should return option.
211        let expr = compile(expr, context)?;
212        if !expr.is_none() {
213            compiled_exprs.push(expr);
214        }
215    }
216
217    Ok(compiled_exprs)
218}
219
220// #todo a version where no context is passed, call it `exec_string` or `run_string`?
221// #todo this implements in essence a do block. Maybe no value should be returned?
222/// Evaluates a Tan expression encoded as a text string.
223pub fn eval_string(input: impl AsRef<str>, context: &mut Context) -> Result<Expr, Vec<Error>> {
224    let exprs = compile_string(input, context)?;
225
226    let mut last_value = Expr::None;
227
228    for expr in exprs {
229        let value = eval(&expr, context);
230
231        let Ok(value) = value else {
232            return Err(vec![value.unwrap_err()]);
233        };
234
235        last_value = value;
236    }
237
238    Ok(last_value)
239}
240
241// #todo implement run() and run_string() (encapsulate Context)
242
243#[cfg(test)]
244mod tests {
245    use crate::api::{has_tan_extension, has_tan_extension_strict, strip_tan_extension};
246
247    #[test]
248    fn strip_tan_extension_usage() {
249        assert_eq!(strip_tan_extension("hello.tan"), "hello");
250        assert_eq!(
251            strip_tan_extension("deep/nesting/hello.tan"),
252            "deep/nesting/hello"
253        );
254        assert_eq!(strip_tan_extension("emoji/works.👅"), "emoji/works");
255    }
256
257    #[test]
258    fn has_tan_extension_usage() {
259        assert!(has_tan_extension("hello.tan"));
260        assert!(has_tan_extension("hello.data.tan"));
261        assert!(has_tan_extension("nested/dir/hello.data.tan"));
262        assert!(has_tan_extension("nested/.hidden/dir/hello.data.tan"));
263        assert!(has_tan_extension(
264            "nested/.local-tan-root/@std/zonk/main.tan"
265        ));
266    }
267
268    #[test]
269    fn has_tan_extension_strict_usage() {
270        assert!(has_tan_extension_strict("hello.tan"));
271        assert!(!has_tan_extension_strict("hello.data.tan"));
272        assert!(has_tan_extension_strict("nested/.hidden/dir/hello.tan"));
273    }
274}