cpclib_asm/parser/
context.rs

1use std::borrow::{Borrow, Cow};
2use std::collections::HashSet;
3use std::ops::Deref;
4use std::path::PathBuf;
5use std::sync::{LazyLock, RwLock};
6
7use cpclib_common::camino::{Utf8Path, Utf8PathBuf};
8use cpclib_common::winnow::BStr;
9use either::Either;
10use regex::Regex;
11
12use super::line_col::LineColLookup;
13use crate::LocatedToken;
14use crate::error::AssemblerError;
15use crate::preamble::*;
16
17/// State to limit the parsing abilities depending on the parsing context
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ParsingState {
20    /// Parse of a standard Z80 code
21    Standard,
22    /// Parse of the content of a function
23    FunctionLimited,
24    /// Parse of the content of a struct
25    StructLimited,
26    /// Forbid directives
27    GeneratedLimited, // TODO rename
28    /// Parse of a symbols file
29    SymbolsLimited
30}
31
32pub trait ParsingStateVerified {
33    fn is_accepted(&self, state: &ParsingState) -> bool;
34}
35
36impl ParsingStateVerified for LocatedToken {
37    fn is_accepted(&self, state: &ParsingState) -> bool {
38        self.deref().is_accepted(state)
39    }
40}
41
42macro_rules! parsing_state_verified_inner {
43    () => {
44        fn is_accepted(&self, state: &ParsingState) -> bool {
45            match state {
46                ParsingState::GeneratedLimited => !self.is_directive(),
47                ParsingState::Standard => {
48                    match self {
49                        Self::Return(..) => false,
50                        _ => true
51                    }
52                },
53                ParsingState::FunctionLimited => {
54                    match self {
55                        Self::Equ { .. } | Self::Let(..) => true,
56                        Self::If { .. }
57                        | Self::Repeat { .. }
58                        | Self::Break
59                        | Self::Switch { .. }
60                        | Self::Iterate { .. } => true,
61                        Self::Return(_) => true,
62                        Self::Assert(..) | Self::Print(_) | Self::Fail(_) | Self::Comment(_) => {
63                            true
64                        },
65                        _ => false
66                    }
67                },
68                ParsingState::StructLimited => {
69                    match self {
70                        Self::Defb(..) | Self::Defw(..) | Self::Str(..) | Self::MacroCall(..) => {
71                            true
72                        },
73                        _ => false
74                    }
75                },
76                ParsingState::SymbolsLimited => {
77                    match self {
78                        Self::Equ { .. } | Self::Let(..) | Self::Comment(_) => true,
79                        _ => false
80                    }
81                },
82            }
83        }
84    };
85}
86
87impl ParsingStateVerified for LocatedTokenInner {
88    parsing_state_verified_inner!();
89}
90
91impl ParsingStateVerified for Token {
92    parsing_state_verified_inner!();
93}
94
95#[derive(Debug, PartialEq, Eq, Clone)]
96pub struct ParserOptions {
97    /// Search path to find files
98    pub search_path: Vec<Utf8PathBuf>,
99    /// When activated, the parser also read and parse the include-like directives (deactivated by default)
100    pub read_referenced_files: bool,
101    pub show_progress: bool,
102    /// Set to true when directives must start by a dot
103    pub dotted_directive: bool,
104    pub assembler_flavor: AssemblerFlavor
105}
106
107impl Default for ParserOptions {
108    fn default() -> Self {
109        ParserOptions {
110            search_path: Default::default(),
111            read_referenced_files: true,
112            dotted_directive: false,
113            show_progress: false,
114            assembler_flavor: AssemblerFlavor::Basm
115        }
116    }
117}
118
119impl ParserOptions {
120    pub fn context_builder(self) -> ParserContextBuilder {
121        ParserContextBuilder {
122            options: self,
123            current_filename: None,
124            context_name: None,
125            state: ParsingState::Standard
126        }
127    }
128}
129
130pub struct ParserContextBuilder {
131    options: ParserOptions,
132    current_filename: Option<Utf8PathBuf>,
133    context_name: Option<String>,
134    state: ParsingState
135}
136
137impl Default for ParserContextBuilder {
138    fn default() -> Self {
139        ParserOptions::default().context_builder()
140    }
141}
142
143impl From<ParserContext> for ParserContextBuilder {
144    fn from(ctx: ParserContext) -> Self {
145        Self {
146            state: ctx.state,
147            current_filename: ctx.current_filename,
148            context_name: ctx.context_name,
149            options: ctx.options
150        }
151    }
152}
153
154impl ParserContextBuilder {
155    pub fn current_filename(&self) -> Option<&Utf8Path> {
156        self.current_filename.as_ref().map(|p| p.as_path())
157    }
158
159    pub fn context_name(&self) -> Option<&str> {
160        self.context_name.as_deref()
161    }
162
163    pub fn set_current_filename<S: Into<Utf8PathBuf>>(mut self, fname: S) -> ParserContextBuilder {
164        self.current_filename = Some(fname.into());
165        self
166    }
167
168    pub fn remove_filename(mut self) -> Self {
169        self.current_filename.take();
170        self
171    }
172
173    pub fn set_context_name<S: Into<String>>(mut self, name: S) -> ParserContextBuilder {
174        self.context_name = Some(name.into());
175        self
176    }
177
178    pub fn set_state(mut self, state: ParsingState) -> Self {
179        self.state = state;
180        self
181    }
182
183    pub fn set_options(mut self, options: ParserOptions) -> Self {
184        self.options = options;
185        self
186    }
187
188    /// Build a ParserContext for the given source code
189    #[inline]
190    pub fn build(self, code: &str) -> ParserContext {
191        let code: &'static str = unsafe { std::mem::transmute(code) };
192        let str: &'static BStr = unsafe { std::mem::transmute(BStr::new(code)) };
193        ParserContext {
194            options: self.options,
195            current_filename: self.current_filename,
196            context_name: self.context_name,
197            state: self.state,
198            source: str,
199            line_col_lut: Default::default()
200        }
201    }
202}
203
204impl ParserOptions {
205    pub fn set_read_referenced_files(&mut self, tag: bool) {
206        self.read_referenced_files = tag;
207    }
208
209    pub fn set_dotted_directives(&mut self, tag: bool) {
210        self.dotted_directive = tag;
211    }
212
213    /// Add a search path and ensure it is ABSOLUTE
214    /// Method crashes if the search path does not exist
215    pub fn add_search_path<P: Into<PathBuf>>(&mut self, path: P) -> Result<(), AssemblerError> {
216        let path = path.into();
217
218        if path.is_dir() {
219            #[cfg(not(target_arch = "wasm32"))]
220            let path = path.canonicalize().unwrap();
221
222            // manual fix for for windows. No idea why
223            let path = path.to_str().unwrap();
224            const PREFIX: &str = "\\\\?\\";
225            let path = if path.starts_with(PREFIX) {
226                path[PREFIX.len()..].to_string()
227            }
228            else {
229                path.to_string()
230            };
231
232            // Really add
233            self.search_path.push(path.into());
234            Ok(())
235        }
236        else {
237            Err(AssemblerError::IOError {
238                msg: format!(
239                    "{} is not a path and cannot be added in the search path",
240                    path.to_str().unwrap()
241                )
242            })
243        }
244    }
245
246    /// Add the folder that contains the given file. Ignore if there are issues with the filename
247    pub fn add_search_path_from_file<P: Into<PathBuf>>(
248        &mut self,
249        file: P
250    ) -> Result<(), AssemblerError> {
251        let file = file.into();
252        let path = file.canonicalize();
253
254        match path {
255            Ok(path) => {
256                let path = path.parent().unwrap().to_owned();
257                self.add_search_path(path)
258            },
259
260            Err(err) => {
261                Err(AssemblerError::IOError {
262                    msg: format!(
263                        "Unable to add search path for {}. {}",
264                        file.to_str().unwrap(),
265                        err
266                    )
267                })
268            },
269        }
270    }
271
272    /// Return the real path name that correspond to the requested file.
273    /// Do it in a case insensitive way (for compatibility reasons)
274    pub fn get_path_for(
275        &self,
276        fname: &str,
277        env: Option<&Env>
278    ) -> Result<Utf8PathBuf, either::Either<AssemblerError, Vec<String>>> {
279        use globset::*;
280        let mut does_not_exists = Vec::new();
281        static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{+[^\}]+\}+").unwrap());
282
283        let re = RE.deref();
284        // Make the expansion in the filename
285        let fname: Cow<str> = if let Some(env) = env {
286            let mut fname = fname.to_owned();
287
288            let mut replace = HashSet::new();
289            for cap in re.captures_iter(&fname) {
290                if cap[0] != fname {
291                    replace.insert(cap[0].to_owned());
292                }
293            }
294
295            // make the replacement
296            for model in replace.iter() {
297                let local_symbol = &model[1..model.len() - 1]; // remove {}
298                let local_value = match env
299                    .symbols()
300                    .any_value(local_symbol)
301                    .map(|vl| vl.map(|vl| vl.value()))
302                {
303                    Ok(Some(Value::String(s))) => s.to_string(),
304                    Ok(Some(Value::Expr(e))) => e.to_string(),
305                    Ok(Some(Value::Counter(e))) => e.to_string(),
306                    Ok(Some(unkn)) => {
307                        unimplemented!("{:?}", unkn)
308                    },
309                    Ok(None) => {
310                        return Err(Either::Left(AssemblerError::UnknownSymbol {
311                            symbol: model.into(),
312                            closest: env.symbols().closest_symbol(model, SymbolFor::Any).unwrap()
313                        }));
314                    },
315                    Err(e) => return Err(Either::Left(e.into()))
316                };
317                fname = fname.replace(model, &local_value);
318            }
319            Cow::Owned(fname)
320        }
321        else {
322            Cow::Borrowed(fname)
323        };
324
325        let fname: &str = fname.borrow();
326
327        // early exit if the fname goes in an embedding file
328        if fname.starts_with("inner://") {
329            return Ok(Utf8Path::new(fname).into());
330        }
331
332        let fname = Utf8Path::new(fname);
333
334        // check if file exists
335        if fname.is_file() {
336            return Ok(fname.into());
337        }
338        does_not_exists.push(fname.as_str().to_owned());
339
340        // otherwhise, try with the current directory of the environment
341        if let Some(env) = env.as_ref() {
342            if let Some(search) = env.get_current_working_directory() {
343                let current_path = search.join(fname);
344                if current_path.is_file() {
345                    return Ok(current_path.try_into().unwrap());
346                }
347                else {
348                    does_not_exists.push(current_path.to_string());
349                }
350            }
351        }
352
353        // otherwhise try with the folder set up at the beginning
354        {
355            // loop over all possibilities
356            for search in &self.search_path {
357                assert!(Utf8Path::new(&search).is_dir());
358                let current_path = search.join(fname);
359
360                if current_path.is_file() {
361                    return Ok(current_path);
362                }
363                else {
364                    let glob = GlobBuilder::new(current_path.as_path().as_str())
365                        .case_insensitive(true)
366                        .literal_separator(true)
367                        .build()
368                        .unwrap();
369                    let matcher = glob.compile_matcher();
370
371                    for entry in std::fs::read_dir(search).unwrap() {
372                        let entry = entry.unwrap();
373                        let path = entry.path();
374                        if matcher.is_match(&path) {
375                            return Ok(path.try_into().unwrap());
376                        }
377                    }
378
379                    does_not_exists.push(current_path.as_str().to_owned());
380                }
381            }
382        }
383
384        // No file found
385        Err(Either::Right(does_not_exists))
386    }
387
388    pub fn set_flavor(&mut self, flavor: AssemblerFlavor) -> &mut Self {
389        self.assembler_flavor = flavor;
390        self
391    }
392
393    #[inline(always)]
394    pub fn is_orgams(&self) -> bool {
395        self.assembler_flavor == AssemblerFlavor::Orgams
396    }
397}
398/// Context information that can guide the parser
399/// TODO add assembling flags
400#[derive(Debug)]
401pub struct ParserContext {
402    /// Limitation on the kind of intruction to parse.
403    /// The current state is at the end (it is modified when in a struct)
404    pub state: ParsingState,
405    /// Filename that is currently parsed
406    pub current_filename: Option<Utf8PathBuf>,
407    /// Current context (mainly when playing with macros)
408    pub context_name: Option<String>,
409    pub options: ParserOptions,
410    /// Full source code of the parsing state
411    pub source: &'static BStr,
412    pub line_col_lut: RwLock<Option<LineColLookup<'static>>>
413}
414
415impl Eq for ParserContext {}
416
417impl PartialEq for ParserContext {
418    #[inline]
419    fn eq(&self, other: &Self) -> bool {
420        self.state == other.state
421            && self.current_filename == other.current_filename
422            && self.context_name == other.context_name
423            && self.source == other.source
424            && self.options == other.options
425    }
426}
427
428impl Clone for ParserContext {
429    fn clone(&self) -> Self {
430        panic!();
431
432        Self {
433            current_filename: self.current_filename.clone(),
434            context_name: self.context_name.clone(),
435            state: self.state,
436            source: self.source,
437            options: self.options.clone(),
438            line_col_lut: RwLock::default() /* no need to copy paste the datastructure if it is never used */
439        }
440    }
441}
442
443// impl Default for ParserContext {
444// fn default() -> Self {
445// ParserContext {
446// current_filename: None,
447// context_name: None,
448// search_path: Default::default(),
449// read_referenced_files: true,
450// parse_warning: Default::default(),
451// state: ParsingState::Standard,
452// dotted_directive: false,
453// source: &NO_CODE,
454// show_progress: false
455// }
456// }
457// }
458
459impl ParserContext {
460    pub fn clone_with_state(&self, state: ParsingState) -> Self {
461        Self {
462            current_filename: self.current_filename.clone(),
463            context_name: self.context_name.clone(),
464            source: self.source,
465            options: self.options.clone(),
466            line_col_lut: Default::default(), // no need to duplicate the structure
467            state
468        }
469    }
470}
471
472#[allow(missing_docs)]
473impl ParserContext {
474    #[inline]
475    pub fn context_name(&self) -> Option<&str> {
476        self.context_name.as_deref()
477    }
478
479    #[inline]
480    pub fn filename(&self) -> Option<&Utf8Path> {
481        self.current_filename.as_ref().map(|p| p.as_path())
482    }
483
484    //#[deprecated(note="Totally unsafe. Every test should be modified to not use it")]
485    #[inline]
486    pub fn build_span<S: ?Sized + AsRef<[u8]>>(&self, src: &S) -> Z80Span {
487        Z80Span::new_extra(src, self)
488    }
489
490    /// Specify the path that contains the code
491    #[inline]
492    pub fn set_current_filename<P: Into<Utf8PathBuf>>(&mut self, file: P) {
493        let file = file.into();
494        self.current_filename = Some(
495            file.canonicalize()
496                .map(|p| Utf8PathBuf::from_path_buf(p).unwrap())
497                .unwrap_or(file)
498        )
499    }
500
501    #[inline]
502    pub fn remove_filename(&mut self) {
503        self.current_filename = None;
504    }
505
506    #[inline]
507    pub fn set_context_name(&mut self, name: &str) {
508        self.context_name = Some(name.to_owned());
509    }
510
511    #[inline]
512    pub fn complete_source(&self) -> &str {
513        unsafe { std::mem::transmute(self.source.deref()) }
514    }
515
516    #[inline(always)]
517    pub fn options(&self) -> &ParserOptions {
518        &self.options
519    }
520
521    #[inline]
522    pub fn state(&self) -> &ParsingState {
523        &self.state
524    }
525
526    #[inline]
527    pub fn relative_line_and_column(&self, offset: usize) -> (usize, usize) {
528        if self.line_col_lut.read().unwrap().is_none() {
529            let src: &'static str = unsafe { std::mem::transmute(self.source.deref()) };
530
531            self.line_col_lut
532                .write()
533                .unwrap()
534                .replace(LineColLookup::new(src));
535        }
536
537        let res = self
538            .line_col_lut
539            .read()
540            .unwrap()
541            .as_ref()
542            .unwrap()
543            .get(offset);
544
545        res
546    }
547}
548// pub(crate) static DEFAULT_CTX: ParserContext = ParserContext {
549// context_name: None,
550// current_filename: None,
551// read_referenced_files: false,
552// search_path: Vec::new(),
553// parse_warning: Default::default()
554// };
555
556#[cfg(test)]
557mod test_super {
558    use super::*;
559
560    #[test]
561    fn test_function_state() {
562        assert!(Token::Return(0.into()).is_accepted(&ParsingState::FunctionLimited));
563    }
564    #[test]
565
566    fn test_normal_state() {
567        assert!(!Token::Return(0.into()).is_accepted(&ParsingState::Standard));
568    }
569}