1use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};
2
3use crate::db::LogicDb;
4
5pub fn consult_table_path(
10 cx: &mut Cx,
11 db: &mut LogicDb,
12 source: &Value,
13 path: &str,
14) -> Result<usize> {
15 let expr = read_table_path_expr(cx, source, path)?;
16 consult_expr(db, expr)
17}
18
19pub(crate) fn consult_expr(db: &mut LogicDb, expr: Expr) -> Result<usize> {
20 match expr {
21 Expr::List(items) => {
22 let mut count = 0usize;
23 for item in items {
24 db.assert_clause_expr(item)?;
25 count += 1;
26 }
27 Ok(count)
28 }
29 other => {
30 db.assert_clause_expr(other)?;
31 Ok(1)
32 }
33 }
34}
35
36fn read_table_path_expr(cx: &mut Cx, source: &Value, path: &str) -> Result<Expr> {
37 let segments = relative_table_path(path)?;
38 let (leaf, parents) = segments
39 .split_last()
40 .ok_or_else(|| Error::Eval("consult path must not be empty".to_owned()))?;
41 let mut current = source.clone();
42 for segment in parents {
43 let dir = current.object().as_dir().ok_or_else(|| {
44 Error::Eval(format!(
45 "consult source does not expose directory segment {segment}"
46 ))
47 })?;
48 current = dir
49 .opendir(cx, Symbol::new(*segment))?
50 .ok_or_else(|| Error::Eval(format!("consult source does not contain {path}")))?;
51 }
52 let table = current
53 .object()
54 .as_table_impl()
55 .ok_or_else(|| Error::Eval("consult source does not implement Table".to_owned()))?;
56 let key = Symbol::new(*leaf);
57 if let Some(dir) = current.object().as_dir()
58 && dir.is_dir(cx, key.clone())?
59 {
60 return Err(Error::Eval(format!(
61 "consult source path {path} is a directory"
62 )));
63 }
64 if !table.has(cx, key.clone())? {
65 return Err(Error::Eval(format!(
66 "consult source does not contain {path}"
67 )));
68 }
69 table.get(cx, key)?.object().as_expr(cx)
70}
71
72fn relative_table_path(path: &str) -> Result<Vec<&str>> {
73 if path.is_empty() || path.starts_with('/') || path.ends_with('/') || path.contains('\\') {
74 return Err(Error::Eval(format!(
75 "consult path must be a non-empty relative table path: {path}"
76 )));
77 }
78 let segments: Vec<_> = path.split('/').collect();
79 if segments
80 .iter()
81 .any(|segment| segment.is_empty() || *segment == "." || *segment == "..")
82 {
83 return Err(Error::Eval(format!(
84 "consult path must be a non-empty relative table path: {path}"
85 )));
86 }
87 Ok(segments)
88}