hwpforge_core/document.rs
1//! The Document type with typestate pattern.
2//!
3//! [`Document<S>`] is the aggregate root of the Core DOM. It uses the
4//! **typestate pattern** to enforce document lifecycle at compile time:
5//!
6//! - [`Draft`] -- mutable, can add/remove sections
7//! - [`Validated`] -- immutable structure, safe for serialization/export
8//!
9//! The transition `Draft -> Validated` is one-way via [`Document::validate()`],
10//! which consumes the draft (move semantics prevent reuse).
11//!
12//! # Design Decisions
13//!
14//! - **Typestate, not enum** -- invalid operations are compile errors
15//! (not runtime panics). See Appendix D in the detailed plan.
16//! - **Deserialize always to Draft** -- serialized data may be modified
17//! externally; re-validation is mandatory.
18//! - **No `Styled` state in Phase 1** -- deferred to Phase 2 when
19//! Blueprint (StyleRegistry) is available.
20//!
21//! # Examples
22//!
23//! ```
24//! use hwpforge_core::document::{Document, Draft, Validated};
25//! use hwpforge_core::section::Section;
26//! use hwpforge_core::paragraph::Paragraph;
27//! use hwpforge_core::run::Run;
28//! use hwpforge_core::PageSettings;
29//! use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
30//!
31//! let mut doc = Document::new();
32//! doc.add_section(Section::with_paragraphs(
33//! vec![Paragraph::with_runs(
34//! vec![Run::text("Hello", CharShapeIndex::new(0))],
35//! ParaShapeIndex::new(0),
36//! )],
37//! PageSettings::a4(),
38//! ));
39//!
40//! let validated: Document<Validated> = doc.validate().unwrap();
41//! assert_eq!(validated.section_count(), 1);
42//! ```
43//!
44//! ```compile_fail
45//! // A validated document cannot add sections:
46//! use hwpforge_core::document::{Document, Validated};
47//! use hwpforge_core::section::Section;
48//! use hwpforge_core::PageSettings;
49//!
50//! # fn get_validated() -> Document<Validated> { todo!() }
51//! let mut validated = get_validated();
52//! validated.add_section(Section::new(PageSettings::a4()));
53//! // ERROR: no method named `add_section` found for `Document<Validated>`
54//! ```
55
56use std::marker::PhantomData;
57
58use schemars::JsonSchema;
59use serde::{Deserialize, Serialize};
60
61use crate::error::CoreResult;
62use crate::metadata::Metadata;
63use crate::paragraph::Paragraph;
64use crate::section::Section;
65use crate::validate::validate_sections;
66
67/// Marker type: the document is a mutable draft.
68///
69/// A `Document<Draft>` can be modified (add sections, set metadata)
70/// and then validated via [`Document::validate()`].
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Draft;
73
74/// Marker type: the document has passed structural validation.
75///
76/// A `Document<Validated>` is guaranteed to have:
77/// - At least 1 section
78/// - Every section has at least 1 paragraph
79/// - Every paragraph has at least 1 run
80/// - All table/control structural invariants hold
81///
82/// The only way to obtain a `Document<Validated>` is through
83/// [`Document<Draft>::validate()`].
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Validated;
86
87/// The document aggregate root with compile-time state tracking.
88///
89/// The generic parameter `S` determines which operations are available:
90///
91/// | State | Mutable | Serializable | Exportable |
92/// |-------|---------|-------------|-----------|
93/// | [`Draft`] | Yes | Yes | No (must validate first) |
94/// | [`Validated`] | No | Yes | Yes |
95///
96/// # Typestate Safety
97///
98/// The `_state` field is private and zero-sized. There is no way to
99/// construct a `Document<Validated>` except through `validate()`.
100///
101/// # Examples
102///
103/// ```
104/// use hwpforge_core::document::Document;
105/// use hwpforge_core::Metadata;
106///
107/// let doc = Document::with_metadata(Metadata::new().with_title("Report"));
108/// assert!(doc.is_empty());
109/// ```
110pub struct Document<S = Draft> {
111 sections: Vec<Section>,
112 metadata: Metadata,
113 _state: PhantomData<S>,
114}
115
116// ---------------------------------------------------------------------------
117// Shared methods (any state)
118// ---------------------------------------------------------------------------
119
120impl<S> Document<S> {
121 /// Returns a slice of all sections.
122 ///
123 /// # Examples
124 ///
125 /// ```
126 /// use hwpforge_core::document::Document;
127 ///
128 /// let doc = Document::new();
129 /// assert!(doc.sections().is_empty());
130 /// ```
131 pub fn sections(&self) -> &[Section] {
132 &self.sections
133 }
134
135 /// Returns a reference to the document metadata.
136 pub fn metadata(&self) -> &Metadata {
137 &self.metadata
138 }
139
140 /// Returns the number of sections.
141 pub fn section_count(&self) -> usize {
142 self.sections.len()
143 }
144
145 /// Returns `true` if the document has no sections.
146 pub fn is_empty(&self) -> bool {
147 self.sections.is_empty()
148 }
149}
150
151// ---------------------------------------------------------------------------
152// Draft-only methods
153// ---------------------------------------------------------------------------
154
155impl Document<Draft> {
156 /// Creates a new empty draft document with default metadata.
157 ///
158 /// # Examples
159 ///
160 /// ```
161 /// use hwpforge_core::document::Document;
162 ///
163 /// let doc = Document::new();
164 /// assert!(doc.is_empty());
165 /// ```
166 pub fn new() -> Self {
167 Self { sections: Vec::new(), metadata: Metadata::default(), _state: PhantomData }
168 }
169
170 /// Creates a new draft document with the given metadata.
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use hwpforge_core::document::Document;
176 /// use hwpforge_core::Metadata;
177 ///
178 /// let doc = Document::with_metadata(Metadata::new().with_title("Test"));
179 /// assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
180 /// ```
181 pub fn with_metadata(metadata: Metadata) -> Self {
182 Self { sections: Vec::new(), metadata, _state: PhantomData }
183 }
184
185 /// 문서의 모든 문단(전 섹션 본문·머리말·꼬리말·바탕쪽 + 표 셀·캡션·
186 /// 글상자·각주/미주·메모 등 중첩 문단 전부)을 문서 순서로 방문한다.
187 ///
188 /// 캐시 정규화([`Self::strip_layout_caches`]) 등 전 문단 일괄 변환의
189 /// 진입점이다. `Draft` 전용 — `Validated` 는 typestate 상 불변이므로
190 /// 비교가 필요하면 검증 전 사본에서 수행한다.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use hwpforge_core::document::Document;
196 /// use hwpforge_core::page::PageSettings;
197 /// use hwpforge_core::paragraph::Paragraph;
198 /// use hwpforge_core::section::Section;
199 /// use hwpforge_foundation::ParaShapeIndex;
200 ///
201 /// let mut doc = Document::new();
202 /// doc.add_section(Section::with_paragraphs(
203 /// vec![Paragraph::new(ParaShapeIndex::new(0))],
204 /// PageSettings::a4(),
205 /// ));
206 /// let mut count = 0;
207 /// doc.for_each_paragraph_mut(|_| count += 1);
208 /// assert_eq!(count, 1);
209 /// ```
210 pub fn for_each_paragraph_mut<F: FnMut(&mut Paragraph)>(&mut self, mut f: F) {
211 for section in &mut self.sections {
212 section.walk_paragraphs_mut(&mut f);
213 }
214 }
215
216 /// 모든 문단의 줄 조판 캐시([`Paragraph::layout_cache`])를 제거한다.
217 ///
218 /// 문서 동등성 비교(admission/golden)의 정규화 단계: 네이티브 입력은
219 /// 캐시를 보유하고 우리 재인코드 산출물은 미보유가 정상이므로, 비교
220 /// 전 양쪽 사본에서 이 함수를 호출해 캐시 차이를 제거한다.
221 ///
222 /// # Examples
223 ///
224 /// ```
225 /// use hwpforge_core::document::Document;
226 /// use hwpforge_core::layout::LayoutCache;
227 /// use hwpforge_core::page::PageSettings;
228 /// use hwpforge_core::paragraph::Paragraph;
229 /// use hwpforge_core::section::Section;
230 /// use hwpforge_foundation::ParaShapeIndex;
231 ///
232 /// let mut para = Paragraph::new(ParaShapeIndex::new(0));
233 /// para.layout_cache = Some(LayoutCache::default());
234 /// let mut doc = Document::new();
235 /// doc.add_section(Section::with_paragraphs(vec![para], PageSettings::a4()));
236 /// doc.strip_layout_caches();
237 /// assert!(doc.sections()[0].paragraphs[0].layout_cache.is_none());
238 /// ```
239 pub fn strip_layout_caches(&mut self) {
240 self.for_each_paragraph_mut(|p| {
241 p.layout_cache = None;
242 // 표 수준 decode-only 캐시도 함께 제거 — 방문 문단의 직계 run 만
243 // 보면 충분하다 (중첩 표의 host 는 셀 문단이고, walker 가 셀
244 // 문단도 방문한다).
245 for run in &mut p.runs {
246 if let crate::run::RunContent::Table(table) = &mut run.content {
247 table.layout_cache = None;
248 }
249 }
250 });
251 }
252
253 /// Appends a section to the draft document.
254 ///
255 /// # Examples
256 ///
257 /// ```
258 /// use hwpforge_core::document::Document;
259 /// use hwpforge_core::section::Section;
260 /// use hwpforge_core::PageSettings;
261 ///
262 /// let mut doc = Document::new();
263 /// doc.add_section(Section::new(PageSettings::a4()));
264 /// assert_eq!(doc.section_count(), 1);
265 /// ```
266 pub fn add_section(&mut self, section: Section) {
267 self.sections.push(section);
268 }
269
270 /// Sets the document metadata.
271 pub fn set_metadata(&mut self, metadata: Metadata) {
272 self.metadata = metadata;
273 }
274
275 /// Returns a mutable reference to the metadata.
276 pub fn metadata_mut(&mut self) -> &mut Metadata {
277 &mut self.metadata
278 }
279
280 /// Returns a mutable slice of sections.
281 pub fn sections_mut(&mut self) -> &mut [Section] {
282 &mut self.sections
283 }
284
285 /// Validates the document structure and transitions to `Validated`.
286 ///
287 /// Consumes `self` (move semantics). On success, returns a
288 /// `Document<Validated>`. On failure, returns a `CoreError`.
289 ///
290 /// # Errors
291 ///
292 /// Returns [`CoreError::Validation`](crate::error::CoreError::Validation) if the document violates any
293 /// structural invariant (empty sections, empty paragraphs, etc.).
294 ///
295 /// # Examples
296 ///
297 /// ```
298 /// use hwpforge_core::document::Document;
299 /// use hwpforge_core::section::Section;
300 /// use hwpforge_core::paragraph::Paragraph;
301 /// use hwpforge_core::run::Run;
302 /// use hwpforge_core::PageSettings;
303 /// use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
304 ///
305 /// let mut doc = Document::new();
306 /// doc.add_section(Section::with_paragraphs(
307 /// vec![Paragraph::with_runs(
308 /// vec![Run::text("Hello", CharShapeIndex::new(0))],
309 /// ParaShapeIndex::new(0),
310 /// )],
311 /// PageSettings::a4(),
312 /// ));
313 ///
314 /// let validated = doc.validate().unwrap();
315 /// assert_eq!(validated.section_count(), 1);
316 /// ```
317 ///
318 /// ```
319 /// use hwpforge_core::document::Document;
320 ///
321 /// let doc = Document::new(); // empty
322 /// assert!(doc.validate().is_err());
323 /// ```
324 pub fn validate(self) -> CoreResult<Document<Validated>> {
325 validate_sections(&self.sections)?;
326 Ok(Document { sections: self.sections, metadata: self.metadata, _state: PhantomData })
327 }
328}
329
330impl Default for Document<Draft> {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336// ---------------------------------------------------------------------------
337// Manual trait impls (avoid T: Trait bounds on phantom type S)
338// ---------------------------------------------------------------------------
339
340impl<S> std::fmt::Debug for Document<S> {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.debug_struct("Document")
343 .field("sections", &self.sections)
344 .field("metadata", &self.metadata)
345 .finish()
346 }
347}
348
349impl<S> Clone for Document<S> {
350 fn clone(&self) -> Self {
351 Self {
352 sections: self.sections.clone(),
353 metadata: self.metadata.clone(),
354 _state: PhantomData,
355 }
356 }
357}
358
359impl<S> PartialEq for Document<S> {
360 fn eq(&self, other: &Self) -> bool {
361 self.sections == other.sections && self.metadata == other.metadata
362 }
363}
364
365impl<S> Eq for Document<S> {}
366
367impl<S> std::fmt::Display for Document<S> {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 write!(f, "Document({} sections)", self.sections.len())
370 }
371}
372
373// ---------------------------------------------------------------------------
374// Serde: serialize any state, deserialize only to Draft
375// ---------------------------------------------------------------------------
376
377impl<S> Serialize for Document<S> {
378 fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
379 use serde::ser::SerializeStruct;
380 let mut state = serializer.serialize_struct("Document", 2)?;
381 state.serialize_field("sections", &self.sections)?;
382 state.serialize_field("metadata", &self.metadata)?;
383 state.end()
384 }
385}
386
387impl<'de> Deserialize<'de> for Document<Draft> {
388 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
389 #[derive(Deserialize)]
390 struct DocumentData {
391 sections: Vec<Section>,
392 metadata: Metadata,
393 }
394
395 let data = DocumentData::deserialize(deserializer)?;
396 Ok(Document { sections: data.sections, metadata: data.metadata, _state: PhantomData })
397 }
398}
399
400// ---------------------------------------------------------------------------
401// JsonSchema: hide PhantomData
402// ---------------------------------------------------------------------------
403
404impl<S> JsonSchema for Document<S> {
405 fn schema_name() -> std::borrow::Cow<'static, str> {
406 "Document".into()
407 }
408
409 fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
410 schemars::json_schema!({
411 "type": "object",
412 "properties": {
413 "sections": gen.subschema_for::<Vec<Section>>(),
414 "metadata": gen.subschema_for::<crate::metadata::Metadata>(),
415 },
416 "required": ["sections", "metadata"]
417 })
418 }
419}
420
421// ---------------------------------------------------------------------------
422// Send + Sync verification
423// ---------------------------------------------------------------------------
424
425const _: () = {
426 #[allow(dead_code)]
427 fn assert_send<T: Send>() {}
428 #[allow(dead_code)]
429 fn assert_sync<T: Sync>() {}
430 #[allow(dead_code)]
431 fn verify() {
432 assert_send::<Document<Draft>>();
433 assert_sync::<Document<Draft>>();
434 assert_send::<Document<Validated>>();
435 assert_sync::<Document<Validated>>();
436 }
437};
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::error::CoreError;
443 use crate::page::PageSettings;
444 use crate::paragraph::Paragraph;
445 use crate::run::Run;
446 use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
447
448 fn valid_section() -> Section {
449 Section::with_paragraphs(
450 vec![Paragraph::with_runs(
451 vec![Run::text("Hello", CharShapeIndex::new(0))],
452 ParaShapeIndex::new(0),
453 )],
454 PageSettings::a4(),
455 )
456 }
457
458 // === Construction ===
459
460 #[test]
461 fn new_creates_empty_draft() {
462 let doc = Document::new();
463 assert!(doc.is_empty());
464 assert_eq!(doc.section_count(), 0);
465 assert!(doc.metadata().title.is_none());
466 }
467
468 #[test]
469 fn with_metadata() {
470 let meta = Metadata { title: Some("Test".to_string()), ..Metadata::default() };
471 let doc = Document::with_metadata(meta);
472 assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
473 }
474
475 #[test]
476 fn default_is_new() {
477 let a = Document::new();
478 let b = Document::default();
479 assert_eq!(a, b);
480 }
481
482 // === Draft mutations ===
483
484 #[test]
485 fn add_section() {
486 let mut doc = Document::new();
487 doc.add_section(valid_section());
488 assert_eq!(doc.section_count(), 1);
489 assert!(!doc.is_empty());
490 }
491
492 #[test]
493 fn add_multiple_sections() {
494 let mut doc = Document::new();
495 doc.add_section(valid_section());
496 doc.add_section(valid_section());
497 doc.add_section(valid_section());
498 assert_eq!(doc.section_count(), 3);
499 }
500
501 #[test]
502 fn set_metadata() {
503 let mut doc = Document::new();
504 doc.set_metadata(Metadata { title: Some("New".to_string()), ..Metadata::default() });
505 assert_eq!(doc.metadata().title.as_deref(), Some("New"));
506 }
507
508 #[test]
509 fn metadata_mut() {
510 let mut doc = Document::new();
511 doc.metadata_mut().title = Some("Mutated".to_string());
512 assert_eq!(doc.metadata().title.as_deref(), Some("Mutated"));
513 }
514
515 #[test]
516 fn sections_mut() {
517 let mut doc = Document::new();
518 doc.add_section(valid_section());
519 doc.add_section(valid_section());
520 assert_eq!(doc.sections_mut().len(), 2);
521 }
522
523 // === Validation (Draft -> Validated) ===
524
525 #[test]
526 fn validate_success() {
527 let mut doc = Document::new();
528 doc.add_section(valid_section());
529 let validated = doc.validate().unwrap();
530 assert_eq!(validated.section_count(), 1);
531 }
532
533 #[test]
534 fn validate_empty_document_fails() {
535 let doc = Document::new();
536 let err = doc.validate().unwrap_err();
537 assert!(matches!(err, CoreError::Validation(_)));
538 }
539
540 #[test]
541 fn validate_empty_section_fails() {
542 let mut doc = Document::new();
543 doc.add_section(Section::new(PageSettings::a4()));
544 assert!(doc.validate().is_err());
545 }
546
547 #[test]
548 fn validate_consumes_draft() {
549 let mut doc = Document::new();
550 doc.add_section(valid_section());
551 let _validated = doc.validate().unwrap();
552 // doc is moved -- attempting to use it would be a compile error
553 }
554
555 // === Validated state ===
556
557 #[test]
558 fn validated_has_read_methods() {
559 let mut doc = Document::new();
560 doc.add_section(valid_section());
561 let validated = doc.validate().unwrap();
562
563 assert_eq!(validated.section_count(), 1);
564 assert!(!validated.is_empty());
565 assert_eq!(validated.sections().len(), 1);
566 assert!(validated.metadata().title.is_none());
567 }
568
569 // === Display ===
570
571 #[test]
572 fn display_draft() {
573 let doc = Document::new();
574 assert_eq!(doc.to_string(), "Document(0 sections)");
575 }
576
577 #[test]
578 fn display_validated() {
579 let mut doc = Document::new();
580 doc.add_section(valid_section());
581 let validated = doc.validate().unwrap();
582 assert_eq!(validated.to_string(), "Document(1 sections)");
583 }
584
585 // === Equality ===
586
587 #[test]
588 fn equality_draft() {
589 let mut a = Document::new();
590 a.add_section(valid_section());
591 let mut b = Document::new();
592 b.add_section(valid_section());
593 assert_eq!(a, b);
594 }
595
596 #[test]
597 fn equality_validated() {
598 let mut a = Document::new();
599 a.add_section(valid_section());
600 let mut b = Document::new();
601 b.add_section(valid_section());
602 let va = a.validate().unwrap();
603 let vb = b.validate().unwrap();
604 assert_eq!(va, vb);
605 }
606
607 // === Clone ===
608
609 #[test]
610 fn clone_draft() {
611 let mut doc = Document::new();
612 doc.add_section(valid_section());
613 let cloned = doc.clone();
614 assert_eq!(doc, cloned);
615 }
616
617 #[test]
618 fn clone_validated() {
619 let mut doc = Document::new();
620 doc.add_section(valid_section());
621 let validated = doc.validate().unwrap();
622 let cloned = validated.clone();
623 assert_eq!(validated, cloned);
624 }
625
626 // === Serde ===
627
628 #[test]
629 fn serde_roundtrip_draft() {
630 let mut doc = Document::new();
631 doc.add_section(valid_section());
632 doc.set_metadata(Metadata { title: Some("Test".to_string()), ..Metadata::default() });
633
634 let json = serde_json::to_string(&doc).unwrap();
635 let back: Document<Draft> = serde_json::from_str(&json).unwrap();
636 assert_eq!(doc, back);
637 }
638
639 #[test]
640 fn serde_roundtrip_validated_deserializes_to_draft() {
641 let mut doc = Document::new();
642 doc.add_section(valid_section());
643 let validated = doc.validate().unwrap();
644
645 let json = serde_json::to_string(&validated).unwrap();
646 // Deserialize always produces Draft
647 let back: Document<Draft> = serde_json::from_str(&json).unwrap();
648 // Must re-validate
649 let re_validated = back.validate().unwrap();
650 assert_eq!(validated, re_validated);
651 }
652
653 #[test]
654 fn serde_empty_document() {
655 let doc = Document::new();
656 let json = serde_json::to_string(&doc).unwrap();
657 let back: Document<Draft> = serde_json::from_str(&json).unwrap();
658 assert_eq!(doc, back);
659 }
660
661 // === Complex document ===
662
663 #[test]
664 fn complex_document_roundtrip() {
665 use crate::control::Control;
666 use crate::image::{Image, ImageFormat};
667 use crate::table::{Table, TableCell, TableRow};
668 use hwpforge_foundation::HwpUnit;
669
670 let cell = TableCell::new(
671 vec![Paragraph::with_runs(
672 vec![Run::text("cell", CharShapeIndex::new(0))],
673 ParaShapeIndex::new(0),
674 )],
675 HwpUnit::from_mm(50.0).unwrap(),
676 );
677 let table = Table::new(vec![TableRow::new(vec![cell])]);
678
679 let link = Control::Hyperlink {
680 text: "click".to_string(),
681 url: "https://example.com".to_string(),
682 };
683
684 let img = Image::new(
685 "test.png",
686 HwpUnit::from_mm(10.0).unwrap(),
687 HwpUnit::from_mm(10.0).unwrap(),
688 ImageFormat::Png,
689 );
690
691 let section = Section::with_paragraphs(
692 vec![
693 Paragraph::with_runs(
694 vec![
695 Run::text("Hello ", CharShapeIndex::new(0)),
696 Run::text("world", CharShapeIndex::new(1)),
697 ],
698 ParaShapeIndex::new(0),
699 ),
700 Paragraph::with_runs(
701 vec![Run::table(table, CharShapeIndex::new(0))],
702 ParaShapeIndex::new(1),
703 ),
704 Paragraph::with_runs(
705 vec![Run::control(link, CharShapeIndex::new(0))],
706 ParaShapeIndex::new(0),
707 ),
708 Paragraph::with_runs(
709 vec![Run::image(img, CharShapeIndex::new(0))],
710 ParaShapeIndex::new(0),
711 ),
712 ],
713 PageSettings::a4(),
714 );
715
716 let mut doc = Document::with_metadata(Metadata {
717 title: Some("Complex Doc".to_string()),
718 author: Some("Author".to_string()),
719 keywords: vec!["test".to_string()],
720 ..Metadata::default()
721 });
722 doc.add_section(section);
723
724 let validated = doc.validate().unwrap();
725 let json = serde_json::to_string_pretty(&validated).unwrap();
726 let back: Document<Draft> = serde_json::from_str(&json).unwrap();
727 let re_validated = back.validate().unwrap();
728 assert_eq!(validated, re_validated);
729 }
730
731 // === Debug ===
732
733 #[test]
734 fn debug_output() {
735 let doc = Document::new();
736 let s = format!("{doc:?}");
737 assert!(s.contains("Document"), "debug: {s}");
738 assert!(s.contains("sections"), "debug: {s}");
739 }
740}