1use sim_kernel::{
2 ClassRef, Cx, Error, Expr, MatchScore, Object, ObjectCompat, ObjectEncode, ObjectEncoding,
3 Origin, Ref, Result, Shape, ShapeDoc, ShapeMatch, Symbol, Value,
4};
5
6pub const RAISED_SYMBOL: &str = "control/Raised";
8
9fn raised_symbol() -> Symbol {
10 Symbol::qualified("control", "Raised")
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct RaisedBrowseBudget {
16 max_payload_bytes: usize,
17}
18
19impl RaisedBrowseBudget {
20 pub fn new(max_payload_bytes: usize) -> Result<Self> {
22 if max_payload_bytes == 0 {
23 return Err(Error::Eval(
24 "raised browse payload budget must be non-zero".to_owned(),
25 ));
26 }
27 Ok(Self { max_payload_bytes })
28 }
29
30 pub fn max_payload_bytes(self) -> usize {
32 self.max_payload_bytes
33 }
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct RaisedBrowseProjection {
39 pub payload: String,
41 pub truncated: bool,
43 pub original_payload_bytes: usize,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Raised {
54 class: ClassRef,
55 payload: Value,
56 origin: Origin,
57 profile: Symbol,
58}
59
60impl Raised {
61 pub fn new(class: ClassRef, payload: Value, origin: Origin, profile: Symbol) -> Result<Self> {
66 if payload.object().downcast_ref::<Self>().is_some() {
67 return Err(Error::Eval(
68 "Raised payload cannot be another Raised envelope".to_owned(),
69 ));
70 }
71 Ok(Self {
72 class,
73 payload,
74 origin,
75 profile,
76 })
77 }
78
79 pub fn class_ref(&self) -> &ClassRef {
81 &self.class
82 }
83
84 pub fn payload(&self) -> &Value {
86 &self.payload
87 }
88
89 pub fn origin(&self) -> &Origin {
91 &self.origin
92 }
93
94 pub fn profile(&self) -> &Symbol {
96 &self.profile
97 }
98
99 pub fn browse(
101 &self,
102 cx: &mut Cx,
103 budget: RaisedBrowseBudget,
104 ) -> Result<RaisedBrowseProjection> {
105 let rendered = self.payload.object().display(cx)?;
106 let original_payload_bytes = rendered.len();
107 let mut end = rendered.len().min(budget.max_payload_bytes);
108 while !rendered.is_char_boundary(end) {
109 end -= 1;
110 }
111 Ok(RaisedBrowseProjection {
112 payload: rendered[..end].to_owned(),
113 truncated: end < rendered.len(),
114 original_payload_bytes,
115 })
116 }
117
118 fn constructor_args(&self, cx: &mut Cx) -> Result<Vec<Expr>> {
119 Ok(vec![
120 self.class.object().as_expr(cx)?,
121 self.payload.object().as_expr(cx)?,
122 Expr::Vector(vec![
123 Expr::String(self.origin.codec.0.to_string()),
124 Expr::String(self.origin.source.0.clone()),
125 Expr::String(self.origin.span.start.to_string()),
126 Expr::String(self.origin.span.end.to_string()),
127 Expr::String(format!("{:?}", self.origin.trivia)),
128 ]),
129 Expr::Symbol(self.profile.clone()),
130 ])
131 }
132}
133
134impl Object for Raised {
135 fn display(&self, cx: &mut Cx) -> Result<String> {
136 let projection = self.browse(cx, RaisedBrowseBudget::new(256)?)?;
137 let suffix = if projection.truncated {
138 " [truncated]"
139 } else {
140 ""
141 };
142 Ok(format!(
143 "#<{} profile={} payload={}{}>",
144 RAISED_SYMBOL, self.profile, projection.payload, suffix
145 ))
146 }
147
148 fn as_any(&self) -> &dyn std::any::Any {
149 self
150 }
151}
152
153impl ObjectCompat for Raised {
154 fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
155 Ok(Expr::Extension {
156 tag: Symbol::qualified("citizen", "read-construct"),
157 payload: Box::new(Expr::Vector(vec![
158 Expr::Symbol(raised_symbol()),
159 Expr::Vector(self.constructor_args(cx)?),
160 ])),
161 })
162 }
163
164 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
165 let projection = self.browse(cx, RaisedBrowseBudget::new(256)?)?;
166 cx.factory().table(vec![
167 (Symbol::new("class"), self.class.clone()),
168 (Symbol::new("payload"), self.payload.clone()),
169 (
170 Symbol::new("origin-source"),
171 cx.factory().string(self.origin.source.0.clone())?,
172 ),
173 (
174 Symbol::new("profile"),
175 cx.factory().symbol(self.profile.clone())?,
176 ),
177 (
178 Symbol::new("payload-rendered"),
179 cx.factory().string(projection.payload)?,
180 ),
181 (
182 Symbol::new("payload-truncated"),
183 cx.factory().bool(projection.truncated)?,
184 ),
185 ])
186 }
187
188 fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
189 Some(self)
190 }
191}
192
193impl ObjectEncode for Raised {
194 fn object_encoding(&self, cx: &mut Cx) -> Result<ObjectEncoding> {
195 Ok(ObjectEncoding::Constructor {
196 class: raised_symbol(),
197 args: self.constructor_args(cx)?,
198 })
199 }
200}
201
202#[derive(Clone, Copy, Debug, Default)]
204pub struct RaisedShape;
205
206impl Shape for RaisedShape {
207 fn symbol(&self) -> Option<Symbol> {
208 Some(raised_symbol())
209 }
210
211 fn check_value(&self, _cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
212 Ok(if value.object().downcast_ref::<Raised>().is_some() {
213 ShapeMatch::accept(MatchScore::exact(1))
214 } else {
215 ShapeMatch::reject("expected control/Raised")
216 })
217 }
218
219 fn check_expr(&self, _cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
220 let accepted = matches!(expr, Expr::Extension { tag, payload }
221 if tag == &Symbol::qualified("citizen", "read-construct")
222 && matches!(payload.as_ref(), Expr::Vector(parts)
223 if matches!(parts.as_slice(), [Expr::Symbol(class), Expr::Vector(args)]
224 if class == &raised_symbol() && args.len() == 4)));
225 Ok(if accepted {
226 ShapeMatch::accept(MatchScore::exact(1))
227 } else {
228 ShapeMatch::reject("expected four-field control/Raised read-construct")
229 })
230 }
231
232 fn describe(&self, _cx: &mut Cx) -> Result<ShapeDoc> {
233 Ok(ShapeDoc::new("non-recursive raised completion envelope")
234 .with_detail("class, payload, origin, and profile"))
235 }
236}
237
238#[sim_citizen_derive::non_citizen(
239 reason = "live continuation capture handle; descriptor data is the continuation and capture refs",
240 kind = "handle",
241 descriptor = "core/Ref"
242)]
243#[derive(Clone, Debug, PartialEq, Eq)]
249pub struct ContinuationValue {
250 continuation: Ref,
251 capture_result: Ref,
252 multishot: bool,
253}
254
255impl ContinuationValue {
256 pub fn new(continuation: Ref, capture_result: Ref, multishot: bool) -> Self {
259 Self {
260 continuation,
261 capture_result,
262 multishot,
263 }
264 }
265
266 pub fn continuation(&self) -> &Ref {
268 &self.continuation
269 }
270
271 pub fn capture_result(&self) -> &Ref {
273 &self.capture_result
274 }
275
276 pub fn multishot(&self) -> bool {
278 self.multishot
279 }
280}
281
282impl Object for ContinuationValue {
283 fn display(&self, _cx: &mut Cx) -> Result<String> {
284 Ok(format!("#<control-continuation {:?}>", self.continuation))
285 }
286
287 fn as_any(&self) -> &dyn std::any::Any {
288 self
289 }
290}
291
292impl ObjectCompat for ContinuationValue {
293 fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
294 Ok(Expr::Call {
295 operator: Box::new(Expr::Symbol(Symbol::qualified("control", "continuation"))),
296 args: vec![ref_expr(&self.continuation)],
297 })
298 }
299}
300
301#[sim_citizen_derive::non_citizen(
302 reason = "control result ref wrapper; canonical data is the referenced value",
303 kind = "marker",
304 descriptor = "core/Ref"
305)]
306#[derive(Clone, Debug, PartialEq, Eq)]
311pub struct ControlResultValue {
312 reference: Ref,
313}
314
315impl ControlResultValue {
316 pub fn new(reference: Ref) -> Self {
318 Self { reference }
319 }
320
321 pub fn reference(&self) -> &Ref {
323 &self.reference
324 }
325}
326
327impl Object for ControlResultValue {
328 fn display(&self, _cx: &mut Cx) -> Result<String> {
329 Ok(format!("#<control-result {:?}>", self.reference))
330 }
331
332 fn as_any(&self) -> &dyn std::any::Any {
333 self
334 }
335}
336
337impl ObjectCompat for ControlResultValue {
338 fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
339 Ok(ref_expr(&self.reference))
340 }
341}
342
343pub(crate) fn ref_expr(reference: &Ref) -> Expr {
344 match reference {
345 Ref::Symbol(symbol) => Expr::Symbol(symbol.clone()),
346 other => Expr::String(format!("{other:?}")),
347 }
348}
349
350#[cfg(test)]
351mod raised_tests {
352 use std::sync::{Arc, Mutex};
353
354 use sim_kernel::{CodecId, DefaultFactory, NoopEvalPolicy, Span};
355
356 use crate::{
357 CleanupStack, FrameLimits, RaisedResumePacket, RaisedResumeResult, RaisedUnwind,
358 ResumableFrame,
359 };
360
361 use super::*;
362
363 fn fixture(cx: &mut Cx, payload: &str) -> Raised {
364 Raised::new(
365 cx.factory().symbol(Symbol::new("guest/Error")).unwrap(),
366 cx.factory().string(payload.to_owned()).unwrap(),
367 Origin {
368 codec: CodecId(7),
369 source: sim_kernel::SourceId("fixture.sim".to_owned()),
370 span: Span { start: 2, end: 9 },
371 trivia: Vec::new(),
372 },
373 Symbol::new("guest/profile-v1"),
374 )
375 .unwrap()
376 }
377
378 #[test]
379 fn checked_constructor_rejects_a_raised_payload() {
380 let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
381 let inner = fixture(&mut cx, "inner");
382 let inner = cx.factory().opaque(Arc::new(inner)).unwrap();
383 let error = Raised::new(
384 cx.factory().symbol(Symbol::new("guest/Error")).unwrap(),
385 inner,
386 Origin {
387 codec: CodecId(7),
388 source: sim_kernel::SourceId("fixture.sim".to_owned()),
389 span: Span { start: 0, end: 1 },
390 trivia: Vec::new(),
391 },
392 Symbol::new("guest/profile-v1"),
393 )
394 .unwrap_err();
395 assert!(error.to_string().contains("cannot be another Raised"));
396 }
397
398 #[test]
399 fn api_shape_has_exactly_four_non_recursive_fields() {
400 let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
401 let Raised {
402 class,
403 payload,
404 origin,
405 profile,
406 } = fixture(&mut cx, "payload");
407 let _: ClassRef = class;
408 let _: Value = payload;
409 let _: Origin = origin;
410 let _: Symbol = profile;
411 }
412
413 #[test]
414 fn browse_and_shape_report_budget_truncation_and_read_construct() {
415 let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
416 let raised = fixture(&mut cx, "abcdefgh");
417 assert_eq!(
418 raised
419 .browse(&mut cx, RaisedBrowseBudget::new(4).unwrap())
420 .unwrap(),
421 RaisedBrowseProjection {
422 payload: "abcd".to_owned(),
423 truncated: true,
424 original_payload_bytes: 8,
425 }
426 );
427 let expr = raised.as_expr(&mut cx).unwrap();
428 assert!(RaisedShape.check_expr(&mut cx, &expr).unwrap().accepted);
429 let value = cx.factory().opaque(Arc::new(raised)).unwrap();
430 assert!(RaisedShape.check_value(&mut cx, value).unwrap().accepted);
431 }
432
433 #[test]
434 fn raised_unwinds_two_cleanups_then_resumes_with_stable_receipts() {
435 let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
436 let receipts = Arc::new(Mutex::new(Vec::new()));
437 let mut cleanups: CleanupStack<RaisedUnwind<(), (), ()>> = CleanupStack::new();
438 for name in ["outer", "inner"] {
439 let receipts = Arc::clone(&receipts);
440 cleanups.push(move |_| receipts.lock().unwrap().push(name));
441 }
442 let reason = cleanups.unwind(RaisedUnwind::Exception(fixture(&mut cx, "boom")));
443 let RaisedUnwind::Exception(raised) = reason else {
444 unreachable!()
445 };
446 let mut frame = ResumableFrame::new(FrameLimits { depth: 2, work: 2 }, {
449 let mut started = false;
450 move |packet: RaisedResumePacket<()>, _: &mut crate::StepBudget| match packet {
451 RaisedResumePacket::Start if !started => {
452 started = true;
453 Ok::<_, crate::FrameError>(RaisedResumeResult::Yielded(()))
454 }
455 RaisedResumePacket::Throw(raised) => Ok(RaisedResumeResult::Failed(raised)),
456 _ => unreachable!(),
457 }
458 });
459 assert!(matches!(
460 frame.resume::<(), (), Raised>(RaisedResumePacket::Start),
461 Ok(RaisedResumeResult::Yielded(()))
462 ));
463 assert!(matches!(
464 frame.resume::<(), (), Raised>(RaisedResumePacket::Throw(raised)),
465 Ok(RaisedResumeResult::Failed(_))
466 ));
467 receipts.lock().unwrap().push("resumed");
468 assert_eq!(&*receipts.lock().unwrap(), &["inner", "outer", "resumed"]);
469 }
470}