1use std::collections::BTreeMap;
36use std::sync::{Arc, Mutex, MutexGuard};
37
38use blue_lang_waku::{Capability, Provided};
39use serde_json::{json, Value as Json};
40
41use crate::Pid;
42
43pub const FORMAT: &str = "blue-receipt";
45pub const VERSION: u64 = 1;
47
48fn locked<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
49 m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
50}
51
52#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct Frame {
55 pub granted: Vec<Capability>,
57 pub provided: Vec<Provided>,
60 pub withheld: Vec<Capability>,
62}
63
64fn labels(caps: &[Capability]) -> Json {
65 Json::Array(caps.iter().map(|c| json!(c.label())).collect())
66}
67
68fn capability(label: &str) -> Option<Capability> {
69 Capability::host_effects()
70 .into_iter()
71 .find(|c| c.label() == label)
72}
73
74fn capabilities(j: Option<&Json>, what: &str) -> Result<Vec<Capability>, String> {
75 j.and_then(Json::as_array)
76 .ok_or_else(|| format!("a hold without `{what}`"))?
77 .iter()
78 .map(|l| {
79 l.as_str()
80 .and_then(capability)
81 .ok_or_else(|| format!("`{l}` in `{what}` is not a host capability"))
82 })
83 .collect()
84}
85
86impl Frame {
87 #[must_use]
89 pub fn block(&self, host_io: u64) -> Json {
90 json!({
91 "granted": labels(&self.granted),
92 "withheld": labels(&self.withheld),
93 "host_io": host_io,
94 })
95 }
96
97 #[must_use]
100 pub fn provided_json(&self) -> Json {
101 Json::Array(
102 self.provided
103 .iter()
104 .map(|p| serde_json::to_value(p).unwrap_or(Json::Null))
105 .collect(),
106 )
107 }
108
109 #[must_use]
111 pub fn to_json(&self) -> Json {
112 json!({
113 "granted": labels(&self.granted),
114 "provided": self.provided_json(),
115 "withheld": labels(&self.withheld),
116 })
117 }
118
119 pub fn from_json(j: &Json) -> Result<Self, String> {
126 let provided = j
127 .get("provided")
128 .and_then(Json::as_array)
129 .ok_or("a hold without `provided`")?
130 .iter()
131 .map(|p| serde_json::from_value(p.clone()).map_err(|e| format!("a provider: {e}")))
132 .collect::<Result<_, _>>()?;
133 Ok(Self {
134 granted: capabilities(j.get("granted"), "granted")?,
135 provided,
136 withheld: capabilities(j.get("withheld"), "withheld")?,
137 })
138 }
139
140 #[must_use]
142 pub fn withheld_names(&self) -> Vec<(&'static str, Capability)> {
143 self.withheld
144 .iter()
145 .flat_map(|c| c.names().into_iter().map(move |n| (n, *c)))
146 .collect()
147 }
148}
149
150#[derive(Debug, Default)]
151struct Ledger {
152 current: Pid,
153 total: u64,
154 by_pid: BTreeMap<Pid, u64>,
155}
156
157#[derive(Clone, Debug, Default)]
159pub struct HostIo(Arc<Mutex<Ledger>>);
160
161impl HostIo {
162 #[must_use]
163 pub fn new() -> Self {
164 Self::default()
165 }
166
167 pub fn tally(&self) {
169 let mut l = locked(&self.0);
170 l.total += 1;
171 let pid = l.current;
172 *l.by_pid.entry(pid).or_default() += 1;
173 }
174
175 #[must_use]
177 pub fn total(&self) -> u64 {
178 locked(&self.0).total
179 }
180
181 #[must_use]
183 pub fn of(&self, pid: Pid) -> u64 {
184 locked(&self.0).by_pid.get(&pid).copied().unwrap_or(0)
185 }
186
187 pub fn enter(&self, pid: Pid) {
189 locked(&self.0).current = pid;
190 }
191
192 pub fn take(&self, pid: Pid) -> u64 {
194 locked(&self.0).by_pid.remove(&pid).unwrap_or(0)
195 }
196}
197
198type Deferred<T> = Box<dyn FnOnce() -> T + Send>;
199
200enum Table<T> {
201 Unknown,
202 Deferred(Deferred<T>),
203 Known(T),
204}
205
206struct Lazy<T>(Arc<Mutex<Table<T>>>);
209
210impl<T> Clone for Lazy<T> {
211 fn clone(&self) -> Self {
212 Self(Arc::clone(&self.0))
213 }
214}
215
216impl<T> Default for Lazy<T> {
217 fn default() -> Self {
218 Self(Arc::new(Mutex::new(Table::Unknown)))
219 }
220}
221
222impl<T> Lazy<T> {
223 fn defer(&self, compute: impl FnOnce() -> T + Send + 'static) {
224 *locked(&self.0) = Table::Deferred(Box::new(compute));
225 }
226
227 fn read<R>(&self, f: impl FnOnce(&T) -> Option<R>) -> Option<R> {
228 let mut state = locked(&self.0);
229 if let Table::Deferred(_) = &*state {
230 if let Table::Deferred(compute) = std::mem::replace(&mut *state, Table::Unknown) {
231 *state = Table::Known(compute());
232 }
233 }
234 match &*state {
235 Table::Known(table) => f(table),
236 Table::Unknown | Table::Deferred(_) => None,
237 }
238 }
239
240 fn state(&self) -> &'static str {
241 match &*locked(&self.0) {
242 Table::Unknown => "unknown",
243 Table::Deferred(_) => "deferred",
244 Table::Known(_) => "known",
245 }
246 }
247}
248
249#[derive(Clone, Default)]
251pub struct Identities(Lazy<BTreeMap<String, String>>);
252
253impl std::fmt::Debug for Identities {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 write!(f, "Identities({})", self.0.state())
256 }
257}
258
259impl Identities {
260 pub fn defer(&self, hash: impl FnOnce() -> BTreeMap<String, String> + Send + 'static) {
262 self.0.defer(hash);
263 }
264
265 #[must_use]
267 pub fn of(&self, key: &str) -> Option<String> {
268 self.0.read(|table| table.get(key).cloned())
269 }
270}
271
272#[derive(Clone, Debug)]
275pub struct Source {
276 pub file: String,
277 pub text: Arc<str>,
278}
279
280#[derive(Clone, Debug, PartialEq, Eq)]
282pub struct Place {
283 pub file: String,
284 pub line: usize,
285 pub column: usize,
286}
287
288impl std::fmt::Display for Place {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 write!(f, "{}:{}:{}", self.file, self.line, self.column)
291 }
292}
293
294#[derive(Clone, Default)]
297pub struct Places(Lazy<BTreeMap<String, Source>>);
298
299impl std::fmt::Debug for Places {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 write!(f, "Places({})", self.0.state())
302 }
303}
304
305impl Places {
306 pub fn defer(&self, read: impl FnOnce() -> BTreeMap<String, Source> + Send + 'static) {
308 self.0.defer(read);
309 }
310
311 #[must_use]
316 pub fn locate(&self, key: &str, span: tatara_lisp::Span) -> Option<Place> {
317 if span.is_synthetic() {
318 return None;
319 }
320 self.0.read(|table| {
321 let source = table.get(key)?;
322 (span.end <= source.text.len()).then(|| {
323 let (line, column) = tatara_lisp::Span::line_col(&source.text, span.start);
324 Place {
325 file: source.file.clone(),
326 line,
327 column,
328 }
329 })
330 })
331 }
332}
333
334#[derive(Clone, Copy, Debug, PartialEq, Eq)]
336pub enum Subject {
337 Actor { pid: Pid, incarnation: usize },
339 Run,
341}
342
343#[derive(Clone, Debug, PartialEq, Eq)]
345pub struct Receipt {
346 pub subject: Subject,
347 pub definition: Option<String>,
349 pub def_hash: Option<String>,
351 pub frame: Frame,
352 pub host_io: u64,
354 pub exit_kind: String,
355 pub exit_text: String,
356}
357
358impl Receipt {
359 #[must_use]
361 pub fn to_json(&self) -> Json {
362 let (subject, pid, incarnation) = match self.subject {
363 Subject::Actor { pid, incarnation } => ("actor", pid.0, incarnation),
364 Subject::Run => ("run", crate::ROOT.0, 0),
365 };
366 json!({
367 "format": FORMAT,
368 "version": VERSION,
369 "subject": subject,
370 "pid": pid,
371 "incarnation": incarnation,
372 "definition": self.definition,
373 "def_hash": self.def_hash,
374 "exit": { "kind": self.exit_kind, "text": self.exit_text },
375 "frame": self.frame.block(self.host_io),
376 "provided": self.frame.provided_json(),
377 })
378 }
379
380 #[must_use]
382 pub fn bytes(&self) -> Vec<u8> {
383 canonical(&self.to_json()).into_bytes()
384 }
385}
386
387pub type ReceiptSink = Arc<dyn Fn(&Receipt) + Send + Sync>;
389
390#[must_use]
392pub fn canonical(j: &Json) -> String {
393 let mut out = String::new();
394 write_canonical(j, &mut out);
395 out
396}
397
398fn write_canonical(j: &Json, out: &mut String) {
399 match j {
400 Json::Object(m) => {
401 let mut keys: Vec<&String> = m.keys().collect();
402 keys.sort();
403 out.push('{');
404 for (i, k) in keys.into_iter().enumerate() {
405 if i > 0 {
406 out.push(',');
407 }
408 out.push_str(&Json::String(k.clone()).to_string());
409 out.push(':');
410 write_canonical(&m[k], out);
411 }
412 out.push('}');
413 }
414 Json::Array(items) => {
415 out.push('[');
416 for (i, v) in items.iter().enumerate() {
417 if i > 0 {
418 out.push(',');
419 }
420 write_canonical(v, out);
421 }
422 out.push(']');
423 }
424 other => out.push_str(&other.to_string()),
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 fn frame() -> Frame {
433 Frame {
434 granted: vec![Capability::FileSystem],
435 provided: vec![
436 blue_lang_waku::Provision::DeterministicStepped {
437 at_ms: 5,
438 step_ms: 1,
439 }
440 .into(),
441 Provided {
442 provision: blue_lang_waku::Provision::NullSink,
443 scope: Some(std::collections::BTreeSet::from(["kazu".to_string()])),
444 },
445 ],
446 withheld: vec![
447 Capability::Process,
448 Capability::Environment,
449 Capability::Network,
450 Capability::Entropy,
451 ],
452 }
453 }
454
455 #[test]
456 fn the_frame_block_is_granted_withheld_and_host_io_and_nothing_else() {
457 let block = frame().block(3);
458 let keys: Vec<&String> = block.as_object().expect("object").keys().collect();
459 let mut sorted = keys.clone();
460 sorted.sort();
461 assert_eq!(sorted, ["granted", "host_io", "withheld"]);
462 assert_eq!(
463 canonical(&block),
464 r#"{"granted":["filesystem"],"host_io":3,"withheld":["process","environment","network","entropy"]}"#
465 );
466 }
467
468 #[test]
469 fn a_hold_survives_the_trace_format() {
470 let f = frame();
471 assert_eq!(Frame::from_json(&f.to_json()).expect("parse"), f);
472 let mut bad = f.to_json();
473 bad["withheld"] = json!(["disk"]);
474 assert!(Frame::from_json(&bad).unwrap_err().contains("disk"));
475 assert!(Frame::from_json(&json!({})).is_err());
476 }
477
478 #[test]
479 fn withheld_names_are_every_name_of_every_withheld_capability() {
480 let names = frame().withheld_names();
481 assert!(names.contains(&("getenv", Capability::Environment)));
482 assert!(names.iter().all(|(_, c)| *c != Capability::FileSystem));
483 assert_eq!(
484 names.len(),
485 frame()
486 .withheld
487 .iter()
488 .map(|c| c.names().len())
489 .sum::<usize>()
490 );
491 assert!(Frame::default().withheld_names().is_empty());
492 }
493
494 #[test]
495 fn the_ledger_tallies_to_the_process_running_and_takes_per_incarnation() {
496 let io = HostIo::new();
497 io.tally();
498 io.enter(Pid(3));
499 io.tally();
500 io.tally();
501 io.enter(crate::ROOT);
502 io.tally();
503 assert_eq!(io.total(), 4);
504 assert_eq!(io.of(crate::ROOT), 2);
505 assert_eq!(io.of(Pid(3)), 2);
506 assert_eq!(io.take(Pid(3)), 2);
507 assert_eq!(io.of(Pid(3)), 0);
508 assert_eq!(io.total(), 4, "the run total keeps every call");
509 assert_eq!(HostIo::new().total(), 0);
510 }
511
512 #[test]
513 fn identities_are_hashed_once_and_only_when_asked() {
514 let ids = Identities::default();
515 assert_eq!(ids.of("%root/f"), None);
516 let calls = Arc::new(Mutex::new(0));
517 let c = Arc::clone(&calls);
518 ids.defer(move || {
519 *locked(&c) += 1;
520 BTreeMap::from([("%root/f".to_string(), "b3:00".to_string())])
521 });
522 assert_eq!(*locked(&calls), 0);
523 assert_eq!(ids.of("%root/f").as_deref(), Some("b3:00"));
524 assert_eq!(ids.clone().of("%root/g"), None);
525 assert_eq!(*locked(&calls), 1);
526 }
527
528 #[test]
529 fn a_place_is_read_once_and_only_inside_its_own_file() {
530 let places = Places::default();
531 let at = tatara_lisp::Span::new(10, 14);
532 assert_eq!(places.locate("%root/f", at), None);
533 let calls = Arc::new(Mutex::new(0));
534 let c = Arc::clone(&calls);
535 places.defer(move || {
536 *locked(&c) += 1;
537 BTreeMap::from([(
538 "%root/f".to_string(),
539 Source {
540 file: "w.b".into(),
541 text: Arc::from("def f()\n g(1)\nend\n"),
542 },
543 )])
544 });
545 assert_eq!(*locked(&calls), 0);
546 let place = places.locate("%root/f", at).expect("in its file");
547 assert_eq!(place.to_string(), "w.b:2:3");
548 assert_eq!(
549 places.locate("%root/f", tatara_lisp::Span::new(10, 99)),
550 None
551 );
552 assert_eq!(
553 places.locate("%root/f", tatara_lisp::Span::synthetic()),
554 None
555 );
556 assert_eq!(places.clone().locate("%root/g", at), None);
557 assert_eq!(*locked(&calls), 1);
558 }
559
560 #[test]
561 fn canonical_text_sorts_keys_at_every_depth_and_has_no_whitespace() {
562 let j = json!({"b": [ {"z": 1, "a": "x y"} ], "a": null});
563 assert_eq!(canonical(&j), r#"{"a":null,"b":[{"a":"x y","z":1}]}"#);
564 let r = Receipt {
565 subject: Subject::Actor {
566 pid: Pid(2),
567 incarnation: 1,
568 },
569 definition: Some("%root/w".into()),
570 def_hash: Some("b3:ab".into()),
571 frame: frame(),
572 host_io: 7,
573 exit_kind: "normal".into(),
574 exit_text: "normal".into(),
575 };
576 let text = String::from_utf8(r.bytes()).expect("utf8");
577 assert!(!text.contains(' ') && !text.contains('\n'), "{text}");
578 let back: Json = serde_json::from_str(&text).expect("json");
579 assert_eq!(canonical(&back), text);
580 assert_eq!(back["frame"]["host_io"], 7);
581 assert_eq!(back["subject"], "actor");
582 assert_eq!(back["provided"][0]["provider"], "deterministic-stepped");
583 assert!(
584 back["provided"][0].get("for").is_none(),
585 "a whole-program provider"
586 );
587 assert_eq!(
588 back["provided"][1],
589 json!({"provider": "null-sink", "for": ["kazu"]})
590 );
591 }
592}