1use std::cell::{Cell, RefCell};
7use std::collections::BTreeMap;
8
9use crate::ast::{BinaryOp, Expr, UnaryOp};
10use crate::functions;
11use crate::note::{Corpus, Note};
12use crate::value::{BaseDate, Value};
13
14pub struct EvalCtx<'a> {
19 pub note: &'a Note,
20 pub corpus: &'a Corpus,
21 pub this: Option<&'a Note>,
22 pub formulas: &'a BTreeMap<String, Expr>,
24 active: RefCell<Vec<String>>,
26 cache: RefCell<BTreeMap<String, Value>>,
28 depth: Cell<usize>,
31}
32
33pub const MAX_EVAL_DEPTH: usize = 512;
37
38impl<'a> EvalCtx<'a> {
39 pub fn new(note: &'a Note, corpus: &'a Corpus, formulas: &'a BTreeMap<String, Expr>) -> Self {
40 Self {
41 note,
42 corpus,
43 this: None,
44 formulas,
45 active: RefCell::new(Vec::new()),
46 cache: RefCell::new(BTreeMap::new()),
47 depth: Cell::new(0),
48 }
49 }
50
51 pub fn with_this(mut self, this: Option<&'a Note>) -> Self {
52 self.this = this;
53 self
54 }
55
56 pub fn eval_formula(&self, name: &str) -> Value {
59 if let Some(v) = self.cache.borrow().get(name) {
60 return v.clone();
61 }
62 if self.active.borrow().iter().any(|n| n == name) {
63 return Value::Null; }
65 let Some(expr) = self.formulas.get(name) else {
66 return Value::Null;
67 };
68 self.active.borrow_mut().push(name.to_string());
69 let v = self.eval(expr);
70 self.active.borrow_mut().pop();
71 self.cache.borrow_mut().insert(name.to_string(), v.clone());
72 v
73 }
74
75 pub fn eval(&self, expr: &Expr) -> Value {
78 let d = self.depth.get();
79 if d >= MAX_EVAL_DEPTH {
80 return Value::Null;
81 }
82 self.depth.set(d + 1);
83 let v = self.eval_inner(expr);
84 self.depth.set(d);
85 v
86 }
87
88 fn eval_inner(&self, expr: &Expr) -> Value {
89 match expr {
90 Expr::Null => Value::Null,
91 Expr::Bool(b) => Value::Bool(*b),
92 Expr::Number(n) => Value::Number(*n),
93 Expr::Str(s) => Value::Str(s.clone()),
94 Expr::Regex(p, f) => Value::Str(format!("/{p}/{f}")), Expr::Ident(name) => self.ident(name),
96 Expr::Member(obj, name) => self.member(obj, name),
97 Expr::Index(obj, idx) => self.index(obj, idx),
98 Expr::Call(callee, args) => self.call(callee, args),
99 Expr::Unary(op, e) => self.unary(*op, e),
100 Expr::Binary(op, l, r) => self.binary(*op, l, r),
101 }
102 }
103
104 fn ident(&self, name: &str) -> Value {
108 match name {
109 "file" | "note" | "formula" => Value::Null,
111 "this" => self
112 .this
113 .map(|n| Value::Object(n.properties.clone()))
114 .unwrap_or(Value::Null),
115 _ => self.note.note_property(name),
116 }
117 }
118
119 fn member(&self, obj: &Expr, name: &str) -> Value {
120 if let Expr::Ident(ns) = obj {
122 match ns.as_str() {
123 "file" => return self.note.file_property(name),
124 "note" => return self.note.note_property(name),
125 "formula" => return self.eval_formula(name),
126 "this" => {
127 let this = self.this.unwrap_or(self.note);
128 if name == "file" {
131 return Value::Object(file_object(this));
134 }
135 return this.note_property(name);
136 }
137 _ => {}
138 }
139 }
140 let recv = self.eval(obj);
142 value_member(&recv, name)
143 }
144
145 fn index(&self, obj: &Expr, idx: &Expr) -> Value {
146 let recv = self.eval(obj);
147 let key = self.eval(idx);
148 match (&recv, &key) {
149 (Value::List(items), Value::Number(n)) => match int_index(*n, items.len()) {
150 Some(i) => items[i].clone(),
151 None => Value::Null,
152 },
153 (Value::Object(map), Value::Str(k)) => map.get(k).cloned().unwrap_or(Value::Null),
154 (Value::Str(s), Value::Number(n)) => {
155 let chars: Vec<char> = s.chars().collect();
156 match int_index(*n, chars.len()) {
157 Some(i) => Value::Str(chars[i].to_string()),
158 None => Value::Null,
159 }
160 }
161 _ => Value::Null,
162 }
163 }
164
165 fn call(&self, callee: &Expr, args: &[Expr]) -> Value {
166 match callee {
167 Expr::Ident(name) => functions::global_call(name, args, self),
169 Expr::Member(obj, method) => {
172 if let Expr::Ident(ns) = &**obj {
173 match ns.as_str() {
174 "file" => return functions::file_method(self.note, method, args, self),
175 "this" => {
176 let this = self.this.unwrap_or(self.note);
177 return functions::file_method(this, method, args, self);
178 }
179 _ => {}
180 }
181 }
182 if let Expr::Member(inner, f) = &**obj {
184 if matches!(&**inner, Expr::Ident(n) if n == "this") && f == "file" {
185 let this = self.this.unwrap_or(self.note);
186 return functions::file_method(this, method, args, self);
187 }
188 }
189 let recv = self.eval(obj);
190 functions::method_call(&recv, method, args, self)
191 }
192 _ => Value::Null,
193 }
194 }
195
196 fn unary(&self, op: UnaryOp, e: &Expr) -> Value {
197 let v = self.eval(e);
198 match op {
199 UnaryOp::Not => Value::Bool(!v.is_truthy()),
200 UnaryOp::Neg => v
201 .as_number()
202 .map(|n| Value::Number(-n))
203 .unwrap_or(Value::Null),
204 }
205 }
206
207 fn binary(&self, op: BinaryOp, l: &Expr, r: &Expr) -> Value {
208 match op {
210 BinaryOp::And => {
211 let lv = self.eval(l);
212 return if !lv.is_truthy() {
213 Value::Bool(false)
214 } else {
215 Value::Bool(self.eval(r).is_truthy())
216 };
217 }
218 BinaryOp::Or => {
219 let lv = self.eval(l);
220 return if lv.is_truthy() {
221 Value::Bool(true)
222 } else {
223 Value::Bool(self.eval(r).is_truthy())
224 };
225 }
226 _ => {}
227 }
228 let lv = self.eval(l);
229 let rv = self.eval(r);
230 match op {
231 BinaryOp::Eq => Value::Bool(lv.loose_eq(&rv)),
232 BinaryOp::NotEq => Value::Bool(!lv.loose_eq(&rv)),
233 BinaryOp::Lt => Value::Bool(lv.loose_cmp(&rv).is_lt()),
234 BinaryOp::Gt => Value::Bool(lv.loose_cmp(&rv).is_gt()),
235 BinaryOp::LtEq => Value::Bool(lv.loose_cmp(&rv).is_le()),
236 BinaryOp::GtEq => Value::Bool(lv.loose_cmp(&rv).is_ge()),
237 BinaryOp::Add => add(&lv, &rv),
238 BinaryOp::Sub => sub(&lv, &rv),
239 BinaryOp::Mul => arith(&lv, &rv, |a, b| a * b),
240 BinaryOp::Div => arith(&lv, &rv, |a, b| a / b),
241 BinaryOp::Mod => arith(&lv, &rv, |a, b| a % b),
242 BinaryOp::And | BinaryOp::Or => unreachable!(),
243 }
244 }
245}
246
247fn file_object(note: &Note) -> BTreeMap<String, Value> {
249 let mut m = BTreeMap::new();
250 for f in [
251 "name", "basename", "path", "folder", "ext", "size", "ctime", "mtime", "tags", "links",
252 ] {
253 m.insert(f.to_string(), note.file_property(f));
254 }
255 m
256}
257
258fn int_index(n: f64, len: usize) -> Option<usize> {
262 if !n.is_finite() || n.fract() != 0.0 {
263 return None;
264 }
265 let i = n as i64;
266 let len_i = len as i64;
267 let i = if i < 0 { len_i + i } else { i };
268 if i >= 0 && i < len_i {
269 Some(i as usize)
270 } else {
271 None
272 }
273}
274
275pub fn value_member(recv: &Value, name: &str) -> Value {
278 match recv {
279 Value::List(items) => match name {
280 "length" => Value::Number(items.len() as f64),
281 _ => Value::Null,
282 },
283 Value::Str(s) => match name {
284 "length" => Value::Number(s.chars().count() as f64),
285 _ => Value::Null,
286 },
287 Value::Object(map) => map.get(name).cloned().unwrap_or(Value::Null),
288 Value::Date(d) => date_field(d, name),
289 Value::Link(l) => match name {
290 "path" => Value::Str(l.path.clone()),
291 "display" => l.display.clone().map(Value::Str).unwrap_or(Value::Null),
292 _ => Value::Null,
293 },
294 _ => Value::Null,
295 }
296}
297
298fn date_field(d: &BaseDate, name: &str) -> Value {
299 match name {
300 "year" => Value::Number(d.year as f64),
301 "month" => Value::Number(d.month as f64),
302 "day" => Value::Number(d.day as f64),
303 "hour" => Value::Number(d.hour as f64),
304 "minute" => Value::Number(d.minute as f64),
305 "second" => Value::Number(d.second as f64),
306 "millisecond" => Value::Number(d.millisecond as f64),
307 _ => Value::Null,
308 }
309}
310
311fn add(l: &Value, r: &Value) -> Value {
314 if let (Value::Date(d), Value::Duration(ms)) = (l, r) {
316 return Value::Date(add_duration(d, *ms));
317 }
318 if let (Value::Duration(ms), Value::Date(d)) = (l, r) {
319 return Value::Date(add_duration(d, *ms));
320 }
321 if let (Value::Duration(a), Value::Duration(b)) = (l, r) {
322 return Value::Duration(a + b);
323 }
324 if matches!(l, Value::Str(_)) || matches!(r, Value::Str(_)) {
326 return Value::Str(format!("{}{}", l.as_str_coerced(), r.as_str_coerced()));
327 }
328 match (l.as_number(), r.as_number()) {
329 (Some(a), Some(b)) => Value::Number(a + b),
330 _ => Value::Str(format!("{}{}", l.as_str_coerced(), r.as_str_coerced())),
331 }
332}
333
334fn sub(l: &Value, r: &Value) -> Value {
336 if let (Value::Date(d), Value::Duration(ms)) = (l, r) {
337 return Value::Date(add_duration(d, -*ms));
338 }
339 if let (Value::Date(a), Value::Date(b)) = (l, r) {
340 return Value::Duration(a.epoch_millis() - b.epoch_millis());
341 }
342 if let (Value::Duration(a), Value::Duration(b)) = (l, r) {
343 return Value::Duration(a - b);
344 }
345 arith(l, r, |a, b| a - b)
346}
347
348fn arith(l: &Value, r: &Value, f: impl Fn(f64, f64) -> f64) -> Value {
349 match (l.as_number(), r.as_number()) {
350 (Some(a), Some(b)) => Value::Number(f(a, b)),
351 _ => Value::Null,
352 }
353}
354
355pub fn add_duration(d: &BaseDate, ms: i64) -> BaseDate {
357 let total = d.epoch_millis() + ms;
358 date_from_epoch_millis(total, d.has_time || ms % 86_400_000 != 0)
359}
360
361pub fn date_from_epoch_millis(ms: i64, has_time: bool) -> BaseDate {
364 let mut days = ms.div_euclid(86_400_000);
365 let mut rem = ms.rem_euclid(86_400_000);
366 let millisecond = (rem % 1000) as u32;
367 rem /= 1000;
368 let second = (rem % 60) as u32;
369 rem /= 60;
370 let minute = (rem % 60) as u32;
371 rem /= 60;
372 let hour = (rem % 24) as u32;
373 days += 719468;
375 let era = if days >= 0 { days } else { days - 146096 } / 146097;
376 let doe = days - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + era * 400;
379 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = (doy - (153 * mp + 2) / 5 + 1) as u32; let month = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32; let year = if month <= 2 { y + 1 } else { y };
384 BaseDate {
385 year,
386 month,
387 day,
388 hour,
389 minute,
390 second,
391 millisecond,
392 has_time,
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::note::Corpus;
400 use crate::parser::parse;
401 use crate::value::BaseLink;
402
403 fn eval_str(src: &str, note: &Note) -> Value {
404 let corpus = Corpus::new(vec![note.clone()]);
405 let formulas = BTreeMap::new();
406 let ctx = EvalCtx::new(note, &corpus, &formulas);
407 ctx.eval(&parse(src).unwrap())
408 }
409
410 fn note_with(props: &[(&str, Value)]) -> Note {
411 let mut n = Note::default();
412 for (k, v) in props {
413 n.properties.insert(k.to_string(), v.clone());
414 }
415 n
416 }
417
418 #[test]
419 fn arithmetic_and_precedence() {
420 let n = Note::default();
421 assert!(matches!(eval_str("1 + 2 * 3", &n), Value::Number(x) if x == 7.0));
422 assert!(matches!(eval_str("(1 + 2) * 3", &n), Value::Number(x) if x == 9.0));
423 assert!(matches!(eval_str("10 % 3", &n), Value::Number(x) if x == 1.0));
424 }
425
426 #[test]
427 fn comparisons_and_logic() {
428 let n = note_with(&[("age", Value::Number(30.0))]);
429 assert!(matches!(eval_str("age > 18", &n), Value::Bool(true)));
430 assert!(matches!(
431 eval_str("age > 18 && age < 40", &n),
432 Value::Bool(true)
433 ));
434 assert!(matches!(
435 eval_str("age < 18 || age > 25", &n),
436 Value::Bool(true)
437 ));
438 assert!(matches!(eval_str("!(age > 18)", &n), Value::Bool(false)));
439 }
440
441 #[test]
442 fn bare_and_note_property() {
443 let n = note_with(&[("type", Value::Str("home".into()))]);
444 assert!(matches!(
445 eval_str(r#"type == "home""#, &n),
446 Value::Bool(true)
447 ));
448 assert!(matches!(
449 eval_str(r#"note.type == "home""#, &n),
450 Value::Bool(true)
451 ));
452 }
453
454 #[test]
455 fn file_properties() {
456 let mut n = Note::default();
457 n.name = "Intro.md".into();
458 n.ext = "md".into();
459 n.folder = "guide".into();
460 assert!(matches!(
461 eval_str(r#"file.ext == "md""#, &n),
462 Value::Bool(true)
463 ));
464 assert!(matches!(
465 eval_str(r#"file.name == "Intro.md""#, &n),
466 Value::Bool(true)
467 ));
468 }
469
470 #[test]
471 fn string_concat_and_number_coercion() {
472 let n = note_with(&[("price", Value::Number(3.0))]);
473 assert!(matches!(eval_str(r#""$" + price"#, &n), Value::Str(s) if s == "$3"));
474 }
475
476 #[test]
477 fn list_index_and_length() {
478 let n = note_with(&[(
479 "cats",
480 Value::List(vec![Value::Str("a".into()), Value::Str("b".into())]),
481 )]);
482 assert!(matches!(eval_str("cats[0]", &n), Value::Str(s) if s == "a"));
483 assert!(matches!(eval_str("cats[-1]", &n), Value::Str(s) if s == "b"));
484 assert!(matches!(eval_str("cats.length", &n), Value::Number(x) if x == 2.0));
485 }
486
487 #[test]
488 fn string_negative_index_counts_from_end() {
489 let n = note_with(&[("title", Value::Str("abc".into()))]);
490 assert!(matches!(eval_str("title[-1]", &n), Value::Str(s) if s == "c"));
491 assert!(matches!(eval_str("title[0]", &n), Value::Str(s) if s == "a"));
492 assert!(matches!(eval_str("title[5]", &n), Value::Null));
493 }
494
495 #[test]
496 fn fractional_and_nan_index_are_null() {
497 let n = note_with(&[(
498 "cats",
499 Value::List(vec![Value::Str("a".into()), Value::Str("b".into())]),
500 )]);
501 assert!(matches!(eval_str("cats[1.9]", &n), Value::Null));
502 assert!(matches!(eval_str("cats[0 / 0]", &n), Value::Null));
504 }
505
506 #[test]
507 fn deep_member_chain_eval_does_not_overflow() {
508 let n = Note::default();
512 let corpus = Corpus::new(vec![n.clone()]);
513 let formulas = BTreeMap::new();
514 let ctx = EvalCtx::new(&n, &corpus, &formulas);
515 let expr = format!("a{}", ".a".repeat(2000)); let parsed = crate::parser::parse(&expr).expect("under node budget → parses");
517 assert!(matches!(ctx.eval(&parsed), Value::Null));
518 }
519
520 #[test]
521 fn date_field_access() {
522 let n = note_with(&[(
523 "created",
524 Value::Date(crate::note::parse_date("2024-05-06").unwrap()),
525 )]);
526 assert!(matches!(eval_str("created.year", &n), Value::Number(x) if x == 2024.0));
527 assert!(matches!(eval_str("created.month", &n), Value::Number(x) if x == 5.0));
528 }
529
530 #[test]
531 fn date_minus_date_is_duration() {
532 let n = note_with(&[
533 (
534 "a",
535 Value::Date(crate::note::parse_date("2024-01-02").unwrap()),
536 ),
537 (
538 "b",
539 Value::Date(crate::note::parse_date("2024-01-01").unwrap()),
540 ),
541 ]);
542 assert!(matches!(eval_str("a - b", &n), Value::Duration(ms) if ms == 86_400_000));
544 }
545
546 #[test]
547 fn unknown_symbols_are_null_not_panic() {
548 let n = Note::default();
549 assert!(matches!(eval_str("nonexistent", &n), Value::Null));
550 assert!(matches!(eval_str("nope.deep.field", &n), Value::Null));
551 assert!(matches!(eval_str("missing[3]", &n), Value::Null));
552 }
553
554 #[test]
555 fn formula_evaluation_with_cache() {
556 let n = note_with(&[("price", Value::Number(10.0)), ("age", Value::Number(2.0))]);
557 let corpus = Corpus::new(vec![n.clone()]);
558 let mut formulas = BTreeMap::new();
559 formulas.insert("ppu".to_string(), parse("price / age").unwrap());
560 let ctx = EvalCtx::new(&n, &corpus, &formulas);
561 assert!(matches!(ctx.eval(&parse("formula.ppu").unwrap()), Value::Number(x) if x == 5.0));
562 }
563
564 #[test]
565 fn formula_cycle_yields_null() {
566 let n = Note::default();
567 let corpus = Corpus::new(vec![n.clone()]);
568 let mut formulas = BTreeMap::new();
569 formulas.insert("a".to_string(), parse("formula.b").unwrap());
570 formulas.insert("b".to_string(), parse("formula.a").unwrap());
571 let ctx = EvalCtx::new(&n, &corpus, &formulas);
572 assert!(matches!(
573 ctx.eval(&parse("formula.a").unwrap()),
574 Value::Null
575 ));
576 }
577
578 #[test]
579 fn link_contains_via_method() {
580 let n = note_with(&[(
581 "categories",
582 Value::List(vec![Value::Link(BaseLink::new("Categories/Books"))]),
583 )]);
584 assert!(matches!(
585 eval_str(
586 r#"categories.contains(link("Categories/Books", "Books"))"#,
587 &n
588 ),
589 Value::Bool(true)
590 ));
591 assert!(matches!(
592 eval_str(r#"categories.contains(link("Books"))"#, &n),
593 Value::Bool(true)
594 ));
595 }
596
597 #[test]
598 fn round_trip_epoch_conversion() {
599 let d = crate::note::parse_date("2024-05-06 07:08:09").unwrap();
600 let back = date_from_epoch_millis(d.epoch_millis(), true);
601 assert_eq!((back.year, back.month, back.day), (2024, 5, 6));
602 assert_eq!((back.hour, back.minute, back.second), (7, 8, 9));
603 }
604}