1use 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
12pub const CODE_UNIT_STRING_SYMBOL: &str = "text/CodeUnitString";
14
15fn symbol() -> Symbol {
16 Symbol::qualified("text", "CodeUnitString")
17}
18
19pub 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
34pub 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
64pub 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#[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#[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
219pub 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}