1#[derive(Debug, Clone)]
5pub struct Token {
6 pub text: String,
7 pub line: usize,
9 pub column: usize,
11}
12
13impl Token {
14 fn new(text: String, line: usize, column: usize) -> Self {
15 Token { text, line, column }
16 }
17}
18
19impl PartialEq<&str> for Token {
20 fn eq(&self, other: &&str) -> bool {
21 self.text == *other
22 }
23}
24
25impl PartialEq<str> for Token {
26 fn eq(&self, other: &str) -> bool {
27 self.text == other
28 }
29}
30
31pub(super) fn annotate_error_with_line(msg: String, tok: Option<&Token>) -> String {
32 if msg.starts_with("at line ") {
33 return msg;
34 }
35 let line = tok.map(|t| t.line).unwrap_or(0);
36 format!("at line {}: {}", line + 1, msg)
37}
38
39pub(super) fn is_float_literal(token: &str) -> bool {
48 let s = token.strip_prefix('-').unwrap_or(token);
50
51 if s.is_empty() {
53 return false;
54 }
55
56 s.contains('.') || s.contains('e') || s.contains('E')
58}
59
60pub(super) fn unescape_string(s: &str) -> Result<String, String> {
86 let mut result = String::new();
87 let mut chars = s.chars();
88
89 while let Some(ch) = chars.next() {
90 if ch == '\\' {
91 match chars.next() {
92 Some('"') => result.push('"'),
93 Some('\\') => result.push('\\'),
94 Some('n') => result.push('\n'),
95 Some('r') => result.push('\r'),
96 Some('t') => result.push('\t'),
97 Some('x') => {
98 let hex1 = chars.next().ok_or_else(|| {
100 "Incomplete hex escape sequence '\\x' - expected 2 hex digits".to_string()
101 })?;
102 let hex2 = chars.next().ok_or_else(|| {
103 format!(
104 "Incomplete hex escape sequence '\\x{}' - expected 2 hex digits",
105 hex1
106 )
107 })?;
108
109 let hex_str: String = [hex1, hex2].iter().collect();
110 let byte_val = u8::from_str_radix(&hex_str, 16).map_err(|_| {
111 format!(
112 "Invalid hex escape sequence '\\x{}' - expected 2 hex digits (00-FF)",
113 hex_str
114 )
115 })?;
116
117 result.push(byte_val as char);
118 }
119 Some(c) => {
120 return Err(format!(
121 "Unknown escape sequence '\\{}' in string literal. \
122 Supported: \\\" \\\\ \\n \\r \\t \\xNN",
123 c
124 ));
125 }
126 None => {
127 return Err("String ends with incomplete escape sequence '\\'".to_string());
128 }
129 }
130 } else {
131 result.push(ch);
132 }
133 }
134
135 Ok(result)
136}
137
138pub(super) fn tokenize(source: &str) -> Vec<Token> {
139 let mut tokens = Vec::new();
140 let mut current = String::new();
141 let mut current_start_line = 0;
142 let mut current_start_col = 0;
143 let mut in_string = false;
144 let mut prev_was_backslash = false;
145
146 let mut line = 0;
148 let mut col = 0;
149
150 for ch in source.chars() {
151 if in_string {
152 current.push(ch);
153 if ch == '"' && !prev_was_backslash {
154 in_string = false;
156 tokens.push(Token::new(
157 current.clone(),
158 current_start_line,
159 current_start_col,
160 ));
161 current.clear();
162 prev_was_backslash = false;
163 } else if ch == '\\' && !prev_was_backslash {
164 prev_was_backslash = true;
166 } else {
167 prev_was_backslash = false;
169 }
170 if ch == '\n' {
172 line += 1;
173 col = 0;
174 } else {
175 col += 1;
176 }
177 } else if ch == '"' {
178 if !current.is_empty() {
179 tokens.push(Token::new(
180 current.clone(),
181 current_start_line,
182 current_start_col,
183 ));
184 current.clear();
185 }
186 in_string = true;
187 current_start_line = line;
188 current_start_col = col;
189 current.push(ch);
190 prev_was_backslash = false;
191 col += 1;
192 } else if ch.is_whitespace() {
193 if !current.is_empty() {
194 tokens.push(Token::new(
195 current.clone(),
196 current_start_line,
197 current_start_col,
198 ));
199 current.clear();
200 }
201 if ch == '\n' {
203 tokens.push(Token::new("\n".to_string(), line, col));
204 line += 1;
205 col = 0;
206 } else {
207 col += 1;
208 }
209 } else if "():;[]{},".contains(ch) {
210 if !current.is_empty() {
211 tokens.push(Token::new(
212 current.clone(),
213 current_start_line,
214 current_start_col,
215 ));
216 current.clear();
217 }
218 tokens.push(Token::new(ch.to_string(), line, col));
219 col += 1;
220 } else {
221 if current.is_empty() {
222 current_start_line = line;
223 current_start_col = col;
224 }
225 current.push(ch);
226 col += 1;
227 }
228 }
229
230 if in_string {
232 tokens.push(Token::new(
235 "<<<UNCLOSED_STRING>>>".to_string(),
236 current_start_line,
237 current_start_col,
238 ));
239 } else if !current.is_empty() {
240 tokens.push(Token::new(current, current_start_line, current_start_col));
241 }
242
243 tokens
244}