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