1use std::collections::{BTreeMap, BTreeSet};
2use std::ops::Range;
3
4use powerio_core::{Error, Source, SourceBuffer, SourceId, SourceSpan};
5
6use crate::diagnostics::codes;
7
8const MAX_BYTES: usize = 64 << 20;
9const MAX_LINE: usize = 1 << 20;
10const MAX_ORIGINS: usize = 1 << 20;
11const MAX_FILES: usize = 4096;
12const MAX_DEPTH: usize = 64;
13const MAX_DEFINES: usize = 4096;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum GlmDialect {
19 #[default]
20 Pnnl5_3,
21 Arras4_3,
22}
23
24#[derive(Clone, Debug, Default)]
27pub struct PreprocessOptions {
28 pub dialect: GlmDialect,
29 pub defines: BTreeMap<String, String>,
30 pub reinclude: bool,
32}
33
34#[derive(Clone, Debug)]
36pub struct ExpansionOrigin {
37 pub expanded: Range<usize>,
38 pub original: SourceSpan,
39 pub exact: bool,
42}
43
44#[derive(Clone, Debug)]
47pub struct PreparedGlm {
48 pub source: Source,
49 text: String,
50 origins: Vec<ExpansionOrigin>,
51 dependencies: Vec<SourceId>,
53 absolute_include: bool,
54}
55
56impl PreparedGlm {
57 #[must_use]
58 pub fn text(&self) -> &str {
59 &self.text
60 }
61
62 #[must_use]
63 pub fn origins(&self) -> &[ExpansionOrigin] {
64 &self.origins
65 }
66
67 #[must_use]
68 pub fn dependencies(&self) -> &[SourceId] {
69 &self.dependencies
70 }
71
72 pub(super) const fn has_absolute_include(&self) -> bool {
73 self.absolute_include
74 }
75
76 #[must_use]
80 pub fn original_spans(&self, range: Range<usize>) -> Vec<SourceSpan> {
81 self.original_span_iter(range).collect()
82 }
83
84 pub(crate) fn original_span_iter(
85 &self,
86 mut range: Range<usize>,
87 ) -> impl Iterator<Item = SourceSpan> + '_ {
88 if range.start >= range.end || range.end > self.text.len() {
89 range = 0..0;
90 }
91 let first = self
92 .origins
93 .partition_point(|o| o.expanded.end <= range.start);
94 self.origins[first..]
95 .iter()
96 .take_while(move |o| o.expanded.start < range.end)
97 .map(move |o| {
98 if o.exact {
99 let start = o.original.byte_start()
100 + (range.start.max(o.expanded.start) - o.expanded.start) as u64;
101 let end = o.original.byte_start()
102 + (range.end.min(o.expanded.end) - o.expanded.start) as u64;
103 SourceSpan::new(o.original.source().clone(), start, end)
104 .expect("intersected expansion ranges are ordered")
105 } else {
106 o.original.clone()
107 }
108 })
109 }
110}
111
112pub fn preprocess_source(source: Source, options: PreprocessOptions) -> Result<PreparedGlm, Error> {
131 let result: Result<_, Error> = (|| {
132 let primary = source.primary_buffer()?;
133 let define_bytes = options.defines.iter().fold(0usize, |n, (k, v)| {
134 n.saturating_add(k.len()).saturating_add(v.len())
135 });
136 let mut context = Context {
137 source: &source,
138 include_base: primary.clone(),
139 literal_if: options.dialect == GlmDialect::Arras4_3,
140 relax_undefined_if: false,
141 options,
142 output: String::new(),
143 origins: Vec::new(),
144 dependencies: Vec::new(),
145 absolute_include: false,
146 visited: BTreeSet::new(),
147 active: BTreeSet::new(),
148 input_bytes: 0,
149 define_bytes,
150 boundary: Boundary::default(),
151 };
152 context.check_defines(&primary, 0..0)?;
153 context.file(&primary, 0)?;
154 Ok((
155 context.output,
156 context.origins,
157 context.dependencies,
158 context.absolute_include,
159 ))
160 })();
161 match result {
162 Ok((text, origins, dependencies, absolute_include)) => Ok(PreparedGlm {
163 source,
164 text,
165 origins,
166 dependencies,
167 absolute_include,
168 }),
169 Err(error) => Err(error.with_source(source)),
170 }
171}
172
173struct Context<'a> {
174 source: &'a Source,
175 include_base: SourceBuffer,
176 options: PreprocessOptions,
177 output: String,
178 origins: Vec<ExpansionOrigin>,
179 dependencies: Vec<SourceId>,
180 absolute_include: bool,
181 visited: BTreeSet<SourceId>,
182 active: BTreeSet<SourceId>,
183 input_bytes: usize,
184 define_bytes: usize,
185 boundary: Boundary,
186 literal_if: bool,
187 relax_undefined_if: bool,
188}
189
190#[derive(Default)]
193#[expect(
194 clippy::struct_excessive_bools,
195 reason = "incremental lexical boundary flags span independent expansion chunks"
196)]
197struct Boundary {
198 depth: usize,
199 quote: Option<u8>,
200 escaped: bool,
201 slash: bool,
202 block_comment: bool,
203 star: bool,
204 line_comment: bool,
205 pending: bool,
206 invalid: bool,
207}
208
209impl Boundary {
210 fn ready(&self) -> bool {
211 self.depth == 0
212 && self.quote.is_none()
213 && !self.block_comment
214 && !self.slash
215 && !self.pending
216 && !self.invalid
217 }
218
219 fn observe(&mut self, text: &str) {
220 for byte in text.bytes() {
221 if self.line_comment {
222 if byte == b'\n' {
223 self.line_comment = false;
224 }
225 continue;
226 }
227 if self.block_comment {
228 if self.star && byte == b'/' {
229 self.block_comment = false;
230 self.star = false;
231 } else {
232 self.star = byte == b'*';
233 }
234 continue;
235 }
236 if let Some(quote) = self.quote {
237 if self.escaped {
238 self.escaped = false;
239 } else if byte == b'\\' {
240 self.escaped = true;
241 } else if byte == quote {
242 self.quote = None;
243 }
244 continue;
245 }
246 if self.slash {
247 self.slash = false;
248 match byte {
249 b'/' => {
250 self.line_comment = true;
251 continue;
252 }
253 b'*' => {
254 self.block_comment = true;
255 continue;
256 }
257 _ => self.pending = true,
258 }
259 }
260 match byte {
261 b'/' => self.slash = true,
262 b'#' => self.line_comment = true,
263 b'\'' | b'"' => {
264 self.quote = Some(byte);
265 self.pending = true;
266 }
267 b'{' => {
268 self.depth += 1;
269 self.pending = true;
270 }
271 b'}' => {
272 if self.depth == 0 {
273 self.invalid = true;
274 } else {
275 self.depth -= 1;
276 self.pending = self.depth != 0;
277 }
278 }
279 b';' if self.depth == 0 => self.pending = false,
280 b if b.is_ascii_whitespace() => {}
281 _ => self.pending = true,
282 }
283 }
284 }
285}
286
287struct Conditional {
288 parent: bool,
289 condition: bool,
290 has_else: bool,
291 opening: Range<usize>,
292}
293
294fn span(buffer: &SourceBuffer, range: Range<usize>) -> SourceSpan {
295 SourceSpan::new(buffer.id().clone(), range.start as u64, range.end as u64)
296 .expect("preprocessor ranges are ordered")
297}
298
299fn error(buffer: &SourceBuffer, range: Range<usize>, message: impl Into<String>) -> Error {
300 Error::new(&codes::PARSE_GLM_PREPROCESSING, message).with_span(span(buffer, range))
301}
302
303fn limit(buffer: &SourceBuffer, range: Range<usize>) -> Error {
304 Error::new(
305 &codes::PARSE_GLM_LIMIT_EXCEEDED,
306 "GLM preprocessing resource limit exceeded",
307 )
308 .with_span(span(buffer, range))
309}
310
311fn valid_name(name: &str) -> bool {
312 !name.is_empty() && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
313}
314
315impl Context<'_> {
316 fn check_defines(&self, buffer: &SourceBuffer, range: Range<usize>) -> Result<(), Error> {
317 if self.options.defines.len() > MAX_DEFINES
318 || self.options.defines.values().any(|v| v.len() > MAX_LINE)
319 || self.define_bytes > MAX_BYTES
320 {
321 return Err(limit(buffer, range));
322 }
323 if self.options.defines.keys().any(|k| !valid_name(k)) {
324 return Err(error(
325 buffer,
326 range,
327 "GLM defines require alphanumeric/underscore names",
328 ));
329 }
330 if self
331 .options
332 .defines
333 .values()
334 .any(|v| v.contains(['\r', '\n', '\0']))
335 {
336 return Err(error(
337 buffer,
338 range,
339 "GLM explicit variables must be single-line non-NUL text",
340 ));
341 }
342 Ok(())
343 }
344
345 fn file(&mut self, buffer: &SourceBuffer, depth: usize) -> Result<(), Error> {
346 let whole = 0..buffer.bytes().len();
347 if depth == MAX_DEPTH || self.active.contains(buffer.id()) {
348 return Err(error(buffer, whole, "GLM include cycle or depth limit"));
349 }
350 if self.visited.contains(buffer.id()) && !self.options.reinclude {
351 return Err(error(
352 buffer,
353 whole,
354 "GLM file included more than once without reinclude",
355 ));
356 }
357 self.input_bytes = self.input_bytes.saturating_add(buffer.bytes().len());
358 if self.input_bytes > MAX_BYTES {
359 return Err(limit(buffer, whole));
360 }
361 if self.visited.insert(buffer.id().clone()) {
362 if self.visited.len() > MAX_FILES || self.input_bytes > MAX_BYTES {
363 return Err(limit(buffer, whole));
364 }
365 self.dependencies.push(buffer.id().clone());
366 }
367 let text = std::str::from_utf8(buffer.bytes())
368 .map_err(|_| error(buffer, whole.clone(), "GLM source is not UTF-8"))?;
369 if text.contains('\0') {
370 return Err(error(buffer, whole, "GLM source contains a NUL byte"));
371 }
372 self.active.insert(buffer.id().clone());
373 let mut stack = Vec::<Conditional>::new();
374 let mut enabled = true;
375 let mut offset = if buffer.has_utf8_bom() { 3 } else { 0 };
376 for line in text[offset..].split_inclusive('\n') {
377 let range = offset..offset + line.len();
378 if line.len() > MAX_LINE {
379 return Err(limit(buffer, range));
380 }
381 let trimmed = line.trim_start();
382 if let Some(directive) = trimmed.strip_prefix('#') {
383 let directive = directive.split_once(" //").map_or(directive, |(a, _)| a);
385 let directive = directive
386 .split_once("\t//")
387 .map_or(directive, |(a, _)| a)
388 .trim_end();
389 let (name, term) = directive
390 .split_once(char::is_whitespace)
391 .unwrap_or((directive, ""));
392 self.directive(
393 buffer,
394 range.clone(),
395 name,
396 term.trim_start(),
397 &mut stack,
398 &mut enabled,
399 depth,
400 )?;
401 } else if enabled {
402 self.expand_to_output(buffer, range.clone())?;
403 }
404 offset = range.end;
405 }
406 if let Some(top) = stack.last() {
407 return Err(error(
408 buffer,
409 top.opening.clone(),
410 "GLM conditional has no #endif in its file",
411 ));
412 }
413 self.active.remove(buffer.id());
414 Ok(())
415 }
416
417 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
418 fn directive(
419 &mut self,
420 buffer: &SourceBuffer,
421 range: Range<usize>,
422 name: &str,
423 term: &str,
424 stack: &mut Vec<Conditional>,
425 enabled: &mut bool,
426 depth: usize,
427 ) -> Result<(), Error> {
428 match name {
429 "ifdef" | "ifndef" | "if" => {
430 if stack.len() == MAX_DEPTH {
431 return Err(limit(buffer, range));
432 }
433 let condition = if *enabled {
434 let term = self.expand_value(buffer, range.clone(), term)?;
435 if name == "if" {
436 self.condition(buffer, range.clone(), &term)?
437 } else {
438 if !valid_name(&term) {
439 return Err(error(buffer, range, "invalid GLM conditional variable"));
440 }
441 self.options.defines.contains_key(&term) == (name == "ifdef")
442 }
443 } else {
444 false
445 };
446 stack.push(Conditional {
447 parent: *enabled,
448 condition,
449 has_else: false,
450 opening: range,
451 });
452 *enabled &= condition;
453 }
454 "else" => {
455 let top = stack
456 .last_mut()
457 .ok_or_else(|| error(buffer, range.clone(), "GLM #else without #if"))?;
458 if top.has_else || !term.is_empty() {
459 return Err(error(buffer, range, "duplicate or malformed GLM #else"));
460 }
461 top.has_else = true;
462 *enabled = top.parent && !top.condition;
463 }
464 "endif" => {
465 let top = stack
466 .pop()
467 .ok_or_else(|| error(buffer, range.clone(), "GLM #endif without #if"))?;
468 if !term.is_empty() {
469 return Err(error(buffer, range, "GLM #endif has unexpected text"));
470 }
471 *enabled = top.parent;
472 }
473 _ if !*enabled => {}
474 "define" => {
475 let value = self.expand_value(buffer, range.clone(), term)?;
476 let (key, value) = value.split_once('=').unwrap_or((&value, ""));
477 if !valid_name(key) {
478 return Err(error(
479 buffer,
480 range,
481 "expected #define NAME=value or #define NAME",
482 ));
483 }
484 if let Some(old) = self.options.defines.get(key) {
485 self.define_bytes -= old.len();
486 } else {
487 self.define_bytes += key.len();
488 }
489 self.define_bytes += value.len();
490 if self.define_bytes > MAX_BYTES
491 || (self.options.defines.len() == MAX_DEFINES
492 && !self.options.defines.contains_key(key))
493 {
494 return Err(limit(buffer, range));
495 }
496 self.options
497 .defines
498 .insert(key.to_owned(), value.to_owned());
499 }
500 "include" => {
501 if !self.boundary.ready() {
502 return Err(error(
503 buffer,
504 range,
505 "GLM includes require a complete top-level declaration boundary",
506 ));
507 }
508 let value = self.expand_value(buffer, range.clone(), term)?;
509 let value = value.strip_suffix(';').unwrap_or(&value).trim_end();
510 let path = value
511 .strip_prefix('"')
512 .and_then(|s| s.strip_suffix('"'))
513 .filter(|s| !s.is_empty() && !s.contains(['"', '\\', '\n', '\r', ':']));
514 let path = path.ok_or_else(|| {
515 error(
516 buffer,
517 range.clone(),
518 "only local quoted GLM includes are supported",
519 )
520 })?;
521 let included = self
522 .source
523 .referenced_buffer_with_limit(
524 &self.include_base,
525 path,
526 (MAX_BYTES - self.input_bytes) as u64,
527 )
528 .map_err(|e| {
529 if e.diagnostics().first().is_some_and(|d| {
530 d.code() == powerio_core::codes::READ_IO_BUFFER_BUDGET.code
531 }) {
532 limit(buffer, range.clone()).with_cause(e)
533 } else {
534 e.with_span(span(buffer, range.clone()))
535 }
536 })?;
537 self.absolute_include |= crate::portable_path::is_portable_absolute(path);
538 self.file(&included, depth + 1)?;
539 if !self.boundary.ready() {
540 return Err(error(
541 buffer,
542 range,
543 "included GLM file ends inside a declaration",
544 ));
545 }
546 self.append("\n", span(buffer, range), false)?;
547 }
548 "set" | "warning" => {
549 if name == "set" {
552 let term = self.expand_value(buffer, range.clone(), term)?;
553 if let Some((key, value)) = term.split_once('=')
554 && (key == "reinclude"
555 || (self.options.dialect == GlmDialect::Arras4_3
556 && matches!(key, "literal_if" | "relax_undefined_if")))
557 {
558 let value = match value {
559 "TRUE" | "true" | "1" => true,
560 "FALSE" | "false" | "0" => false,
561 _ => {
562 return Err(error(
563 buffer,
564 range,
565 "GLM preprocessing switch requires an explicit boolean",
566 ));
567 }
568 };
569 match key {
570 "reinclude" => self.options.reinclude = value,
571 "literal_if" => self.literal_if = value,
572 _ => self.relax_undefined_if = value,
573 }
574 }
575 }
576 self.expand_to_output(buffer, range)?;
577 }
578 _ => {
579 return Err(error(
580 buffer,
581 range,
582 format!("unsupported active GLM directive #{name}"),
583 ));
584 }
585 }
586 Ok(())
587 }
588
589 fn condition(
590 &self,
591 buffer: &SourceBuffer,
592 range: Range<usize>,
593 term: &str,
594 ) -> Result<bool, Error> {
595 if self.options.dialect == GlmDialect::Arras4_3 {
596 return self.arras_condition(buffer, range, term);
597 }
598 let at = term
599 .find(['!', '<', '>', '='])
600 .ok_or_else(|| error(buffer, range.clone(), "GLM #if requires NAME<op>value"))?;
601 let (key, rest) = term.split_at(at);
602 let op_end = rest
603 .find(|c| !matches!(c, '!' | '<' | '>' | '='))
604 .unwrap_or(rest.len());
605 let (op, rhs) = rest.split_at(op_end);
606 let lhs = self.options.defines.get(key).ok_or_else(|| {
607 error(
608 buffer,
609 range.clone(),
610 "GLM #if variable is not explicitly defined",
611 )
612 })?;
613 match op {
614 "<" => Ok(lhs.as_str() < rhs),
615 ">" => Ok(lhs.as_str() > rhs),
616 "<=" => Ok(lhs.as_str() <= rhs),
617 ">=" => Ok(lhs.as_str() >= rhs),
618 "==" => Ok(lhs == rhs),
619 "!=" => Ok(lhs != rhs),
620 _ => Err(error(buffer, range, "invalid GLM #if comparison operator")),
621 }
622 }
623
624 fn arras_condition(
625 &self,
626 buffer: &SourceBuffer,
627 range: Range<usize>,
628 term: &str,
629 ) -> Result<bool, Error> {
630 let (left, rest) = if let Some(quoted) = term.strip_prefix('"') {
631 quoted.split_once('"').ok_or_else(|| {
632 error(buffer, range.clone(), "unterminated Arras #if left operand")
633 })?
634 } else {
635 let end = term
636 .find(|c: char| c.is_ascii_whitespace() || matches!(c, '!' | '<' | '>' | '='))
637 .ok_or_else(|| error(buffer, range.clone(), "missing Arras #if comparison"))?;
638 term.split_at(end)
639 };
640 let rest = rest.trim_start();
641 let op = ["not_in", "in", "<=", ">=", "==", "!=", "<", ">"]
642 .into_iter()
643 .find(|op| rest.starts_with(op))
644 .ok_or_else(|| error(buffer, range.clone(), "invalid Arras #if comparison"))?;
645 let rhs = rest[op.len()..].trim_start();
646 let rhs = if let Some(quoted) = rhs.strip_prefix('"') {
647 quoted
648 .strip_suffix('"')
649 .filter(|s| !s.contains('"'))
650 .ok_or_else(|| {
651 error(
652 buffer,
653 range.clone(),
654 "unterminated Arras #if right operand",
655 )
656 })?
657 } else {
658 if rhs.chars().any(char::is_whitespace) {
659 return Err(error(
660 buffer,
661 range,
662 "Arras #if operands containing spaces must be quoted",
663 ));
664 }
665 rhs
666 };
667 let lhs = if self.literal_if {
668 left
669 } else {
670 match self.options.defines.get(left) {
671 Some(value) => value,
672 None if self.relax_undefined_if => "",
673 None => {
674 return Err(error(
675 buffer,
676 range,
677 "Arras #if variable is not explicitly defined",
678 ));
679 }
680 }
681 };
682 match op {
683 "<" => Ok(lhs < rhs),
684 ">" => Ok(lhs > rhs),
685 "<=" => Ok(lhs <= rhs),
686 ">=" => Ok(lhs >= rhs),
687 "==" => Ok(lhs == rhs),
688 "!=" => Ok(lhs != rhs),
689 "in" => Ok(lhs.contains(rhs)),
690 "not_in" => Ok(!lhs.contains(rhs)),
691 _ => unreachable!("operator was selected from a fixed set"),
692 }
693 }
694
695 fn expand_value(
696 &self,
697 buffer: &SourceBuffer,
698 range: Range<usize>,
699 text: &str,
700 ) -> Result<String, Error> {
701 let mut output = String::new();
702 for (literal, variable) in
703 substitutions(text).map_err(|m| error(buffer, range.clone(), m))?
704 {
705 let value = if let Some(key) = variable {
706 self.options.defines.get(key).ok_or_else(|| {
707 error(
708 buffer,
709 range.clone(),
710 format!("GLM variable {key} is not explicitly defined"),
711 )
712 })?
713 } else {
714 literal
715 };
716 if output.len().saturating_add(value.len()) > MAX_LINE {
717 return Err(limit(buffer, range));
718 }
719 output.push_str(value);
720 }
721 Ok(output)
722 }
723
724 fn expand_to_output(
725 &mut self,
726 buffer: &SourceBuffer,
727 range: Range<usize>,
728 ) -> Result<(), Error> {
729 let text = std::str::from_utf8(&buffer.bytes()[range.clone()])
730 .expect("file validates UTF-8 before splitting lines");
731 let comment = if text.trim_start().starts_with('#') {
732 text.find(" //").into_iter().chain(text.find("\t//")).min()
733 } else {
734 text.find("//")
735 };
736 let body = comment.map_or(text, |end| &text[..end]);
737 let initial_size = self.output.len();
738 let mut offset = range.start;
739 for (literal, variable) in
740 substitutions(body).map_err(|m| error(buffer, range.clone(), m))?
741 {
742 let original = span(buffer, offset..offset + literal.len());
743 if let Some(key) = variable {
744 let value = self
745 .options
746 .defines
747 .get(key)
748 .ok_or_else(|| {
749 error(
750 buffer,
751 offset..offset + literal.len(),
752 format!("GLM variable {key} is not explicitly defined"),
753 )
754 })?
755 .clone();
756 self.append(&value, original, false)?;
757 } else {
758 self.append(literal, original, true)?;
759 }
760 offset += literal.len();
761 if self.output.len() - initial_size > MAX_LINE {
762 return Err(limit(buffer, range));
763 }
764 }
765 if comment.is_some() && text.ends_with('\n') {
766 self.append("\n", span(buffer, range.end - 1..range.end), true)?;
767 }
768 if !text.trim_start().starts_with('#')
769 && self.output[initial_size..].trim_start().starts_with('#')
770 {
771 return Err(error(
772 buffer,
773 range,
774 "GLM variable expansion generated a directive; explicit directive syntax is required",
775 ));
776 }
777 Ok(())
778 }
779
780 fn append(&mut self, text: &str, original: SourceSpan, exact: bool) -> Result<(), Error> {
781 if text.is_empty() {
782 return Ok(());
783 }
784 if self.output.len().saturating_add(text.len()) > MAX_BYTES
785 || self.origins.len() == MAX_ORIGINS
786 {
787 return Err(Error::new(
788 &codes::PARSE_GLM_LIMIT_EXCEEDED,
789 "GLM expansion exceeds its byte or source-map budget",
790 )
791 .with_span(original));
792 }
793 let start = self.output.len();
794 self.output.push_str(text);
795 self.boundary.observe(text);
796 self.origins.push(ExpansionOrigin {
797 expanded: start..self.output.len(),
798 original,
799 exact,
800 });
801 Ok(())
802 }
803}
804
805fn substitutions(mut text: &str) -> Result<Vec<(&str, Option<&str>)>, &'static str> {
807 let mut result = Vec::new();
808 while let Some(start) = text.find("${") {
809 result.push((&text[..start], None));
810 text = &text[start..];
811 let end = text
812 .find('}')
813 .ok_or("unterminated GLM variable reference")?;
814 let key = &text[2..end];
815 if !valid_name(key) {
816 return Err("GLM variable reference requires a simple explicit name");
817 }
818 result.push((&text[..=end], Some(key)));
819 text = &text[end + 1..];
820 }
821 result.push((text, None));
822 Ok(result)
823}