Skip to main content

sim_expr_tree_core/
policy.rs

1use sim_codec::{DecodeLimits, DecodePosition};
2use sim_kernel::EncodePosition;
3
4/// Absolute ceiling for bytes admitted to one encoded source or result face.
5pub const HARD_MAX_FACE_BYTES: usize = 8 * 1024 * 1024;
6/// Absolute ceiling for expression nesting inspected for one face.
7pub const HARD_MAX_FACE_DEPTH: usize = 512;
8/// Absolute ceiling for expression nodes or runtime collection items inspected
9/// for one face.
10pub const HARD_MAX_FACE_ITEMS: usize = 200_000;
11
12/// Independent resource ceilings for one encoded source or result face.
13///
14/// Construction clamps every requested field to a non-configurable ceiling so
15/// persisted policy can never turn a display operation into an unbounded walk.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct FaceBudget {
18    max_bytes: usize,
19    max_depth: usize,
20    max_items: usize,
21}
22
23impl FaceBudget {
24    /// Creates a hard-clamped face budget.
25    #[must_use]
26    pub const fn new(max_bytes: usize, max_depth: usize, max_items: usize) -> Self {
27        Self {
28            max_bytes: if max_bytes > HARD_MAX_FACE_BYTES {
29                HARD_MAX_FACE_BYTES
30            } else {
31                max_bytes
32            },
33            max_depth: if max_depth > HARD_MAX_FACE_DEPTH {
34                HARD_MAX_FACE_DEPTH
35            } else {
36                max_depth
37            },
38            max_items: if max_items > HARD_MAX_FACE_ITEMS {
39                HARD_MAX_FACE_ITEMS
40            } else {
41                max_items
42            },
43        }
44    }
45
46    /// Maximum encoded bytes and aggregate scalar payload bytes.
47    #[must_use]
48    pub const fn max_bytes(self) -> usize {
49        self.max_bytes
50    }
51
52    /// Maximum expression nesting depth.
53    #[must_use]
54    pub const fn max_depth(self) -> usize {
55        self.max_depth
56    }
57
58    /// Maximum expression nodes or runtime collection items.
59    #[must_use]
60    pub const fn max_items(self) -> usize {
61        self.max_items
62    }
63}
64
65impl Default for FaceBudget {
66    fn default() -> Self {
67        Self::new(256 * 1024, 128, 16_384)
68    }
69}
70
71/// Patch applied at the tree, directory, or cell policy level.
72///
73/// Codec selectors are stable exported names, not process-local `CodecId`
74/// values. The nested option permits a descendant to explicitly clear an
75/// inherited source or result codec.
76#[derive(Clone, Debug, Default, PartialEq, Eq)]
77pub struct CodecPolicyPatch {
78    /// Inherit, clear, or replace the source codec name.
79    pub source_codec: Option<Option<String>>,
80    /// Replace the source decode/encode position.
81    pub source_position: Option<DecodePosition>,
82    /// Replace the bounded source decode limits.
83    pub decode_limits: Option<DecodeLimits>,
84    /// Replace the source-face resource budget.
85    pub source_budget: Option<FaceBudget>,
86    /// Inherit, clear, or replace the result codec name.
87    pub result_codec: Option<Option<String>>,
88    /// Replace the result encode position.
89    pub result_position: Option<EncodePosition>,
90    /// Replace the result-face resource budget.
91    pub result_budget: Option<FaceBudget>,
92}
93
94impl CodecPolicyPatch {
95    /// No local policy changes.
96    #[must_use]
97    pub fn empty() -> Self {
98        Self::default()
99    }
100
101    /// Selects the same codec for source and result faces.
102    #[must_use]
103    pub fn set_codec(codec: impl Into<String>) -> Self {
104        let codec = codec.into();
105        Self {
106            source_codec: Some(Some(codec.clone())),
107            result_codec: Some(Some(codec)),
108            ..Self::default()
109        }
110    }
111
112    /// Clears inherited source and result codecs.
113    #[must_use]
114    pub fn clear_codec() -> Self {
115        Self {
116            source_codec: Some(None),
117            result_codec: Some(None),
118            ..Self::default()
119        }
120    }
121
122    /// Selects a source codec without changing any other field.
123    #[must_use]
124    pub fn source_codec(codec: impl Into<String>) -> Self {
125        Self {
126            source_codec: Some(Some(codec.into())),
127            ..Self::default()
128        }
129    }
130
131    /// Selects a result codec without changing any other field.
132    #[must_use]
133    pub fn result_codec(codec: impl Into<String>) -> Self {
134        Self {
135            result_codec: Some(Some(codec.into())),
136            ..Self::default()
137        }
138    }
139
140    pub(crate) fn apply_to(&self, effective: &mut EffectiveCodecPolicy) {
141        if let Some(codec) = &self.source_codec {
142            effective.source_codec = codec.clone();
143        }
144        if let Some(position) = self.source_position {
145            effective.source_position = position;
146        }
147        if let Some(limits) = self.decode_limits {
148            effective.decode_limits = bound_decode_limits(limits);
149        }
150        if let Some(budget) = self.source_budget {
151            effective.source_budget =
152                FaceBudget::new(budget.max_bytes(), budget.max_depth(), budget.max_items());
153        }
154        if let Some(codec) = &self.result_codec {
155            effective.result_codec = codec.clone();
156        }
157        if let Some(position) = self.result_position {
158            effective.result_position = position;
159        }
160        if let Some(budget) = self.result_budget {
161            effective.result_budget =
162                FaceBudget::new(budget.max_bytes(), budget.max_depth(), budget.max_items());
163        }
164    }
165}
166
167/// Fully resolved codec and face policy at one namespace node.
168#[derive(Clone, Debug, PartialEq, Eq)]
169pub struct EffectiveCodecPolicy {
170    source_codec: Option<String>,
171    source_position: DecodePosition,
172    decode_limits: DecodeLimits,
173    source_budget: FaceBudget,
174    result_codec: Option<String>,
175    result_position: EncodePosition,
176    result_budget: FaceBudget,
177}
178
179impl EffectiveCodecPolicy {
180    /// Creates the bounded default policy with no selected codecs.
181    #[must_use]
182    pub fn empty() -> Self {
183        Self::default()
184    }
185
186    /// Resolves patches in tree-to-leaf order, replacing only fields each
187    /// patch explicitly carries.
188    #[must_use]
189    pub fn derive(patches: impl IntoIterator<Item = CodecPolicyPatch>) -> Self {
190        let mut effective = Self::default();
191        for patch in patches {
192            patch.apply_to(&mut effective);
193        }
194        effective
195    }
196
197    /// Active source codec name, when selected.
198    #[must_use]
199    pub fn source_codec(&self) -> Option<&str> {
200        self.source_codec.as_deref()
201    }
202
203    /// Source decode position and matching source-face encode position.
204    #[must_use]
205    pub const fn source_position(&self) -> DecodePosition {
206        self.source_position
207    }
208
209    /// Bounded limits used for edited source.
210    #[must_use]
211    pub const fn decode_limits(&self) -> DecodeLimits {
212        self.decode_limits
213    }
214
215    /// Independent source-face budget.
216    #[must_use]
217    pub const fn source_budget(&self) -> FaceBudget {
218        self.source_budget
219    }
220
221    /// Active result codec name, when selected.
222    #[must_use]
223    pub fn result_codec(&self) -> Option<&str> {
224        self.result_codec.as_deref()
225    }
226
227    /// Result encode position.
228    #[must_use]
229    pub const fn result_position(&self) -> EncodePosition {
230        self.result_position
231    }
232
233    /// Independent result-face budget.
234    #[must_use]
235    pub const fn result_budget(&self) -> FaceBudget {
236        self.result_budget
237    }
238}
239
240impl Default for EffectiveCodecPolicy {
241    fn default() -> Self {
242        Self {
243            source_codec: None,
244            source_position: DecodePosition::Data,
245            decode_limits: DecodeLimits::default(),
246            source_budget: FaceBudget::default(),
247            result_codec: None,
248            result_position: EncodePosition::Data,
249            result_budget: FaceBudget::default(),
250        }
251    }
252}
253
254fn bound_decode_limits(requested: DecodeLimits) -> DecodeLimits {
255    let hard = DecodeLimits::default();
256    DecodeLimits {
257        max_input_bytes: requested.max_input_bytes.min(hard.max_input_bytes),
258        max_tokens: requested.max_tokens.min(hard.max_tokens),
259        max_expr_nodes: requested.max_expr_nodes.min(hard.max_expr_nodes),
260        max_depth: requested.max_depth.min(hard.max_depth),
261        max_string_bytes: requested.max_string_bytes.min(hard.max_string_bytes),
262        max_blob_bytes: requested.max_blob_bytes.min(hard.max_blob_bytes),
263        max_collection_len: requested.max_collection_len.min(hard.max_collection_len),
264        max_trivia_items: requested.max_trivia_items.min(hard.max_trivia_items),
265    }
266}