1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, TryReserveError};
2use std::fmt;
3
4use serde_json::Value;
5
6use crate::validation::valid_nonempty_text;
7use crate::{
8 Diagnostic, DiagnosticId, Error, HistoryEntry, HistoryId, Producer, Source, SourceDescriptor,
9 SourceId, SourceMapEntry, SourceRelation, SourceSpan,
10};
11
12#[derive(Clone, Debug)]
13struct ModuleRecords {
14 producer: Producer,
15 sources: Vec<SourceDescriptor>,
16 source_map: Vec<SourceMapEntry>,
17 history: Vec<HistoryEntry>,
18 extensions: BTreeMap<String, Value>,
19 retained_source: Option<Source>,
20 source_positions: HashMap<SourceId, usize>,
25 diagnostic_ids: HashSet<DiagnosticId>,
26 history_ids: HashSet<HistoryId>,
27}
28
29impl Default for ModuleRecords {
30 fn default() -> Self {
31 Self {
32 producer: Producer::powerio(),
33 sources: Vec::new(),
34 source_map: Vec::new(),
35 history: Vec::new(),
36 extensions: BTreeMap::new(),
37 retained_source: None,
38 source_positions: HashMap::new(),
39 diagnostic_ids: HashSet::new(),
40 history_ids: HashSet::new(),
41 }
42 }
43}
44
45fn allocation_refused(cause: TryReserveError) -> Error {
46 Error::new(
47 &crate::codes::REQUEST_RECORD_ALLOCATION_REFUSED,
48 "cannot reserve the record identity index",
49 )
50 .with_cause(cause)
51}
52
53pub struct PioModule<T> {
58 value: T,
59 diagnostics: Vec<Diagnostic>,
60 records: ModuleRecords,
61}
62
63impl<T: Clone> Clone for PioModule<T> {
64 fn clone(&self) -> Self {
65 Self {
66 value: self.value.clone(),
67 diagnostics: self.diagnostics.clone(),
68 records: self.records.clone(),
69 }
70 }
71}
72
73impl<T> PioModule<T> {
74 #[must_use]
75 pub fn new(value: T) -> Self {
76 Self {
77 value,
78 diagnostics: Vec::new(),
79 records: ModuleRecords::default(),
80 }
81 }
82
83 #[must_use]
84 pub fn into_value(self) -> T {
85 self.value
86 }
87
88 #[must_use]
90 pub const fn value(&self) -> &T {
91 &self.value
92 }
93
94 pub fn value_mut(&mut self) -> &mut T {
103 self.records.retained_source = None;
104 self.sever_value_targets();
105 &mut self.value
106 }
107
108 #[must_use]
111 pub fn diagnostics(&self) -> &[Diagnostic] {
112 &self.diagnostics
113 }
114
115 #[must_use]
116 pub const fn producer(&self) -> &Producer {
117 &self.records.producer
118 }
119
120 #[must_use]
121 pub fn sources(&self) -> &[SourceDescriptor] {
122 &self.records.sources
123 }
124
125 #[must_use]
126 pub fn source_map(&self) -> &[SourceMapEntry] {
127 &self.records.source_map
128 }
129
130 #[must_use]
131 pub fn history(&self) -> &[HistoryEntry] {
132 &self.records.history
133 }
134
135 #[must_use]
136 pub const fn extensions(&self) -> &BTreeMap<String, Value> {
137 &self.records.extensions
138 }
139
140 #[must_use]
141 pub const fn source(&self) -> Option<&Source> {
142 self.records.retained_source.as_ref()
143 }
144
145 #[must_use]
146 pub fn with_producer(mut self, producer: Producer) -> Self {
147 self.records.producer = producer;
148 self
149 }
150
151 #[must_use]
152 pub fn with_source(mut self, source: Source) -> Self {
153 self.records.retained_source = Some(source);
154 self
155 }
156
157 #[must_use]
162 pub fn sever_source(mut self) -> Self {
163 self.records.retained_source = None;
164 self
165 }
166
167 pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Result<Self, Error> {
171 self.add_diagnostic(diagnostic)?;
172 Ok(self)
173 }
174
175 pub fn add_source_descriptor(&mut self, source: SourceDescriptor) -> Result<(), Error> {
176 if self.records.sources.len() >= crate::validation::MAX_MODULE_SOURCES {
177 return Err(record_cap("sources", crate::validation::MAX_MODULE_SOURCES));
178 }
179 if self.records.source_positions.contains_key(source.id()) {
180 return Err(Error::new(
181 &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
182 format!("duplicate source ID `{}`", source.id()),
183 ));
184 }
185 self.records
186 .source_positions
187 .try_reserve(1)
188 .map_err(allocation_refused)?;
189 self.records
190 .source_positions
191 .insert(source.id().clone(), self.records.sources.len());
192 self.records.sources.push(source);
193 Ok(())
194 }
195
196 pub fn add_source_map_entry(&mut self, entry: SourceMapEntry) -> Result<(), Error> {
197 if self.records.source_map.len() >= crate::validation::MAX_MODULE_SOURCE_MAP_ENTRIES {
198 return Err(record_cap(
199 "source map entries",
200 crate::validation::MAX_MODULE_SOURCE_MAP_ENTRIES,
201 ));
202 }
203 for span in entry.spans() {
204 validate_span(span, &self.records.sources, &self.records.source_positions)?;
205 }
206 self.records.source_map.push(entry);
207 Ok(())
208 }
209
210 pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) -> Result<(), Error> {
211 if self.diagnostics.len() >= crate::validation::MAX_MODULE_DIAGNOSTICS {
212 return Err(record_cap(
213 "diagnostics",
214 crate::validation::MAX_MODULE_DIAGNOSTICS,
215 ));
216 }
217 if let Some(id) = diagnostic.id()
218 && self.records.diagnostic_ids.contains(id)
219 {
220 return Err(Error::new(
221 &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
222 format!("duplicate diagnostic ID `{id}`"),
223 ));
224 }
225 for span in diagnostic.spans() {
226 validate_span(span, &self.records.sources, &self.records.source_positions)?;
227 }
228 if let Some(id) = diagnostic.id() {
229 self.records
230 .diagnostic_ids
231 .try_reserve(1)
232 .map_err(allocation_refused)?;
233 self.records.diagnostic_ids.insert(id.clone());
234 }
235 self.diagnostics.push(diagnostic);
236 Ok(())
237 }
238
239 pub fn add_history_entry(&mut self, entry: HistoryEntry) -> Result<(), Error> {
240 if self.records.history.len() >= crate::validation::MAX_MODULE_HISTORY_ENTRIES {
241 return Err(record_cap(
242 "history entries",
243 crate::validation::MAX_MODULE_HISTORY_ENTRIES,
244 ));
245 }
246 if self.records.history_ids.contains(entry.id()) {
247 return Err(Error::new(
248 &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
249 format!("duplicate history ID `{}`", entry.id()),
250 ));
251 }
252 self.records
253 .history_ids
254 .try_reserve(1)
255 .map_err(allocation_refused)?;
256 self.records.history_ids.insert(entry.id().clone());
257 self.records.history.push(entry);
258 Ok(())
259 }
260
261 pub fn insert_extension(
262 &mut self,
263 namespace: impl Into<String>,
264 value: Value,
265 ) -> Result<Option<Value>, Error> {
266 let namespace = namespace.into();
267 if !valid_extension_namespace(&namespace) {
268 return Err(Error::new(
269 &crate::codes::REQUEST_RECORD_INVALID_EXTENSION,
270 "extension keys must be bounded namespaced strings",
271 ));
272 }
273 if self.records.extensions.len() >= crate::validation::MAX_MODULE_EXTENSION_KEYS
274 && !self.records.extensions.contains_key(&namespace)
275 {
276 return Err(Error::new(
277 &crate::codes::REQUEST_RECORD_TOO_LARGE,
278 format!(
279 "a module carries at most {} extension keys",
280 crate::validation::MAX_MODULE_EXTENSION_KEYS
281 ),
282 ));
283 }
284 Ok(self.records.extensions.insert(namespace, value))
285 }
286
287 pub fn verify_records(&self) -> Result<(), Error> {
289 let source_ids: BTreeSet<_> = self
290 .records
291 .sources
292 .iter()
293 .map(SourceDescriptor::id)
294 .collect();
295 if source_ids.len() != self.records.sources.len() {
296 return Err(Error::new(
297 &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
298 "module contains duplicate source IDs",
299 ));
300 }
301 for entry in &self.records.source_map {
302 for span in entry.spans() {
303 validate_span(span, &self.records.sources, &self.records.source_positions)?;
304 }
305 }
306
307 let diagnostic_ids: BTreeSet<&DiagnosticId> =
308 self.diagnostics.iter().filter_map(Diagnostic::id).collect();
309 if diagnostic_ids.len()
310 != self
311 .diagnostics
312 .iter()
313 .filter(|diagnostic| diagnostic.id().is_some())
314 .count()
315 {
316 return Err(Error::new(
317 &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
318 "module contains duplicate diagnostic IDs",
319 ));
320 }
321 for diagnostic in &self.diagnostics {
322 for span in diagnostic.spans() {
323 validate_span(span, &self.records.sources, &self.records.source_positions)?;
324 }
325 for related in diagnostic.related() {
326 if !diagnostic_ids.contains(related) {
327 return Err(Error::new(
328 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
329 format!("diagnostic refers to unknown diagnostic `{related}`"),
330 ));
331 }
332 }
333 }
334
335 let history_ids: BTreeSet<_> = self.records.history.iter().map(HistoryEntry::id).collect();
336 if history_ids.len() != self.records.history.len() {
337 return Err(Error::new(
338 &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
339 "module contains duplicate history IDs",
340 ));
341 }
342 if self
343 .records
344 .extensions
345 .keys()
346 .any(|namespace| !valid_extension_namespace(namespace))
347 {
348 return Err(Error::new(
349 &crate::codes::REQUEST_RECORD_INVALID_EXTENSION,
350 "module contains an extension key that is not namespaced",
351 ));
352 }
353 Ok(())
354 }
355
356 pub fn sever_value_targets(&mut self) {
364 for diagnostic in &mut self.diagnostics {
365 diagnostic.clear_target();
366 }
367 self.records.source_map.clear();
368 }
369
370 pub fn derive_value<U>(
375 self,
376 producer: Producer,
377 history: HistoryEntry,
378 derive: impl FnOnce(T) -> U,
379 ) -> Result<PioModule<U>, Error> {
380 self.try_derive_value(producer, history, |value| Ok(derive(value)))
381 }
382
383 pub fn try_derive_value<U>(
385 self,
386 producer: Producer,
387 history: HistoryEntry,
388 derive: impl FnOnce(T) -> Result<U, Error>,
389 ) -> Result<PioModule<U>, Error> {
390 let Self {
391 value,
392 mut diagnostics,
393 mut records,
394 } = self;
395 let value = derive(value)?;
396 records.producer = producer;
397 records.retained_source = None;
398 records.source_map.clear();
399 for diagnostic in &mut diagnostics {
400 diagnostic.clear_target();
401 }
402
403 let mut derived = PioModule {
404 value,
405 diagnostics,
406 records,
407 };
408 derived.add_history_entry(history)?;
409 derived.verify_records()?;
410 Ok(derived)
411 }
412
413 pub fn try_derive_from_source<U>(
426 self,
427 input_target: &str,
428 source: Source,
429 producer: Producer,
430 history: HistoryEntry,
431 derive: impl FnOnce(T, &Source) -> Result<(U, Vec<Diagnostic>), Error>,
432 ) -> Result<PioModule<U>, Error> {
433 SourceMapEntry::new(input_target, SourceRelation::Transformed, Vec::new())?;
435
436 let Self {
437 value,
438 mut diagnostics,
439 mut records,
440 } = self;
441 let (value, mut source_diagnostics) = derive(value, &source)?;
442
443 for diagnostic in &mut diagnostics {
444 diagnostic.prefix_target(input_target)?;
445 }
446 let old_source_map = std::mem::take(&mut records.source_map);
447 for entry in old_source_map {
448 records.source_map.push(SourceMapEntry::new(
449 format!("{input_target}{}", entry.target()),
450 entry.relation(),
451 entry.spans().to_vec(),
452 )?);
453 }
454
455 let mut source_ids = HashMap::new();
456 let mut root_spans = Vec::new();
457 for (index, buffer) in source.acquired_buffers().into_iter().enumerate() {
458 let mut suffix = index + 1;
459 let id = loop {
460 let candidate = SourceId::new(format!("solution-source-{suffix}"))?;
461 if !records.source_positions.contains_key(&candidate)
462 && !source_ids.values().any(|existing| existing == &candidate)
463 {
464 break candidate;
465 }
466 suffix += 1;
467 };
468 source_ids.insert(buffer.id().clone(), id.clone());
469
470 let name = std::path::Path::new(buffer.name())
471 .file_name()
472 .and_then(|name| name.to_str())
473 .unwrap_or_else(|| buffer.name());
474 let mut descriptor =
475 SourceDescriptor::new(id.clone(), name, buffer.bytes().len() as u64)?;
476 if let Some(format) = source.format() {
477 descriptor = descriptor.with_format(format.clone());
478 }
479 if records.sources.len() >= crate::validation::MAX_MODULE_SOURCES {
480 return Err(record_cap("sources", crate::validation::MAX_MODULE_SOURCES));
481 }
482 records
483 .source_positions
484 .try_reserve(1)
485 .map_err(allocation_refused)?;
486 records
487 .source_positions
488 .insert(id.clone(), records.sources.len());
489 records.sources.push(descriptor);
490 root_spans.push(SourceSpan::new(id, 0, buffer.bytes().len() as u64)?);
491 }
492
493 for diagnostic in &mut source_diagnostics {
494 diagnostic.remap_span_sources(|id| {
495 source_ids.get(id).cloned().unwrap_or_else(|| id.clone())
496 })?;
497 }
498 records.producer = producer;
499 records.retained_source = Some(source);
500
501 let mut derived = PioModule {
502 value,
503 diagnostics,
504 records,
505 };
506 for spans in root_spans.chunks(crate::validation::MAX_SOURCE_MAP_SPANS) {
507 derived.add_source_map_entry(SourceMapEntry::new(
508 "",
509 SourceRelation::Aggregated,
510 spans.to_vec(),
511 )?)?;
512 }
513 for diagnostic in source_diagnostics {
514 derived.add_diagnostic(diagnostic)?;
515 }
516 derived.add_history_entry(history)?;
517 derived.verify_records()?;
518 Ok(derived)
519 }
520
521 #[must_use]
523 pub fn map_value<U>(self, convert: impl FnOnce(T) -> U) -> PioModule<U> {
524 PioModule {
525 value: convert(self.value),
526 diagnostics: self.diagnostics,
527 records: self.records,
528 }
529 }
530
531 pub fn try_map_value<U, E>(
538 self,
539 convert: impl FnOnce(T) -> Result<U, E>,
540 ) -> Result<PioModule<U>, E> {
541 let Self {
542 value,
543 diagnostics,
544 records,
545 } = self;
546 Ok(PioModule {
547 value: convert(value)?,
548 diagnostics,
549 records,
550 })
551 }
552
553 pub fn take_source(&mut self) -> Option<Source> {
557 self.records.retained_source.take()
558 }
559
560 pub fn parsed(value: T, source: Source, diagnostics: Vec<Diagnostic>) -> Result<Self, Error> {
569 let mut module = Self::new(value);
570 let mut root_spans = Vec::new();
571 for buffer in source.acquired_buffers() {
572 let name = std::path::Path::new(buffer.name())
576 .file_name()
577 .and_then(|n| n.to_str())
578 .unwrap_or_else(|| buffer.name());
579 let mut descriptor =
580 SourceDescriptor::new(buffer.id().clone(), name, buffer.bytes().len() as u64)?;
581 if let Some(format) = source.format() {
582 descriptor = descriptor.with_format(format.clone());
583 }
584 module.add_source_descriptor(descriptor)?;
585 root_spans.push(SourceSpan::new(
586 buffer.id().clone(),
587 0,
588 buffer.bytes().len() as u64,
589 )?);
590 }
591 for spans in root_spans.chunks(crate::validation::MAX_SOURCE_MAP_SPANS) {
598 module.add_source_map_entry(SourceMapEntry::new(
599 "",
600 SourceRelation::Aggregated,
601 spans.to_vec(),
602 )?)?;
603 }
604 let mut module = module.with_source(source);
605 for record in diagnostics {
606 module.add_diagnostic(record)?;
607 }
608 Ok(module)
609 }
610
611 #[doc(hidden)]
613 #[allow(clippy::result_large_err)]
616 pub fn __try_map_value<U>(
617 self,
618 convert: impl FnOnce(T) -> Result<U, T>,
619 ) -> Result<PioModule<U>, PioModule<T>> {
620 let Self {
621 value,
622 diagnostics,
623 records,
624 } = self;
625 match convert(value) {
626 Ok(value) => Ok(PioModule {
627 value,
628 diagnostics,
629 records,
630 }),
631 Err(value) => Err(PioModule {
632 value,
633 diagnostics,
634 records,
635 }),
636 }
637 }
638}
639
640impl<T: Clone> PioModule<T> {
641 pub fn stage_edit(&mut self) -> StagedEdit<'_, T> {
648 let candidate = PioModule {
649 value: self.value.clone(),
650 diagnostics: self.diagnostics.clone(),
651 records: self.records.clone(),
652 };
653 StagedEdit {
654 module: self,
655 candidate,
656 }
657 }
658}
659
660pub struct StagedEdit<'a, T: Clone> {
662 module: &'a mut PioModule<T>,
663 candidate: PioModule<T>,
664}
665
666impl<T: Clone> StagedEdit<'_, T> {
667 #[must_use]
668 pub const fn value(&self) -> &T {
669 &self.candidate.value
670 }
671
672 pub const fn value_mut(&mut self) -> &mut T {
673 &mut self.candidate.value
674 }
675
676 #[must_use]
677 pub fn diagnostics(&self) -> &[Diagnostic] {
678 &self.candidate.diagnostics
679 }
680
681 pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) -> Result<(), Error> {
683 self.candidate.add_diagnostic(diagnostic)
684 }
685
686 pub fn commit(self, producer: Producer, history: HistoryEntry) -> Result<(), Error> {
691 let Self {
692 module,
693 mut candidate,
694 } = self;
695 candidate.records.producer = producer;
696 candidate.records.retained_source = None;
697 candidate.records.source_map.clear();
698 candidate.add_history_entry(history)?;
699 candidate.verify_records()?;
700 *module = candidate;
701 Ok(())
702 }
703}
704
705impl<T: fmt::Debug> fmt::Debug for PioModule<T> {
706 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
707 formatter
708 .debug_struct("PioModule")
709 .field("value", &self.value)
710 .field("diagnostics", &self.diagnostics)
711 .field("records", &self.records)
712 .finish()
713 }
714}
715
716fn record_cap(what: &str, max: usize) -> Error {
718 Error::new(
719 &crate::codes::REQUEST_RECORD_TOO_LARGE,
720 format!("the module already holds the maximum {max} {what}"),
721 )
722}
723
724fn validate_span(
725 span: &SourceSpan,
726 sources: &[SourceDescriptor],
727 positions: &HashMap<SourceId, usize>,
728) -> Result<(), Error> {
729 let Some(source) = positions
730 .get(span.source())
731 .and_then(|position| sources.get(*position))
732 else {
733 return Err(Error::new(
734 &crate::codes::REQUEST_RECORD_INVALID_SPAN,
735 format!("source span refers to unknown source `{}`", span.source()),
736 ));
737 };
738 if span.byte_end() > source.byte_length() {
739 return Err(Error::new(
740 &crate::codes::REQUEST_RECORD_INVALID_SPAN,
741 format!(
742 "source span end {} exceeds source `{}` length {}",
743 span.byte_end(),
744 span.source(),
745 source.byte_length()
746 ),
747 ));
748 }
749 Ok(())
750}
751
752fn valid_extension_namespace(namespace: &str) -> bool {
753 valid_nonempty_text(namespace)
754 && !namespace.starts_with('.')
755 && !namespace.ends_with('.')
756 && namespace.contains('.')
757 && namespace.split('.').all(|segment| !segment.is_empty())
758}
759
760#[cfg(test)]
761mod tests {
762 use std::rc::Rc;
763 use std::sync::Arc;
764
765 use super::*;
766 use crate::{DiagnosticSeverity, HistoryKind, SourceRelation};
767
768 #[test]
769 fn modules_accept_unregistered_application_values() {
770 struct ApplicationValue(Rc<()>);
771 let module = PioModule::new(ApplicationValue(Rc::new(())));
772 assert_eq!(Rc::strong_count(&module.value.0), 1);
773 }
774
775 #[test]
776 fn map_value_moves_records_and_retained_source_without_allocation() {
777 let bytes: Arc<[u8]> = b"source".as_slice().into();
778 let source = Source::from_memory("case.m", Arc::clone(&bytes)).unwrap();
779 let diagnostic = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "kept");
780 let module = PioModule::new(String::from("value"))
781 .with_source(source)
782 .with_diagnostic(diagnostic)
783 .unwrap();
784 let diagnostics_pointer = module.diagnostics().as_ptr();
785 let source_pointer = module
786 .source()
787 .unwrap()
788 .primary_buffer()
789 .unwrap()
790 .bytes()
791 .as_ptr();
792 let mapped = module.map_value(String::into_bytes);
793 assert_eq!(mapped.value, b"value");
794 assert_eq!(mapped.diagnostics().as_ptr(), diagnostics_pointer);
795 assert_eq!(
796 mapped
797 .source()
798 .unwrap()
799 .primary_buffer()
800 .unwrap()
801 .bytes()
802 .as_ptr(),
803 source_pointer
804 );
805 }
806
807 #[test]
808 fn failed_try_map_returns_the_original_module_and_records() {
809 let module = PioModule::new(String::from("value"))
810 .with_diagnostic(Diagnostic::of(
811 &crate::codes::VALIDATE_TIME_SERIES_SHAPE,
812 "kept",
813 ))
814 .unwrap();
815 let diagnostics_pointer = module.diagnostics().as_ptr();
816 let recovered = module
817 .__try_map_value::<usize>(Err)
818 .expect_err("conversion fails");
819 assert_eq!(recovered.value, "value");
820 assert_eq!(recovered.diagnostics().as_ptr(), diagnostics_pointer);
821 }
822
823 #[test]
824 fn parsed_module_maps_the_value_to_its_source() {
825 let source = Source::from_memory("case.m", b"source".as_slice()).unwrap();
826 let module = PioModule::parsed(1_u8, source, Vec::new()).unwrap();
827 assert_eq!(module.source_map().len(), 1);
828 let entry = &module.source_map()[0];
829 assert_eq!(entry.target(), "");
830 assert_eq!(entry.relation(), SourceRelation::Aggregated);
831 assert_eq!(entry.spans().len(), 1);
832 assert_eq!(entry.spans()[0].byte_start(), 0);
833 assert_eq!(entry.spans()[0].byte_end(), 6);
834 }
835
836 #[test]
837 fn record_references_and_namespaces_are_checked() {
838 let source_id = SourceId::new("input").unwrap();
839 let mut module = PioModule::new(1_u8);
840 module
841 .add_source_descriptor(SourceDescriptor::new(source_id.clone(), "case.m", 4).unwrap())
842 .unwrap();
843 let span = SourceSpan::new(source_id, 0, 4).unwrap();
844 module
845 .add_source_map_entry(
846 SourceMapEntry::new("/value", SourceRelation::Exact, vec![span.clone()]).unwrap(),
847 )
848 .unwrap();
849 module
850 .add_diagnostic(
851 Diagnostic::new(
852 crate::DiagnosticCode::new("PARTNER.TEST.FINDING").unwrap(),
853 DiagnosticSeverity::Note,
854 "note",
855 )
856 .with_id(DiagnosticId::new("d1").unwrap())
857 .with_span(span)
858 .unwrap(),
859 )
860 .unwrap();
861 module
862 .add_history_entry(
863 HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Parse, "parse")
864 .unwrap(),
865 )
866 .unwrap();
867 module
868 .insert_extension("org.example", Value::Bool(true))
869 .unwrap();
870 assert!(module.verify_records().is_ok());
871 assert!(
872 module
873 .insert_extension("not-namespaced", Value::Null)
874 .is_err()
875 );
876
877 let invalid = SourceSpan::new(SourceId::new("input").unwrap(), 0, 5).unwrap();
878 assert!(
879 module
880 .add_source_map_entry(
881 SourceMapEntry::new("", SourceRelation::Exact, vec![invalid]).unwrap()
882 )
883 .is_err()
884 );
885 }
886
887 #[test]
888 fn semantic_derivation_records_provenance_and_invalidates_old_value_locators() {
889 let source = Source::from_memory("case.m", b"source".as_slice()).unwrap();
890 let span = SourceSpan::new(
891 source.primary_buffer().unwrap().id().clone(),
892 0,
893 b"source".len() as u64,
894 )
895 .unwrap();
896 let diagnostic = Diagnostic::new(
897 crate::DiagnosticCode::new("PARTNER.TEST.DERIVE").unwrap(),
898 DiagnosticSeverity::Note,
899 "kept",
900 )
901 .with_id(DiagnosticId::new("d1").unwrap())
902 .with_target("/old/value")
903 .unwrap()
904 .with_span(span)
905 .unwrap();
906 let mut module = PioModule::parsed(3_u8, source, vec![diagnostic]).unwrap();
907 module
908 .add_history_entry(
909 HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Parse, "parse")
910 .unwrap(),
911 )
912 .unwrap();
913 module
914 .insert_extension("org.example", Value::Bool(true))
915 .unwrap();
916
917 let derived = module
918 .derive_value(
919 Producer::new("tellegen", "1.0.0").unwrap(),
920 HistoryEntry::new(
921 HistoryId::new("h2").unwrap(),
922 HistoryKind::Transform,
923 "build instance",
924 )
925 .unwrap(),
926 |value| usize::from(value) * 2,
927 )
928 .unwrap();
929
930 assert_eq!(derived.value, 6);
931 assert_eq!(derived.producer().name(), "tellegen");
932 assert_eq!(derived.sources().len(), 1);
933 assert!(derived.source().is_none());
934 assert_eq!(derived.source_map(), []);
935 assert_eq!(derived.diagnostics().len(), 1);
936 assert!(derived.diagnostics()[0].target().is_none());
937 assert_eq!(derived.diagnostics()[0].spans().len(), 1);
938 assert_eq!(derived.history().len(), 2);
939 assert_eq!(
940 derived.extensions().get("org.example"),
941 Some(&Value::Bool(true))
942 );
943 }
944
945 #[test]
946 fn staged_edits_commit_atomically_or_leave_the_module_untouched() {
947 let source = Source::from_memory("case.m", b"source".as_slice()).unwrap();
948 let diagnostic = Diagnostic::new(
949 crate::DiagnosticCode::new("PARTNER.TEST.EDIT").unwrap(),
950 DiagnosticSeverity::Note,
951 "existing",
952 )
953 .with_id(DiagnosticId::new("d1").unwrap())
954 .with_target("/0")
955 .unwrap();
956 let mut module = PioModule::parsed([1_u8, 2], source, vec![diagnostic]).unwrap();
957 module
958 .add_history_entry(
959 HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Parse, "parse")
960 .unwrap(),
961 )
962 .unwrap();
963
964 {
965 let mut staged = module.stage_edit();
966 staged.value_mut()[0] = 8;
967 }
968 assert_eq!(module.value, [1, 2]);
969 assert!(module.source().is_some());
970
971 let mut staged = module.stage_edit();
972 staged.value_mut()[0] = 9;
973 let error = staged
974 .commit(
975 Producer::new("editor", "1.0.0").unwrap(),
976 HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Edit, "edit")
977 .unwrap(),
978 )
979 .expect_err("duplicate history makes the staged commit fail");
980 assert_eq!(error.category(), crate::ErrorCategory::Request);
981 assert_eq!(module.value, [1, 2]);
982 assert!(module.source().is_some());
983 assert_eq!(module.history().len(), 1);
984
985 let mut staged = module.stage_edit();
986 staged.value_mut()[0] = 7;
987 staged
988 .add_diagnostic(
989 Diagnostic::new(
990 crate::DiagnosticCode::new("PARTNER.TEST.EDITED").unwrap(),
991 DiagnosticSeverity::Note,
992 "changed",
993 )
994 .with_id(DiagnosticId::new("d2").unwrap())
995 .with_target("/0")
996 .unwrap(),
997 )
998 .unwrap();
999 staged
1000 .commit(
1001 Producer::new("editor", "1.0.0").unwrap(),
1002 HistoryEntry::new(HistoryId::new("h2").unwrap(), HistoryKind::Edit, "edit")
1003 .unwrap(),
1004 )
1005 .unwrap();
1006
1007 assert_eq!(module.value, [7, 2]);
1008 assert_eq!(module.producer().name(), "editor");
1009 assert!(module.source().is_none());
1010 assert_eq!(module.source_map(), []);
1011 assert_eq!(module.sources().len(), 1);
1012 assert_eq!(module.diagnostics().len(), 2);
1013 assert_eq!(module.diagnostics()[0].target(), Some("/0"));
1014 assert_eq!(module.history().len(), 2);
1015 }
1016}