#!/usr/bin/env python3 import enum import sys from collections import namedtuple class actions(enum.Enum): change, left, right, nothing = range(4) State = namedtuple('State', ['action', 'transitions', 'reachable']) class SFParseError(Exception): pass def parse_smallfuck(s): def parse(s, index = 0, in_loop = False): parsed = [] while True: assert index <= len(s) if index == len(s): if in_loop: raise SFParseError('Unexpected end of text (expected "]")') else: break c = s[index] index += 1 if c == ']': if in_loop: break else: raise SFParseError('Unexpected "]"') elif c == '[': loop, index = parse(s, index, True) parsed.append(loop) elif c in ['<', '>', '*']: parsed.append(c) return parsed, index parsed, index = parse(s) return parsed def turingify(parsed): states = [] def add_state(action, transitions): nonlocal states assert action in actions assert len(transitions) == 2 index = len(states) states.append(State(action, transitions, [False, False])) return index def worker(parsed, end = None): nonlocal states while len(parsed) > 0: c = parsed[-1] parsed = parsed[:-1] if type(c) == list: loop_test = add_state(actions.nothing, [end, None]) loop_body = worker(c, loop_test) # We want the loop to repeat if the cell is set states[loop_test].transitions[1] = loop_body end = loop_test elif c == '*': end = add_state(actions.change, [end, end]) elif c == '<': end = add_state(actions.left, [end, end]) elif c == '>': end = add_state(actions.right, [end, end]) else: assert not "Unreachable" return end return worker(parsed), states def prettyprint_states(states): for i in range(len(states)): state = states[i] action = {actions.change: '* ', actions.left: '< ', actions.right: '> ', actions.nothing: ''}[state.action] if state.reachable == [True, True]: reachable = '(*)' elif state.reachable == [True, False]: reachable = '(0)' elif state.reachable == [False, True]: reachable = '(1)' elif state.reachable == [False, False]: reachable = '(!)' else: reachable = '' print('%4i%s: %s%s' % (i, reachable, action, state.transitions)) def reachability(start, states): def copy_state(index): state = states[index] action = state.action transitions = state.transitions[:] reachable = [False if i is None else i for i in state.reachable] return State(action, transitions, reachable) # Since we always end up with start at index 0, no need to create processed_start processed = [copy_state(start)] processed[0].reachable[0] = True processed[0].reachable[1] = True translations = {start: 0} processed_sweep = 0 while processed_sweep < len(processed): for i in range(len(processed[processed_sweep].transitions)): index_states = processed[processed_sweep].transitions[i] # Nothing to be done if we point to halt state if index_states is None: continue # Copy the state to processed if it isn't already there if index_states not in translations: index_processed = len(processed) processed.append(copy_state(index_states)) translations[index_states] = index_processed # Fix transition to the state processed[processed_sweep].transitions[i] = translations[index_states] # Mark how we got to the state processed[translations[index_states]].reachable[i] = True processed_sweep += 1 return processed def string_id(state, value, processed): def intlog(num, base): # This is floor(log_{base} num) assert num > 0 result = 0 while num > base - 1: num = num // base result += 1 return result assert value == 0 or value == 1 assert state is None or 0 <= state < len(processed) # One for each (cell value, state) combination and one for the cell values without a state max_num = 2 * len(processed) + 2 - 1 # Base 62 because we have 0-9A-Za-y, +1 because the intlog is one less than the digits needed width = intlog(max_num, 61) + 1 if state is not None: num = 2 * (state + 1) + value else: num = value digits = [] while num > 0: digit = num % 61 num = num // 61 if digit < 10: digits.append(chr(ord('0') + digit)) elif digit < 36: digits.append(chr(ord('A') + digit - 10)) else: digits.append(chr(ord('a') + digit - 36)) string = ''.join(reversed(digits)) # Pad to width string = '0' * (width - len(string)) + string return string def create_replacements(processed): replacements = [] for index in range(len(processed)): state = processed[index] if state.action == actions.nothing: for value in [0, 1]: if state.reachable[value]: start = string_id(index, value, processed) end = string_id(state.transitions[value], value, processed) replacements.append((start, end)) elif state.action == actions.change: for original_value in [0, 1]: if state.reachable[original_value]: changed_value = original_value ^ 1 start = string_id(index, original_value, processed) end = string_id(state.transitions[changed_value], changed_value, processed) replacements.append((start, end)) elif state.action == actions.left: for this_value in [0, 1]: if state.reachable[this_value]: for left_value in [0, 1]: this_start = string_id(index, this_value, processed) left_start = string_id(None, left_value, processed) start = '%s %s' % (left_start, this_start) this_end = string_id(None, this_value, processed) left_end = string_id(state.transitions[left_value], left_value, processed) end = '%s %s' % (left_end, this_end) replacements.append((start, end)) elif state.action == actions.right: for this_value in [0, 1]: if state.reachable[this_value]: for right_value in [0, 1, None]: this_start = string_id(index, this_value, processed) if right_value is not None: right_start = string_id(None, right_value, processed) else: right_start = 'z' start = '%s %s' % (this_start, right_start) this_end = string_id(None, this_value, processed) if right_value is not None: right_end = string_id(state.transitions[right_value], right_value, processed) else: right_end = string_id(state.transitions[0], 0, processed) + ' z' end = '%s %s' % (this_end, right_end) replacements.append((start, end)) else: assert not "Unreachable" return replacements def xedify(replacements): replaced = '|'.join(start for start, end in replacements) replacement = '|'.join(end for start, end in replacements) return 'x/%s/%s/' % (replaced, replacement) def process_tape(tape, processed): assert all(i in ['0', '1', '^0', '^1'] for i in tape) processed_tape = [] for i in tape: if i == '0': processed_tape.append(string_id(None, 0, processed)) elif i == '1': processed_tape.append(string_id(None, 1, processed)) elif i == '^0': processed_tape.append(string_id(0, 0, processed)) elif i == '^1': processed_tape.append(string_id(0, 1, processed)) else: assert not "Unreachable" return ' '.join(processed_tape) + ' z' def run_replacements(replacements, processed_tape): while True: made_replacement = False for start, end in replacements: if start in processed_tape: processed_tape = processed_tape.replace(start, end) made_replacement = True break if not made_replacement: break print(processed_tape) def main(): if len(sys.argv) == 2 and sys.argv[1] == '-r': run = True else: run = False program = input('program: ') start_noreachability, states_noreachability = turingify(parse_smallfuck(program)) processed = reachability(start_noreachability, states_noreachability) replacements = create_replacements(processed) print(xedify(replacements)) print('\nSend empty to exit. ^0/^1 for initial tape head position') while True: tape = input('tape: ') if tape == '': break tape = tape.split(' ') processed_tape = process_tape(tape, processed) print(processed_tape) if run: run_replacements(replacements, processed_tape) if __name__ == '__main__': main()