1#![allow(clippy::needless_lifetimes)]
3
4use std::env;
5use std::fmt;
6use std::iter::Enumerate;
7use std::num::NonZeroUsize;
8use std::str::FromStr;
9
10use anyhow::Result;
11use kcl_error::KclErrorDetails;
12use parse_display::Display;
13use serde::Deserialize;
14use serde::Serialize;
15use tower_lsp::lsp_types::SemanticTokenType;
16use winnow::stream::ContainsToken;
17use winnow::stream::Stream;
18use winnow::{self};
19
20use crate::CompilationIssue;
21use crate::ModuleId;
22use crate::RuntimeFlag;
23use crate::SourceRange;
24use crate::errors::KclError;
25use crate::kcl_runtime_flags;
26use crate::parsing::ast::types::ItemVisibility;
27use crate::parsing::ast::types::VariableKind;
28use crate::runtime_flags::RuntimeFlagResolve;
29use crate::runtime_flags::resolve_from_sources;
30
31mod tokeniser;
32
33pub(crate) mod adapter;
34
35#[cfg(test)]
36mod compat_tests;
37
38#[cfg(test)]
39mod error_matrix_tests;
40
41pub(crate) use tokeniser::RESERVED_SKETCH_BLOCK_WORDS;
42pub(crate) use tokeniser::RESERVED_WORDS;
43
44pub const NUM_SUFFIXES: [&str; 10] = ["mm", "cm", "m", "inch", "in", "ft", "yd", "deg", "rad", "?"];
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
48#[repr(u32)]
49pub enum NumericSuffix {
50 None,
51 Count,
52 Length,
53 Angle,
54 Mm,
55 Cm,
56 M,
57 Inch,
58 Ft,
59 Yd,
60 Deg,
61 Rad,
62 Unknown,
63}
64
65impl NumericSuffix {
66 #[allow(dead_code)]
67 pub fn is_none(self) -> bool {
68 self == Self::None
69 }
70
71 pub fn is_some(self) -> bool {
72 self != Self::None
73 }
74
75 pub fn digestable_id(&self) -> &[u8] {
76 match self {
77 NumericSuffix::None => &[],
78 NumericSuffix::Count => b"_",
79 NumericSuffix::Unknown => b"?",
80 NumericSuffix::Length => b"Length",
81 NumericSuffix::Angle => b"Angle",
82 NumericSuffix::Mm => b"mm",
83 NumericSuffix::Cm => b"cm",
84 NumericSuffix::M => b"m",
85 NumericSuffix::Inch => b"in",
86 NumericSuffix::Ft => b"ft",
87 NumericSuffix::Yd => b"yd",
88 NumericSuffix::Deg => b"deg",
89 NumericSuffix::Rad => b"rad",
90 }
91 }
92}
93
94impl FromStr for NumericSuffix {
95 type Err = CompilationIssue;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 match s {
99 "_" | "Count" => Ok(NumericSuffix::Count),
100 "Length" => Ok(NumericSuffix::Length),
101 "Angle" => Ok(NumericSuffix::Angle),
102 "mm" | "millimeters" => Ok(NumericSuffix::Mm),
103 "cm" | "centimeters" => Ok(NumericSuffix::Cm),
104 "m" | "meters" => Ok(NumericSuffix::M),
105 "inch" | "in" => Ok(NumericSuffix::Inch),
106 "ft" | "feet" => Ok(NumericSuffix::Ft),
107 "yd" | "yards" => Ok(NumericSuffix::Yd),
108 "deg" | "degrees" => Ok(NumericSuffix::Deg),
109 "rad" | "radians" => Ok(NumericSuffix::Rad),
110 "?" => Ok(NumericSuffix::Unknown),
111 _ => Err(CompilationIssue::err(SourceRange::default(), "invalid unit of measure")),
112 }
113 }
114}
115
116impl fmt::Display for NumericSuffix {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 NumericSuffix::None => Ok(()),
120 NumericSuffix::Count => write!(f, "_"),
121 NumericSuffix::Unknown => write!(f, "_?"),
122 NumericSuffix::Length => write!(f, "Length"),
123 NumericSuffix::Angle => write!(f, "Angle"),
124 NumericSuffix::Mm => write!(f, "mm"),
125 NumericSuffix::Cm => write!(f, "cm"),
126 NumericSuffix::M => write!(f, "m"),
127 NumericSuffix::Inch => write!(f, "in"),
128 NumericSuffix::Ft => write!(f, "ft"),
129 NumericSuffix::Yd => write!(f, "yd"),
130 NumericSuffix::Deg => write!(f, "deg"),
131 NumericSuffix::Rad => write!(f, "rad"),
132 }
133 }
134}
135
136#[derive(Clone, Debug, PartialEq)]
137pub(crate) struct TokenStream {
138 tokens: Vec<Token>,
139}
140
141impl TokenStream {
142 fn new(tokens: Vec<Token>) -> Self {
143 Self { tokens }
144 }
145
146 pub(super) fn remove_unknown(&mut self) -> Vec<Token> {
147 let tokens = std::mem::take(&mut self.tokens);
148 let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
149 .into_iter()
150 .partition(|token| token.token_type != TokenType::Unknown);
151 self.tokens = tokens;
152 unknown_tokens
153 }
154
155 pub fn iter(&self) -> impl Iterator<Item = &Token> {
156 self.tokens.iter()
157 }
158
159 pub fn is_empty(&self) -> bool {
160 self.tokens.is_empty()
161 }
162
163 pub fn as_slice(&self) -> TokenSlice<'_> {
164 TokenSlice::from(self)
165 }
166}
167
168impl<'a> From<&'a TokenStream> for TokenSlice<'a> {
169 fn from(stream: &'a TokenStream) -> Self {
170 TokenSlice {
171 start: 0,
172 end: stream.tokens.len(),
173 stream,
174 }
175 }
176}
177
178impl IntoIterator for TokenStream {
179 type Item = Token;
180
181 type IntoIter = std::vec::IntoIter<Token>;
182
183 fn into_iter(self) -> Self::IntoIter {
184 self.tokens.into_iter()
185 }
186}
187
188#[derive(Debug, Clone)]
189pub(crate) struct TokenSlice<'a> {
190 stream: &'a TokenStream,
191 start: usize,
193 end: usize,
195}
196
197impl<'a> std::ops::Deref for TokenSlice<'a> {
198 type Target = [Token];
199
200 fn deref(&self) -> &Self::Target {
201 &self.stream.tokens[self.start..self.end]
202 }
203}
204
205impl<'a> TokenSlice<'a> {
206 pub fn token(&self, i: usize) -> &Token {
207 &self.stream.tokens[i + self.start]
208 }
209
210 pub fn iter(&self) -> impl Iterator<Item = &Token> {
211 (**self).iter()
212 }
213
214 pub fn without_ends(&self) -> Self {
215 Self {
216 start: self.start + 1,
217 end: self.end - 1,
218 stream: self.stream,
219 }
220 }
221
222 pub fn as_source_range(&self) -> SourceRange {
223 let stream_len = self.stream.tokens.len();
224 let first_token = if stream_len == self.start {
225 &self.stream.tokens[self.start - 1]
226 } else {
227 self.token(0)
228 };
229 let last_token = if stream_len == self.end {
230 &self.stream.tokens[stream_len - 1]
231 } else {
232 self.token(self.end - self.start)
233 };
234 SourceRange::new(first_token.start, last_token.end, last_token.module_id)
235 }
236}
237
238impl<'a> IntoIterator for TokenSlice<'a> {
239 type Item = &'a Token;
240
241 type IntoIter = std::slice::Iter<'a, Token>;
242
243 fn into_iter(self) -> Self::IntoIter {
244 self.stream.tokens[self.start..self.end].iter()
245 }
246}
247
248impl<'a> Stream for TokenSlice<'a> {
249 type Token = Token;
250 type Slice = Self;
251 type IterOffsets = Enumerate<std::vec::IntoIter<Token>>;
252 type Checkpoint = Checkpoint;
253
254 fn iter_offsets(&self) -> Self::IterOffsets {
255 #[allow(clippy::unnecessary_to_owned)]
256 self.to_vec().into_iter().enumerate()
257 }
258
259 fn eof_offset(&self) -> usize {
260 self.len()
261 }
262
263 fn next_token(&mut self) -> Option<Self::Token> {
264 let token = self.first()?.clone();
265 self.start += 1;
266 Some(token)
267 }
268
269 fn peek_token(&self) -> Option<Self::Token> {
271 Some(self.first()?.clone())
272 }
273
274 fn offset_for<P>(&self, predicate: P) -> Option<usize>
275 where
276 P: Fn(Self::Token) -> bool,
277 {
278 self.iter().position(|b| predicate(b.clone()))
279 }
280
281 fn offset_at(&self, tokens: usize) -> Result<usize, winnow::error::Needed> {
282 if let Some(needed) = tokens.checked_sub(self.len()).and_then(NonZeroUsize::new) {
283 Err(winnow::error::Needed::Size(needed))
284 } else {
285 Ok(tokens)
286 }
287 }
288
289 fn next_slice(&mut self, offset: usize) -> Self::Slice {
290 assert!(self.start + offset <= self.end);
291
292 let next = TokenSlice {
293 stream: self.stream,
294 start: self.start,
295 end: self.start + offset,
296 };
297 self.start += offset;
298 next
299 }
300
301 fn peek_slice(&self, offset: usize) -> Self::Slice {
303 assert!(self.start + offset <= self.end);
304
305 TokenSlice {
306 stream: self.stream,
307 start: self.start,
308 end: self.start + offset,
309 }
310 }
311
312 fn checkpoint(&self) -> Self::Checkpoint {
313 Checkpoint(self.start, self.end)
314 }
315
316 fn reset(&mut self, checkpoint: &Self::Checkpoint) {
317 self.start = checkpoint.0;
318 self.end = checkpoint.1;
319 }
320
321 fn trace(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
322 write!(f, "{self:?}")
323 }
324}
325
326impl<'a> winnow::stream::Offset for TokenSlice<'a> {
327 fn offset_from(&self, start: &Self) -> usize {
328 self.start - start.start
329 }
330}
331
332impl<'a> winnow::stream::Offset<Checkpoint> for TokenSlice<'a> {
333 fn offset_from(&self, start: &Checkpoint) -> usize {
334 self.start - start.0
335 }
336}
337
338impl winnow::stream::Offset for Checkpoint {
339 fn offset_from(&self, start: &Self) -> usize {
340 self.0 - start.0
341 }
342}
343
344impl<'a> winnow::stream::StreamIsPartial for TokenSlice<'a> {
345 type PartialState = ();
346
347 fn complete(&mut self) -> Self::PartialState {}
348
349 fn restore_partial(&mut self, _: Self::PartialState) {}
350
351 fn is_partial_supported() -> bool {
352 false
353 }
354}
355
356impl<'a> winnow::stream::FindSlice<&str> for TokenSlice<'a> {
357 fn find_slice(&self, substr: &str) -> Option<std::ops::Range<usize>> {
358 self.iter()
359 .enumerate()
360 .find_map(|(i, b)| if b.value == substr { Some(i..self.end) } else { None })
361 }
362}
363
364#[derive(Clone, Debug)]
365pub struct Checkpoint(usize, usize);
366
367#[derive(Debug, PartialEq, Eq, Copy, Clone, Display)]
369#[display(style = "camelCase")]
370pub enum TokenType {
371 Number,
373 Word,
375 Operator,
377 String,
379 Keyword,
381 Type,
383 Brace,
385 Hash,
387 Bang,
389 Dollar,
391 Whitespace,
393 Comma,
395 Colon,
397 DoubleColon,
399 Period,
401 DoublePeriod,
403 DoublePeriodLessThan,
405 LineComment,
407 BlockComment,
409 Function,
411 Unknown,
413 QuestionMark,
415 At,
417 SemiColon,
419}
420
421impl TryFrom<TokenType> for SemanticTokenType {
423 type Error = anyhow::Error;
424 fn try_from(token_type: TokenType) -> Result<Self> {
425 Ok(match token_type {
428 TokenType::Number => Self::NUMBER,
429 TokenType::Word => Self::VARIABLE,
430 TokenType::Keyword => Self::KEYWORD,
431 TokenType::Type => Self::TYPE,
432 TokenType::Operator => Self::OPERATOR,
433 TokenType::QuestionMark => Self::OPERATOR,
434 TokenType::String => Self::STRING,
435 TokenType::Bang => Self::OPERATOR,
436 TokenType::LineComment => Self::COMMENT,
437 TokenType::BlockComment => Self::COMMENT,
438 TokenType::Function => Self::FUNCTION,
439 TokenType::Whitespace
440 | TokenType::Brace
441 | TokenType::Comma
442 | TokenType::Colon
443 | TokenType::DoubleColon
444 | TokenType::Period
445 | TokenType::DoublePeriod
446 | TokenType::DoublePeriodLessThan
447 | TokenType::Hash
448 | TokenType::Dollar
449 | TokenType::At
450 | TokenType::SemiColon
451 | TokenType::Unknown => {
452 anyhow::bail!("unsupported token type: {:?}", token_type)
453 }
454 })
455 }
456}
457
458impl TokenType {
459 pub fn is_whitespace(&self) -> bool {
460 matches!(self, Self::Whitespace)
461 }
462
463 pub fn is_comment(&self) -> bool {
464 matches!(self, Self::LineComment | Self::BlockComment)
465 }
466}
467
468#[derive(Debug, PartialEq, Eq, Clone)]
469pub struct Token {
470 pub token_type: TokenType,
471 pub start: usize,
473 pub end: usize,
475 pub(super) module_id: ModuleId,
476 pub(super) value: String,
477}
478
479impl ContainsToken<Token> for (TokenType, &str) {
480 fn contains_token(&self, token: Token) -> bool {
481 self.0 == token.token_type && self.1 == token.value
482 }
483}
484
485impl ContainsToken<Token> for TokenType {
486 fn contains_token(&self, token: Token) -> bool {
487 *self == token.token_type
488 }
489}
490
491impl Token {
492 pub fn from_range(
493 range: std::ops::Range<usize>,
494 module_id: ModuleId,
495 token_type: TokenType,
496 value: String,
497 ) -> Self {
498 Self {
499 start: range.start,
500 end: range.end,
501 module_id,
502 value,
503 token_type,
504 }
505 }
506 pub fn is_code_token(&self) -> bool {
507 !matches!(
508 self.token_type,
509 TokenType::Whitespace | TokenType::LineComment | TokenType::BlockComment
510 )
511 }
512
513 pub fn as_source_range(&self) -> SourceRange {
514 SourceRange::new(self.start, self.end, self.module_id)
515 }
516
517 pub fn as_source_ranges(&self) -> Vec<SourceRange> {
518 vec![self.as_source_range()]
519 }
520
521 pub fn visibility_keyword(&self) -> Option<ItemVisibility> {
522 if !matches!(self.token_type, TokenType::Keyword) {
523 return None;
524 }
525 match self.value.as_str() {
526 "export" => Some(ItemVisibility::Export),
527 _ => None,
528 }
529 }
530
531 pub fn numeric_value(&self) -> Option<f64> {
532 if self.token_type != TokenType::Number {
533 return None;
534 }
535 let value = &self.value;
536 let value = value
537 .split_once(|c: char| c == '_' || c.is_ascii_alphabetic())
538 .map(|(s, _)| s)
539 .unwrap_or(value);
540 value.parse().ok()
541 }
542
543 pub fn uint_value(&self) -> Option<u32> {
544 if self.token_type != TokenType::Number {
545 return None;
546 }
547 let value = &self.value;
548 let value = value
549 .split_once(|c: char| c == '_' || c.is_ascii_alphabetic())
550 .map(|(s, _)| s)
551 .unwrap_or(value);
552 value.parse().ok()
553 }
554
555 pub fn numeric_suffix(&self) -> NumericSuffix {
556 if self.token_type != TokenType::Number {
557 return NumericSuffix::None;
558 }
559
560 if self.value.ends_with('_') {
561 return NumericSuffix::Count;
562 }
563
564 for suffix in NUM_SUFFIXES {
565 if self.value.ends_with(suffix) {
566 return suffix.parse().unwrap();
567 }
568 }
569
570 NumericSuffix::None
571 }
572
573 pub fn declaration_keyword(&self) -> Option<VariableKind> {
577 if !matches!(self.token_type, TokenType::Keyword) {
578 return None;
579 }
580 Some(match self.value.as_str() {
581 "fn" => VariableKind::Fn,
582 "var" | "let" | "const" => VariableKind::Const,
583 _ => return None,
584 })
585 }
586}
587
588impl From<Token> for SourceRange {
589 fn from(token: Token) -> Self {
590 Self::new(token.start, token.end, token.module_id)
591 }
592}
593
594impl From<&Token> for SourceRange {
595 fn from(token: &Token) -> Self {
596 Self::new(token.start, token.end, token.module_id)
597 }
598}
599
600pub(crate) const KCL_LEXER_ENV_VAR: &str = "KCL_LEXER";
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
611pub(crate) enum LexerMode {
612 Old,
613 New,
614}
615
616impl RuntimeFlagResolve for LexerMode {
617 fn on() -> Self {
618 Self::New
619 }
620
621 fn off() -> Self {
622 Self::Old
623 }
624
625 fn resolve_default() -> Self {
626 Self::DEFAULT
627 }
628
629 fn parse_env_var(value: &str) -> Self {
630 Self::parse(value)
631 }
632}
633
634impl LexerMode {
635 const DEFAULT: Self = Self::Old;
637
638 pub(crate) fn resolve() -> Self {
640 let env_value = match env::var(KCL_LEXER_ENV_VAR) {
641 Ok(value) => Some(value),
642 Err(env::VarError::NotPresent) => None,
643 Err(env::VarError::NotUnicode(value)) => {
644 Self::warn_once(|| {
646 format!(
647 "{KCL_LEXER_ENV_VAR} must be valid unicode; got `{}`. Defaulting to `old`.",
648 value.to_string_lossy()
649 )
650 });
651 None
652 }
653 };
654
655 Self::resolve_from_sources(
656 kcl_runtime_flags().use_new_lexer_parser,
657 Self::test_override_for_resolve(),
658 env_value.as_deref(),
659 )
660 }
661
662 fn resolve_from_sources(runtime_flag: RuntimeFlag, test_override: Option<Self>, env_value: Option<&str>) -> Self {
663 resolve_from_sources(runtime_flag, test_override, env_value)
664 }
665
666 #[cfg(test)]
667 fn test_override_for_resolve() -> Option<Self> {
668 Self::test_override()
669 }
670
671 #[cfg(not(test))]
672 fn test_override_for_resolve() -> Option<Self> {
673 None
674 }
675
676 fn parse(value: &str) -> Self {
677 let value = value.trim();
678 if value.eq_ignore_ascii_case("old") {
679 return Self::Old;
680 }
681 if value.eq_ignore_ascii_case("new") {
682 return Self::New;
683 }
684
685 Self::warn_once(|| {
688 format!("Unsupported {KCL_LEXER_ENV_VAR} value `{value}`; expected `old` or `new`. Defaulting to `old`.")
689 });
690 Self::Old
691 }
692
693 fn warn_once(make_message: impl FnOnce() -> String) {
699 static WARNED: std::sync::Once = std::sync::Once::new();
700 WARNED.call_once(|| crate::log::log(make_message()));
701 }
702
703 #[cfg(test)]
704 fn test_override_value(self) -> u8 {
705 match self {
706 Self::Old => 1,
707 Self::New => 2,
708 }
709 }
710
711 #[cfg(test)]
712 fn test_override() -> Option<Self> {
713 match TEST_LEXER_MODE_OVERRIDE.load(std::sync::atomic::Ordering::SeqCst) {
714 1 => Some(Self::Old),
715 2 => Some(Self::New),
716 _ => None,
717 }
718 }
719
720 #[cfg(test)]
727 pub(crate) fn override_for_test(mode: Self) -> LexerModeOverrideGuard {
728 let previous = TEST_LEXER_MODE_OVERRIDE.swap(mode.test_override_value(), std::sync::atomic::Ordering::SeqCst);
729 LexerModeOverrideGuard { previous }
730 }
731}
732
733#[cfg(test)]
734static TEST_LEXER_MODE_OVERRIDE: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
735
736#[cfg(test)]
737pub(crate) struct LexerModeOverrideGuard {
738 previous: u8,
739}
740
741#[cfg(test)]
742impl Drop for LexerModeOverrideGuard {
743 fn drop(&mut self) {
744 TEST_LEXER_MODE_OVERRIDE.store(self.previous, std::sync::atomic::Ordering::SeqCst);
745 }
746}
747
748pub fn lex(s: &str, module_id: ModuleId) -> Result<TokenStream, KclError> {
754 match LexerMode::resolve() {
755 LexerMode::Old => lex_legacy(s, module_id),
756 LexerMode::New => {
757 let result = adapter::lex_with_diagnostics(s, module_id);
758 match result.to_lexical_error() {
759 Some(err) => Err(err),
760 None => Ok(result.tokens),
761 }
762 }
763 }
764}
765
766fn lex_legacy(s: &str, module_id: ModuleId) -> Result<TokenStream, KclError> {
767 tokeniser::lex(s, module_id).map_err(|err| {
768 let (input, offset): (Vec<char>, usize) = (err.input().chars().collect(), err.offset());
769 let module_id = err.input().state.module_id;
770
771 if offset >= input.len() {
772 return KclError::new_lexical(KclErrorDetails::new(
778 "unexpected EOF while parsing".to_owned(),
779 vec![SourceRange::new(offset, offset, module_id)],
780 ));
781 }
782
783 let bad_token = &input[offset];
786 KclError::new_lexical(KclErrorDetails::new(
789 format!("found unknown token '{bad_token}'"),
790 vec![SourceRange::new(offset, offset + 1, module_id)],
791 ))
792 })
793}
794
795#[cfg(test)]
796mod lexer_mode_tests {
797 use super::LexerMode;
798 use super::lex;
799 use crate::KclRuntimeFlags;
800 use crate::ModuleId;
801 use crate::RuntimeFlag;
802
803 fn set_runtime_lexer_flag(flag: RuntimeFlag) {
804 crate::set_kcl_runtime_flags(KclRuntimeFlags {
805 use_new_lexer_parser: flag,
806 ..Default::default()
807 });
808 }
809
810 fn reset_runtime_lexer_flags() {
811 crate::set_kcl_runtime_flags(KclRuntimeFlags::DEFAULT);
812 }
813
814 #[test]
815 fn default_mode_is_old() {
816 reset_runtime_lexer_flags();
817 assert_eq!(LexerMode::DEFAULT, LexerMode::Old);
818 }
819
820 #[test]
821 fn parse_accepts_known_values_case_insensitively() {
822 assert_eq!(LexerMode::parse("old"), LexerMode::Old);
823 assert_eq!(LexerMode::parse(" NEW "), LexerMode::New);
824 }
825
826 #[test]
827 fn parse_falls_back_to_old_on_unknown_value() {
828 assert_eq!(LexerMode::parse("rowan"), LexerMode::Old);
830 }
831
832 #[test]
833 fn override_guard_sets_and_restores_mode() {
834 reset_runtime_lexer_flags();
835 {
838 let _guard = LexerMode::override_for_test(LexerMode::New);
839 assert_eq!(LexerMode::resolve(), LexerMode::New);
840 }
841 let _guard = LexerMode::override_for_test(LexerMode::Old);
842 assert_eq!(LexerMode::resolve(), LexerMode::Old);
843 }
844
845 #[test]
846 fn runtime_flags_default_to_unset() {
847 reset_runtime_lexer_flags();
848 assert_eq!(
849 crate::kcl_runtime_flags(),
850 KclRuntimeFlags {
851 use_new_lexer_parser: RuntimeFlag::Unset,
852 ..Default::default()
853 }
854 );
855 }
856
857 #[test]
858 fn runtime_flag_on_selects_new_lexer() {
859 reset_runtime_lexer_flags();
860 set_runtime_lexer_flag(RuntimeFlag::On);
861 assert_eq!(LexerMode::resolve(), LexerMode::New);
862 }
863
864 #[test]
865 fn runtime_flag_off_selects_old_lexer() {
866 reset_runtime_lexer_flags();
867 set_runtime_lexer_flag(RuntimeFlag::Off);
868 assert_eq!(LexerMode::resolve(), LexerMode::Old);
869 }
870
871 #[test]
872 fn runtime_flag_takes_priority_over_test_override_and_env() {
873 assert_eq!(
874 LexerMode::resolve_from_sources(RuntimeFlag::Off, Some(LexerMode::New), Some("new")),
875 LexerMode::Old
876 );
877 assert_eq!(
878 LexerMode::resolve_from_sources(RuntimeFlag::On, Some(LexerMode::Old), Some("old")),
879 LexerMode::New
880 );
881 }
882
883 #[test]
884 fn unset_runtime_flag_allows_env_to_select_lexer() {
885 assert_eq!(
886 LexerMode::resolve_from_sources(RuntimeFlag::Unset, None, Some("new")),
887 LexerMode::New
888 );
889 assert_eq!(
890 LexerMode::resolve_from_sources(RuntimeFlag::Unset, None, Some("old")),
891 LexerMode::Old
892 );
893 }
894
895 #[test]
896 fn unset_runtime_flag_and_missing_env_selects_default_lexer() {
897 assert_eq!(
898 LexerMode::resolve_from_sources(RuntimeFlag::Unset, None, None),
899 LexerMode::DEFAULT
900 );
901 }
902
903 #[test]
904 fn test_override_takes_priority_over_env() {
905 assert_eq!(
906 LexerMode::resolve_from_sources(RuntimeFlag::Unset, Some(LexerMode::Old), Some("new")),
907 LexerMode::Old
908 );
909 assert_eq!(
910 LexerMode::resolve_from_sources(RuntimeFlag::Unset, Some(LexerMode::New), Some("old")),
911 LexerMode::New
912 );
913 }
914
915 #[test]
925 fn lex_dispatches_to_new_lexer() {
926 reset_runtime_lexer_flags();
927 let _guard = LexerMode::override_for_test(LexerMode::New);
928 assert_eq!(LexerMode::resolve(), LexerMode::New);
929
930 let module_id = ModuleId::default();
931
932 let tokens = lex("x = 1", module_id).expect("new lexer should tokenize valid input");
934 assert!(!tokens.is_empty(), "expected a non-empty token stream");
935
936 let err = lex("\"abc", module_id).expect_err("unterminated string is a lexical error");
938 assert_eq!(err.error_type(), "lexical");
939 assert_eq!(err.message(), "unterminated string literal");
940 }
941}