1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::time::{Duration, Instant};
5
6use anyhow::{Context, Result};
7use tempfile::{Builder, TempDir};
8
9use super::{
10 ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession, cache_store, hash_source,
11 run_version_command, try_cached_execution,
12};
13
14pub struct ZigEngine {
15 executable: Option<PathBuf>,
16}
17
18impl Default for ZigEngine {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl ZigEngine {
25 pub fn new() -> Self {
26 Self {
27 executable: resolve_zig_binary(),
28 }
29 }
30
31 fn ensure_executable(&self) -> Result<&Path> {
32 self.executable.as_deref().ok_or_else(|| {
33 anyhow::anyhow!(
34 "Zig support requires the `zig` executable. Install it from https://ziglang.org/download/ and ensure it is on your PATH."
35 )
36 })
37 }
38
39 fn write_temp_source(&self, code: &str) -> Result<(TempDir, PathBuf)> {
40 let dir = Builder::new()
41 .prefix("run-zig")
42 .tempdir()
43 .context("failed to create temporary directory for Zig source")?;
44 let path = dir.path().join("snippet.zig");
45 let mut contents = code.to_string();
46 if !contents.ends_with('\n') {
47 contents.push('\n');
48 }
49 std::fs::write(&path, contents).with_context(|| {
50 format!("failed to write temporary Zig source to {}", path.display())
51 })?;
52 Ok((dir, path))
53 }
54
55 fn run_source(&self, source: &Path, args: &[String]) -> Result<std::process::Output> {
56 let executable = self.ensure_executable()?;
57 let mut cmd = Command::new(executable);
58 cmd.arg("run")
59 .arg(source)
60 .stdout(Stdio::piped())
61 .stderr(Stdio::piped());
62 if !args.is_empty() {
63 cmd.arg("--").args(args);
64 }
65 cmd.stdin(Stdio::inherit());
66 if let Some(dir) = source.parent() {
67 cmd.current_dir(dir);
68 }
69 cmd.output().with_context(|| {
70 format!(
71 "failed to execute {} with source {}",
72 executable.display(),
73 source.display()
74 )
75 })
76 }
77}
78
79impl LanguageEngine for ZigEngine {
80 fn id(&self) -> &'static str {
81 "zig"
82 }
83
84 fn display_name(&self) -> &'static str {
85 "Zig"
86 }
87
88 fn aliases(&self) -> &[&'static str] {
89 &["ziglang"]
90 }
91
92 fn supports_sessions(&self) -> bool {
93 self.executable.is_some()
94 }
95
96 fn validate(&self) -> Result<()> {
97 let executable = self.ensure_executable()?;
98 let mut cmd = Command::new(executable);
99 cmd.arg("version")
100 .stdout(Stdio::null())
101 .stderr(Stdio::null());
102 cmd.status()
103 .with_context(|| format!("failed to invoke {}", executable.display()))?
104 .success()
105 .then_some(())
106 .ok_or_else(|| anyhow::anyhow!("{} is not executable", executable.display()))
107 }
108
109 fn toolchain_version(&self) -> Result<Option<String>> {
110 let executable = self.ensure_executable()?;
111 let mut cmd = Command::new(executable);
112 cmd.arg("version");
113 let context = format!("{}", executable.display());
114 run_version_command(cmd, &context)
115 }
116
117 fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
118 let args = payload.args();
119
120 if let Some(code) = match payload {
122 ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
123 Some(code.as_str())
124 }
125 _ => None,
126 } {
127 let snippet = wrap_inline_snippet(code);
128 let src_hash = hash_source(&snippet);
129 if let Some(output) = try_cached_execution(src_hash) {
130 let start = Instant::now();
131 let mut stdout = String::from_utf8_lossy(&output.stdout).into_owned();
132 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
133 if output.status.success() && !stderr.contains("error:") {
134 if stdout.is_empty() {
135 stdout = stderr.clone();
136 } else if !stderr.is_empty() {
137 stdout.push_str(&stderr);
138 }
139 }
140 return Ok(ExecutionOutcome {
141 language: self.id().to_string(),
142 exit_code: output.status.code(),
143 stdout,
144 stderr: if output.status.success() && !stderr.contains("error:") {
145 String::new()
146 } else {
147 stderr
148 },
149 duration: start.elapsed(),
150 });
151 }
152 }
153
154 let start = Instant::now();
155 let (temp_dir, source_path, cache_key) = match payload {
156 ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
157 let snippet = wrap_inline_snippet(code);
158 let h = hash_source(&snippet);
159 let (dir, path) = self.write_temp_source(&snippet)?;
160 (Some(dir), path, Some(h))
161 }
162 ExecutionPayload::File { path, .. } => {
163 if path.extension().and_then(|e| e.to_str()) != Some("zig") {
164 let code = std::fs::read_to_string(path)?;
165 let (dir, new_path) = self.write_temp_source(&code)?;
166 (Some(dir), new_path, None)
167 } else {
168 (None, path.clone(), None)
169 }
170 }
171 };
172
173 if let Some(h) = cache_key {
175 let executable = self.ensure_executable()?;
176 let dir = source_path.parent().unwrap_or(std::path::Path::new("."));
177 let bin_path = dir.join("snippet");
178 let mut build_cmd = Command::new(executable);
179 build_cmd
180 .arg("build-exe")
181 .arg(&source_path)
182 .arg("-femit-bin=snippet")
183 .stdout(Stdio::piped())
184 .stderr(Stdio::piped())
185 .current_dir(dir);
186
187 if let Ok(build_output) = build_cmd.output()
188 && build_output.status.success()
189 && bin_path.exists()
190 {
191 cache_store(h, &bin_path);
192 let mut run_cmd = Command::new(&bin_path);
193 run_cmd
194 .args(args)
195 .stdout(Stdio::piped())
196 .stderr(Stdio::piped())
197 .stdin(Stdio::inherit());
198 if let Ok(output) = run_cmd.output() {
199 drop(temp_dir);
200 let mut stdout = String::from_utf8_lossy(&output.stdout).into_owned();
201 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
202 if output.status.success() && !stderr.contains("error:") {
203 if stdout.is_empty() {
204 stdout = stderr.clone();
205 } else if !stderr.is_empty() {
206 stdout.push_str(&stderr);
207 }
208 }
209 return Ok(ExecutionOutcome {
210 language: self.id().to_string(),
211 exit_code: output.status.code(),
212 stdout,
213 stderr: if output.status.success() && !stderr.contains("error:") {
214 String::new()
215 } else {
216 stderr
217 },
218 duration: start.elapsed(),
219 });
220 }
221 }
222 }
223
224 let output = self.run_source(&source_path, args)?;
226 drop(temp_dir);
227
228 let mut combined_stdout = String::from_utf8_lossy(&output.stdout).into_owned();
229 let stderr_str = String::from_utf8_lossy(&output.stderr).into_owned();
230
231 if output.status.success() && !stderr_str.contains("error:") {
232 if !combined_stdout.is_empty() && !stderr_str.is_empty() {
233 combined_stdout.push_str(&stderr_str);
234 } else if combined_stdout.is_empty() {
235 combined_stdout = stderr_str.clone();
236 }
237 }
238
239 Ok(ExecutionOutcome {
240 language: self.id().to_string(),
241 exit_code: output.status.code(),
242 stdout: combined_stdout,
243 stderr: if output.status.success() && !stderr_str.contains("error:") {
244 String::new()
245 } else {
246 stderr_str
247 },
248 duration: start.elapsed(),
249 })
250 }
251
252 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
253 let executable = self.ensure_executable()?.to_path_buf();
254 Ok(Box::new(ZigSession::new(executable)?))
255 }
256}
257
258fn resolve_zig_binary() -> Option<PathBuf> {
259 which::which("zig").ok()
260}
261
262const ZIG_NUMERIC_SUFFIXES: [&str; 17] = [
263 "usize", "isize", "u128", "i128", "f128", "f80", "u64", "i64", "f64", "u32", "i32", "f32",
264 "u16", "i16", "f16", "u8", "i8",
265];
266
267fn wrap_inline_snippet(code: &str) -> String {
268 let trimmed = code.trim();
269 if trimmed.is_empty() || trimmed.contains("pub fn main") {
270 let mut owned = code.to_string();
271 if !owned.ends_with('\n') {
272 owned.push('\n');
273 }
274 return owned;
275 }
276
277 let mut body = String::new();
278 for line in code.lines() {
279 body.push_str(" ");
280 body.push_str(line);
281 if !line.ends_with('\n') {
282 body.push('\n');
283 }
284 }
285 if body.is_empty() {
286 body.push_str(" const stdout = std.io.getStdOut().writer(); _ = stdout.print(\"\\n\", .{}) catch {};\n");
287 }
288
289 format!("const std = @import(\"std\");\n\npub fn main() !void {{\n{body}}}\n")
290}
291
292struct ZigSession {
293 executable: PathBuf,
294 workspace: TempDir,
295 items: Vec<String>,
296 statements: Vec<String>,
297 last_stdout: String,
298 last_stderr: String,
299}
300
301enum ZigSnippetKind {
302 Declaration,
303 Statement,
304 Expression,
305}
306
307impl ZigSession {
308 fn new(executable: PathBuf) -> Result<Self> {
309 let workspace = TempDir::new().context("failed to create Zig session workspace")?;
310 let session = Self {
311 executable,
312 workspace,
313 items: Vec::new(),
314 statements: Vec::new(),
315 last_stdout: String::new(),
316 last_stderr: String::new(),
317 };
318 session.persist_source()?;
319 Ok(session)
320 }
321
322 fn source_path(&self) -> PathBuf {
323 self.workspace.path().join("session.zig")
324 }
325
326 fn persist_source(&self) -> Result<()> {
327 let source = self.render_source();
328 fs::write(self.source_path(), source)
329 .with_context(|| "failed to write Zig session source".to_string())
330 }
331
332 fn render_source(&self) -> String {
333 let mut source = String::from("const std = @import(\"std\");\n\n");
334
335 for item in &self.items {
336 source.push_str(item);
337 if !item.ends_with('\n') {
338 source.push('\n');
339 }
340 source.push('\n');
341 }
342
343 source.push_str("pub fn main() !void {\n");
344 if self.statements.is_empty() {
345 source.push_str(" return;\n");
346 } else {
347 for snippet in &self.statements {
348 for line in snippet.lines() {
349 source.push_str(" ");
350 source.push_str(line);
351 source.push('\n');
352 }
353 }
354 }
355 source.push_str("}\n");
356
357 source
358 }
359
360 fn run_program(&self) -> Result<std::process::Output> {
361 let mut cmd = Command::new(&self.executable);
362 cmd.arg("run")
363 .arg("session.zig")
364 .stdout(Stdio::piped())
365 .stderr(Stdio::piped())
366 .current_dir(self.workspace.path());
367 cmd.output().with_context(|| {
368 format!(
369 "failed to execute {} for Zig session",
370 self.executable.display()
371 )
372 })
373 }
374
375 fn run_standalone_program(&self, code: &str) -> Result<ExecutionOutcome> {
376 let start = Instant::now();
377 let path = self.workspace.path().join("standalone.zig");
378 let mut contents = code.to_string();
379 if !contents.ends_with('\n') {
380 contents.push('\n');
381 }
382 fs::write(&path, contents)
383 .with_context(|| "failed to write Zig standalone source".to_string())?;
384
385 let mut cmd = Command::new(&self.executable);
386 cmd.arg("run")
387 .arg("standalone.zig")
388 .stdout(Stdio::piped())
389 .stderr(Stdio::piped())
390 .current_dir(self.workspace.path());
391 let output = cmd.output().with_context(|| {
392 format!(
393 "failed to execute {} for Zig standalone snippet",
394 self.executable.display()
395 )
396 })?;
397
398 let mut stdout = Self::normalize_output(&output.stdout);
399 let stderr = Self::normalize_output(&output.stderr);
400
401 if output.status.success() && !stderr.contains("error:") {
402 if stdout.is_empty() {
403 stdout = stderr.clone();
404 } else {
405 stdout.push_str(&stderr);
406 }
407 }
408
409 Ok(ExecutionOutcome {
410 language: self.language_id().to_string(),
411 exit_code: output.status.code(),
412 stdout,
413 stderr: if output.status.success() && !stderr.contains("error:") {
414 String::new()
415 } else {
416 stderr
417 },
418 duration: start.elapsed(),
419 })
420 }
421
422 fn run_current(&mut self, start: Instant) -> Result<(ExecutionOutcome, bool)> {
423 self.persist_source()?;
424 let output = self.run_program()?;
425 let mut stdout_full = Self::normalize_output(&output.stdout);
426 let stderr_full = Self::normalize_output(&output.stderr);
427
428 let success = output.status.success();
429
430 if success && !stderr_full.is_empty() && !stderr_full.contains("error:") {
431 if stdout_full.is_empty() {
432 stdout_full = stderr_full.clone();
433 } else {
434 stdout_full.push_str(&stderr_full);
435 }
436 }
437
438 let (stdout, stderr) = if success {
439 let stdout_delta = Self::diff_outputs(&self.last_stdout, &stdout_full);
440 let stderr_clean = if !stderr_full.contains("error:") {
441 String::new()
442 } else {
443 stderr_full.clone()
444 };
445 let stderr_delta = Self::diff_outputs(&self.last_stderr, &stderr_clean);
446 self.last_stdout = stdout_full;
447 self.last_stderr = stderr_clean;
448 (stdout_delta, stderr_delta)
449 } else {
450 (stdout_full, stderr_full)
451 };
452
453 let outcome = ExecutionOutcome {
454 language: self.language_id().to_string(),
455 exit_code: output.status.code(),
456 stdout,
457 stderr,
458 duration: start.elapsed(),
459 };
460
461 Ok((outcome, success))
462 }
463
464 fn apply_declaration(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
465 let normalized = normalize_snippet(code);
466 let mut snippet = normalized;
467 if !snippet.ends_with('\n') {
468 snippet.push('\n');
469 }
470 self.items.push(snippet);
471 let start = Instant::now();
472 let (outcome, success) = self.run_current(start)?;
473 if !success {
474 let _ = self.items.pop();
475 self.persist_source()?;
476 }
477 Ok((outcome, success))
478 }
479
480 fn apply_statement(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
481 let normalized = normalize_snippet(code);
482 let snippet = ensure_trailing_newline(&normalized);
483 self.statements.push(snippet);
484 let start = Instant::now();
485 let (outcome, success) = self.run_current(start)?;
486 if !success {
487 let _ = self.statements.pop();
488 self.persist_source()?;
489 }
490 Ok((outcome, success))
491 }
492
493 fn apply_expression(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
494 let normalized = normalize_snippet(code);
495 let wrapped = wrap_expression(&normalized);
496 self.statements.push(wrapped);
497 let start = Instant::now();
498 let (outcome, success) = self.run_current(start)?;
499 if !success {
500 let _ = self.statements.pop();
501 self.persist_source()?;
502 }
503 Ok((outcome, success))
504 }
505
506 fn reset(&mut self) -> Result<()> {
507 self.items.clear();
508 self.statements.clear();
509 self.last_stdout.clear();
510 self.last_stderr.clear();
511 self.persist_source()
512 }
513
514 fn normalize_output(bytes: &[u8]) -> String {
515 String::from_utf8_lossy(bytes)
516 .replace("\r\n", "\n")
517 .replace('\r', "")
518 }
519
520 fn diff_outputs(previous: &str, current: &str) -> String {
521 current
522 .strip_prefix(previous)
523 .map(|s| s.to_string())
524 .unwrap_or_else(|| current.to_string())
525 }
526}
527
528impl LanguageSession for ZigSession {
529 fn language_id(&self) -> &str {
530 "zig"
531 }
532
533 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
534 let trimmed = code.trim();
535 if trimmed.is_empty() {
536 return Ok(ExecutionOutcome {
537 language: self.language_id().to_string(),
538 exit_code: None,
539 stdout: String::new(),
540 stderr: String::new(),
541 duration: Duration::default(),
542 });
543 }
544
545 if trimmed.eq_ignore_ascii_case(":reset") {
546 self.reset()?;
547 return Ok(ExecutionOutcome {
548 language: self.language_id().to_string(),
549 exit_code: None,
550 stdout: String::new(),
551 stderr: String::new(),
552 duration: Duration::default(),
553 });
554 }
555
556 if trimmed.eq_ignore_ascii_case(":help") {
557 return Ok(ExecutionOutcome {
558 language: self.language_id().to_string(),
559 exit_code: None,
560 stdout:
561 "Zig commands:\n :reset - clear session state\n :help - show this message\n"
562 .to_string(),
563 stderr: String::new(),
564 duration: Duration::default(),
565 });
566 }
567
568 if trimmed.contains("pub fn main") {
569 return self.run_standalone_program(code);
570 }
571
572 match classify_snippet(trimmed) {
573 ZigSnippetKind::Declaration => {
574 let (outcome, _) = self.apply_declaration(code)?;
575 Ok(outcome)
576 }
577 ZigSnippetKind::Statement => {
578 let (outcome, _) = self.apply_statement(code)?;
579 Ok(outcome)
580 }
581 ZigSnippetKind::Expression => {
582 let (outcome, _) = self.apply_expression(trimmed)?;
583 Ok(outcome)
584 }
585 }
586 }
587
588 fn shutdown(&mut self) -> Result<()> {
589 Ok(())
590 }
591}
592
593fn classify_snippet(code: &str) -> ZigSnippetKind {
594 if looks_like_declaration(code) {
595 ZigSnippetKind::Declaration
596 } else if looks_like_statement(code) {
597 ZigSnippetKind::Statement
598 } else {
599 ZigSnippetKind::Expression
600 }
601}
602
603fn looks_like_declaration(code: &str) -> bool {
604 let trimmed = code.trim_start();
605 matches!(
606 trimmed,
607 t if t.starts_with("const ")
608 || t.starts_with("var ")
609 || t.starts_with("pub ")
610 || t.starts_with("fn ")
611 || t.starts_with("usingnamespace ")
612 || t.starts_with("extern ")
613 || t.starts_with("comptime ")
614 || t.starts_with("test ")
615 )
616}
617
618fn looks_like_statement(code: &str) -> bool {
619 let trimmed = code.trim_end();
620 trimmed.contains('\n')
621 || trimmed.ends_with(';')
622 || trimmed.ends_with('}')
623 || trimmed.ends_with(':')
624 || trimmed.starts_with("//")
625 || trimmed.starts_with("/*")
626}
627
628fn ensure_trailing_newline(code: &str) -> String {
629 let mut snippet = code.to_string();
630 if !snippet.ends_with('\n') {
631 snippet.push('\n');
632 }
633 snippet
634}
635
636fn wrap_expression(code: &str) -> String {
637 format!("std.debug.print(\"{{any}}\\n\", .{{ {} }});", code)
638}
639
640fn normalize_snippet(code: &str) -> String {
641 rewrite_numeric_suffixes(code)
642}
643
644fn rewrite_numeric_suffixes(code: &str) -> String {
645 let bytes = code.as_bytes();
646 let mut result = String::with_capacity(code.len());
647 let mut i = 0;
648 while i < bytes.len() {
649 let ch = bytes[i] as char;
650
651 if ch == '"' {
652 let (segment, advance) = extract_string_literal(&code[i..]);
653 result.push_str(segment);
654 i += advance;
655 continue;
656 }
657
658 if ch == '\'' {
659 let (segment, advance) = extract_char_literal(&code[i..]);
660 result.push_str(segment);
661 i += advance;
662 continue;
663 }
664
665 if ch == '/' && i + 1 < bytes.len() {
666 let next = bytes[i + 1] as char;
667 if next == '/' {
668 result.push_str(&code[i..]);
669 break;
670 }
671 if next == '*' {
672 let (segment, advance) = extract_block_comment(&code[i..]);
673 result.push_str(segment);
674 i += advance;
675 continue;
676 }
677 }
678
679 if ch.is_ascii_digit() {
680 if i > 0 {
681 let prev = bytes[i - 1] as char;
682 if prev.is_ascii_alphanumeric() || prev == '_' {
683 result.push(ch);
684 i += 1;
685 continue;
686 }
687 }
688
689 let literal_end = scan_numeric_literal(bytes, i);
690 if literal_end > i {
691 if let Some((suffix, suffix_len)) = match_suffix(&code[literal_end..])
692 && !is_identifier_char(bytes, literal_end + suffix_len)
693 {
694 let literal = &code[i..literal_end];
695 result.push_str("@as(");
696 result.push_str(suffix);
697 result.push_str(", ");
698 result.push_str(literal);
699 result.push(')');
700 i = literal_end + suffix_len;
701 continue;
702 }
703
704 result.push_str(&code[i..literal_end]);
705 i = literal_end;
706 continue;
707 }
708 }
709
710 result.push(ch);
711 i += 1;
712 }
713
714 if result.len() == code.len() {
715 code.to_string()
716 } else {
717 result
718 }
719}
720
721fn extract_string_literal(source: &str) -> (&str, usize) {
722 let bytes = source.as_bytes();
723 let mut i = 1; while i < bytes.len() {
725 match bytes[i] {
726 b'\\' => {
727 i += 2;
728 }
729 b'"' => {
730 i += 1;
731 break;
732 }
733 _ => i += 1,
734 }
735 }
736 (&source[..i], i)
737}
738
739fn extract_char_literal(source: &str) -> (&str, usize) {
740 let bytes = source.as_bytes();
741 let mut i = 1; while i < bytes.len() {
743 match bytes[i] {
744 b'\\' => {
745 i += 2;
746 }
747 b'\'' => {
748 i += 1;
749 break;
750 }
751 _ => i += 1,
752 }
753 }
754 (&source[..i], i)
755}
756
757fn extract_block_comment(source: &str) -> (&str, usize) {
758 if let Some(idx) = source[2..].find("*/") {
759 let end = 2 + idx + 2;
760 (&source[..end], end)
761 } else {
762 (source, source.len())
763 }
764}
765
766fn scan_numeric_literal(bytes: &[u8], start: usize) -> usize {
767 let len = bytes.len();
768 if start >= len {
769 return start;
770 }
771
772 let mut i = start;
773
774 if bytes[i] == b'0' && i + 1 < len {
775 match bytes[i + 1] {
776 b'x' | b'X' => {
777 i += 2;
778 while i < len {
779 match bytes[i] {
780 b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' | b'_' => i += 1,
781 _ => break,
782 }
783 }
784 return i;
785 }
786 b'o' | b'O' => {
787 i += 2;
788 while i < len {
789 match bytes[i] {
790 b'0'..=b'7' | b'_' => i += 1,
791 _ => break,
792 }
793 }
794 return i;
795 }
796 b'b' | b'B' => {
797 i += 2;
798 while i < len {
799 match bytes[i] {
800 b'0' | b'1' | b'_' => i += 1,
801 _ => break,
802 }
803 }
804 return i;
805 }
806 _ => {}
807 }
808 }
809
810 i = start;
811 let mut seen_dot = false;
812 while i < len {
813 match bytes[i] {
814 b'0'..=b'9' | b'_' => i += 1,
815 b'.' if !seen_dot => {
816 if i + 1 < len && bytes[i + 1].is_ascii_digit() {
817 seen_dot = true;
818 i += 1;
819 } else {
820 break;
821 }
822 }
823 b'e' | b'E' | b'p' | b'P' => {
824 let mut j = i + 1;
825 if j < len && (bytes[j] == b'+' || bytes[j] == b'-') {
826 j += 1;
827 }
828 let mut exp_digits = 0;
829 while j < len {
830 match bytes[j] {
831 b'0'..=b'9' | b'_' => {
832 exp_digits += 1;
833 j += 1;
834 }
835 _ => break,
836 }
837 }
838 if exp_digits == 0 {
839 break;
840 }
841 i = j;
842 }
843 _ => break,
844 }
845 }
846
847 i
848}
849
850fn match_suffix(rest: &str) -> Option<(&'static str, usize)> {
851 for &suffix in &ZIG_NUMERIC_SUFFIXES {
852 if rest.starts_with(suffix) {
853 return Some((suffix, suffix.len()));
854 }
855 }
856 None
857}
858
859fn is_identifier_char(bytes: &[u8], index: usize) -> bool {
860 if index >= bytes.len() {
861 return false;
862 }
863 let ch = bytes[index] as char;
864 ch.is_ascii_alphanumeric() || ch == '_'
865}