1use std::sync::Arc;
2
3use sim_kernel::{Consistency, Cx, Error, EvalReply, Expr, Result, Symbol, Value};
4use sim_lib_server::{
5 EvalSite, FrameKind, ServerAddress, ServerFrame, eval_request_from_frame,
6 server_frame_from_reply,
7};
8use sim_table_core::{TableOp, TableOpError, decode_table_op};
9
10pub fn wrap_remote_table_site(inner: Arc<dyn EvalSite>, root: Value) -> Arc<dyn EvalSite> {
12 Arc::new(RemoteTableSite { inner, root })
13}
14
15#[derive(Clone)]
17pub struct RemoteTableSite {
18 inner: Arc<dyn EvalSite>,
19 root: Value,
20}
21
22impl RemoteTableSite {
23 fn current_dir(&self, cx: &mut Cx, path: &[Symbol]) -> Result<Value> {
24 let mut current = self.root.clone();
25 for segment in path {
26 let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
27 expected: "directory table",
28 found: "non-directory",
29 })?;
30 current = dir.opendir(cx, segment.clone())?.ok_or_else(|| {
31 Error::Eval(format!("table/remote: missing path segment {segment}"))
32 })?;
33 }
34 Ok(current)
35 }
36
37 fn parse_call(expr: Expr) -> Result<(Symbol, Vec<Expr>)> {
38 match expr {
39 Expr::Call { operator, args } => match *operator {
40 Expr::Symbol(symbol) => Ok((symbol, args)),
41 _ => Err(Error::TypeMismatch {
42 expected: "symbol operator",
43 found: "non-symbol",
44 }),
45 },
46 _ => Err(Error::TypeMismatch {
47 expected: "call",
48 found: "non-call",
49 }),
50 }
51 }
52
53 fn parse_path(expr: &Expr) -> Result<Vec<Symbol>> {
54 let Expr::List(items) = expr else {
55 return Err(Error::TypeMismatch {
56 expected: "path list",
57 found: "non-list",
58 });
59 };
60 items
61 .iter()
62 .map(|item| match item {
63 Expr::Symbol(symbol) => Ok(symbol.clone()),
64 _ => Err(Error::TypeMismatch {
65 expected: "path symbol",
66 found: "non-symbol",
67 }),
68 })
69 .collect()
70 }
71
72 fn arity_message(name: &str) -> String {
75 match name {
76 "get" => "table/get expects path and key".to_owned(),
77 "set" => "table/set expects path, key, and value".to_owned(),
78 "has" => "table/has expects path and key".to_owned(),
79 "del" => "table/del expects path and key".to_owned(),
80 "keys" => "table/keys expects only a path".to_owned(),
81 "entries" => "table/entries expects only a path".to_owned(),
82 "len" => "table/len expects only a path".to_owned(),
83 "clear" => "table/clear expects only a path".to_owned(),
84 "mkdir" => "table/mkdir expects path and name".to_owned(),
85 "opendir" => "table/opendir expects path and name".to_owned(),
86 "rmdir" => "table/rmdir expects path and name".to_owned(),
87 "dir?" => "table/dir? expects path and name".to_owned(),
88 other => format!("table/{other}: malformed request"),
89 }
90 }
91
92 fn child_token(path: &[Symbol], name: &Symbol) -> String {
93 let mut segments = path.iter().map(Symbol::to_string).collect::<Vec<_>>();
94 segments.push(name.to_string());
95 format!("/{}", segments.join("/"))
96 }
97
98 fn reply_value(
99 &self,
100 cx: &mut Cx,
101 frame: &ServerFrame,
102 value: Value,
103 consistency: Consistency,
104 ) -> Result<ServerFrame> {
105 let reply = EvalReply {
106 value,
107 diagnostics: cx.take_diagnostics(),
108 trace: None,
109 };
110 server_frame_from_reply(cx, &self.reply_codec(frame), reply, consistency)
111 }
112
113 fn reply_codec(&self, frame: &ServerFrame) -> Symbol {
114 if let Some(hint) = &frame.envelope.reply_codec_hint
115 && self.inner.codecs().iter().any(|codec| codec == hint)
116 {
117 return hint.clone();
118 }
119 frame.codec.clone()
120 }
121
122 fn answer_table_request(&self, cx: &mut Cx, frame: ServerFrame) -> Result<Option<ServerFrame>> {
123 if frame.kind != FrameKind::Request {
124 return Ok(None);
125 }
126 let consistency = frame.envelope.consistency;
127 let request = eval_request_from_frame(cx, &frame)?;
128 let (operator, args) = Self::parse_call(request.expr)?;
129 if operator.namespace.as_deref() != Some("table") {
130 return Ok(None);
131 }
132 let [path_expr, rest @ ..] = args.as_slice() else {
133 return Err(Error::Eval(
134 "table/remote: missing path argument".to_owned(),
135 ));
136 };
137 let path = Self::parse_path(path_expr)?;
138
139 let op = match decode_table_op(&Expr::Call {
144 operator: Box::new(Expr::Symbol(operator.clone())),
145 args: rest.to_vec(),
146 }) {
147 Ok(op) => op,
148 Err(TableOpError::UnknownOp(_) | TableOpError::NotATableCall) => return Ok(None),
149 Err(TableOpError::BadArity(name)) => {
150 return Err(Error::Eval(Self::arity_message(&name)));
151 }
152 Err(TableOpError::BadArg(_)) => {
153 return Err(Error::TypeMismatch {
154 expected: "symbol",
155 found: "non-symbol",
156 });
157 }
158 };
159
160 let current = self.current_dir(cx, &path)?;
161 let table = current
162 .object()
163 .as_table_impl()
164 .ok_or(Error::TypeMismatch {
165 expected: "table",
166 found: "non-table",
167 })?;
168 let value = match op {
169 TableOp::Get(key) => table.get(cx, key),
170 TableOp::Set(key, value) => {
171 table.set(cx, key, cx.factory().expr(value)?)?;
172 cx.factory().nil()
173 }
174 TableOp::Has(key) => {
175 let present = table.has(cx, key)?;
176 cx.factory().bool(present)
177 }
178 TableOp::Delete(key) => table.del(cx, key),
179 TableOp::Keys => {
180 let keys = table.keys(cx)?;
181 let values = keys
182 .into_iter()
183 .map(|symbol| cx.factory().symbol(symbol))
184 .collect::<Result<Vec<_>>>()?;
185 cx.factory().list(values)
186 }
187 TableOp::Entries => {
188 let entries = table.entries(cx)?;
189 cx.factory().table(entries)
190 }
191 TableOp::Len => {
192 let len = table.len(cx)?;
193 cx.factory()
194 .number_literal(Symbol::qualified("numbers", "f64"), len.to_string())
195 }
196 TableOp::Clear => {
197 table.clear(cx)?;
198 cx.factory().nil()
199 }
200 TableOp::Mkdir(name) => {
201 let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
202 expected: "directory table",
203 found: "non-directory",
204 })?;
205 let _ = dir.mkdir(cx, name.clone())?;
206 cx.factory().string(Self::child_token(&path, &name))
207 }
208 TableOp::Opendir(name) => {
209 let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
210 expected: "directory table",
211 found: "non-directory",
212 })?;
213 match dir.opendir(cx, name.clone())? {
214 Some(_) => cx.factory().string(Self::child_token(&path, &name)),
215 None => cx.factory().nil(),
216 }
217 }
218 TableOp::Rmdir(name) => {
219 let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
220 expected: "directory table",
221 found: "non-directory",
222 })?;
223 dir.rmdir(cx, name)
224 }
225 TableOp::IsDir(name) => {
226 let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
227 expected: "directory table",
228 found: "non-directory",
229 })?;
230 let is_dir = dir.is_dir(cx, name)?;
231 cx.factory().bool(is_dir)
232 }
233 }?;
234 Ok(Some(self.reply_value(cx, &frame, value, consistency)?))
235 }
236}
237
238impl EvalSite for RemoteTableSite {
239 fn site_kind(&self) -> &'static str {
240 "remote-table"
241 }
242
243 fn address(&self) -> &ServerAddress {
244 self.inner.address()
245 }
246
247 fn codecs(&self) -> &[Symbol] {
248 self.inner.codecs()
249 }
250
251 fn answer(&self, cx: &mut Cx, frame: ServerFrame) -> Result<ServerFrame> {
252 if let Some(reply) = self.answer_table_request(cx, frame.clone())? {
253 return Ok(reply);
254 }
255 self.inner.answer(cx, frame)
256 }
257
258 fn answer_with_timeout(
259 &self,
260 cx: &mut Cx,
261 frame: ServerFrame,
262 timeout: Option<std::time::Duration>,
263 ) -> Result<ServerFrame> {
264 if let Some(reply) = self.answer_table_request(cx, frame.clone())? {
265 return Ok(reply);
266 }
267 self.inner.answer_with_timeout(cx, frame, timeout)
268 }
269
270 fn close_connection(&self, cx: &mut Cx) -> Result<()> {
271 self.inner.close_connection(cx)
272 }
273
274 fn as_any(&self) -> &dyn std::any::Any {
275 self
276 }
277}