Skip to main content

tancore/
lang.rs

1use std::{
2    collections::HashMap,
3    path::PathBuf,
4    sync::{Mutex, OnceLock},
5};
6
7use libloading::Library;
8
9use tan::{
10    api::compile_string,
11    context::Context,
12    error::Error,
13    eval::{
14        eval,
15        util::{eval_file, resolve_module_path},
16    },
17    expr::{annotate, expr_clone, Expr},
18    util::{
19        args::{unpack_arg, unpack_map_arg, unpack_stringable_arg, unpack_symbolic_arg},
20        module_util::require_module,
21        standard_names::CURRENT_MODULE_PATH,
22    },
23};
24
25// #todo move to another place.
26static FOREIGN_DYN_LIB_MAP: OnceLock<Mutex<HashMap<String, Library>>> = OnceLock::new();
27
28// #todo (if (= (get-type obj) Amount) ...) ; type, get-type, type-of
29// #todo implement `type?` e.g. (if (type? obj Amount) ...)
30// #todo op to set annotation.
31
32// #todo consider meta instead of ann
33// #todo consider get-ann? or (better ann-of)
34// #todo where is this used?
35// #todo extract *_impl function.
36pub fn ann(args: &[Expr], context: &mut Context) -> Result<Expr, Error> {
37    if args.len() != 1 {
38        return Err(Error::invalid_arguments(
39            "`ann` requires one argument",
40            None,
41        ));
42    }
43
44    // #todo support multiple arguments.
45
46    let expr = args.first().unwrap();
47
48    let expr = eval(expr, context)?;
49
50    if let Some(ann) = expr.annotations() {
51        Ok(Expr::map(ann.clone()))
52    } else {
53        // #todo what to return here?
54        // Ok(Expr::map(HashMap::new()))
55        Ok(Expr::None)
56    }
57}
58
59// #insight clojure passes the expression as the first argument.
60// #todo consider 'inverse' design, e.g. (with-ann anns expr)
61// (with-ann expr {:type Amount})
62pub fn with_ann(args: &[Expr]) -> Result<Expr, Error> {
63    let annotations = unpack_map_arg(args, 0, "annotations")?;
64    let target = unpack_arg(args, 1, "target")?;
65    Ok(Expr::annotated(expr_clone(target), &annotations))
66}
67
68// #todo implement (with-type ...) with Tan code?
69pub fn with_type(args: &[Expr]) -> Result<Expr, Error> {
70    let type_expr = unpack_arg(args, 0, "type")?;
71    let target = unpack_arg(args, 1, "target")?;
72    Ok(annotate(expr_clone(target), "type", type_expr.clone()))
73}
74
75// #insight this function returns a string, it does not write to stdout.
76// #todo also have a version that prints to stdout?
77pub fn debug_expr(args: &[Expr]) -> Result<Expr, Error> {
78    let [expr, ..] = args else {
79        // #todo better error
80        return Err(Error::invalid_arguments("invalid arguments", None));
81    };
82
83    let s = match expr {
84        Expr::Annotated(expr, ann) => format!("ANN({expr:?}, {ann:?})"),
85        _ => format!("{expr:?}"),
86    };
87
88    Ok(Expr::string(s))
89}
90
91pub fn type_of(args: &[Expr], context: &mut Context) -> Result<Expr, Error> {
92    let [expr] = args else {
93        // #todo better error
94        return Err(Error::invalid_arguments("one argument expected", None));
95    };
96
97    Ok(expr.dyn_type(context))
98}
99
100pub fn is_a(args: &[Expr], context: &mut Context) -> Result<Expr, Error> {
101    let typ = unpack_symbolic_arg(args, 0, "typ")?;
102    let expr = unpack_arg(args, 1, "expr")?;
103
104    let dyn_type = expr.dyn_type(context);
105    let Some(dyn_typ) = dyn_type.as_type() else {
106        // #todo add range here.
107        return Err(Error::general("cannot compute dyn type"));
108    };
109
110    Ok(Expr::Bool(typ == dyn_typ))
111}
112
113// #todo implement read from Port, and provide variant where a Port is a File.
114// #insight could replace with (eval (read file-contents))
115// #todo maybe should be just eval_module?
116// #todo consider naming just `load`.
117pub fn load_file(args: &[Expr], context: &mut Context) -> Result<Expr, Error> {
118    let [path] = args else {
119        return Err(Error::invalid_arguments("requires `path` argument", None));
120    };
121
122    let Some(path) = path.as_stringable() else {
123        return Err(Error::invalid_arguments(
124            "`path` argument should be a Stringable",
125            path.range(),
126        ));
127    };
128
129    // #todo canonicalize path, support relative paths?
130    // #todo similar code in eval "use", refactor!
131
132    // #think: do we need a nested scope? I think not! should be an option or multiple functions
133
134    // #todo I _think_this causes the crazy 'templates/layout' bug.
135
136    // let prev_scope = context.scope.clone();
137    // context.scope = Rc::new(Scope::new(prev_scope.clone()));
138
139    // #todo #hack temp solution here! somehow unify with eval_module.
140
141    let prev_current_module_path = context.scope.get(CURRENT_MODULE_PATH);
142
143    // #todo use context.insert_special!
144    context.scope.insert(
145        CURRENT_MODULE_PATH,
146        Expr::string(
147            PathBuf::from(&path)
148                .parent()
149                .unwrap()
150                .to_string_lossy()
151                .to_string(),
152        ),
153    );
154
155    let result = eval_file(path, context);
156
157    if let Some(prev_current_module_path) = prev_current_module_path {
158        context
159            .scope
160            .insert(CURRENT_MODULE_PATH, prev_current_module_path);
161    }
162
163    // context.scope = prev_scope;
164
165    match result {
166        Ok(value) => Ok(value),
167        Err(errors) => {
168            // #todo precise formating is _required_ here!
169            // eprintln!("{}", format_errors(&errors));
170            // #todo for some reason, errors are lost here.
171            // #todo seems that (map ...) drinks the errors.
172            dbg!(&errors);
173            // #todo add note with information here!
174            // #todo add custom error here, e.g. failed_file_load
175            // println!("/////// {errors:?}");
176            // println!("{}", std::backtrace::Backtrace::force_capture());
177            Err(Error::failed_use(path, errors))
178        }
179    }
180}
181
182// #insight plain eval is already provided as a builtin/special form.
183
184// #todo this is not really needed, can use `(eval (read string))` instead.
185pub fn eval_string(args: &[Expr], context: &mut Context) -> Result<Expr, Error> {
186    // #todo support all `Stringable`s
187
188    if let Some(input) = args.first() {
189        let Some(input_str) = input.as_string() else {
190            return Err(Error::invalid_arguments(
191                "expected String argument",
192                input.range(),
193            ));
194        };
195
196        // #todo should create a throwaway context instead?
197
198        // #todo think carefully which eval function to use.
199        // let result = eval_string(input, &mut context);
200        let result = compile_string(input_str, context);
201
202        if let Ok(exprs) = result {
203            // #todo what would be the correct initialization?
204            let mut value = Expr::None;
205            for expr in exprs {
206                value = eval(&expr, context)?;
207                // if let Err(mut error) = eval(&expr, context) {
208                //     // #todo add a unit test to check that the file_path is added here!
209                //     // #todo just make error.file_path optional and avoid this hack here!!!
210                //     // if error.file_path == INPUT_PSEUDO_FILE_PATH {
211                //     //     error.file_path = file_path.clone();
212                //     // }
213
214                //     // #todo better error here!
215                //     return Err(error);
216                // }
217            }
218            Ok(value)
219        } else {
220            // #todo something more clever needed here!
221            // #todo use an aggregate Error, something like Error::failed_use()
222            dbg!(&result);
223            Err(Error::general("cannot read string, eval failed"))
224        }
225    } else {
226        Err(Error::invalid_arguments("expected one argument", None))
227    }
228}
229
230/// #todo Find a good name for this function.
231fn curry(params: &[Expr], body: &[Expr]) -> Option<(Vec<Expr>, Vec<Expr>)> {
232    params.first().map(|param| {
233        let rest_params = &params[1..];
234
235        let curried_body = if let Some((nested_params, nested_body)) = curry(rest_params, body) {
236            vec![Expr::List(vec![
237                Expr::Type("Func".to_owned()),
238                Expr::array(nested_params),
239                nested_body.first().unwrap().clone(), // #todo Remove unwrap!
240            ])]
241        } else {
242            body.to_owned()
243        };
244
245        (vec![param.clone()], curried_body)
246    })
247}
248
249// #todo Also support passing argument, implements partial application:
250//
251// (let greet (Func [msg name] "${msg} ${name}"))
252// (let hello (curry greet "Hello"))
253// (let hello "George") ; => "Hello George"
254//
255// Currently you can do:
256// (let hello ((curry greet) "Hello"))
257// (let hello "George") ; => "Hello George"
258//
259// #todo Convert to macro, evaluate at compile/static time.
260// #todo #insight Cannot curry a function with zero parameters, just return the function unchanged!
261// (let add1 (Func [x y] (+ x y 1)))
262// (let curried-add1 (curry add1))
263pub fn func_curry(args: &[Expr]) -> Result<Expr, Error> {
264    let Some(func) = args.first() else {
265        return Err(Error::invalid_arguments("expected `func` argument", None));
266    };
267
268    let Expr::Func(params, body, func_scope, filename) = func.unpack() else {
269        return Err(Error::invalid_arguments(
270            "`func` argument should be a Func",
271            None,
272        ));
273    };
274
275    let (params, curried_body) = curry(params, body).unwrap_or((vec![], body.clone()));
276
277    // #todo Also re-apply annotations.
278
279    let curried_func = Expr::Func(params, curried_body, func_scope.clone(), filename.clone());
280    Ok(curried_func)
281}
282
283// #todo Move to main `tan` crate?
284// #todo consider link_foreign_dyn_lib (and unlink_...)
285// #todo introduce unlink_foreign_dyn_lib for completeness.
286// #todo find a better name, consider `use` or `load` instead of `install`.
287pub fn link_foreign_dyn_lib(args: &[Expr], context: &mut Context) -> Result<Expr, Error> {
288    let dyn_lib_path = unpack_stringable_arg(args, 0, "path")?;
289
290    // #todo find a better name for canonicalize_module_path.
291    // #todo use another function, not module_path specific, don't rewrite if it starts with "/"
292    let dyn_lib_path = resolve_module_path(dyn_lib_path, context)?;
293
294    // #todo canonicalize the path, resolve paths relative to CURRENT_MODULE_PATH
295
296    // #todo add unit tests!
297
298    unsafe {
299        // #insight #WARNING
300        // to make sure the (foreign) dynamic library is not dropped, move
301        // it to a static hashmap. If the library gets dropped, calling a function
302        // in the library would trigger a core dump.
303        let foreign_dyn_lib_map = FOREIGN_DYN_LIB_MAP.get_or_init(|| Mutex::new(HashMap::new()));
304
305        // #insight the MutexGuard is implicitly dropped.
306        if foreign_dyn_lib_map
307            .lock()
308            // #todo abstract the handling of poisoned lock.
309            .expect("poisoned lock")
310            .contains_key(&dyn_lib_path)
311        {
312            // #todo Should provide an option to overrid/reinstall?
313            // #todo consider not throwing an error, and just nop?
314            // #todo consider just a warning (add support for warnings)
315            // #todo more specific error variant needed.
316            // return Err(Error::general(&format!(
317            //     "foreign dyn lib `{dyn_lib_path}` is already installed"
318            // )));
319            return Ok(Expr::None);
320        }
321
322        let library = match Library::new(&dyn_lib_path) {
323            Ok(library) => library,
324            Err(error) => {
325                // #todo seems the error is not surfaced when used in a tab module, or something worse happens.
326                // #todo more specific error variant needed.
327                return Err(Error::general(&format!(
328                    "cannot open foreign dyn lib `{dyn_lib_path}`: {error}"
329                )));
330            }
331        };
332
333        let link_foreign_dyn_lib =
334            match library.get::<unsafe fn(&mut Context) -> i32>(b"install_foreign_dyn_lib\0") {
335                Ok(link_foreign_dyn_lib) => link_foreign_dyn_lib,
336                Err(error) => {
337                    return Err(Error::general(&format!(
338                        "cannot get link_foreign_dyn_lib for `{dyn_lib_path}`: {error}"
339                    )));
340                }
341            };
342
343        link_foreign_dyn_lib(context);
344
345        // #insight
346        // to make sure the (foreign) dynamic library is not dropped, move
347        // it to a static hashmap.
348        foreign_dyn_lib_map
349            .lock()
350            .expect("poisoned lock")
351            .insert(dyn_lib_path, library);
352    }
353
354    Ok(Expr::None)
355}
356
357pub fn setup_lib_lang(context: &mut Context) {
358    let module = require_module("prelude", context);
359
360    // #todo separate read/read-string.
361
362    module.insert_invocable("ann", Expr::foreign_func_mut_context(&ann));
363    module.insert_invocable("with-ann", Expr::foreign_func(&with_ann));
364    // #todo #deprecate Use cast instead.
365    module.insert_invocable("with-type", Expr::foreign_func(&with_type));
366    // #todo Is this a better name? Is it OK to reserve? Maybe it's better to just use #Type.
367    // #todo Should be temp.
368    module.insert_invocable("cast", Expr::foreign_func(&with_type));
369
370    // #todo the `!` is confusing here.
371    // #todo `dbg` is not following naming conventions, but maybe OK for this case?
372    // #insight this function returns a string, it does not write to stdout!
373    module.insert_invocable("dbg!", Expr::foreign_func(&debug_expr));
374
375    module.insert_invocable("type-of", Expr::foreign_func_mut_context(&type_of));
376    module.insert_invocable("is-a?", Expr::foreign_func_mut_context(&is_a));
377
378    // #todo we also need an (eval ...) function.
379
380    // #todo hmm, needs a differerent name
381    // #todo use `(eval (read string))` instead?
382    module.insert_invocable("eval-string", Expr::foreign_func_mut_context(&eval_string));
383    module.insert_invocable(
384        "eval-string$$String",
385        Expr::foreign_func_mut_context(&eval_string),
386    );
387
388    module.insert_invocable("load-file", Expr::foreign_func_mut_context(&load_file));
389
390    // #todo Move to a namespace, e.g. `/func`.
391    module.insert_invocable("curry", Expr::foreign_func(&func_curry));
392
393    module.insert_invocable(
394        "link-foreign-dyn-lib",
395        Expr::foreign_func_mut_context(&link_foreign_dyn_lib),
396    );
397}