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 = ¶ms[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}