nandi/rustnimpublic Fork
⑂ 71ee2a9
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Run the cases in parallel, and report every failure in a body

Two things were serial that had no reason to be.

The differential suite ran its 73 cases one after another, so the wall
clock was spent waiting on rustc and nim while seven of eight cores sat
idle: 130s, against 4m34 of user time. The cases are independent -- each
has its own scratch directory and reads nothing another writes -- so they
now run on `available_parallelism()` threads, results collected in case
order so the output does not depend on scheduling. 130s -> 28s.

Each case also got a *fresh* `--nimcache`, which meant recompiling the
same standard-library modules 73 times; measured at 1.00s cold against
0.53s warm for a trivial module. The cache is now per worker rather than
per case: warm, and with no writers racing the way one shared cache would
have. `RUSTNIM_JOBS` overrides the thread count.

And `--report` still stopped at the first failing statement inside an
item, so a body with three unsupported calls named one of them per run --
the queue that flag exists to avoid, just one level down. It now records
the failure and carries on to the next statement. Four of the six passes
were likewise fail-fast even under `--report`; they and the two that were
not now share one `note()` that decides accumulate-or-stop in a single
place. Outside `--report` the first failure still stops the run: a partial
translation must never reach a file.

`cargo test --release`: 36.5s, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T10:22:28-07:00 Browse files
71ee2a9 parent: 67f537f
modified src/lower.rs +64 -28
@@ -614,7 +614,8 @@ impl Lowerer {
614614 for (m, f) in files {
615615 self.cur_mod = m.clone();
616616 for item in &f.items {
617- self.collect_aliases(item).map_err(|e| self.where_(&e))?;
617+ let r = self.collect_aliases(item);
618+ self.note(r)?;
618619 }
619620 }
620621
@@ -637,7 +638,8 @@ impl Lowerer {
637638 for (m, f) in files {
638639 self.cur_mod = m.clone();
639640 for item in &f.items {
640- self.collect(item).map_err(|e| self.where_(&e))?;
641+ let r = self.collect(item);
642+ self.note(r)?;
641643 }
642644 }
643645
@@ -662,7 +664,8 @@ impl Lowerer {
662664 for (m, f) in files {
663665 self.cur_mod = m.clone();
664666 for item in &f.items {
665- self.emit_assoc_consts(item).map_err(|e| self.where_(&e))?;
667+ let r = self.emit_assoc_consts(item);
668+ self.note(r)?;
666669 }
667670 }
668671
@@ -670,14 +673,8 @@ impl Lowerer {
670673 for (m, f) in files {
671674 self.cur_mod = m.clone();
672675 for item in &f.items {
673- if let Err(e) = self.item_types(item) {
674- let e = self.where_(&e);
675- if self.report {
676- self.errors.push(e);
677- } else {
678- return Err(e);
679- }
680- }
676+ let r = self.item_types(item);
677+ self.note(r)?;
681678 }
682679 }
683680
@@ -710,14 +707,8 @@ impl Lowerer {
710707 for (m, f) in files {
711708 self.cur_mod = m.clone();
712709 for item in &f.items {
713- if let Err(e) = self.item(item) {
714- let e = self.where_(&e);
715- if self.report {
716- self.errors.push(e);
717- } else {
718- return Err(e);
719- }
720- }
710+ let r = self.item(item);
711+ self.note(r)?;
721712 }
722713 }
723714
@@ -757,7 +748,7 @@ impl Lowerer {
757748 }
758749 let mut hist: Vec<(String, usize)> = counts.into_iter().collect();
759750 hist.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
760- let mut out = format!("{} item(s) could not be translated.\n\nby cause:\n", self.errors.len());
751+ let mut out = format!("{} failure(s); the run translated nothing.\n\nby cause:\n", self.errors.len());
761752 for (w, n) in hist.iter().take(40) {
762753 out.push_str(&format!(" {n:>5} {w}\n"));
763754 }
@@ -3782,9 +3773,9 @@ impl Lowerer {
37823773 for (i, st) in b.stmts.iter().enumerate() {
37833774 let last = i + 1 == n;
37843775 let rest = &b.stmts[i + 1..];
3785- match st {
3776+ let r: Result<(), String> = match st {
37863777 Stmt::Expr(e, None) if last && expressible(e) => {
3787- tail = Some(self.expr_at(e, expect)?)
3778+ self.expr_at(e, expect).map(|v| tail = Some(v))
37883779 }
37893780 Stmt::Expr(e, None) if last => {
37903781 // A trailing `if`/`match` with statement arms, or a loop.
@@ -3793,18 +3784,41 @@ impl Lowerer {
37933784 match &target {
37943785 Some((t, ty)) => {
37953786 let (t, ty) = (t.clone(), ty.clone());
3796- self.assign_from(e, &t, ty.as_ref())?;
3787+ self.assign_from(e, &t, ty.as_ref())
37973788 }
3798- None => self.stmt(st)?,
3789+ None => self.stmt(st),
37993790 }
38003791 }
38013792 Stmt::Local(l) => {
3802- self.local(l, rest)?;
3803- for lk in std::mem::take(&mut self.pending_locks) {
3804- self.line(&format!("release({})", lk));
3793+ let r = self.local(l, rest);
3794+ if r.is_ok() {
3795+ for lk in std::mem::take(&mut self.pending_locks) {
3796+ self.line(&format!("release({})", lk));
3797+ }
38053798 }
3799+ r
3800+ }
3801+ _ => self.stmt(st),
3802+ };
3803+ if let Err(e) = r {
3804+ // Outside `--report` the first failing statement stops the
3805+ // run, as it always has.
3806+ if !self.report {
3807+ return Err(e);
38063808 }
3807- _ => self.stmt(st)?,
3809+ // Under `--report`, carry on to the next statement. A body
3810+ // with three unsupported calls in it named one of them per
3811+ // run, so finding out what a crate actually needs took three
3812+ // runs instead of one. Nothing is written in report mode, so
3813+ // the half-lowered body this leaves behind goes nowhere; what
3814+ // it buys is the other two names.
3815+ //
3816+ // Later statements may now fail for want of a binding the
3817+ // failed one would have introduced. That noise is the price,
3818+ // and the histogram this feeds already folds repeats together.
3819+ let e = self.where_(&e);
3820+ self.errors.push(e);
3821+ self.line("discard \"rustnim: statement not translated\"");
38083822 }
38093823 }
38103824 self.target = target;
@@ -3827,6 +3841,28 @@ impl Lowerer {
38273841 Ok(())
38283842 }
38293843
3844+ /// Fail-fast, or record and carry on.
3845+ ///
3846+ /// Outside `--report` the first failure stops the run: a partial
3847+ /// translation must never reach a file. Under `--report` the failure is
3848+ /// recorded and the pass continues, so one run names every item that
3849+ /// could not be translated instead of turning a list into a queue --
3850+ /// fix the first, run again, meet the second.
3851+ fn note(&mut self, r: Result<(), String>) -> Result<(), String> {
3852+ match r {
3853+ Ok(()) => Ok(()),
3854+ Err(e) => {
3855+ let e = self.where_(&e);
3856+ if self.report {
3857+ self.errors.push(e);
3858+ Ok(())
3859+ } else {
3860+ Err(e)
3861+ }
3862+ }
3863+ }
3864+ }
3865+
38303866 fn stmt(&mut self, s: &Stmt) -> Result<(), String> {
38313867 let r = self.stmt_inner(s);
38323868 // Any lock still queued was taken by a temporary guard, which Rust
@@ -614,7 +614,8 @@ impl Lowerer {
614 for (m, f) in files {614 for (m, f) in files {
615 self.cur_mod = m.clone();615 self.cur_mod = m.clone();
616 for item in &f.items {616 for item in &f.items {
617- self.collect_aliases(item).map_err(|e| self.where_(&e))?;617+ let r = self.collect_aliases(item);
618+ self.note(r)?;
618 }619 }
619 }620 }
620 621
@@ -637,7 +638,8 @@ impl Lowerer {
637 for (m, f) in files {638 for (m, f) in files {
638 self.cur_mod = m.clone();639 self.cur_mod = m.clone();
639 for item in &f.items {640 for item in &f.items {
640- self.collect(item).map_err(|e| self.where_(&e))?;641+ let r = self.collect(item);
642+ self.note(r)?;
641 }643 }
642 }644 }
643 645
@@ -662,7 +664,8 @@ impl Lowerer {
662 for (m, f) in files {664 for (m, f) in files {
663 self.cur_mod = m.clone();665 self.cur_mod = m.clone();
664 for item in &f.items {666 for item in &f.items {
665- self.emit_assoc_consts(item).map_err(|e| self.where_(&e))?;667+ let r = self.emit_assoc_consts(item);
668+ self.note(r)?;
666 }669 }
667 }670 }
668 671
@@ -670,14 +673,8 @@ impl Lowerer {
670 for (m, f) in files {673 for (m, f) in files {
671 self.cur_mod = m.clone();674 self.cur_mod = m.clone();
672 for item in &f.items {675 for item in &f.items {
673- if let Err(e) = self.item_types(item) {676+ let r = self.item_types(item);
674- let e = self.where_(&e);677+ self.note(r)?;
675- if self.report {
676- self.errors.push(e);
677- } else {
678- return Err(e);
679- }
680- }
681 }678 }
682 }679 }
683 680
@@ -710,14 +707,8 @@ impl Lowerer {
710 for (m, f) in files {707 for (m, f) in files {
711 self.cur_mod = m.clone();708 self.cur_mod = m.clone();
712 for item in &f.items {709 for item in &f.items {
713- if let Err(e) = self.item(item) {710+ let r = self.item(item);
714- let e = self.where_(&e);711+ self.note(r)?;
715- if self.report {
716- self.errors.push(e);
717- } else {
718- return Err(e);
719- }
720- }
721 }712 }
722 }713 }
723 714
@@ -757,7 +748,7 @@ impl Lowerer {
757 }748 }
758 let mut hist: Vec<(String, usize)> = counts.into_iter().collect();749 let mut hist: Vec<(String, usize)> = counts.into_iter().collect();
759 hist.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));750 hist.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
760- let mut out = format!("{} item(s) could not be translated.\n\nby cause:\n", self.errors.len());751+ let mut out = format!("{} failure(s); the run translated nothing.\n\nby cause:\n", self.errors.len());
761 for (w, n) in hist.iter().take(40) {752 for (w, n) in hist.iter().take(40) {
762 out.push_str(&format!(" {n:>5} {w}\n"));753 out.push_str(&format!(" {n:>5} {w}\n"));
763 }754 }
@@ -3782,9 +3773,9 @@ impl Lowerer {
3782 for (i, st) in b.stmts.iter().enumerate() {3773 for (i, st) in b.stmts.iter().enumerate() {
3783 let last = i + 1 == n;3774 let last = i + 1 == n;
3784 let rest = &b.stmts[i + 1..];3775 let rest = &b.stmts[i + 1..];
3785- match st {3776+ let r: Result<(), String> = match st {
3786 Stmt::Expr(e, None) if last && expressible(e) => {3777 Stmt::Expr(e, None) if last && expressible(e) => {
3787- tail = Some(self.expr_at(e, expect)?)3778+ self.expr_at(e, expect).map(|v| tail = Some(v))
3788 }3779 }
3789 Stmt::Expr(e, None) if last => {3780 Stmt::Expr(e, None) if last => {
3790 // A trailing `if`/`match` with statement arms, or a loop.3781 // A trailing `if`/`match` with statement arms, or a loop.
@@ -3793,18 +3784,41 @@ impl Lowerer {
3793 match &target {3784 match &target {
3794 Some((t, ty)) => {3785 Some((t, ty)) => {
3795 let (t, ty) = (t.clone(), ty.clone());3786 let (t, ty) = (t.clone(), ty.clone());
3796- self.assign_from(e, &t, ty.as_ref())?;3787+ self.assign_from(e, &t, ty.as_ref())
3797 }3788 }
3798- None => self.stmt(st)?,3789+ None => self.stmt(st),
3799 }3790 }
3800 }3791 }
3801 Stmt::Local(l) => {3792 Stmt::Local(l) => {
3802- self.local(l, rest)?;3793+ let r = self.local(l, rest);
3803- for lk in std::mem::take(&mut self.pending_locks) {3794+ if r.is_ok() {
3804- self.line(&format!("release({})", lk));3795+ for lk in std::mem::take(&mut self.pending_locks) {
3796+ self.line(&format!("release({})", lk));
3797+ }
3805 }3798 }
3799+ r
3800+ }
3801+ _ => self.stmt(st),
3802+ };
3803+ if let Err(e) = r {
3804+ // Outside `--report` the first failing statement stops the
3805+ // run, as it always has.
3806+ if !self.report {
3807+ return Err(e);
3806 }3808 }
3807- _ => self.stmt(st)?,3809+ // Under `--report`, carry on to the next statement. A body
3810+ // with three unsupported calls in it named one of them per
3811+ // run, so finding out what a crate actually needs took three
3812+ // runs instead of one. Nothing is written in report mode, so
3813+ // the half-lowered body this leaves behind goes nowhere; what
3814+ // it buys is the other two names.
3815+ //
3816+ // Later statements may now fail for want of a binding the
3817+ // failed one would have introduced. That noise is the price,
3818+ // and the histogram this feeds already folds repeats together.
3819+ let e = self.where_(&e);
3820+ self.errors.push(e);
3821+ self.line("discard \"rustnim: statement not translated\"");
3808 }3822 }
3809 }3823 }
3810 self.target = target;3824 self.target = target;
@@ -3827,6 +3841,28 @@ impl Lowerer {
3827 Ok(())3841 Ok(())
3828 }3842 }
3829 3843
3844+ /// Fail-fast, or record and carry on.
3845+ ///
3846+ /// Outside `--report` the first failure stops the run: a partial
3847+ /// translation must never reach a file. Under `--report` the failure is
3848+ /// recorded and the pass continues, so one run names every item that
3849+ /// could not be translated instead of turning a list into a queue --
3850+ /// fix the first, run again, meet the second.
3851+ fn note(&mut self, r: Result<(), String>) -> Result<(), String> {
3852+ match r {
3853+ Ok(()) => Ok(()),
3854+ Err(e) => {
3855+ let e = self.where_(&e);
3856+ if self.report {
3857+ self.errors.push(e);
3858+ Ok(())
3859+ } else {
3860+ Err(e)
3861+ }
3862+ }
3863+ }
3864+ }
3865+
3830 fn stmt(&mut self, s: &Stmt) -> Result<(), String> {3866 fn stmt(&mut self, s: &Stmt) -> Result<(), String> {
3831 let r = self.stmt_inner(s);3867 let r = self.stmt_inner(s);
3832 // Any lock still queued was taken by a temporary guard, which Rust3868 // Any lock still queued was taken by a temporary guard, which Rust
modified tests/differential.rs +57 -17
@@ -249,7 +249,7 @@ fn tail(s: &str, lines: usize) -> String {
249249
250250 // --------------------------------------------------------------- one case
251251
252-fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
252+fn run_case(case: &Path, work: &Path, nim: &Path, cache: &Path) -> Outcome {
253253 let name = case.file_stem().unwrap().to_string_lossy().into_owned();
254254 // For a directory case, `main.rs` is the crate root and carries the
255255 // directives; every `.rs` beside it is passed to rustnim as well.
@@ -390,7 +390,7 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
390390 .arg("--hints:off")
391391 .arg("--warnings:off")
392392 .arg("--colors:off")
393- .arg(format!("--nimcache:{}", dir.join("nimcache").display()))
393+ .arg(format!("--nimcache:{}", cache.display()))
394394 .arg(format!("-o:{}", nim_bin.display()))
395395 .arg(&nim_src)
396396 .env("TMPDIR", &dir),
@@ -480,22 +480,62 @@ fn differential() {
480480 assert!(!cases.is_empty(), "no cases in {}", cases_dir.display());
481481
482482 let filter = std::env::var("RUSTNIM_CASE").ok();
483- let mut results: BTreeMap<String, Outcome> = BTreeMap::new();
484- for case in &cases {
485- let name = case.file_stem().unwrap().to_string_lossy().into_owned();
486- if let Some(f) = &filter {
487- if !name.contains(f.as_str()) {
488- continue;
489- }
490- }
491- let outcome = run_case(case, &work, &nim);
492- match &outcome {
493- Outcome::Pass => eprintln!("ok {name}"),
494- Outcome::Skip(why) => eprintln!("skip {name} ({why})"),
495- Outcome::Fail { .. } => eprintln!("FAIL {name}"),
483+ let cases: Vec<PathBuf> = cases
484+ .into_iter()
485+ .filter(|c| match &filter {
486+ None => true,
487+ Some(f) => c
488+ .file_stem()
489+ .unwrap()
490+ .to_string_lossy()
491+ .contains(f.as_str()),
492+ })
493+ .collect();
494+
495+ // The cases are independent -- each one gets its own scratch directory and
496+ // reads nothing the others write -- so they are run on several threads.
497+ // Serially this suite spends its wall clock waiting on rustc and nim while
498+ // seven of eight cores sit idle.
499+ //
500+ // Each worker keeps its *own* Nim cache, rather than each case getting a
501+ // fresh one. A cold cache costs about half a second per case, all of it
502+ // recompiling the same standard-library modules; one cache per worker is
503+ // both warm and free of the write races a single shared cache would have.
504+ let jobs: usize = std::env::var("RUSTNIM_JOBS")
505+ .ok()
506+ .and_then(|v| v.parse().ok())
507+ .filter(|n| *n > 0)
508+ .unwrap_or_else(|| {
509+ std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
510+ })
511+ .min(cases.len().max(1));
512+
513+ let next = std::sync::atomic::AtomicUsize::new(0);
514+ let results: std::sync::Mutex<BTreeMap<String, Outcome>> =
515+ std::sync::Mutex::new(BTreeMap::new());
516+ std::thread::scope(|scope| {
517+ for w in 0..jobs {
518+ let (next, results, cases, work, nim) = (&next, &results, &cases, &work, &nim);
519+ scope.spawn(move || {
520+ let cache = work.join(format!("nimcache-{w}"));
521+ loop {
522+ let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
523+ let Some(case) = cases.get(i) else { break };
524+ let name = case.file_stem().unwrap().to_string_lossy().into_owned();
525+ let outcome = run_case(case, work, nim, &cache);
526+ match &outcome {
527+ Outcome::Pass => eprintln!("ok {name}"),
528+ Outcome::Skip(why) => eprintln!("skip {name} ({why})"),
529+ Outcome::Fail { .. } => eprintln!("FAIL {name}"),
530+ }
531+ results.lock().unwrap().insert(name, outcome);
532+ }
533+ });
496534 }
497- results.insert(name, outcome);
498- }
535+ });
536+ // Reported in case order regardless of which thread finished when: a test
537+ // whose output depends on scheduling is a test you cannot diff.
538+ let results = results.into_inner().unwrap();
499539
500540 let mut failed = Vec::new();
501541 let (mut pass, mut skip) = (0, 0);
@@ -249,7 +249,7 @@ fn tail(s: &str, lines: usize) -> String {
249 249
250 // --------------------------------------------------------------- one case250 // --------------------------------------------------------------- one case
251 251
252-fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {252+fn run_case(case: &Path, work: &Path, nim: &Path, cache: &Path) -> Outcome {
253 let name = case.file_stem().unwrap().to_string_lossy().into_owned();253 let name = case.file_stem().unwrap().to_string_lossy().into_owned();
254 // For a directory case, `main.rs` is the crate root and carries the254 // For a directory case, `main.rs` is the crate root and carries the
255 // directives; every `.rs` beside it is passed to rustnim as well.255 // directives; every `.rs` beside it is passed to rustnim as well.
@@ -390,7 +390,7 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
390 .arg("--hints:off")390 .arg("--hints:off")
391 .arg("--warnings:off")391 .arg("--warnings:off")
392 .arg("--colors:off")392 .arg("--colors:off")
393- .arg(format!("--nimcache:{}", dir.join("nimcache").display()))393+ .arg(format!("--nimcache:{}", cache.display()))
394 .arg(format!("-o:{}", nim_bin.display()))394 .arg(format!("-o:{}", nim_bin.display()))
395 .arg(&nim_src)395 .arg(&nim_src)
396 .env("TMPDIR", &dir),396 .env("TMPDIR", &dir),
@@ -480,22 +480,62 @@ fn differential() {
480 assert!(!cases.is_empty(), "no cases in {}", cases_dir.display());480 assert!(!cases.is_empty(), "no cases in {}", cases_dir.display());
481 481
482 let filter = std::env::var("RUSTNIM_CASE").ok();482 let filter = std::env::var("RUSTNIM_CASE").ok();
483- let mut results: BTreeMap<String, Outcome> = BTreeMap::new();483+ let cases: Vec<PathBuf> = cases
484- for case in &cases {484+ .into_iter()
485- let name = case.file_stem().unwrap().to_string_lossy().into_owned();485+ .filter(|c| match &filter {
486- if let Some(f) = &filter {486+ None => true,
487- if !name.contains(f.as_str()) {487+ Some(f) => c
488- continue;488+ .file_stem()
489- }489+ .unwrap()
490- }490+ .to_string_lossy()
491- let outcome = run_case(case, &work, &nim);491+ .contains(f.as_str()),
492- match &outcome {492+ })
493- Outcome::Pass => eprintln!("ok {name}"),493+ .collect();
494- Outcome::Skip(why) => eprintln!("skip {name} ({why})"),494+
495- Outcome::Fail { .. } => eprintln!("FAIL {name}"),495+ // The cases are independent -- each one gets its own scratch directory and
496+ // reads nothing the others write -- so they are run on several threads.
497+ // Serially this suite spends its wall clock waiting on rustc and nim while
498+ // seven of eight cores sit idle.
499+ //
500+ // Each worker keeps its *own* Nim cache, rather than each case getting a
501+ // fresh one. A cold cache costs about half a second per case, all of it
502+ // recompiling the same standard-library modules; one cache per worker is
503+ // both warm and free of the write races a single shared cache would have.
504+ let jobs: usize = std::env::var("RUSTNIM_JOBS")
505+ .ok()
506+ .and_then(|v| v.parse().ok())
507+ .filter(|n| *n > 0)
508+ .unwrap_or_else(|| {
509+ std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
510+ })
511+ .min(cases.len().max(1));
512+
513+ let next = std::sync::atomic::AtomicUsize::new(0);
514+ let results: std::sync::Mutex<BTreeMap<String, Outcome>> =
515+ std::sync::Mutex::new(BTreeMap::new());
516+ std::thread::scope(|scope| {
517+ for w in 0..jobs {
518+ let (next, results, cases, work, nim) = (&next, &results, &cases, &work, &nim);
519+ scope.spawn(move || {
520+ let cache = work.join(format!("nimcache-{w}"));
521+ loop {
522+ let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
523+ let Some(case) = cases.get(i) else { break };
524+ let name = case.file_stem().unwrap().to_string_lossy().into_owned();
525+ let outcome = run_case(case, work, nim, &cache);
526+ match &outcome {
527+ Outcome::Pass => eprintln!("ok {name}"),
528+ Outcome::Skip(why) => eprintln!("skip {name} ({why})"),
529+ Outcome::Fail { .. } => eprintln!("FAIL {name}"),
530+ }
531+ results.lock().unwrap().insert(name, outcome);
532+ }
533+ });
496 }534 }
497- results.insert(name, outcome);535+ });
498- }536+ // Reported in case order regardless of which thread finished when: a test
537+ // whose output depends on scheduling is a test you cannot diff.
538+ let results = results.into_inner().unwrap();
499 539
500 let mut failed = Vec::new();540 let mut failed = Vec::new();
501 let (mut pass, mut skip) = (0, 0);541 let (mut pass, mut skip) = (0, 0);