Skip to main content

vize_l2/lang/js/
jsx_body.rs

1//! Original syntax and completed File facts owned with same-walk JSX structure.
2
3use super::{FileProducer, ProgramInput, ProgramInputError, ProgramScope};
4use crate::artifact::ArtifactError;
5use crate::file::{FileArtifact, RejectedFile};
6use alloc::boxed::Box;
7use oxc_parser::ProgramObservation;
8use vize_l0::{Allocator, SourceBlock};
9
10mod recorder;
11mod records;
12use recorder::Recorder;
13pub use records::{JsxChildren, JsxNode};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum JsxFileError {
17    SyntaxAdmission,
18    Input(ProgramInputError),
19    Artifact(ArtifactError),
20    InvalidProfile,
21    AlreadyWalked,
22    NotWalked,
23    Interrupted,
24    IncompleteFile,
25    IncompleteBody,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum Phase {
30    Pending,
31    Interrupted,
32    Complete,
33}
34
35/// Consumes the actual opaque parser owner, never a caller's AST/File pair.
36/// There is no public recorder, seal, observer substitution or mutable syntax.
37///
38/// ```compile_fail
39/// use oxc_ast::ast::Program;
40/// use vize_l0::{Allocator, SourceBlock};
41/// use vize_l2::lang::js::JsxFileProducer;
42/// fn forge<'a>(arena: &'a Allocator, program: Program<'a>, block: SourceBlock<'a>) {
43///     let _ = JsxFileProducer::new(arena, program, block, 0);
44/// }
45/// ```
46pub struct JsxFileProducer<'a> {
47    observation: ProgramObservation<'a>,
48    block: SourceBlock<'a>,
49    index: usize,
50    producer: FileProducer<'a>,
51    recorder: Recorder<'a>,
52    phase: Phase,
53}
54
55/// A completed source body retains its actual original observation and File.
56///
57/// ```compile_fail
58/// use vize_l2::lang::js::JsxFile;
59/// fn forge<'a>() { let _ = JsxFile { observation: (), file: (), records: vec![] }; }
60/// ```
61pub struct JsxFile<'a> {
62    observation: ProgramObservation<'a>,
63    file: FileArtifact<'a>,
64    records: alloc::vec::Vec<records::Record<'a>>,
65}
66
67/// Failed admission, walk or sealing retains original syntax and actual facts.
68pub struct RejectedJsxFile<'a> {
69    observation: ProgramObservation<'a>,
70    error: JsxFileError,
71    file: Option<FileArtifact<'a>>,
72    rejected_file: Option<Box<RejectedFile<'a>>>,
73    records: alloc::vec::Vec<records::Record<'a>>,
74}
75
76impl<'a> JsxFileProducer<'a> {
77    pub fn new(
78        allocator: &'a Allocator,
79        observation: ProgramObservation<'a>,
80        block: SourceBlock<'a>,
81        container_index: usize,
82    ) -> Result<Self, Box<RejectedJsxFile<'a>>> {
83        let admission = observation
84            .admitted()
85            .ok_or(JsxFileError::SyntaxAdmission)
86            .and_then(|admitted| {
87                let input = ProgramInput::checked(admitted, block, container_index)
88                    .map_err(JsxFileError::Input)?;
89                if !input.source_type().is_jsx() || !input.source_type().is_module() {
90                    return Err(JsxFileError::InvalidProfile);
91                }
92                Ok(())
93            });
94        if let Err(error) = admission {
95            return Err(Box::new(RejectedJsxFile::new(observation, error)));
96        }
97        let producer = match FileProducer::new(allocator, block.root_source()) {
98            Ok(producer) => producer,
99            Err(error) => {
100                return Err(Box::new(RejectedJsxFile::new(
101                    observation,
102                    JsxFileError::Artifact(error),
103                )));
104            }
105        };
106        Ok(Self {
107            observation,
108            block,
109            index: container_index,
110            producer,
111            recorder: Recorder::new(),
112            phase: Phase::Pending,
113        })
114    }
115
116    /// Park the owning state before the sole visit. A caught unwind cannot retry
117    /// or turn an interrupted original File/body into a completed view.
118    pub fn walk(&mut self) -> Result<(), JsxFileError> {
119        self.walk_with(|producer, input, recorder| {
120            producer
121                .program_observed(input, ProgramScope::Module, recorder)
122                .map(|_| ())
123        })
124    }
125
126    fn walk_with(
127        &mut self,
128        visit: impl FnOnce(
129            &mut FileProducer<'a>,
130            ProgramInput<'_, 'a>,
131            &mut Recorder<'a>,
132        ) -> Result<(), ProgramInputError>,
133    ) -> Result<(), JsxFileError> {
134        if self.phase != Phase::Pending {
135            return Err(JsxFileError::AlreadyWalked);
136        }
137        self.phase = Phase::Interrupted;
138        let admitted = self
139            .observation
140            .admitted()
141            .ok_or(JsxFileError::SyntaxAdmission)?;
142        let input =
143            ProgramInput::checked(admitted, self.block, self.index).map_err(JsxFileError::Input)?;
144        visit(&mut self.producer, input, &mut self.recorder).map_err(JsxFileError::Input)?;
145        self.phase = Phase::Complete;
146        Ok(())
147    }
148
149    #[must_use]
150    pub fn observation(&self) -> &ProgramObservation<'a> {
151        &self.observation
152    }
153
154    pub fn finish(self) -> Result<JsxFile<'a>, Box<RejectedJsxFile<'a>>> {
155        let Self {
156            observation,
157            producer,
158            recorder,
159            phase,
160            ..
161        } = self;
162        let body_complete = recorder.frames_complete() && recorder.jsx_count() > 0;
163        let mut rejection = RejectedJsxFile::new(
164            observation,
165            match phase {
166                Phase::Pending => JsxFileError::NotWalked,
167                Phase::Interrupted => JsxFileError::Interrupted,
168                Phase::Complete => JsxFileError::IncompleteBody,
169            },
170        );
171        rejection.records = recorder.records;
172        let file = match producer.finish() {
173            Ok(file) => file,
174            Err(file) => {
175                rejection.error = JsxFileError::Artifact(file.artifact.error);
176                rejection.rejected_file = Some(Box::new(file));
177                return Err(Box::new(rejection));
178            }
179        };
180        if phase != Phase::Complete {
181            rejection.file = Some(file);
182            return Err(Box::new(rejection));
183        }
184        // Structural File sealing alone does not imply semantic completion.
185        if !file.is_complete() {
186            rejection.error = JsxFileError::IncompleteFile;
187            rejection.file = Some(file);
188            return Err(Box::new(rejection));
189        }
190        if !body_complete {
191            rejection.file = Some(file);
192            return Err(Box::new(rejection));
193        }
194        Ok(JsxFile {
195            observation: rejection.observation,
196            file,
197            records: rejection.records,
198        })
199    }
200}
201
202impl<'a> JsxFile<'a> {
203    #[must_use]
204    pub fn observation(&self) -> &ProgramObservation<'a> {
205        &self.observation
206    }
207    #[must_use]
208    pub fn file(&self) -> &FileArtifact<'a> {
209        &self.file
210    }
211    #[must_use]
212    pub fn node(&self, index: usize) -> Option<JsxNode<'_, 'a>> {
213        self.records.get(index)?;
214        Some(JsxNode::new(self, index))
215    }
216    pub fn nodes(&self) -> impl Iterator<Item = JsxNode<'_, 'a>> {
217        (0..self.records.len()).map(|index| JsxNode::new(self, index))
218    }
219}
220
221impl<'a> RejectedJsxFile<'a> {
222    fn new(observation: ProgramObservation<'a>, error: JsxFileError) -> Self {
223        Self {
224            observation,
225            error,
226            file: None,
227            rejected_file: None,
228            records: alloc::vec::Vec::new(),
229        }
230    }
231    #[must_use]
232    pub fn error(&self) -> JsxFileError {
233        self.error
234    }
235    #[must_use]
236    pub fn observation(&self) -> &ProgramObservation<'a> {
237        &self.observation
238    }
239    #[must_use]
240    pub fn file(&self) -> Option<&FileArtifact<'a>> {
241        self.file.as_ref()
242    }
243    #[must_use]
244    pub fn rejected_file(&self) -> Option<&RejectedFile<'a>> {
245        self.rejected_file.as_deref()
246    }
247    /// Partial recording is diagnostic evidence, never a completed node view.
248    #[must_use]
249    pub fn recorded_nodes(&self) -> usize {
250        self.records.len()
251    }
252}
253
254#[cfg(test)]
255mod tests;