Skip to main content

sim_text/
projection.rs

1//! Runtime, codec, browse, Shape, and read-construction projections.
2
3use std::any::Any;
4
5use sim_kernel::{
6    Cx, Error, Expr, MatchScore, Object, ObjectCompat, ObjectEncode, ObjectEncoding,
7    ReadConstructor, Result, Shape, ShapeDoc, ShapeMatch, ShapeRef, Symbol, Value,
8};
9
10use crate::{CodeUnitString, CodeUnitStringError};
11
12/// Stable class/tag symbol used by the standard read-construct representation.
13pub const CODE_UNIT_STRING_SYMBOL: &str = "text/CodeUnitString";
14
15fn symbol() -> Symbol {
16    Symbol::qualified("text", "CodeUnitString")
17}
18
19/// Project exact code units into the codec-neutral expression graph.
20///
21/// The payload is big-endian bytes, making the representation independent of
22/// host byte order and capable of carrying lone surrogates without coercion.
23pub fn code_unit_string_to_expr(text: &CodeUnitString) -> Expr {
24    let mut bytes = Vec::with_capacity(text.len().saturating_mul(2));
25    for unit in text.code_units() {
26        bytes.extend_from_slice(&unit.to_be_bytes());
27    }
28    Expr::Extension {
29        tag: symbol(),
30        payload: Box::new(Expr::Bytes(bytes)),
31    }
32}
33
34/// Recover exact code units from the tagged codec-neutral representation.
35pub fn code_unit_string_from_expr(expr: &Expr) -> Result<CodeUnitString> {
36    let Expr::Extension { tag, payload } = expr else {
37        return Err(Error::Eval("expected tagged exact-unit string".to_owned()));
38    };
39    if tag != &symbol() {
40        return Err(Error::Eval(format!(
41            "expected tag {}, found {tag}",
42            symbol()
43        )));
44    }
45    let Expr::Bytes(bytes) = payload.as_ref() else {
46        return Err(Error::Eval(
47            "exact-unit string payload must be bytes".to_owned(),
48        ));
49    };
50    if bytes.len() % 2 != 0 {
51        return Err(Error::Eval(format!(
52            "exact-unit string payload has odd byte length {}",
53            bytes.len()
54        )));
55    }
56    Ok(CodeUnitString::from_code_units(
57        bytes
58            .chunks_exact(2)
59            .map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
60            .collect(),
61    ))
62}
63
64/// Browse table for an exact-unit string.
65///
66/// `code-unit-length` and `scalar-text` are deliberately separate: invalid
67/// UTF-16 has no scalar-text member, so browsing never relabels it as text.
68pub fn code_unit_string_browse(cx: &mut Cx, text: &CodeUnitString) -> Result<Value> {
69    let mut entries = vec![
70        (
71            Symbol::new("kind"),
72            cx.factory().string(CODE_UNIT_STRING_SYMBOL.to_owned())?,
73        ),
74        (
75            Symbol::new("code-unit-length"),
76            cx.factory().string(text.len().to_string())?,
77        ),
78        (
79            Symbol::new("code-units-be"),
80            cx.factory().bytes(match code_unit_string_to_expr(text) {
81                Expr::Extension { payload, .. } => match *payload {
82                    Expr::Bytes(bytes) => bytes,
83                    _ => unreachable!(),
84                },
85                _ => unreachable!(),
86            })?,
87        ),
88    ];
89    if let Ok(scalar) = text.to_scalar() {
90        entries.push((Symbol::new("scalar-text"), cx.factory().string(scalar)?));
91    }
92    cx.factory().table(entries)
93}
94
95impl Object for CodeUnitString {
96    fn display(&self, _cx: &mut Cx) -> Result<String> {
97        let body = self
98            .code_units()
99            .map(|unit| format!("{unit:04x}"))
100            .collect::<Vec<_>>()
101            .join(" ");
102        Ok(format!("#<{} [{body}]>", CODE_UNIT_STRING_SYMBOL))
103    }
104
105    fn as_any(&self) -> &dyn Any {
106        self
107    }
108}
109
110impl ObjectCompat for CodeUnitString {
111    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
112        Ok(code_unit_string_to_expr(self))
113    }
114
115    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
116        code_unit_string_browse(cx, self)
117    }
118
119    fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
120        Some(self)
121    }
122}
123
124impl ObjectEncode for CodeUnitString {
125    fn object_encoding(&self, _cx: &mut Cx) -> Result<ObjectEncoding> {
126        let Expr::Extension { payload, .. } = code_unit_string_to_expr(self) else {
127            unreachable!()
128        };
129        Ok(ObjectEncoding::Constructor {
130            class: symbol(),
131            args: vec![*payload],
132        })
133    }
134}
135
136/// Shape that accepts exact-unit strings, never ordinary scalar strings.
137#[derive(Clone, Copy, Debug, Default)]
138pub struct CodeUnitStringShape;
139
140impl Shape for CodeUnitStringShape {
141    fn symbol(&self) -> Option<Symbol> {
142        Some(symbol())
143    }
144
145    fn check_value(&self, _cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
146        Ok(
147            if value.object().downcast_ref::<CodeUnitString>().is_some() {
148                ShapeMatch::accept(MatchScore::exact(1))
149            } else {
150                ShapeMatch::reject("expected exact-unit string")
151            },
152        )
153    }
154
155    fn check_expr(&self, _cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
156        Ok(if code_unit_string_from_expr(expr).is_ok() {
157            ShapeMatch::accept(MatchScore::exact(1))
158        } else {
159            ShapeMatch::reject("expected tagged exact-unit string")
160        })
161    }
162
163    fn describe(&self, _cx: &mut Cx) -> Result<ShapeDoc> {
164        Ok(ShapeDoc::new("exact UTF-16 code-unit string")
165            .with_detail("distinct from scalar Unicode text"))
166    }
167}
168
169/// Standard read constructor for [`CodeUnitString`].
170#[derive(Clone, Copy, Debug, Default)]
171pub struct CodeUnitStringReadConstructor;
172
173impl Object for CodeUnitStringReadConstructor {
174    fn display(&self, _cx: &mut Cx) -> Result<String> {
175        Ok(format!("#<read-constructor {}>", CODE_UNIT_STRING_SYMBOL))
176    }
177
178    fn as_any(&self) -> &dyn Any {
179        self
180    }
181}
182
183impl ObjectCompat for CodeUnitStringReadConstructor {
184    fn as_read_constructor(&self) -> Option<&dyn ReadConstructor> {
185        Some(self)
186    }
187}
188
189impl ReadConstructor for CodeUnitStringReadConstructor {
190    fn symbol(&self) -> Symbol {
191        symbol()
192    }
193
194    fn args_shape(&self, cx: &mut Cx) -> Result<ShapeRef> {
195        cx.factory().nil()
196    }
197
198    fn construct_read(&self, cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
199        let [arg] = args.as_slice() else {
200            return Err(Error::Eval(
201                "exact-unit string read constructor expects one byte string".to_owned(),
202            ));
203        };
204        let expr = arg.object().as_expr(cx)?;
205        let Expr::Bytes(bytes) = expr else {
206            return Err(Error::Eval(
207                "exact-unit string read constructor expects bytes".to_owned(),
208            ));
209        };
210        let tagged = Expr::Extension {
211            tag: symbol(),
212            payload: Box::new(Expr::Bytes(bytes)),
213        };
214        cx.factory()
215            .opaque(std::sync::Arc::new(code_unit_string_from_expr(&tagged)?))
216    }
217}
218
219/// Convert for a scalar-text-only codec, refusing at the exact bad unit.
220pub fn scalar_text(text: &CodeUnitString) -> core::result::Result<String, CodeUnitStringError> {
221    text.to_scalar()
222}
223
224#[cfg(test)]
225mod tests {
226    use std::sync::Arc;
227
228    use sim_kernel::{DefaultFactory, NoopEvalPolicy};
229
230    use crate::{CodeUnitOffset, InvalidSurrogate};
231
232    use super::*;
233
234    #[test]
235    fn lone_surrogate_round_trips_tagged_bytes_and_scalar_codec_refuses_located() {
236        let exact = CodeUnitString::from_code_units(vec![0x0061, 0xd800, 0x0062]);
237        let encoded = code_unit_string_to_expr(&exact);
238        assert_eq!(code_unit_string_from_expr(&encoded).unwrap(), exact);
239        assert_eq!(
240            scalar_text(&exact),
241            Err(CodeUnitStringError::LoneSurrogate(InvalidSurrogate {
242                offset: CodeUnitOffset::new(1),
243                unit: 0xd800,
244            }))
245        );
246    }
247
248    #[test]
249    fn shape_and_browse_keep_exact_units_distinct_from_scalar_text() {
250        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
251        let exact = CodeUnitString::from_code_units(vec![0xd800]);
252        let value = cx.factory().opaque(Arc::new(exact.clone())).unwrap();
253        assert!(
254            CodeUnitStringShape
255                .check_value(&mut cx, value)
256                .unwrap()
257                .accepted
258        );
259        assert!(
260            !CodeUnitStringShape
261                .check_expr(&mut cx, &Expr::String("text".to_owned()))
262                .unwrap()
263                .accepted
264        );
265        let table = code_unit_string_browse(&mut cx, &exact).unwrap();
266        let entries = table
267            .object()
268            .as_table_impl()
269            .unwrap()
270            .entries(&mut cx)
271            .unwrap();
272        assert!(
273            !entries
274                .iter()
275                .any(|(key, _)| key == &Symbol::new("scalar-text"))
276        );
277    }
278}