#! /usr/bin/env perl # Bouncy Counters implementation. By ais523. use warnings; use strict; use Math::BigInt; use feature 'say'; # Read in the program. my @counter; my %program; my @seen_counter; my %seen_on_left; my %seen_on_right; my $errors = 0; while(<>) { s/^\s+//; s/\s+$//; /^#/ and next; # comment /^$/ and next; # blank lines are allowed # A counter definition. if (/^(\d+)\s*=\s*(\d+)$/) { if (defined(my $l = $seen_counter[$1])) { say STDERR "Duplicate counter on lines $l, $.: $1"; ++$errors; next; } $seen_counter[$1] = $.; $counter[$1] = Math::BigInt->new($2); next; } # Ensure it's a side definition. unless (/^(\w+?\d[+-])\s+(\w+?\d[+-])$/) { say STDERR "Unrecognised syntax on line $.: '$_'"; ++$errors; next; } if (defined(my $l = $seen_on_left{$1})) { say STDERR "Duplicate LHS on lines $l, $.: $1"; ++$errors; } if (defined(my $l = $seen_on_right{$2})) { say STDERR "Duplicate RHS on lines $l, $.: $2"; ++$errors; } $program{$1} = $2; $seen_on_left{$1} = $.; $seen_on_right{$2} = $.; } # Ensure that the LHS and RHS match and all counters are defined. for my $lhs (sort keys %seen_on_left) { unless ($seen_on_right{$lhs}) { my $l = $seen_on_left{$lhs}; say STDERR "LHS never appears as an RHS on line $l: $lhs"; ++$errors; } } for my $rhs (sort keys %seen_on_right) { unless ($seen_on_left{$rhs}) { my $l = $seen_on_right{$rhs}; say STDERR "RHS never appears as an LHS on line $l: $rhs"; ++$errors; } } for my $side (keys %program) { next unless $seen_on_left{$side} && $seen_on_right{$side}; $side =~ /^\w*?(\d+)[+-]$/ or die "misparsed side definition"; unless (exists $counter[$1]) { say STDERR "Undeclared counter (on LHS on line ", $seen_on_left{$side}, ", RHS on line ", $seen_on_right{$side}, "): $side"; } } $errors and exit 65; # 65 = data format error # Find start sides. my %start = (); for my $side (keys %program) { my $dir = chop $side; # A start side has a + version but no - version. if ($dir eq '+' && !exists $program{"$side-"}) { $start{$side} = 1; } # A stop side has a - version but no + version. # However, there's no need to calculate them in advvance; the # fact that a side is a stop side is discovered during execution. } # Find used counter names. my @counternames = (); for my $countername (0..$#counter) { exists $counter[$countername] and push @counternames, $countername; } for (;;) { # Display the counter values. say STDERR "Counters:"; for my $countername (@counternames) { say STDERR " $countername: ", $counter[$countername]; } # Find the start sides the program can start/restart at. # A program can start at a start side if the corresponding counter # has a value of 0. If there are no possibilities, the program halts. # If there are multiple possibilities, ask the user. my @start_sides = (); my %start_sides = (); for my $dirless_side (sort keys %start) { $dirless_side =~ /^\w*?(\d+)$/ or die "misparsed side"; if ($counter[$1]->is_zero()) { push @start_sides, $dirless_side; $start_sides{$dirless_side} = 1; } } 0 == scalar @start_sides and last; # program has halted my $start_side = $start_sides[0]; if (@start_sides > 1) { for (;;) { print STDERR "Start where? [@start_sides]: "; unless (defined($start_side = <>)) { say STDERR "\nEOF on standard input, exiting."; exit 0; } chomp $start_side; $start_side eq '' and exit 0; $start_sides{$start_side} and last; say STDERR "Invalid starting location. (Enter blank line to exit.)"; } } my $dirless_side = $start_side; my $side = $start_side . '+'; # Main loop. while (exists $program{$side}) { my $next = $program{$side}; $next =~ /^(\w*?(\d+))([+-])$/ or die "misparsed side definition"; $dirless_side = $1; my $countername = $2; my $dir = $3; if ($dir eq '-' && $counter[$countername]->is_zero()) { # the counter decrements and immediately increments again # no need to change the counter, but the dir changes $dir = '+'; } elsif ($dir eq '-') { $counter[$countername] -= 1; } else { $counter[$countername] += 1; } $side = $dirless_side . $dir; } say STDERR "Stopped at $dirless_side."; }