Skip to main content

sim_expr_tree_calc/calc/
face.rs

1use sim_codec::{
2    DecodePosition, DecodedForm, Input, Output, decode_default_with_codec_and_limits,
3    encode_with_codec,
4};
5use sim_expr_tree_core::FaceBudget;
6use sim_kernel::{
7    CapabilitySet, Cx, EncodeOptions, EncodePosition, Expr, ReadPolicy, Symbol, TrustLevel,
8    read_eval_capability,
9};
10use sim_table_core::TablePath;
11
12use super::*;
13
14mod budget;
15mod model;
16
17use budget::{bounded_value_expr, inspect_expr};
18pub use model::{
19    EncodedFace, FaceContent, FaceDimension, FaceIssue, FaceMetadata, FacePosition,
20    SourceEditOutcome,
21};
22
23const MAX_METADATA_MESSAGE_BYTES: usize = 1_024;
24
25impl ExprTreeCalc {
26    /// Decodes edited source through the inherited installed codec under
27    /// explicit position, limits, caller trust, and diminished cell authority.
28    ///
29    /// A rejected edit leaves the prior source unchanged.
30    pub fn edit_cell_source(
31        &mut self,
32        path: TablePath,
33        input: Input,
34        caller_policy: ReadPolicy,
35    ) -> SourceEditOutcome {
36        let policy = self.effective_codec_policy(&path);
37        let position = policy.source_position();
38        let Some(codec_name) = policy.source_codec().map(str::to_owned) else {
39            return SourceEditOutcome {
40                metadata: metadata(
41                    None,
42                    position.into(),
43                    FaceIssue::Unsupported {
44                        reason: "no source codec selected".to_owned(),
45                    },
46                ),
47            };
48        };
49
50        if position == DecodePosition::Eval && caller_policy.trust == TrustLevel::Untrusted {
51            return SourceEditOutcome {
52                metadata: metadata(
53                    Some(codec_name),
54                    position.into(),
55                    FaceIssue::CodecFailure {
56                        message: "eval-position source requires a trusted caller".to_owned(),
57                    },
58                ),
59            };
60        }
61
62        let authority = self.effective_authority(&path);
63        let read_policy = diminished_read_policy(caller_policy, authority.capabilities(), position);
64        let codec = codec_symbol(&codec_name);
65        let limits = policy.decode_limits();
66        let mut cx = (self.context_factory)();
67        let decoded = cx.with_capabilities(authority.capabilities().clone(), |cx| {
68            decode_default_with_codec_and_limits(cx, &codec, input, read_policy, position, limits)
69        });
70        match decoded {
71            Ok(decoded) => {
72                let source = match decoded {
73                    DecodedForm::Datum(datum) => Expr::from(datum),
74                    DecodedForm::Term(term) => Expr::from(term),
75                };
76                self.set_cell(path, source);
77                SourceEditOutcome {
78                    metadata: metadata(Some(codec_name), position.into(), FaceIssue::Complete),
79                }
80            }
81            Err(error) => SourceEditOutcome {
82                metadata: metadata(
83                    Some(codec_name),
84                    position.into(),
85                    FaceIssue::CodecFailure {
86                        message: bounded_message(error.to_string()),
87                    },
88                ),
89            },
90        }
91    }
92
93    /// Encodes the authored source expression under its inherited codec,
94    /// position, and source-only face budget.
95    #[must_use]
96    pub fn source_face(&self, path: &TablePath) -> EncodedFace {
97        let policy = self.effective_codec_policy(path);
98        let position = encode_position_for_decode(policy.source_position());
99        let Some(codec_name) = policy.source_codec().map(str::to_owned) else {
100            return unsupported_face(None, position, "no source codec selected");
101        };
102        let source = self
103            .state
104            .read()
105            .expect("calc state poisoned")
106            .cells
107            .get(&path_key(path))
108            .cloned();
109        let Some(source) = source else {
110            return unsupported_face(Some(codec_name), position, "cell has no authored source");
111        };
112        let mut cx = (self.context_factory)();
113        encode_expr_face(
114            &mut cx,
115            codec_name,
116            position,
117            &source,
118            policy.source_budget(),
119        )
120    }
121
122    /// Encodes the current arbitrary result under its inherited codec,
123    /// position, and result-only face budget.
124    ///
125    /// The ordinary [`Value`] remains in the calculator regardless of whether
126    /// it has a safe bounded presentation projection.
127    #[must_use]
128    pub fn result_face(&self, path: &TablePath) -> EncodedFace {
129        let policy = self.effective_codec_policy(path);
130        let position = policy.result_position();
131        let Some(codec_name) = policy.result_codec().map(str::to_owned) else {
132            return unsupported_face(None, position, "no result codec selected");
133        };
134        let value = match self.current_cell(path) {
135            Ok(value) => value,
136            Err(error) => {
137                return unsupported_face(
138                    Some(codec_name),
139                    position,
140                    &format!("no current result: {}", bounded_message(error.to_string())),
141                );
142            }
143        };
144        let budget = policy.result_budget();
145        let mut cx = (self.context_factory)();
146        let expr = match bounded_value_expr(&mut cx, &value, budget) {
147            Ok(expr) => expr,
148            Err(issue) => {
149                return EncodedFace {
150                    content: None,
151                    metadata: metadata(Some(codec_name), position.into(), issue),
152                };
153            }
154        };
155        encode_preflighted_expr_face(&mut cx, codec_name, position, &expr, budget)
156    }
157}
158
159fn encode_expr_face(
160    cx: &mut Cx,
161    codec_name: String,
162    position: EncodePosition,
163    expr: &Expr,
164    budget: FaceBudget,
165) -> EncodedFace {
166    if let Err(issue) = inspect_expr(expr, budget) {
167        return EncodedFace {
168            content: None,
169            metadata: metadata(Some(codec_name), position.into(), issue),
170        };
171    }
172    encode_preflighted_expr_face(cx, codec_name, position, expr, budget)
173}
174
175fn encode_preflighted_expr_face(
176    cx: &mut Cx,
177    codec_name: String,
178    position: EncodePosition,
179    expr: &Expr,
180    budget: FaceBudget,
181) -> EncodedFace {
182    let codec = codec_symbol(&codec_name);
183    let options = EncodeOptions {
184        position,
185        ..EncodeOptions::default()
186    };
187    match encode_with_codec(cx, &codec, expr, options) {
188        Ok(output) => {
189            let output_bytes = output_len(&output);
190            if output_bytes > budget.max_bytes() {
191                return EncodedFace {
192                    content: None,
193                    metadata: metadata(
194                        Some(codec_name),
195                        position.into(),
196                        FaceIssue::Truncated {
197                            dimension: FaceDimension::Bytes,
198                            limit: budget.max_bytes(),
199                            observed: output_bytes,
200                        },
201                    ),
202                };
203            }
204            EncodedFace {
205                content: Some(match output {
206                    Output::Text(text) => FaceContent::Text(text),
207                    Output::Bytes(bytes) => FaceContent::Bytes(bytes),
208                }),
209                metadata: metadata(Some(codec_name), position.into(), FaceIssue::Complete),
210            }
211        }
212        Err(error) => EncodedFace {
213            content: None,
214            metadata: metadata(
215                Some(codec_name),
216                position.into(),
217                FaceIssue::CodecFailure {
218                    message: bounded_message(error.to_string()),
219                },
220            ),
221        },
222    }
223}
224
225fn unsupported_face(codec: Option<String>, position: EncodePosition, reason: &str) -> EncodedFace {
226    EncodedFace {
227        content: None,
228        metadata: metadata(
229            codec,
230            position.into(),
231            FaceIssue::Unsupported {
232                reason: bounded_message(reason.to_owned()),
233            },
234        ),
235    }
236}
237
238fn output_len(output: &Output) -> usize {
239    match output {
240        Output::Text(text) => text.len(),
241        Output::Bytes(bytes) => bytes.len(),
242    }
243}
244
245fn encode_position_for_decode(position: DecodePosition) -> EncodePosition {
246    match position {
247        DecodePosition::Eval => EncodePosition::Eval,
248        DecodePosition::Quote => EncodePosition::Quote,
249        DecodePosition::Data => EncodePosition::Data,
250        DecodePosition::Pattern => EncodePosition::Pattern,
251    }
252}
253
254fn diminished_read_policy(
255    caller: ReadPolicy,
256    authority: &CapabilitySet,
257    position: DecodePosition,
258) -> ReadPolicy {
259    let mut capabilities = caller.capabilities.intersect(authority);
260    if position != DecodePosition::Eval {
261        let read_eval = read_eval_capability();
262        capabilities = capabilities
263            .iter()
264            .filter(|capability| *capability != &read_eval)
265            .cloned()
266            .fold(CapabilitySet::new(), CapabilitySet::grant);
267    }
268    ReadPolicy {
269        trust: caller.trust,
270        capabilities,
271    }
272}
273
274fn codec_symbol(name: &str) -> Symbol {
275    name.split_once('/')
276        .or_else(|| name.split_once(':'))
277        .map_or_else(
278            || Symbol::new(name.to_owned()),
279            |(namespace, local)| Symbol::qualified(namespace.to_owned(), local.to_owned()),
280        )
281}
282
283fn metadata(codec: Option<String>, position: FacePosition, issue: FaceIssue) -> FaceMetadata {
284    FaceMetadata {
285        codec,
286        position,
287        issue,
288    }
289}
290
291fn bounded_message(message: String) -> String {
292    if message.len() <= MAX_METADATA_MESSAGE_BYTES {
293        return message;
294    }
295    let mut end = MAX_METADATA_MESSAGE_BYTES;
296    while !message.is_char_boundary(end) {
297        end -= 1;
298    }
299    format!("{}...[metadata truncated]", &message[..end])
300}