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