*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Debe ejecutarse con los -ln
indicadores de la línea de comandos (por lo tanto, +4 bytes). Imprime 0
para números compuestos y 1
para números primos.
Pruébalo en línea!
Creo que este es el primer programa no trivial de Stack Cats.
Explicación
Una introducción rápida a Stack Cats:
- Stack Cats opera en una cinta infinita de pilas, con una cabeza de cinta apuntando a una pila actual. Cada pila se llena inicialmente con una cantidad infinita de ceros. En general, ignoraré estos ceros en mi redacción, así que cuando digo "la parte inferior de la pila" me refiero al valor más bajo que no sea cero y si digo "la pila está vacía" me refiero a que solo hay ceros en ella.
- Antes de que comience el programa,
-1
se inserta a en la pila inicial, y luego se empuja toda la entrada por encima de eso. En este caso, debido a la -n
bandera, la entrada se lee como un entero decimal.
- Al final del programa, la pila actual se usa para la salida. Si hay una
-1
en la parte inferior, se ignorará. Nuevamente, debido a la -n
bandera, los valores de la pila simplemente se imprimen como enteros decimales separados por salto de línea.
- Stack Cats es un lenguaje de programa reversible: cada pieza de código se puede deshacer (sin que Stack Cats realice un seguimiento de un historial explícito). Más específicamente, para revertir cualquier pieza de código, simplemente lo refleja, por ejemplo, se
<<(\-_)
convierte en (_-/)>>
. Este objetivo de diseño impone restricciones bastante severas sobre qué tipos de operadores y construcciones de flujo de control existen en el lenguaje, y qué tipo de funciones puede calcular en el estado de la memoria global.
Para colmo, cada programa de Stack Cats tiene que ser auto-simétrico. Puede notar que este no es el caso para el código fuente anterior. Para eso está la -l
bandera: refleja implícitamente el código a la izquierda, utilizando el primer carácter para el centro. Por lo tanto, el programa real es:
[<(*>=*(:)*[(>*{[[>[:<[>>_(_-<<(-!>)>(>-)):]<^:>!->}<*)*[^:<)*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
La programación efectiva con todo el código es altamente no trivial y poco intuitiva y todavía no se ha dado cuenta de cómo un humano puede hacerlo. Bruscamente forzamos ese programa para tareas más simples, pero no hubiéramos podido acercarnos a eso a mano. Afortunadamente, hemos encontrado un patrón básico que le permite ignorar la mitad del programa. Si bien esto es ciertamente subóptimo, actualmente es la única forma conocida de programar efectivamente en Stack Cats.
Entonces, en esta respuesta, la plantilla de dicho patrón es la siguiente (hay alguna variabilidad en cómo se ejecuta):
[<(...)*(...)>]
Cuando se inicia el programa, la cinta de pila se ve así (por entrada 4
, por ejemplo):
4
... -1 ...
0
^
Se [
mueve la parte superior de la pila hacia la izquierda (y la cabeza de la cinta): a esto le llamamos "empujar". Y se <
mueve la cabeza de la cinta sola. Entonces, después de los dos primeros comandos, tenemos esta situación:
... 4 -1 ...
0 0 0
^
Ahora (...)
es un bucle que se puede usar con bastante facilidad como condicional: el bucle se ingresa y se deja solo cuando la parte superior de la pila actual es positiva. Como actualmente es cero, omitimos la primera mitad del programa. Ahora el comando central es *
. Esto es simplemente XOR 1
, es decir, alterna el bit menos significativo de la parte superior de la pila, y en este caso convierte el 0
en 1
:
... 1 4 -1 ...
0 0 0
^
Ahora nos encontramos con la imagen especular de la (...)
. Esta vez la parte superior de la pila es positivo y nos haga entrar en el código. Antes de analizar lo que sucede dentro de los paréntesis, permítanme explicar cómo terminaremos al final: queremos asegurarnos de que al final de este bloque, tengamos el cabezal de la cinta en un valor positivo nuevamente (para que el bucle termina después de una única iteración y se utiliza simplemente como un condicional lineal), que la pila a la derecha mantiene la salida y que el derecho pila de que tiene una -1
. Si ese es el caso, dejamos el ciclo, nos >
movemos hacia el valor de salida y lo ]
empujamos hacia adentro -1
para que tengamos una pila limpia para la salida.
Eso es eso. Ahora dentro de los paréntesis podemos hacer lo que queramos para verificar la primalidad siempre y cuando nos aseguremos de configurar las cosas como se describe en el párrafo anterior al final (que puede hacerse fácilmente presionando y moviendo la cabeza de la cinta). Primero intenté resolver el problema con el teorema de Wilson, pero terminé con más de 100 bytes, porque el cálculo factorial al cuadrado es bastante costoso en Stack Cats (al menos no he encontrado un camino corto). Así que fui con la división de prueba y eso resultó mucho más simple. Veamos el primer bit lineal:
>:^]
Ya has visto dos de esos comandos. Además, :
intercambia los dos valores superiores de la pila actual y ^
XOR el segundo valor en el valor superior. Esto hace :^
un patrón común para duplicar un valor en una pila vacía (sacamos un cero encima del valor y luego convertimos el cero en 0 XOR x = x
). Luego de esto, la sección de nuestra cinta se ve así:
4
... 1 4 -1 ...
0 0 0
^
El algoritmo de división de prueba que he implementado no funciona para la entrada 1
, por lo que deberíamos omitir el código en ese caso. Podemos asignar fácilmente 1
a 0
y todo lo demás a valores positivos con *
, por lo que aquí es cómo lo hacemos:
*(*...)
Es decir, nos convertimos 1
en 0
, omitimos una gran parte del código si lo conseguimos 0
, pero por dentro lo deshacemos inmediatamente *
para recuperar nuestro valor de entrada. Solo necesitamos asegurarnos nuevamente de que terminamos con un valor positivo al final de los paréntesis para que no comiencen a repetirse. Dentro del condicional, movemos una pila hacia la derecha con >
y luego iniciamos el ciclo principal de división de prueba:
{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}
Las llaves (en lugar de paréntesis) definen un tipo diferente de bucle: es un bucle do-while, lo que significa que siempre se ejecuta al menos una iteración. La otra diferencia es la condición de terminación: al ingresar al bucle, Stack Cat recuerda el valor superior de la pila actual ( 0
en nuestro caso). El bucle se ejecutará hasta que este mismo valor se vuelva a ver al final de una iteración. Esto es conveniente para nosotros: en cada iteración simplemente calculamos el resto del siguiente divisor potencial y lo movemos a esta pila en la que estamos comenzando el ciclo. Cuando encontramos un divisor, el resto es 0
y el ciclo se detiene. Intentaremos los divisores comenzando en n-1
y luego los disminuiremos a 1
. Eso significa a) sabemos que esto terminará cuando lleguemos1
a más tardar y b) podemos determinar si el número es primo o no inspeccionando el último divisor que probamos (si es 1
, es primo, de lo contrario no lo es).
Hagámoslo. Hay una sección lineal corta al principio:
<-!<:^>[:
Ya sabes lo que la mayoría de esas cosas hacen ahora. Los nuevos comandos son -
y !
. Stack Cats no tiene operadores de incremento o decremento. Sin embargo, tiene -
(negación, es decir, multiplicar por -1
) y !
(NO a nivel de bit, es decir, multiplicar por -1
y disminuir). Estos se pueden combinar en un incremento !-
o decremento -!
. Por lo tanto, disminuimos la copia n
en la parte superior -1
, luego hacemos otra copia n
en la pila a la izquierda, luego buscamos el nuevo divisor de prueba y lo colocamos debajo n
. Entonces, en la primera iteración, obtenemos esto:
4
3
... 1 4 -1 ...
0 0 0
^
En iteraciones posteriores, 3
se reemplazará con el siguiente divisor de prueba y así sucesivamente (mientras que las dos copias n
siempre tendrán el mismo valor en este punto).
((-<)<(<!-)>>-_)
Este es el cálculo del módulo. Como los bucles terminan en valores positivos, la idea es comenzar desde -n
y agregarle repetidamente el divisor de prueba d
hasta obtener un valor positivo. Una vez que lo hacemos, restamos el resultado d
y esto nos da el resto. La parte difícil aquí es que no podemos simplemente poner una -n
parte superior de la pila y comenzar un ciclo que agrega d
: si la parte superior de la pila es negativa, no se ingresará el ciclo. Tales son las limitaciones de un lenguaje de programación reversible.
Entonces, para evitar este problema, comenzamos con la n
parte superior de la pila, pero lo negamos solo en la primera iteración. De nuevo, eso suena más simple de lo que parece ser ...
(-<)
Cuando la parte superior de la pila es positiva (es decir, solo en la primera iteración), la negamos con -
. Sin embargo, no podemos hacerlo (-)
porque no estaríamos dejando el ciclo hasta que -
se aplicara dos veces. Así que movemos una celda hacia la izquierda <
porque sabemos que hay un valor positivo allí (el 1
). Bien, ahora hemos negado de manera confiable n
en la primera iteración. Pero tenemos un nuevo problema: la cabeza de la cinta ahora está en una posición diferente en la primera iteración que en cualquier otra. Necesitamos consolidar esto antes de seguir adelante. El siguiente <
mueve la cabeza de la cinta hacia la izquierda. La situación en la primera iteración:
-4
3
... 1 4 -1 ...
0 0 0 0
^
Y en la segunda iteración (recuerde que hemos agregado d
una vez -n
ahora):
-1
3
... 1 4 -1 ...
0 0 0
^
El siguiente condicional fusiona estos caminos nuevamente:
(<!-)
En la primera iteración, el cabezal de la cinta apunta a cero, por lo que se omite por completo. Sin embargo, en otras iteraciones, el cabezal de la cinta apunta a uno, así que ejecutamos esto, nos movemos hacia la izquierda e incrementamos la celda allí. Como sabemos que la celda comienza desde cero, ahora siempre será positiva para que podamos abandonar el ciclo. Esto garantiza que siempre terminemos dos pilas a la izquierda de la pila principal y que ahora podamos retroceder >>
. Luego, al final del ciclo módulo lo hacemos -_
. Usted ya sabe -
. _
es restar lo que ^
es para XOR: si la parte superior de la pila es a
y el valor debajo es b
reemplazado a
por b-a
. Desde la primera vez negada a
sin embargo, -_
reemplaza a
con b+a
, añadiendo asíd
en nuestro total acumulado.
Una vez que finaliza el ciclo (hemos alcanzado un valor positivo), la cinta se ve así:
2
3
... 1 1 4 -1 ...
0 0 0 0
^
El valor más a la izquierda podría ser cualquier número positivo. De hecho, es el número de iteraciones menos uno. Hay otro bit lineal corto ahora:
_<<]>:]<]]
Como dije antes, necesitamos restar el resultado d
para obtener el resto real ( 3-2 = 1 = 4 % 3
), por lo que solo lo hacemos _
una vez más. Luego, necesitamos limpiar la pila que hemos estado incrementando a la izquierda: cuando probamos el siguiente divisor, debe ser cero nuevamente, para que la primera iteración funcione. Entonces nos movemos allí y empujamos ese valor positivo en la otra pila auxiliar con <<]
y luego volvemos a nuestra pila operativa con otra >
. Nos detenemos d
con :
y vuelva a insertarla en el -1
con ]
y luego nos movemos hacia el resto de nuestra pila condicional con <]]
. Ese es el final del ciclo de división de prueba: esto continúa hasta que obtengamos un resto cero, en cuyo caso la pila a la izquierda contienen
El mayor divisor (aparte de n
).
Después de que finaliza el ciclo, hay justo *<
antes de que unamos 1
nuevamente las rutas con la entrada . El *
simplemente se vuelve el cero en una 1
, lo que vamos a necesitar un poco, y luego se pasa al divisor con <
(por lo que estamos en la misma pila que para la entrada 1
).
En este punto, ayuda a comparar tres tipos diferentes de entradas. Primero, el caso especial n = 1
donde no hemos hecho nada de eso de la división de prueba:
0
... 1 1 -1 ...
0 0 0
^
Luego, nuestro ejemplo anterior n = 4
, un número compuesto:
2
1 2 1
... 1 4 -1 1 ...
0 0 0 0
^
Y finalmente, n = 3
un número primo:
3
1 1 1
... 1 3 -1 1 ...
0 0 0 0
^
Entonces, para los números primos, tenemos un 1
en esta pila, y para los números compuestos tenemos 0
un número positivo o mayor que 2
. Nos dirigimos esta situación en el 0
o 1
que necesitamos con la siguiente pieza final de código:
]*(:)*=<*
]
simplemente empuja este valor a la derecha. A continuación, *
se utiliza para simplificar la situación condicionada en gran medida: alternando el bit menos significativo, nos volvemos 1
(primer) en 0
, 0
(compuesto) en el valor positivo 1
, y todos los demás valores positivos seguirán siendo positivo. Ahora solo necesitamos distinguir entre 0
y positivo. Ahí es donde usamos otro (:)
. Si la parte superior de la pila es 0
(y la entrada fue un primo), esto simplemente se omite. Pero si la parte superior de la pila es positiva (y la entrada era un número compuesto), esto se intercambia con el 1
, de modo que ahora tenemos 0
para compuesto y1
para primos: solo dos valores distintos. Por supuesto, son lo opuesto a lo que queremos generar, pero eso se soluciona fácilmente con otro *
.
Ahora todo lo que queda es restaurar el patrón de pilas esperado por nuestro marco circundante: cabeza de cinta en un valor positivo, resultado en la parte superior de la pila a la derecha, y un solo -1
en la pila a la derecha de eso . Esto es para lo que =<*
sirve. =
intercambia la parte superior de las dos pilas adyacentes, moviendo así -1
a la derecha del resultado, por ejemplo, para ingresar 4
nuevamente:
2 0
1 3
... 1 4 1 -1 ...
0 0 0 0 0
^
Luego nos movemos a la izquierda con <
y convertimos ese cero en uno con *
. Y eso es eso.
Si desea profundizar en cómo funciona el programa, puede utilizar las opciones de depuración. Agregue el -d
indicador e insértelo "
donde desee ver el estado actual de la memoria, por ejemplo , de esta manera , o use el -D
indicador para obtener un seguimiento completo de todo el programa . Alternativamente, puede usar EsotericIDE de Timwi, que incluye un intérprete de Stack Cats con un depurador paso a paso.